diff --git a/.gitignore b/.gitignore index edea2eb4b..7a246f14c 100755 --- a/.gitignore +++ b/.gitignore @@ -30,8 +30,13 @@ osxbuild/env /CompilerArgs.nsi /build/bdist.win32/winexe/temp -cppForSwig /*.dll /qrc_img_resources.py /.project /.pydevproject +/.settings +/*.wallet + +/findpass.py +/*.txt +/sandbox.py diff --git a/ArmoryDB.py b/ArmoryDB.py index bdb2b3514..9f85b784b 100644 --- a/ArmoryDB.py +++ b/ArmoryDB.py @@ -15,14 +15,17 @@ # ####################################################################################################### -import leveldb -from armoryengine import * -import struct import os +import struct + +from armoryengine.ArmoryUtils import ARMORY_HOME_DIR, unpackVarInt +from armoryengine.Block import PyBlockHeader +from armoryengine.Transaction import PyTx +import leveldb + #dbheaders_path = '/home/goat/.armory/databases/leveldb_headers' #dbblkdata_path = '/home/goat/.armory/databases/leveldb_blkdata' - dbheaders_path = os.path.join(ARMORY_HOME_DIR, 'databases', 'leveldb_headers') dbblkdata_path = os.path.join(ARMORY_HOME_DIR, 'databases', 'leveldb_blkdata') @@ -245,7 +248,7 @@ def blkdataDBHasBlockHash(self, Hash): if(len(val)==84): key = '\x03' + val[80:84] try: - val = dArmoryDB.bblkdata.Get(key) + val = ArmoryDB.bblkdata.Get(key) return True except: return False diff --git a/ArmoryQt.py b/ArmoryQt.py index 7e59bc275..4ec017682 100644 --- a/ArmoryQt.py +++ b/ArmoryQt.py @@ -1,47 +1,64 @@ #! /usr/bin/python ################################################################################ # # -# Copyright (C) 2011-2013, Armory Technologies, Inc. # +# Copyright (C) 2011-2014, Armory Technologies, Inc. # # Distributed under the GNU Affero General Public License (AGPL v3) # # See LICENSE or http://www.gnu.org/licenses/agpl.html # # # ################################################################################ +from datetime import datetime import hashlib -import random -import time -import os -import sys -import shutil +import logging import math -import threading +import os import platform -import traceback +import random +import shutil +import signal import socket import subprocess -import psutil -import signal +import sys +import threading +import time +import traceback import webbrowser -from datetime import datetime +import psutil +from copy import deepcopy -# PyQt4 Imports from PyQt4.QtCore import * from PyQt4.QtGui import * +from twisted.internet.defer import Deferred +from twisted.internet.protocol import Protocol, ClientFactory -# Over 20,000 lines of python to help us out -from armoryengine import * -from armorymodels import * -from qtdialogs import * -from qtdefines import * +from armoryengine.ALL import * from armorycolors import Colors, htmlColor, QAPP - +from armorymodels import * +from ui.toolsDialogs import MessageSigningVerificationDialog import qrc_img_resources +from qtdefines import * +from qtdialogs import * +from ui.Wizards import WalletWizard, TxWizard +from ui.VerifyOfflinePackage import VerifyOfflinePackageDialog +from ui.UpgradeDownloader import UpgradeDownloaderDialog +from jasvet import verifySignature, readSigBlock +from announcefetch import AnnounceDataFetcher, ANNOUNCE_URL, ANNOUNCE_URL_BACKUP +from armoryengine.parseAnnounce import * +from armoryengine.PyBtcWalletRecovery import WalletConsistencyCheck + +# HACK ALERT: Qt has a bug in OS X where the system font settings will override +# the app's settings when a window is activated (e.g., Armory starts, the user +# switches to another app, and then switches back to Armory). There is a +# workaround, as used by TeXstudio and other programs. +# https://bugreports.qt-project.org/browse/QTBUG-5469 - Bug discussion. +# http://sourceforge.net/p/texstudio/bugs/594/?page=1 - Fix is mentioned. +# http://pyqt.sourceforge.net/Docs/PyQt4/qapplication.html#setDesktopSettingsAware +# - Mentions that this must be called before the app (QAPP) is created. +if OS_MACOSX: + QApplication.setDesktopSettingsAware(False) +# PyQt4 Imports # All the twisted/networking functionality -from twisted.internet.protocol import Protocol, ClientFactory -from twisted.internet.defer import Deferred -from dialogs.toolsDialogs import MessageSigningVerificationDialog - if OS_WINDOWS: from _winreg import * @@ -50,12 +67,10 @@ class ArmoryMainWindow(QMainWindow): """ The primary Armory window """ ############################################################################# + @TimeThisFunction def __init__(self, parent=None): super(ArmoryMainWindow, self).__init__(parent) - TimerStart('MainWindowInit') - - self.bornOnTime = RightNow() # Load the settings file self.settingsPath = CLI_OPTIONS.settingsPath @@ -88,9 +103,6 @@ def __init__(self, parent=None): self.newZeroConfSinceLastUpdate = [] self.lastBDMState = ['Uninitialized', None] self.lastSDMState = 'Uninitialized' - self.detectNotSyncQ = [0,0,0,0,0] - self.noSyncWarnYet = True - self.doHardReset = False self.doShutdown = False self.downloadDict = {} self.notAvailErrorCount = 0 @@ -102,7 +114,45 @@ def __init__(self, parent=None): self.satoshiExeSearchPath = None self.initSyncCircBuff = [] self.latestVer = {} - + self.lastVersionsTxtHash = '' + self.dlgCptWlt = None + self.torrentFinished = False + self.torrentCircBuffer = [] + self.lastAskedUserStopTorrent = 0 + self.wasSynchronizing = False + self.announceIsSetup = False + self.entropyAccum = [] + + # Full list of notifications, and notify IDs that should trigger popups + # when sending or receiving. + self.lastAnnounceUpdate = {} + self.changelog = [] + self.downloadLinks = {} + self.almostFullNotificationList = {} + self.notifyOnSend = set() + self.notifyonRecv = set() + self.versionNotification = {} + self.notifyIgnoreLong = [] + self.notifyIgnoreShort = [] + self.maxPriorityID = None + self.satoshiVersions = ['',''] # [curr, avail] + self.armoryVersions = [getVersionString(BTCARMORY_VERSION), ''] + self.NetworkingFactory = None + + + # Kick off announcement checking, unless they explicitly disabled it + # The fetch happens in the background, we check the results periodically + self.announceFetcher = None + self.setupAnnouncementFetcher() + + #delayed URI parsing dict + self.delayedURIData = {} + self.delayedURIData['qLen'] = 0 + + #Setup the signal to spawn progress dialogs from the main thread + self.connect(self, SIGNAL('initTrigger') , self.initTrigger) + self.connect(self, SIGNAL('execTrigger'), self.execTrigger) + self.connect(self, SIGNAL('checkForNegImports'), self.checkForNegImports) # We want to determine whether the user just upgraded to a new version self.firstLoadNewVersion = False @@ -110,17 +160,18 @@ def __init__(self, parent=None): if self.settings.hasSetting('LastVersionLoad'): lastVerStr = self.settings.get('LastVersionLoad') if not lastVerStr==currVerStr: + LOGINFO('First load of new version: %s', currVerStr) self.firstLoadNewVersion = True self.settings.set('LastVersionLoad', currVerStr) - # Because dynamically retrieving addresses for querying transaction + # Because dynamically retrieving addresses for querying transaction # comments can be so slow, I use this txAddrMap to cache the mappings - # between tx's and addresses relevant to our wallets. It really only - # matters for massive tx with hundreds of outputs -- but such tx do + # between tx's and addresses relevant to our wallets. It really only + # matters for massive tx with hundreds of outputs -- but such tx do # exist and this is needed to accommodate wallets with lots of them. self.txAddrMap = {} - + self.loadWalletsAndSettings() eulaAgreed = self.getSettingOrSetDefault('Agreed_to_EULA', False) @@ -139,12 +190,12 @@ def __init__(self, parent=None): # We need to query this once at the beginning, to avoid having # strange behavior if the user changes the setting but hasn't # restarted yet... - self.doManageSatoshi = \ + self.doAutoBitcoind = \ self.getSettingOrSetDefault('ManageSatoshi', not OS_MACOSX) # If we're going into online mode, start loading blockchain - if self.doManageSatoshi: + if self.doAutoBitcoind: self.startBitcoindIfNecessary() else: self.loadBlockchainIfNecessary() @@ -156,10 +207,23 @@ def __init__(self, parent=None): self.extraHeartbeatSpecial = [] self.extraHeartbeatOnline = [] + + """ + pass a function to extraHeartbeatAlways to run on every heartbeat. + pass a list for more control on the function, as + [func, [args], keep_running], + where: + func is the function + [args] is a list of arguments + keep_running is a bool, pass False to remove the function from + extraHeartbeatAlways on the next iteration + """ + self.extraHeartbeatAlways = [] - self.lblArmoryStatus = QRichLabel('Offline ' % + self.lblArmoryStatus = QRichLabel('Offline ' % htmlColor('TextWarn'), doWrap=False) + self.statusBar().insertPermanentWidget(0, self.lblArmoryStatus) # Keep a persistent printer object for paper backups @@ -175,7 +239,7 @@ def __init__(self, parent=None): viewWidth = 1.2*w sectionSz = 1.3*h viewHeight = 4.4*sectionSz - + self.walletsView.setModel(self.walletModel) self.walletsView.setSelectionBehavior(QTableView.SelectRows) self.walletsView.setSelectionMode(QTableView.SingleSelection) @@ -192,7 +256,7 @@ def __init__(self, parent=None): self.connect(self.walletsView, SIGNAL('doubleClicked(QModelIndex)'), \ self.execDlgWalletDetails) - + w,h = tightSizeNChar(GETFONT('var'), 100) @@ -209,12 +273,7 @@ def __init__(self, parent=None): self.ledgerTable = [] self.ledgerModel = LedgerDispModelSimple(self.ledgerTable, self, self) - #self.ledgerProxy = LedgerDispSortProxy() - #self.ledgerProxy.setSourceModel(self.ledgerModel) - #self.ledgerProxy.setDynamicSortFilter(False) - self.ledgerView = QTableView() - self.ledgerView.setModel(self.ledgerModel) self.ledgerView.setSortingEnabled(True) self.ledgerView.setItemDelegate(LedgerDispDelegate(self)) @@ -240,7 +299,7 @@ def __init__(self, parent=None): cWidth = 20 # num-confirm icon width tWidth = 72 # date icon width initialColResize(self.ledgerView, [cWidth, 0, dateWidth, tWidth, 0.30, 0.40, 0.3]) - + self.connect(self.ledgerView, SIGNAL('doubleClicked(QModelIndex)'), \ self.dblClickLedger) @@ -249,12 +308,12 @@ def __init__(self, parent=None): btnAddWallet = QPushButton("Create Wallet") btnImportWlt = QPushButton("Import or Restore Wallet") - self.connect(btnAddWallet, SIGNAL('clicked()'), self.createNewWallet) + self.connect(btnAddWallet, SIGNAL('clicked()'), self.startWalletWizard) self.connect(btnImportWlt, SIGNAL('clicked()'), self.execImportWallet) # Put the Wallet info into it's own little box lblAvail = QLabel("Available Wallets:") - viewHeader = makeLayoutFrame('Horiz', [lblAvail, \ + viewHeader = makeLayoutFrame(HORIZONTAL, [lblAvail, \ 'Stretch', \ btnAddWallet, \ btnImportWlt, ]) @@ -272,11 +331,7 @@ def __init__(self, parent=None): # Put the labels into scroll areas just in case window size is small. self.tabDashboard = QWidget() - - - - self.SetupDashboard() - + self.setupDashboard() # Combo box to filter ledger display @@ -288,17 +343,17 @@ def __init__(self, parent=None): # Create the new ledger twice: can't update the ledger up/down - # widgets until we know how many ledger entries there are from + # widgets until we know how many ledger entries there are from # the first call def createLedg(): - self.createCombinedLedger() + self.createCombinedLedger() if self.frmLedgUpDown.isVisible(): - self.changeNumShow() + self.changeNumShow() self.connect(self.comboWltSelect, SIGNAL('activated(int)'), createLedg) - self.lblTot = QRichLabel('Maximum Funds:', doWrap=False); - self.lblSpd = QRichLabel('Spendable Funds:', doWrap=False); - self.lblUcn = QRichLabel('Unconfirmed:', doWrap=False); + self.lblTot = QRichLabel('Maximum Funds:', doWrap=False); + self.lblSpd = QRichLabel('Spendable Funds:', doWrap=False); + self.lblUcn = QRichLabel('Unconfirmed:', doWrap=False); self.lblTotalFunds = QRichLabel('-'*12, doWrap=False) self.lblSpendFunds = QRichLabel('-'*12, doWrap=False) @@ -385,7 +440,7 @@ def createLedg(): layoutUpDown.setVerticalSpacing(2) self.frmLedgUpDown.setLayout(layoutUpDown) self.frmLedgUpDown.setFrameStyle(STYLE_SUNKEN) - + frmLower = makeHorizFrame([ frmFilter, \ 'Stretch', \ @@ -397,7 +452,6 @@ def createLedg(): ledgFrame = QFrame() ledgFrame.setFrameStyle(QFrame.Box|QFrame.Sunken) ledgLayout = QGridLayout() - #ledgLayout.addWidget(QLabel("Ledger:"), 0,0) ledgLayout.addWidget(self.ledgerView, 1,0) ledgLayout.addWidget(frmLower, 2,0) ledgLayout.setRowStretch(0, 0) @@ -408,25 +462,29 @@ def createLedg(): self.tabActivity = QWidget() self.tabActivity.setLayout(ledgLayout) + self.tabAnnounce = QWidget() + self.setupAnnounceTab() + + # Add the available tabs to the main tab widget - self.MAINTABS = enum('Dashboard','Transactions') + self.MAINTABS = enum('Dash','Ledger','Announce') self.mainDisplayTabs.addTab(self.tabDashboard, 'Dashboard') self.mainDisplayTabs.addTab(self.tabActivity, 'Transactions') + self.mainDisplayTabs.addTab(self.tabAnnounce, 'Announcements') btnSendBtc = QPushButton("Send Bitcoins") btnRecvBtc = QPushButton("Receive Bitcoins") btnWltProps = QPushButton("Wallet Properties") btnOfflineTx = QPushButton("Offline Transactions") - self.connect(btnWltProps, SIGNAL('clicked()'), self.execDlgWalletDetails) self.connect(btnRecvBtc, SIGNAL('clicked()'), self.clickReceiveCoins) self.connect(btnSendBtc, SIGNAL('clicked()'), self.clickSendBitcoins) self.connect(btnOfflineTx,SIGNAL('clicked()'), self.execOfflineTx) - verStr = 'Armory %s-beta / %s' % (getVersionString(BTCARMORY_VERSION), \ + verStr = 'Armory %s - %s User' % (getVersionString(BTCARMORY_VERSION), \ UserModeStr(self.usermode)) lblInfo = QRichLabel(verStr, doWrap=False) lblInfo.setFont(GETFONT('var',10)) @@ -447,7 +505,7 @@ def createLedg(): btnFrame.sizeHint = lambda: QSize(logoWidth*1.0, 10) btnFrame.setMaximumWidth(logoWidth*1.2) btnFrame.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) - + layout = QGridLayout() layout.addWidget(btnFrame, 0, 0, 1, 1) layout.addWidget(wltFrame, 0, 1, 1, 1) @@ -462,11 +520,7 @@ def createLedg(): self.setMinimumSize(750,500) # Start the user at the dashboard - self.mainDisplayTabs.setCurrentIndex(self.MAINTABS.Dashboard) - - from twisted.internet import reactor - # Show the appropriate information on the dashboard - self.setDashboardDetails(INIT=True) + self.mainDisplayTabs.setCurrentIndex(self.MAINTABS.Dash) ########################################################################## @@ -484,7 +538,7 @@ def createLedg(): self.menusList.append( self.menu.addMenu('&Help') ) #self.menusList.append( self.menu.addMenu('&Network') ) - + def exportTx(): if not TheBDM.getBDMState()=='BlockchainReady': QMessageBox.warning(self, 'Transactions Unavailable', \ @@ -494,7 +548,7 @@ def exportTx(): return else: DlgExportTxHistory(self,self).exec_() - + actExportTx = self.createAction('&Export Transactions', exportTx) actSettings = self.createAction('&Settings', self.openSettings) @@ -507,12 +561,12 @@ def exportTx(): self.menusList[MENUS.File].addAction(actExportLog) self.menusList[MENUS.File].addAction(actCloseApp) - - def chngStd(b): + + def chngStd(b): if b: self.setUserMode(USERMODE.Standard) - def chngAdv(b): + def chngAdv(b): if b: self.setUserMode(USERMODE.Advanced) - def chngDev(b): + def chngDev(b): if b: self.setUserMode(USERMODE.Expert) modeActGrp = QActionGroup(self) @@ -533,13 +587,13 @@ def chngDev(b): LOGINFO('Usermode: %s', currmode) self.firstModeSwitch=True if currmode=='Standard': - self.usermode = USERMODE.Standard + self.usermode = USERMODE.Standard actSetModeStd.setChecked(True) elif currmode=='Advanced': - self.usermode = USERMODE.Advanced + self.usermode = USERMODE.Advanced actSetModeAdv.setChecked(True) elif currmode=='Expert': - self.usermode = USERMODE.Expert + self.usermode = USERMODE.Expert actSetModeDev.setChecked(True) def openMsgSigning(): @@ -564,41 +618,58 @@ def openMsgSigning(): self.menusList[MENUS.Addresses].addAction(actImportKey) self.menusList[MENUS.Addresses].addAction(actSweepKey) - actCreateNew = self.createAction('&Create New Wallet', self.createNewWallet) + actCreateNew = self.createAction('&Create New Wallet', self.startWalletWizard) actImportWlt = self.createAction('&Import or Restore Wallet', self.execImportWallet) actAddressBook = self.createAction('View &Address Book', self.execAddressBook) + actRecoverWlt = self.createAction('Fix Damaged Wallet', self.RecoverWallet) #actRescanOnly = self.createAction('Rescan Blockchain', self.forceRescanDB) #actRebuildAll = self.createAction('Rescan with Database Rebuild', self.forceRebuildAndRescan) self.menusList[MENUS.Wallets].addAction(actCreateNew) self.menusList[MENUS.Wallets].addAction(actImportWlt) self.menusList[MENUS.Wallets].addSeparator() + self.menusList[MENUS.Wallets].addAction(actRecoverWlt) #self.menusList[MENUS.Wallets].addAction(actRescanOnly) #self.menusList[MENUS.Wallets].addAction(actRebuildAll) #self.menusList[MENUS.Wallets].addAction(actMigrateSatoshi) #self.menusList[MENUS.Wallets].addAction(actAddressBook) + def execVersion(): + self.explicitCheckAnnouncements() + self.mainDisplayTabs.setCurrentIndex(self.MAINTABS.Announce) execAbout = lambda: DlgHelpAbout(self).exec_() - execVersion = lambda: self.checkForLatestVersion(wasRequested=True) execTrouble = lambda: webbrowser.open('https://bitcoinarmory.com/troubleshooting/') - actAboutWindow = self.createAction('About Armory', execAbout) - actTroubleshoot = self.createAction('Troubleshooting Armory', execTrouble) - actVersionCheck = self.createAction('Armory Version...', execVersion) - actFactoryReset = self.createAction('Revert All Settings', self.factoryReset) - actClearMemPool = self.createAction('Clear All Unconfirmed', self.clearMemoryPool) - actRescanDB = self.createAction('Rescan Databases', self.rescanNextLoad) - actRebuildDB = self.createAction('Rebuild and Rescan Databases', self.rebuildNextLoad) + execBugReport = lambda: DlgBugReport(self, self).exec_() + + + execVerifySigned = lambda: VerifyOfflinePackageDialog(self, self).exec_() + actAboutWindow = self.createAction(tr('About Armory'), execAbout) + actVersionCheck = self.createAction(tr('Armory Version...'), execVersion) + actDownloadUpgrade = self.createAction(tr('Update Software...'), self.openDownloaderAll) + actVerifySigned = self.createAction(tr('Verify Signed Package...'), execVerifySigned) + actTroubleshoot = self.createAction(tr('Troubleshooting Armory'), execTrouble) + actSubmitBug = self.createAction(tr('Submit Bug Report'), execBugReport) + actClearMemPool = self.createAction(tr('Clear All Unconfirmed'), self.clearMemoryPool) + actRescanDB = self.createAction(tr('Rescan Databases'), self.rescanNextLoad) + actRebuildDB = self.createAction(tr('Rebuild and Rescan Databases'), self.rebuildNextLoad) + actFactoryReset = self.createAction(tr('Factory Reset'), self.factoryReset) + actPrivacyPolicy = self.createAction(tr('Armory Privacy Policy'), self.showPrivacyGeneric) self.menusList[MENUS.Help].addAction(actAboutWindow) - self.menusList[MENUS.Help].addAction(actTroubleshoot) self.menusList[MENUS.Help].addAction(actVersionCheck) - self.menusList[MENUS.Help].addAction(actFactoryReset) + self.menusList[MENUS.Help].addAction(actDownloadUpgrade) + self.menusList[MENUS.Help].addAction(actVerifySigned) + self.menusList[MENUS.Help].addSeparator() + self.menusList[MENUS.Help].addAction(actTroubleshoot) + self.menusList[MENUS.Help].addAction(actSubmitBug) + self.menusList[MENUS.Help].addAction(actPrivacyPolicy) self.menusList[MENUS.Help].addSeparator() self.menusList[MENUS.Help].addAction(actClearMemPool) self.menusList[MENUS.Help].addAction(actRescanDB) self.menusList[MENUS.Help].addAction(actRebuildDB) + self.menusList[MENUS.Help].addAction(actFactoryReset) # Restore any main-window geometry saved in the settings file hexgeom = self.settings.get('MainGeometry') @@ -614,22 +685,40 @@ def openMsgSigning(): self.ledgerView.setColumnWidth(LEDGERCOLS.NumConf, 20) self.ledgerView.setColumnWidth(LEDGERCOLS.TxDir, 72) + haveGUI[0] = True + haveGUI[1] = self + BDMcurrentBlock[1] = 1 - TimerStop('MainWindowInit') + if DO_WALLET_CHECK: + self.checkWallets() + self.setDashboardDetails() + + from twisted.internet import reactor reactor.callLater(0.1, self.execIntroDialog) reactor.callLater(1, self.Heartbeat) + if self.getSettingOrSetDefault('MinimizeOnOpen', False) and not CLI_ARGS: + LOGINFO('MinimizeOnOpen is True') + reactor.callLater(0, self.minimizeArmory) + + if CLI_ARGS: reactor.callLater(1, self.uriLinkClicked, CLI_ARGS[0]) - elif not self.firstLoad: - # Don't need to bother the user on the first load with updating - reactor.callLater(0.2, self.checkForLatestVersion) + #################################################### + def getWatchingOnlyWallets(self): + result = [] + for wltID in self.walletIDList: + if self.walletMap[wltID].watchingOnly: + result.append(wltID) + return result + #################################################### def factoryReset(self): - reply = QMessageBox.information(self,'Revert all Settings?', \ + """ + reply = QMessageBox.information(self,'Factory Reset', \ 'You are about to revert all Armory settings ' 'to the state they were in when Armory was first installed. ' '

' @@ -640,57 +729,192 @@ def factoryReset(self): QMessageBox.Yes | QMessageBox.No) if reply==QMessageBox.Yes: - self.doHardReset = True + self.removeSettingsOnClose = True self.closeForReal() - + """ + + if DlgFactoryReset(self,self).exec_(): + # The dialog already wrote all the flag files, just close now + self.closeForReal() + + + #################################################### + def showPrivacyGeneric(self): + DlgPrivacyPolicy().exec_() + #################################################### def clearMemoryPool(self): - touchFile( os.path.join(ARMORY_HOME_DIR, 'clearmempool.txt') ) + touchFile( os.path.join(ARMORY_HOME_DIR, 'clearmempool.flag') ) msg = tr(""" The next time you restart Armory, all unconfirmed transactions will be cleared allowing you to retry any stuck transactions.""") - if not self.getSettingOrSetDefault('ManageSatoshi', True): + if not self.doAutoBitcoind: msg += tr(""" -

Make sure you also restart Bitcoin-Qt - (or bitcoind) and let it synchronize again before you restart +

Make sure you also restart Bitcoin-Qt + (or bitcoind) and let it synchronize again before you restart Armory. Doing so will clear its memory pool, as well""") QMessageBox.information(self, tr('Memory Pool'), msg, QMessageBox.Ok) + + + #################################################### + def registerWidgetActivateTime(self, widget): + # This is a bit of a hack, but it's a very isolated method to make + # it easy to link widgets to my entropy accumulator + + # I just realized this doesn't do exactly what I originally intended... + # I wanted it to work on arbitrary widgets like QLineEdits, but using + # super is not the answer. What I want is the original class method + # to be called after logging keypress, not its superclass method. + # Nonetheless, it does do what I need it to, as long as you only + # registered frames and dialogs, not individual widgets/controls. + mainWindow = self + + def newKPE(wself, event=None): + mainWindow.logEntropy() + super(wself.__class__, wself).keyPressEvent(event) + + def newKRE(wself, event=None): + mainWindow.logEntropy() + super(wself.__class__, wself).keyReleaseEvent(event) + + def newMPE(wself, event=None): + mainWindow.logEntropy() + super(wself.__class__, wself).mousePressEvent(event) + + def newMRE(wself, event=None): + mainWindow.logEntropy() + super(wself.__class__, wself).mouseReleaseEvent(event) + + from types import MethodType + widget.keyPressEvent = MethodType(newKPE, widget) + widget.keyReleaseEvent = MethodType(newKRE, widget) + widget.mousePressEvent = MethodType(newMPE, widget) + widget.mouseReleaseEvent = MethodType(newMRE, widget) + + + #################################################### + def logEntropy(self): + try: + self.entropyAccum.append(RightNow()) + self.entropyAccum.append(QCursor.pos().x()) + self.entropyAccum.append(QCursor.pos().y()) + except: + LOGEXCEPT('Error logging keypress entropy') + + #################################################### + def getExtraEntropyForKeyGen(self): + # The entropyAccum var has all the timestamps, down to the microsecond, + # of every keypress and mouseclick made during the wallet creation + # wizard. Also logs mouse positions on every press, though it will + # be constant while typing. Either way, even, if they change no text + # and use a 5-char password, we will still pickup about 40 events. + # Then we throw in the [name,time,size] triplets of some volatile + # system directories, and the hash of a file in that directory that + # is expected to have timestamps and system-dependent parameters. + # Finally, take a desktop screenshot... + # All three of these source are likely to have sufficient entropy alone. + source1,self.entropyAccum = self.entropyAccum,[] + + if len(source1)==0: + LOGERROR('Error getting extra entropy from mouse & key presses') + + source2 = [] + + try: + if OS_WINDOWS: + tempDir = os.getenv('TEMP') + extraFiles = [] + elif OS_LINUX: + tempDir = '/var/log' + extraFiles = ['/var/log/Xorg.0.log'] + elif OS_MACOSX: + tempDir = '/var/log' + extraFiles = ['/var/log/system.log'] + + # A simple listing of the directory files, sizes and times is good + if os.path.exists(tempDir): + for fname in os.listdir(tempDir): + fullpath = os.path.join(tempDir, fname) + sz = os.path.getsize(fullpath) + tm = os.path.getmtime(fullpath) + source2.append([fname, sz, tm]) + + # On Linux we also throw in Xorg.0.log + for f in extraFiles: + if os.path.exists(f): + with open(f,'rb') as infile: + source2.append(hash256(infile.read())) + + if len(source2)==0: + LOGWARN('Second source of supplemental entropy will be empty') + + except: + LOGEXCEPT('Error getting extra entropy from filesystem') + + + source3 = '' + try: + pixDesk = QPixmap.grabWindow(QApplication.desktop().winId()) + pixRaw = QByteArray() + pixBuf = QBuffer(pixRaw) + pixBuf.open(QIODevice.WriteOnly) + pixDesk.save(pixBuf, 'PNG') + source3 = pixBuf.buffer().toHex() + except: + LOGEXCEPT('Third source of entropy (desktop screenshot) failed') + + if len(source3)==0: + LOGWARN('Error getting extra entropy from screenshot') + + LOGINFO('Adding %d keypress events to the entropy pool', len(source1)/3) + LOGINFO('Adding %s bytes of filesystem data to the entropy pool', + bytesToHumanSize(len(str(source2)))) + LOGINFO('Adding %s bytes from desktop screenshot to the entropy pool', + bytesToHumanSize(len(str(source3))/2)) + + + allEntropy = ''.join([str(a) for a in [source1, source1, source3]]) + return SecureBinaryData(HMAC256('Armory Entropy', allEntropy)) + + + + #################################################### def rescanNextLoad(self): reply = QMessageBox.warning(self, tr('Queue Rescan?'), tr(""" The next time you restart Armory, it will rescan the blockchain - database, and reconstruct your wallet histories from scratch. + database, and reconstruct your wallet histories from scratch. The rescan will take 10-60 minutes depending on your system.

Do you wish to force a rescan on the next Armory restart?"""), \ QMessageBox.Yes | QMessageBox.No) if reply==QMessageBox.Yes: - touchFile( os.path.join(ARMORY_HOME_DIR, 'rescan.txt') ) + touchFile( os.path.join(ARMORY_HOME_DIR, 'rescan.flag') ) #################################################### def rebuildNextLoad(self): reply = QMessageBox.warning(self, tr('Queue Rebuild?'), tr(""" - The next time you restart Armory, it will rebuild and rescan - the entire blockchain database. This operation can take between + The next time you restart Armory, it will rebuild and rescan + the entire blockchain database. This operation can take between 30 minutes and 4 hours depending on you system speed.

Do you wish to force a rebuild on the next Armory restart?"""), \ QMessageBox.Yes | QMessageBox.No) if reply==QMessageBox.Yes: - touchFile( os.path.join(ARMORY_HOME_DIR, 'rebuild.txt') ) + touchFile( os.path.join(ARMORY_HOME_DIR, 'rebuild.flag') ) #################################################### def loadFailedManyTimesFunc(self, nFail): """ - For now, if the user is having trouble loading the blockchain, all - we do is delete mempool.bin (which is frequently corrupted but not - detected as such. However, we may expand this in the future, if + For now, if the user is having trouble loading the blockchain, all + we do is delete mempool.bin (which is frequently corrupted but not + detected as such. However, we may expand this in the future, if it's determined that more-complicated things are necessary. """ LOGERROR('%d attempts to load blockchain failed. Remove mempool.bin.' % nFail) mempoolfile = os.path.join(ARMORY_HOME_DIR,'mempool.bin') - if os.path.exists(mempoolfile): + if os.path.exists(mempoolfile): os.remove(mempoolfile) else: LOGERROR('File mempool.bin does not exist. Nothing deleted.') @@ -730,7 +954,7 @@ def changeNumShow(self): else: self.currLedgMax = self.currLedgMin + prefWidth - 1 self.currLedgWidth = prefWidth - + self.applyLedgerRange() @@ -765,7 +989,7 @@ def applyLedgerRange(self): self.btnLedgDn.setVisible(self.currLedgMax!=self.ledgerSize) self.createCombinedLedger() - + #################################################### @@ -812,7 +1036,7 @@ def trayRecv(): ############################################################################# @AllowAsync def registerBitcoinWithFF(self): - #the 3 nodes needed to add to register bitcoin as a protocol in FF + #the 3 nodes needed to add to register bitcoin as a protocol in FF rdfschemehandler = 'about=\"urn:scheme:handler:bitcoin\"' rdfscheme = 'about=\"urn:scheme:bitcoin\"' rdfexternalApp = 'about=\"urn:scheme:externalApplication:bitcoin\"' @@ -820,7 +1044,7 @@ def registerBitcoinWithFF(self): #find mimeTypes.rdf file home = os.getenv('HOME') out,err = execAndWait('find %s -type f -name \"mimeTypes.rdf\"' % home) - + for rdfs in out.split('\n'): if rdfs: try: @@ -839,13 +1063,13 @@ def registerBitcoinWithFF(self): rdfsch=i elif rdfscheme in line: rdfsc=i - elif rdfexternalApp in line: + elif rdfexternalApp in line: rdfea=i i+=1 #seek to end of file FFrdf.seek(-11, 2) - i=0; + i=0; #add the missing nodes if rdfsch == -1: @@ -855,23 +1079,23 @@ def registerBitcoinWithFF(self): FFrdf.write(' \n') FFrdf.write(' \n') i+=1 - + if rdfsc == -1: FFrdf.write(' \n') FFrdf.write(' \n') FFrdf.write(' \n') i+=1 - + if rdfea == -1: FFrdf.write(' \n') + FFrdf.write(' NC:path=\"/usr/bin/xdg-open\" />\n') i+=1 - + if i != 0: FFrdf.write('\n') - + FFrdf.close() ############################################################################# @@ -881,10 +1105,13 @@ def setupUriRegistration(self, justDoIt=False): """ LOGINFO('setupUriRegistration') + if USE_TESTNET: + return + if OS_LINUX: out,err = execAndWait('gconftool-2 --get /desktop/gnome/url-handlers/bitcoin/command') out2,err = execAndWait('xdg-mime query default x-scheme-handler/bitcoin') - + #check FF protocol association #checkFF_thread = threading.Thread(target=self.registerBitcoinWithFF) #checkFF_thread.start() @@ -903,7 +1130,7 @@ def setAsDefault(): setAsDefault() elif (not 'armory' in out.lower() or not 'armory.desktop' in out2.lower()) and not self.firstLoad: # If another application has it, ask for permission to change it - # Don't bother the user on the first load with it if verification is + # Don't bother the user on the first load with it if verification is # needed. They have enough to worry about with this weird new program... if not self.getSettingOrSetDefault('DNAA_DefaultApp', False): reply = MsgBoxWithDNAA(MSGBOX.Question, 'Default URL Handler', \ @@ -920,14 +1147,22 @@ def setAsDefault(): action = 'DoNothing' modulepathname = '"' if getattr(sys, 'frozen', False): - app_dir = os.path.dirname(sys.executable) - app_path = os.path.join(app_dir, sys.executable) + app_dir = os.path.dirname(sys.executable) + app_path = os.path.join(app_dir, sys.executable) elif __file__: - return #running from a .py script, not gonna register URI on Windows + return #running from a .py script, not gonna register URI on Windows - modulepathname += app_path + '" %1' - LOGWARN("running from: %s, key: %s", app_path, modulepathname) - + #justDoIt = True + import ctypes + GetModuleFileNameW = ctypes.windll.kernel32.GetModuleFileNameW + GetModuleFileNameW.restype = ctypes.c_int + app_path = ctypes.create_string_buffer(1024) + rtlength = ctypes.c_int() + rtlength = GetModuleFileNameW(None, ctypes.byref(app_path), 1024) + passstr = str(app_path.raw) + + modulepathname += unicode(passstr[0:(rtlength*2)], encoding='utf16') + u'" "%1"' + modulepathname = modulepathname.encode('utf8') rootKey = 'bitcoin\\shell\\open\\command' try: @@ -968,7 +1203,7 @@ def setAsDefault(): action = 'DoIt' elif action=='AskUser' and not self.firstLoad and not dontAsk: # If another application has it, ask for permission to change it - # Don't bother the user on the first load with it if verification is + # Don't bother the user on the first load with it if verification is # needed. They have enough to worry about with this weird new program... reply = MsgBoxWithDNAA(MSGBOX.Question, 'Default URL Handler', \ 'Armory is not set as your default application for handling ' @@ -984,23 +1219,19 @@ def setAsDefault(): action = 'DoIt' else: LOGINFO('User requested not to use Armory as URI handler') - return + return # Finally, do it if we're supposed to! LOGINFO('URL-register action: %s', action) if action=='DoIt': - + LOGINFO('Registering Armory for current user') - baseDir = app_dir + baseDir = os.path.dirname(unicode(passstr[0:(rtlength*2)], encoding='utf16')) regKeys = [] regKeys.append(['Software\\Classes\\bitcoin', '', 'URL:bitcoin Protocol']) regKeys.append(['Software\\Classes\\bitcoin', 'URL Protocol', ""]) regKeys.append(['Software\\Classes\\bitcoin\\shell', '', None]) regKeys.append(['Software\\Classes\\bitcoin\\shell\\open', '', None]) - regKeys.append(['Software\\Classes\\bitcoin\\shell\\open\\command', '', \ - modulepathname]) - regKeys.append(['Software\\Classes\\bitcoin\\DefaultIcon', '', \ - '"%s\\armory48x48.ico"' % baseDir]) for key,name,val in regKeys: dkey = '%s\\%s' % (key,name) @@ -1009,9 +1240,19 @@ def setAsDefault(): SetValueEx(registryKey, name, 0, REG_SZ, val) CloseKey(registryKey) - LOGWARN('app dir: %s', app_dir) - - + regKeysU = [] + regKeysU.append(['Software\\Classes\\bitcoin\\shell\\open\\command', '', \ + modulepathname]) + regKeysU.append(['Software\\Classes\\bitcoin\\DefaultIcon', '', \ + '"%s\\armory48x48.ico"' % baseDir]) + for key,name,val in regKeysU: + dkey = '%s\\%s' % (key,name) + LOGINFO('\tWriting key: [HKEY_CURRENT_USER\\] ' + dkey) + registryKey = CreateKey(HKEY_CURRENT_USER, key) + #hKey = ctypes.c_int(registryKey.handle) + #ctypes.windll.Advapi32.RegSetValueEx(hKey, None, 0, REG_SZ, val, (len(val)+1)) + SetValueEx(registryKey, name, 0, REG_SZ, val) + CloseKey(registryKey) ############################################################################# def execOfflineTx(self): @@ -1020,28 +1261,9 @@ def execOfflineTx(self): # If we got here, one of three buttons was clicked. if dlgSelect.do_create: - selectWlt = [] - for wltID in self.walletIDList: - if self.walletMap[wltID].watchingOnly: - selectWlt.append(wltID) - dlg = DlgWalletSelect(self, self, 'Wallet for Offline Transaction (watching-only list)', \ - wltIDList=selectWlt) - if not dlg.exec_(): - return - else: - wltID = dlg.selectedID - wlt = self.walletMap[wltID] - dlgSend = DlgSendBitcoins(wlt, self, self) - dlgSend.exec_() - return - - elif dlgSelect.do_review: - dlg = DlgReviewOfflineTx(self,self) - dlg.exec_() - + DlgSendBitcoins(self.getSelectedWallet(), self, self, onlyOfflineWallets=True).exec_() elif dlgSelect.do_broadc: - dlg = DlgReviewOfflineTx(self,self) - dlg.exec_() + DlgSignBroadcastOfflineTx(self,self).exec_() ############################################################################# @@ -1068,13 +1290,13 @@ def execIntroDialog(self): self.writeSetting('DNAA_IntroDialog', True) if dlg.requestCreate: - self.createNewWallet(initLabel='Primary Wallet') + self.startWalletWizard() if dlg.requestImport: self.execImportWallet() - + ############################################################################# def makeWalletCopy(self, parent, wlt, copyType='Same', suffix='', changePass=False): if changePass: @@ -1086,7 +1308,7 @@ def makeWalletCopy(self, parent, wlt, copyType='Same', suffix='', changePass=Fal fn = 'armory_%s_%s.watchonly.wallet' % (wlt.uniqueIDB58, suffix) savePath = unicode(self.getFileSave(defaultFilename=fn)) if not len(savePath)>0: - return + return False if copyType.lower()=='same': wlt.writeFreshWalletFile(savePath) @@ -1094,7 +1316,7 @@ def makeWalletCopy(self, parent, wlt, copyType='Same', suffix='', changePass=Fal if wlt.useEncryption: dlg = DlgUnlockWallet(wlt, parent, self, 'Unlock Private Keys') if not dlg.exec_(): - return + return False # Wallet should now be unlocked wlt.makeUnencryptedWalletCopy(savePath) elif copyType.lower()=='encrypt': @@ -1103,20 +1325,21 @@ def makeWalletCopy(self, parent, wlt, copyType='Same', suffix='', changePass=Fal dlgCrypt = DlgChangePassphrase(parent, self, not wlt.useEncryption) if not dlgCrypt.exec_(): QMessageBox.information(parent, tr('Aborted'), tr(""" - No passphrase was selected for the encrypted backup. + No passphrase was selected for the encrypted backup. No backup was created"""), QMessageBox.Ok) newPassphrase = SecureBinaryData(str(dlgCrypt.edtPasswd1.text())) wlt.makeEncryptedWalletCopy(savePath, newPassphrase) else: LOGERROR('Invalid "copyType" supplied to makeWalletCopy: %s', copyType) - return + return False QMessageBox.information(parent, tr('Backup Complete'), tr(""" - Your wallet was successfully backed up to the following + Your wallet was successfully backed up to the following location:

%s""") % savePath, QMessageBox.Ok) - - + return True + + ############################################################################# def createAction(self, txt, slot, isCheckable=False, \ ttip=None, iconpath=None, shortcut=None): @@ -1127,8 +1350,8 @@ def createAction(self, txt, slot, isCheckable=False, \ if iconpath: icon = QIcon(iconpath) - theAction = QAction(icon, txt, self) - + theAction = QAction(icon, txt, self) + if isCheckable: theAction.setCheckable(True) self.connect(theAction, SIGNAL('toggled(bool)'), slot) @@ -1141,7 +1364,7 @@ def createAction(self, txt, slot, isCheckable=False, \ if shortcut: theAction.setShortcut(shortcut) - + return theAction @@ -1164,7 +1387,7 @@ def setUserMode(self, mode): 'the new usermode to go into effect.', QMessageBox.Ok) self.firstModeSwitch = False - + ############################################################################# @@ -1192,161 +1415,414 @@ def setPreferredDateFormat(self, fmtStr): return True + ############################################################################# - def checkForLatestVersion(self, wasRequested=False): - LOGDEBUG('checkForLatestVersion') - # Download latest versions.txt file, accumulate changelog - if CLI_OPTIONS.skipVerCheck: - return + def setupAnnouncementFetcher(self): + skipChk1 = self.getSettingOrSetDefault('SkipAnnounceCheck', False) + skipChk2 = CLI_OPTIONS.skipAnnounceCheck + skipChk3 = CLI_OPTIONS.offline and not CLI_OPTIONS.testAnnounceCode + self.skipAnnounceCheck = skipChk1 or skipChk2 or skipChk3 + + url1 = ANNOUNCE_URL + url2 = ANNOUNCE_URL_BACKUP + fetchPath = os.path.join(ARMORY_HOME_DIR, 'atisignedannounce') + if self.announceFetcher is None: + self.announceFetcher = AnnounceDataFetcher(url1, url2, fetchPath) + self.announceFetcher.setDisabled(self.skipAnnounceCheck) + self.announceFetcher.start() + + # Set last-updated vals to zero to force processing at startup + for fid in ['changelog, downloads','notify','bootstrap']: + self.lastAnnounceUpdate[fid] = 0 + + # If we recently updated the settings to enable or disable checking... + if not self.announceFetcher.isRunning() and not self.skipAnnounceCheck: + self.announceFetcher.setDisabled(False) + self.announceFetcher.setFetchInterval(DEFAULT_FETCH_INTERVAL) + self.announceFetcher.start() + elif self.announceFetcher.isRunning() and self.skipAnnounceCheck: + self.announceFetcher.setDisabled(True) + self.announceFetcher.shutdown() - optChkVer = self.getSettingOrSetDefault('CheckVersion', 'Always') - if optChkVer.lower()=='never' and not wasRequested: - LOGINFO('User requested never check for new versions') - return - if wasRequested and not self.internetAvail: - QMessageBox.critical(self, 'Offline Mode', \ - 'You are in offline mode, which means that version information ' - 'cannot be retrieved from the internet. Please visit ' - 'www.bitcoinarmory.com from an internet-connected computer ' - 'to get the latest version information.', QMessageBox.Ok) - return - versionFile = None - try: - import urllib2 - import socket - socket.setdefaulttimeout(CLI_OPTIONS.nettimeout) - versionLines = urllib2.urlopen(HTTP_VERSION_FILE, timeout=CLI_OPTIONS.nettimeout) - versionLines = versionLines.readlines() - except ImportError: - LOGERROR('No module urllib2 -- cannot get latest version') - return - except (urllib2.URLError, urllib2.HTTPError): - if wasRequested: - QMessageBox.critical(self, 'Unavailable', \ - 'The latest Armory version information could not be retrieved.' - 'Please check www.bitcoinarmory.com for the latest version ' - 'information.', QMessageBox.Ok) - LOGERROR('Could not access latest Armory version information') - LOGERROR('Tried: %s', HTTP_VERSION_FILE) - return - + ############################################################################# + def processAnnounceData(self, forceCheck=False, forceWait=5): + + adf = self.announceFetcher + + + + # The ADF always fetches everything all the time. If forced, do the + # regular fetch first, then examine the individual files without forcing + if forceCheck: + adf.fetchRightNow(forceWait) + + # Check each of the individual files for recent modifications + idFuncPairs = [ + ['announce', self.updateAnnounceTab], + ['changelog', self.processChangelog], + ['downloads', self.processDownloads], + ['notify', self.processNotifications], + ['bootstrap', self.processBootstrap] ] + + # If modified recently + for fid,func in idFuncPairs: + if not fid in self.lastAnnounceUpdate or \ + adf.getFileModTime(fid) > self.lastAnnounceUpdate[fid]: + self.lastAnnounceUpdate[fid] = RightNow() + fileText = adf.getAnnounceFile(fid) + func(fileText) - skipVerify = False - #LOGERROR('**********************************TESTING CODE: REMOVE ME') - #versionLines = open('versions.txt','r').readlines() - #skipVerify = True - #LOGERROR('**********************************TESTING CODE: REMOVE ME') + + + ############################################################################# + def processChangelog(self, txt): try: - currLineIdx = [0] + clp = changelogParser() + self.changelog = clp.parseChangelogText(txt) + except: + # Don't crash on an error, but do log what happened + LOGEXCEPT('Failed to parse changelog data') - def popNextLine(currIdx): - if currIdx[0] < len(versionLines): - outstr = versionLines[ currIdx[0] ] - currIdx[0] += 1 - return outstr.strip() - else: - return None - - thisVerString = getVersionString(BTCARMORY_VERSION) - changeLog = [] - vernum = '' - - line = popNextLine(currLineIdx) - comments = '' - while line != None: - if not line.startswith('#') and len(line)>0: - if line.startswith('VERSION'): - vstr = line.split(' ')[-1] - myVersionInt = getVersionInt(readVersionString(thisVerString)) - latestVerInt = getVersionInt(readVersionString(vstr)) - if myVersionInt>=latestVerInt and not wasRequested: - break - changeLog.append([vstr, []]) - elif line.startswith('-'): - featureTitle = line[2:] - changeLog[-1][1].append([featureTitle, []]) - else: - changeLog[-1][1][-1][1].append(line) - if line.startswith('#'): - comments += line+'\n' - line = popNextLine(currLineIdx) - - # We also store the list of latest - self.latestVer = {} - self.downloadDict = {} - try: - msg = extractSignedDataFromVersionsDotTxt(comments, doVerify=(not skipVerify)) - if len(msg)>0: - dldict,verstrs = parseLinkList(msg) - self.downloadDict = dldict.copy() - self.latestVer = verstrs.copy() - if not TheBDM.getBDMState()=='BlockchainReady': - # Don't dump all this info to the log all the time - LOGINFO('Latest versions:') - LOGINFO(' Satoshi: %s', self.latestVer['SATOSHI']) - LOGINFO(' Armory: %s', self.latestVer['ARMORY']) - else: - raise ECDSA_Error, 'Could not verify' - except: - LOGEXCEPT('Version check error, ignoring downloaded version info') - - - if len(changeLog)==0 and not wasRequested: - LOGINFO('You are running the latest version!') - elif optChkVer[1:]==changeLog[0][0] and not wasRequested: - LOGINFO('Latest version is %s -- Notify user on next version.', optChkVer) + ############################################################################# + def processDownloads(self, txt): + try: + dlp = downloadLinkParser() + self.downloadLinks = dlp.parseDownloadList(txt) + + if self.downloadLinks is None: return + + thisVer = getVersionInt(BTCARMORY_VERSION) + + # Check ARMORY versions + if not 'Armory' in self.downloadLinks: + LOGWARN('No Armory links in the downloads list') + else: + maxVer = 0 + self.versionNotification = {} + for verStr,vermap in self.downloadLinks['Armory'].iteritems(): + dlVer = getVersionInt(readVersionString(verStr)) + if dlVer > maxVer: + maxVer = dlVer + self.armoryVersions[1] = verStr + if thisVer >= maxVer: + continue + + shortDescr = tr('Armory version %s is now available!') % verStr + notifyID = binary_to_hex(hash256(shortDescr)[:4]) + self.versionNotification['UNIQUEID'] = notifyID + self.versionNotification['VERSION'] = '0' + self.versionNotification['STARTTIME'] = '0' + self.versionNotification['EXPIRES'] = '%d' % long(UINT64_MAX) + self.versionNotification['CANCELID'] = '[]' + self.versionNotification['MINVERSION'] = '*' + self.versionNotification['MAXVERSION'] = '<%s' % verStr + self.versionNotification['PRIORITY'] = '3072' + self.versionNotification['ALERTTYPE'] = 'Upgrade' + self.versionNotification['NOTIFYSEND'] = 'False' + self.versionNotification['NOTIFYRECV'] = 'False' + self.versionNotification['SHORTDESCR'] = shortDescr + self.versionNotification['LONGDESCR'] = \ + self.getVersionNotifyLongDescr(verStr).replace('\n','
') + + if 'ArmoryTesting' in self.downloadLinks: + for verStr,vermap in self.downloadLinks['ArmoryTesting'].iteritems(): + dlVer = getVersionInt(readVersionString(verStr)) + if dlVer > maxVer: + maxVer = dlVer + self.armoryVersions[1] = verStr + if thisVer >= maxVer: + continue + + shortDescr = tr('Armory Testing version %s is now available!') % verStr + notifyID = binary_to_hex(hash256(shortDescr)[:4]) + self.versionNotification['UNIQUEID'] = notifyID + self.versionNotification['VERSION'] = '0' + self.versionNotification['STARTTIME'] = '0' + self.versionNotification['EXPIRES'] = '%d' % long(UINT64_MAX) + self.versionNotification['CANCELID'] = '[]' + self.versionNotification['MINVERSION'] = '*' + self.versionNotification['MAXVERSION'] = '<%s' % verStr + self.versionNotification['PRIORITY'] = '1024' + self.versionNotification['ALERTTYPE'] = 'upgrade-testing' + self.versionNotification['NOTIFYSEND'] = 'False' + self.versionNotification['NOTIFYRECV'] = 'False' + self.versionNotification['SHORTDESCR'] = shortDescr + self.versionNotification['LONGDESCR'] = \ + self.getVersionNotifyLongDescr(verStr, True).replace('\n','
') + + + # For Satoshi updates, we don't trigger any notifications like we + # do for Armory above -- we will release a proper announcement if + # necessary. But we want to set a flag to + if not 'Satoshi' in self.downloadLinks: + LOGWARN('No Satoshi links in the downloads list') else: - DlgVersionNotify(self,self, changeLog, wasRequested).exec_() + try: + maxVer = 0 + for verStr,vermap in self.downloadLinks['Satoshi'].iteritems(): + dlVer = getVersionInt(readVersionString(verStr)) + if dlVer > maxVer: + maxVer = dlVer + self.satoshiVersions[1] = verStr + + if not self.NetworkingFactory: + return + + # This is to detect the running versions of Bitcoin-Qt/bitcoind + thisVerStr = self.NetworkingFactory.proto.peerInfo['subver'] + thisVerStr = thisVerStr.strip('/').split(':')[-1] + + if sum([0 if c in '0123456789.' else 1 for c in thisVerStr]) > 0: + return + + self.satoshiVersions[0] = thisVerStr + + except: + pass + + + + except: - if wasRequested: - QMessageBox.critical(self, 'Parse Error', \ - 'The version information is malformed and cannot be understood. ' - 'Please check www.bitcoinarmory.com for the latest version ' - 'information.', QMessageBox.Ok) - LOGEXCEPT('Error trying to parse versions.txt file') - + # Don't crash on an error, but do log what happened + LOGEXCEPT('Failed to parse download link data') + ############################################################################# - def setupNetworking(self): - LOGINFO('Setting up networking...') - TimerStart('setupNetworking') - self.internetAvail = False + def getVersionNotifyLongDescr(self, verStr, testing=False): + shortOS = None + if OS_WINDOWS: + shortOS = 'windows' + elif OS_LINUX: + shortOS = 'ubuntu' + elif OS_MACOSX: + shortOS = 'mac' - # Prevent Armory from being opened twice - from twisted.internet import reactor - import twisted - def uriClick_partial(a): - self.uriLinkClicked(a) + webURL = 'https://bitcoinarmory.com/download/' + if shortOS is not None: + webURL += '#' + shortOS - if CLI_OPTIONS.interport > 1: - try: - self.InstanceListener = ArmoryListenerFactory(self.bringArmoryToFront, \ - uriClick_partial ) - reactor.listenTCP(CLI_OPTIONS.interport, self.InstanceListener) - except twisted.internet.error.CannotListenError: - LOGWARN('Socket already occupied! This must be a duplicate Armory instance!') - QMessageBox.warning(self, 'Only One, Please!', \ - 'Armory is already running! You can only have one instance open ' - 'at a time. Aborting...', QMessageBox.Ok) - os._exit(0) - else: - LOGWARN('*** Listening port is disabled. URI-handling will not work') - + if testing: + return tr(""" + A new testing version of Armory is out. You can upgrade to version + %s through our secure downloader inside Armory (link at the bottom + of this notification window). + """) % (verStr) + + return tr(""" + Your version of Armory is now outdated. Please upgrade to version + %s through our secure downloader inside Armory (link at the bottom + of this notification window). Alternatively, you can get the new + version from our website downloads page at: +

+ %s """) % (verStr, webURL, webURL) - settingSkipCheck = self.getSettingOrSetDefault('SkipOnlineCheck', False) - self.forceOnline = CLI_OPTIONS.forceOnline or settingSkipCheck - if self.forceOnline: - LOGINFO('Forced online mode: True') - # Check general internet connection - self.internetAvail = False - if not self.forceOnline: + + ############################################################################# + def processBootstrap(self, binFile): + # Nothing to process, actually. We'll grab the bootstrap from its + # current location, if needed + pass + + + + ############################################################################# + def notificationIsRelevant(self, notifyID, notifyMap): + currTime = RightNow() + thisVerInt = getVersionInt(BTCARMORY_VERSION) + + # Ignore transactions below the requested priority + minPriority = self.getSettingOrSetDefault('NotifyMinPriority', 2048) + if int(notifyMap['PRIORITY']) < minPriority: + return False + + # Ignore version upgrade notifications if disabled in the settings + if 'upgrade' in notifyMap['ALERTTYPE'].lower() and \ + self.getSettingOrSetDefault('DisableUpgradeNotify', False): + return False + + if notifyID in self.notifyIgnoreShort: + return False + + if notifyMap['STARTTIME'].isdigit(): + if currTime < long(notifyMap['STARTTIME']): + return False + + if notifyMap['EXPIRES'].isdigit(): + if currTime > long(notifyMap['EXPIRES']): + return False + + + try: + minVerStr = notifyMap['MINVERSION'] + minExclude = minVerStr.startswith('>') + minVerStr = minVerStr[1:] if minExclude else minVerStr + minVerInt = getVersionInt(readVersionString(minVerStr)) + minVerInt += 1 if minExclude else 0 + if thisVerInt < minVerInt: + return False + except: + pass + + + try: + maxVerStr = notifyMap['MAXVERSION'] + maxExclude = maxVerStr.startswith('<') + maxVerStr = maxVerStr[1:] if maxExclude else maxVerStr + maxVerInt = getVersionInt(readVersionString(maxVerStr)) + maxVerInt -= 1 if maxExclude else 0 + if thisVerInt > maxVerInt: + return False + except: + pass + + return True + + + ############################################################################# + def processNotifications(self, txt): + + # Keep in mind this will always be run on startup with a blank slate, as + # well as every 30 min while Armory is running. All notifications are + # "new" on startup (though we will allow the user to do-not-show-again + # and store the notification ID in the settings file). + try: + np = notificationParser() + currNotificationList = np.parseNotificationText(txt) + except: + # Don't crash on an error, but do log what happened + LOGEXCEPT('Failed to parse notifications') + + if currNotificationList is None: + currNotificationList = {} + + # If we have a new-version notification, it's not ignroed, and such + # notifications are not disabled, add it to the list + vnotify = self.versionNotification + if vnotify and 'UNIQUEID' in vnotify: + currNotificationList[vnotify['UNIQUEID']] = deepcopy(vnotify) + + # Create a copy of almost all the notifications we have. + # All notifications >= 2048, unless they've explictly allowed testing + # notifications. This will be shown on the "Announcements" tab. + self.almostFullNotificationList = {} + currMin = self.getSettingOrSetDefault('NotifyMinPriority', \ + DEFAULT_MIN_PRIORITY) + minmin = min(currMin, DEFAULT_MIN_PRIORITY) + for nid,valmap in currNotificationList.iteritems(): + if int(valmap['PRIORITY']) >= minmin: + self.almostFullNotificationList[nid] = deepcopy(valmap) + + + tabPriority = 0 + self.maxPriorityID = None + + # Check for new notifications + addedNotifyIDs = set() + irrelevantIDs = set() + for nid,valmap in currNotificationList.iteritems(): + if not self.notificationIsRelevant(nid, valmap): + # Can't remove while iterating over the map + irrelevantIDs.add(nid) + self.notifyIgnoreShort.add(nid) + continue + + if valmap['PRIORITY'].isdigit(): + if int(valmap['PRIORITY']) > tabPriority: + tabPriority = int(valmap['PRIORITY']) + self.maxPriorityID = nid + + if not nid in self.almostFullNotificationList: + addedNotifyIDs.append(nid) + + # Now remove them from the set that we are working with + for nid in irrelevantIDs: + del currNotificationList[nid] + + # Check for notifications we had before but no long have + removedNotifyIDs = [] + for nid,valmap in self.almostFullNotificationList.iteritems(): + if not nid in currNotificationList: + removedNotifyIDs.append(nid) + + + #for nid in removedNotifyIDs: + #self.notifyIgnoreShort.discard(nid) + #self.notifyIgnoreLong.discard(nid) + + + + # Change the "Announcements" tab color if something important is there + tabWidgetBar = self.mainDisplayTabs.tabBar() + tabColor = Colors.Foreground + if tabPriority >= 5120: + tabColor = Colors.TextRed + elif tabPriority >= 4096: + tabColor = Colors.TextRed + elif tabPriority >= 3072: + tabColor = Colors.TextBlue + elif tabPriority >= 2048: + tabColor = Colors.TextBlue + + tabWidgetBar.setTabTextColor(self.MAINTABS.Announce, tabColor) + self.updateAnnounceTab() + + # We only do popups for notifications >=4096, AND upgrade notify + if tabPriority >= 3072: + DlgNotificationWithDNAA(self, self, self.maxPriorityID, \ + currNotificationList[self.maxPriorityID]).show() + elif vnotify: + if not vnotify['UNIQUEID'] in self.notifyIgnoreShort: + DlgNotificationWithDNAA(self,self,vnotify['UNIQUEID'],vnotify).show() + + + + + + + + ############################################################################# + @TimeThisFunction + def setupNetworking(self): + LOGINFO('Setting up networking...') + self.internetAvail = False + + # Prevent Armory from being opened twice + from twisted.internet import reactor + import twisted + def uriClick_partial(a): + self.uriLinkClicked(a) + + if CLI_OPTIONS.interport > 1: + try: + self.InstanceListener = ArmoryListenerFactory(self.bringArmoryToFront, \ + uriClick_partial ) + reactor.listenTCP(CLI_OPTIONS.interport, self.InstanceListener) + except twisted.internet.error.CannotListenError: + LOGWARN('Socket already occupied! This must be a duplicate Armory') + QMessageBox.warning(self, tr('Already Open'), tr(""" + Armory is already running! You can only have one Armory open + at a time. Exiting..."""), QMessageBox.Ok) + os._exit(0) + else: + LOGWARN('*** Listening port is disabled. URI-handling will not work') + + + settingSkipCheck = self.getSettingOrSetDefault('SkipOnlineCheck', False) + self.forceOnline = CLI_OPTIONS.forceOnline or settingSkipCheck + if self.forceOnline: + LOGINFO('Forced online mode: True') + + # Check general internet connection + self.internetAvail = False + if not self.forceOnline: try: import urllib2 response=urllib2.urlopen('http://google.com', timeout=CLI_OPTIONS.nettimeout) @@ -1365,7 +1841,7 @@ def uriClick_partial(a): LOGEXCEPT('Error checking for internet connection') LOGERROR('Run --skip-online-check if you think this is an error') self.internetAvail = False - + LOGINFO('Internet connection is Available: %s', self.internetAvail) LOGINFO('Bitcoin-Qt/bitcoind is Available: %s', self.bitcoindIsAvailable()) @@ -1373,7 +1849,119 @@ def uriClick_partial(a): LOGINFO('Online mode currently possible: %s', self.onlineModeIsPossible()) - TimerStop('setupNetworking') + + + + ############################################################################# + def manageBitcoindAskTorrent(self): + + if not satoshiIsAvailable(): + reply = MsgBoxCustom(MSGBOX.Question, tr('BitTorrent Option'), tr(""" + You are currently configured to run the core Bitcoin software + yourself (Bitcoin-Qt or bitcoind). Normally, you should + start the Bitcoin software first and wait for it to synchronize + with the network before starting Armory. +

+ However, Armory can shortcut most of this initial + synchronization + for you using BitTorrent. If your firewall allows it, + using BitTorrent can be an order of magnitude faster (2x to 20x) + than letting the Bitcoin software download it via P2P. +

+ To synchronize using BitTorrent (recommended): + Click "Use BitTorrent" below, and do not start the Bitcoin + software until after it is complete. +

+ To synchronize using Bitcoin P2P (fallback): + Click "Cancel" below, then close Armory and start Bitcoin-Qt + (or bitcoind). Do not start Armory until you see a green checkmark + in the bottom-right corner of the Bitcoin-Qt window."""), \ + wCancel=True, yesStr='Use BitTorrent') + + if not reply: + QMessageBox.warning(self, tr('Synchronize'), tr(""" + When you are ready to start synchronization, close Armory and + start Bitcoin-Qt or bitcoind. Restart Armory only when + synchronization is complete. If using Bitcoin-Qt, you will see + a green checkmark in the bottom-right corner"""), QMessageBox.Ok) + return False + + else: + reply = MsgBoxCustom(MSGBOX.Question, tr('BitTorrent Option'), tr(""" + You are currently running the core Bitcoin software, but it + is not fully synchronized with the network, yet. Normally, + you should close Armory until Bitcoin-Qt (or bitcoind) is + finished +

+ However, Armory can speed up this initial + synchronization for you using BitTorrent. If your firewall + allows it, using BitTorrent can be an order of magnitude + faster (2x to 20x) + than letting the Bitcoin software download it via P2P. +

+ To synchronize using BitTorrent (recommended): + Close the running Bitcoin software right now. When it is + closed, click "Use BitTorrent" below. Restart the Bitcoin software + when Armory indicates it is complete. +

+ To synchronize using Bitcoin P2P (fallback): + Click "Cancel" below, and then close Armory until the Bitcoin + software is finished synchronizing. If using Bitcoin-Qt, you + will see a green checkmark in the bottom-right corner of the + main window."""), QMessageBox.Ok) + + if reply: + if satoshiIsAvailable(): + QMessageBox.warning(self, tr('Still Running'), tr(""" + The Bitcoin software still appears to be open! + Close it right now + before clicking "Ok." The BitTorrent engine will start + as soon as you do."""), QMessageBox.Ok) + else: + QMessageBox.warning(self, tr('Synchronize'), tr(""" + You chose to finish synchronizing with the network using + the Bitcoin software which is already running. Please close + Armory until it is finished. If you are running Bitcoin-Qt, + you will see a green checkmark in the bottom-right corner, + when it is time to open Armory again."""), QMessageBox.Ok) + return False + + return True + + + ############################################################################ + def findTorrentFileForSDM(self, forceWaitTime=0): + """ + Hopefully the announcement fetcher has already gotten one for us, + or at least we have a default. + """ + + # Only do an explicit announce check if we have no bootstrap at all + # (don't need to spend time doing an explicit check if we have one) + if self.announceFetcher.getFileModTime('bootstrap') == 0: + if forceWaitTime>0: + self.explicitCheckAnnouncements(forceWaitTime) + + # If it's still not there, look for a default file + if self.announceFetcher.getFileModTime('bootstrap') == 0: + LOGERROR('Could not get announce bootstrap; using default') + srcTorrent = os.path.join(GetExecDir(), '../default_bootstrap.torrent') + else: + srcTorrent = self.announceFetcher.getAnnounceFilePath('bootstrap') + + # Maybe we still don't have a torrent for some reason + if not srcTorrent or not os.path.exists(srcTorrent): + return '' + + torrentPath = os.path.join(ARMORY_HOME_DIR, 'bootstrap.dat.torrent') + LOGINFO('Using torrent file: ' + torrentPath) + shutil.copy(srcTorrent, torrentPath) + + return torrentPath + + + + ############################################################################ def startBitcoindIfNecessary(self): @@ -1382,29 +1970,39 @@ def startBitcoindIfNecessary(self): LOGWARN('Not online, will not start bitcoind') return False - if not self.doManageSatoshi: + if not self.doAutoBitcoind: LOGWARN('Tried to start bitcoind, but ManageSatoshi==False') return False if satoshiIsAvailable(): LOGWARN('Tried to start bitcoind, but satoshi already running') return False - - self.setSatoshiPaths() + self.setSatoshiPaths() TheSDM.setDisabled(False) + + torrentIsDisabled = self.getSettingOrSetDefault('DisableTorrent', False) + + # Give the SDM the torrent file...it will use it if it makes sense + if not torrentIsDisabled and TheSDM.shouldTryBootstrapTorrent(): + torrentFile = self.findTorrentFileForSDM(2) + if not torrentFile or not os.path.exists(torrentFile): + LOGERROR('Could not find torrent file') + else: + TheSDM.tryToSetupTorrentDL(torrentFile) + + try: # "satexe" is actually just the install directory, not the direct # path the executable. That dir tree will be searched for bitcoind - TheSDM.setupSDM(None, self.satoshiHomePath, \ - extraExeSearch=self.satoshiExeSearchPath) + TheSDM.setupSDM(extraExeSearch=self.satoshiExeSearchPath) TheSDM.startBitcoind() LOGDEBUG('Bitcoind started without error') return True except: LOGEXCEPT('Failed to setup SDM') self.switchNetworkMode(NETWORKMODE.Offline) - + ############################################################################ def setSatoshiPaths(self): @@ -1419,16 +2017,20 @@ def setSatoshiPaths(self): else: self.satoshiExeSearchPath = [] - + self.satoshiHomePath = BTC_HOME_DIR if self.settings.hasSetting('SatoshiDatadir') and \ CLI_OPTIONS.satoshiHome=='DEFAULT': # Setting override BTC_HOME_DIR only if it wasn't explicitly - # set as the command line. + # set as the command line. self.satoshiHomePath = self.settings.get('SatoshiDatadir') - + LOGINFO('Setting satoshi datadir = %s' % self.satoshiHomePath) + TheBDM.setSatoshiDir(self.satoshiHomePath) - + TheSDM.setSatoshiDir(self.satoshiHomePath) + TheTDM.setSatoshiDir(self.satoshiHomePath) + + ############################################################################ def loadBlockchainIfNecessary(self): LOGINFO('loadBlockchainIfNecessary') @@ -1453,7 +2055,7 @@ def loadBlockchainIfNecessary(self): else: self.switchNetworkMode(NETWORKMODE.Offline) TheBDM.setOnlineMode(False, wait=False) - + @@ -1473,7 +2075,7 @@ def onlineModeIsPossible(self): def bitcoindIsAvailable(self): return satoshiIsAvailable('127.0.0.1', BITCOIN_PORT) - + ############################################################################# def switchNetworkMode(self, newMode): @@ -1483,7 +2085,7 @@ def switchNetworkMode(self, newMode): self.NetworkingFactory = FakeClientFactory() return elif newMode==NETWORKMODE.Full: - + # Actually setup the networking, now from twisted.internet import reactor @@ -1493,8 +2095,8 @@ def showOfflineMsg(): self.lblArmoryStatus.setText( \ 'Disconnected' % htmlColor('TextWarn')) if not self.getSettingOrSetDefault('NotifyDiscon', not OS_MACOSX): - return - + return + try: self.sysTray.showMessage('Disconnected', \ 'Connection to Bitcoin-Qt client lost! Armory cannot send \n' @@ -1509,11 +2111,11 @@ def showOnlineMsg(): self.netMode = NETWORKMODE.Full self.setDashboardDetails() self.lblArmoryStatus.setText(\ - 'Connected (%s blocks) ' % + 'Connected (%s blocks) ' % (htmlColor('TextGreen'), self.currBlockNum)) if not self.getSettingOrSetDefault('NotifyReconn', not OS_MACOSX): return - + try: if self.connectCount>0: self.sysTray.showMessage('Connected', \ @@ -1522,9 +2124,10 @@ def showOnlineMsg(): self.connectCount += 1 except: LOGEXCEPT('Failed to show reconnect notification') - - + + self.NetworkingFactory = ArmoryClientFactory( \ + TheBDM, func_loseConnect=showOfflineMsg, \ func_madeConnect=showOnlineMsg, \ func_newTx=self.newTxFunc) @@ -1532,7 +2135,7 @@ def showOnlineMsg(): reactor.callWhenRunning(reactor.connectTCP, '127.0.0.1', \ BITCOIN_PORT, self.NetworkingFactory) - + ############################################################################# @@ -1556,7 +2159,7 @@ def parseUriLink(self, uriStr, clickOrEnter='click'): uriDict = parseBitcoinURI(uriStr) if TheBDM.getBDMState() in ('Offline','Uninitialized'): LOGERROR('%sed "bitcoin:" link in offline mode.' % ClickOrEnter) - self.bringArmoryToFront() + self.bringArmoryToFront() QMessageBox.warning(self, 'Offline Mode', 'You %sed on a "bitcoin:" link, but Armory is in ' 'offline mode, and is not capable of creating transactions. ' @@ -1564,7 +2167,7 @@ def parseUriLink(self, uriStr, clickOrEnter='click'): 'to the Bitcoin network!' % (clickOrEnter, ClickOrEnter), \ QMessageBox.Ok) return {} - + if len(uriDict)==0: warnMsg = ('It looks like you just %sed a "bitcoin:" link, but ' 'that link is malformed. ' % clickOrEnter) @@ -1619,25 +2222,34 @@ def parseUriLink(self, uriStr, clickOrEnter='click'): ############################################################################# def uriLinkClicked(self, uriStr): LOGINFO('uriLinkClicked') - if not TheBDM.getBDMState()=='BlockchainReady': + if TheBDM.getBDMState()=='Offline': QMessageBox.warning(self, 'Offline', \ - 'You just clicked on a "bitcoin:" link, but Armory is offline ' + 'You just clicked on a "bitcoin:" link, but Armory is offline ' 'and cannot send transactions. Please click the link ' 'again when Armory is online.', \ QMessageBox.Ok) return + elif not TheBDM.getBDMState()=='BlockchainReady': + # BDM isnt ready yet, saved URI strings in the delayed URIDict to + # call later through finishLoadBlockChain + qLen = self.delayedURIData['qLen'] + + self.delayedURIData[qLen] = uriStr + qLen = qLen +1 + self.delayedURIData['qLen'] = qLen + return uriDict = self.parseUriLink(uriStr, 'click') - + if len(uriDict)>0: - self.bringArmoryToFront() + self.bringArmoryToFront() return self.uriSendBitcoins(uriDict) - + ############################################################################# + @TimeThisFunction def loadWalletsAndSettings(self): LOGINFO('loadWalletsAndSettings') - TimerStart('loadWltSettings') self.getSettingOrSetDefault('First_Load', True) self.getSettingOrSetDefault('Load_Count', 0) @@ -1658,7 +2270,7 @@ def loadWalletsAndSettings(self): self.writeSetting('Load_Count', (self.settings.get('Load_Count')+1) % 100) firstDate = self.getSettingOrSetDefault('First_Load_Date', RightNow()) daysSinceFirst = (RightNow() - firstDate) / (60*60*24) - + # Set the usermode, default to standard self.usermode = USERMODE.Standard @@ -1667,14 +2279,26 @@ def loadWalletsAndSettings(self): elif self.settings.get('User_Mode') == 'Expert': self.usermode = USERMODE.Expert + + # The user may have asked to never be notified of a particular + # notification again. We have a short-term list (wiped on every + # load), and a long-term list (saved in settings). We simply + # initialize the short-term list with the long-term list, and add + # short-term ignore requests to it + notifyStr = self.getSettingOrSetDefault('NotifyIgnore', '') + nsz = len(notifyStr) + self.notifyIgnoreLong = set(notifyStr[8*i:8*(i+1)] for i in range(nsz/8)) + self.notifyIgnoreShort = set(notifyStr[8*i:8*(i+1)] for i in range(nsz/8)) + + # Load wallets found in the .armory directory - wltPaths = self.settings.get('Other_Wallets', expectList=True) + wltPaths = [] self.walletMap = {} - self.walletIndices = {} + self.walletIndices = {} self.walletIDSet = set() # I need some linear lists for accessing by index - self.walletIDList = [] + self.walletIDList = [] self.combinedLedger = [] self.ledgerSize = 0 self.ledgerTable = [] @@ -1688,7 +2312,7 @@ def loadWalletsAndSettings(self): fullPath = os.path.join(ARMORY_HOME_DIR, f) if os.path.isfile(fullPath) and not fullPath.endswith('backup.wallet'): openfile = open(fullPath, 'rb') - first8 = openfile.read(8) + first8 = openfile.read(8) openfile.close() if first8=='\xbaWALLET\x00': wltPaths.append(fullPath) @@ -1725,12 +2349,13 @@ def loadWalletsAndSettings(self): # Maintain some linear lists of wallet info self.walletIDSet.add(wltID) self.walletIDList.append(wltID) + wltLoad.mainWnd = self except: LOGEXCEPT( '***WARNING: Wallet could not be loaded: %s (skipping)', fpath) raise - - + + LOGINFO('Number of wallets read in: %d', len(self.walletMap)) for wltID, wlt in self.walletMap.iteritems(): dispStr = (' Wallet (%s):' % wlt.uniqueIDB58).ljust(25) @@ -1739,6 +2364,7 @@ def loadWalletsAndSettings(self): LOGINFO(dispStr) # Register all wallets with TheBDM TheBDM.registerWallet( wlt.cppWallet ) + TheBDM.bdm.registerWallet(wlt.cppWallet) # Get the last directory @@ -1748,7 +2374,6 @@ def loadWalletsAndSettings(self): self.lastDirectory = savedDir self.writeSetting('LastDirectory', savedDir) - TimerStop('loadWltSettings') ############################################################################# def getFileSave(self, title='Save Wallet File', \ @@ -1761,49 +2386,55 @@ def getFileSave(self, title='Save Wallet File', \ if not defaultFilename==None: startPath = os.path.join(startPath, defaultFilename) - + types = ffilter types.append('All files (*)') typesStr = ';; '.join(types) # Found a bug with Swig+Threading+PyQt+OSX -- save/load file dialogs freeze - # User picobit discovered this is avoided if you use the Qt dialogs, instead + # User picobit discovered this is avoided if you use the Qt dialogs, instead # of the native OS dialogs. Use native for all except OSX... if not OS_MACOSX: fullPath = unicode(QFileDialog.getSaveFileName(self, title, startPath, typesStr)) else: fullPath = unicode(QFileDialog.getSaveFileName(self, title, startPath, typesStr, options=QFileDialog.DontUseNativeDialog)) - + fdir,fname = os.path.split(fullPath) if fdir: self.writeSetting('LastDirectory', fdir) return fullPath - + ############################################################################# - def getFileLoad(self, title='Load Wallet File', ffilter=['Wallet files (*.wallet)']): + def getFileLoad(self, title='Load Wallet File', \ + ffilter=['Wallet files (*.wallet)'], \ + defaultDir=None): + LOGDEBUG('getFileLoad') - lastDir = self.settings.get('LastDirectory') - if len(lastDir)==0 or not os.path.exists(lastDir): - lastDir = ARMORY_HOME_DIR + + if defaultDir is None: + defaultDir = self.settings.get('LastDirectory') + if len(defaultDir)==0 or not os.path.exists(defaultDir): + defaultDir = ARMORY_HOME_DIR + types = list(ffilter) types.append(tr('All files (*)')) typesStr = ';; '.join(types) # Found a bug with Swig+Threading+PyQt+OSX -- save/load file dialogs freeze - # User picobit discovered this is avoided if you use the Qt dialogs, instead + # User picobit discovered this is avoided if you use the Qt dialogs, instead # of the native OS dialogs. Use native for all except OSX... if not OS_MACOSX: - fullPath = unicode(QFileDialog.getOpenFileName(self, title, lastDir, typesStr)) + fullPath = unicode(QFileDialog.getOpenFileName(self, title, defaultDir, typesStr)) else: - fullPath = unicode(QFileDialog.getOpenFileName(self, title, lastDir, typesStr, \ + fullPath = unicode(QFileDialog.getOpenFileName(self, title, defaultDir, typesStr, \ options=QFileDialog.DontUseNativeDialog)) self.writeSetting('LastDirectory', os.path.split(fullPath)[0]) return fullPath - + ############################################################################## def getWltSetting(self, wltID, propName): # Sometimes we need to settings specific to individual wallets -- we will @@ -1827,8 +2458,8 @@ def toggleIsMine(self, wltID): self.setWltSetting(wltID, 'IsMine', False) else: self.setWltSetting(wltID, 'IsMine', True) - - + + ############################################################################# @@ -1852,7 +2483,7 @@ def writeSetting(self, settingName, val): def startRescanBlockchain(self, forceFullScan=False): if TheBDM.getBDMState() in ('Offline','Uninitialized'): LOGWARN('Rescan requested but Armory is in offline mode') - return + return if TheBDM.getBDMState()=='Scanning': LOGINFO('Queueing rescan after current scan completes.') @@ -1868,6 +2499,8 @@ def startRescanBlockchain(self, forceFullScan=False): ############################################################################# def forceRescanDB(self): self.needUpdateAfterScan = True + self.lblDashModeBuild.setText( 'Build Databases', \ + size=4, bold=True, color='DisableFG') self.lblDashModeScan.setText( 'Scanning Transaction History', \ size=4, bold=True, color='Foreground') TheBDM.rescanBlockchain('ForceRescan', wait=False) @@ -1876,8 +2509,10 @@ def forceRescanDB(self): ############################################################################# def forceRebuildAndRescan(self): self.needUpdateAfterScan = True - self.lblDashModeScan.setText( 'Preparing Databases', \ + self.lblDashModeBuild.setText( 'Preparing Databases', \ size=4, bold=True, color='Foreground') + self.lblDashModeScan.setText( 'Scan Transaction History', \ + size=4, bold=True, color='DisableFG') #self.resetBdmBeforeScan() # this resets BDM and then re-registeres wlts TheBDM.rescanBlockchain('ForceRebuild', wait=False) self.setDashboardDetails() @@ -1887,50 +2522,83 @@ def forceRebuildAndRescan(self): ############################################################################# + @TimeThisFunction + def initialWalletSync(self): + for wltID in self.walletMap.iterkeys(): + LOGINFO('Syncing wallet: %s', wltID) + self.walletMap[wltID].setBlockchainSyncFlag(BLOCKCHAIN_READONLY) + # Used to do "sync-lite" when we had to rescan for new addresses, + self.walletMap[wltID].syncWithBlockchainLite(0) + #self.walletMap[wltID].syncWithBlockchain(0) + self.walletMap[wltID].detectHighestUsedIndex(True) # expand wlt if necessary + self.walletMap[wltID].fillAddressPool() + + @TimeThisFunction def finishLoadBlockchain(self): - - TimerStart('finishLoadBlockchain') # Now that the blockchain is loaded, let's populate the wallet info if TheBDM.isInitialized(): + + #for wltID in self.walletMap.iterkeys(): + # TheBDM.bdm.unregisterWallet(self.walletMap[wltID].cppWallet) self.currBlockNum = TheBDM.getTopBlockHeight() self.setDashboardDetails() if not self.memPoolInit: - mempoolfile = os.path.join(ARMORY_HOME_DIR,'mempool.bin') - clearpoolfile = os.path.join(ARMORY_HOME_DIR,'clearmempool.txt') + mempoolfile = os.path.join(ARMORY_HOME_DIR,'mempool.bin') + clearpoolfile = os.path.join(ARMORY_HOME_DIR,'clearmempool.flag') if os.path.exists(clearpoolfile): - LOGINFO('clearmempool.txt found. Clearing memory pool') + LOGINFO('clearmempool.flag found. Clearing memory pool') os.remove(clearpoolfile) if os.path.exists(mempoolfile): os.remove(mempoolfile) - else: + else: self.checkMemoryPoolCorruption(mempoolfile) TheBDM.enableZeroConf(mempoolfile) self.memPoolInit = True - TimerStart('initialWalletSync') for wltID in self.walletMap.iterkeys(): LOGINFO('Syncing wallet: %s', wltID) self.walletMap[wltID].setBlockchainSyncFlag(BLOCKCHAIN_READONLY) self.walletMap[wltID].syncWithBlockchainLite(0) self.walletMap[wltID].detectHighestUsedIndex(True) # expand wlt if necessary self.walletMap[wltID].fillAddressPool() - TimerStop('initialWalletSync') - self.createCombinedLedger() self.ledgerSize = len(self.combinedLedger) - self.statusBar().showMessage('Blockchain loaded, wallets sync\'d!', 10000) + self.statusBar().showMessage('Blockchain loaded, wallets sync\'d!', 10000) if self.netMode==NETWORKMODE.Full: LOGINFO('Current block number: %d', self.currBlockNum) self.lblArmoryStatus.setText(\ - 'Connected (%s blocks) ' % + 'Connected (%s blocks) ' % (htmlColor('TextGreen'), self.currBlockNum)) - self.blkReceived = self.getSettingOrSetDefault('LastBlkRecvTime', 0) + + self.blkReceived = TheBDM.getTopBlockHeader().getTimestamp() + self.writeSetting('LastBlkRecv', self.currBlockNum) + self.writeSetting('LastBlkRecvTime', self.blkReceived) currSyncSuccess = self.getSettingOrSetDefault("SyncSuccessCount", 0) self.writeSetting('SyncSuccessCount', min(currSyncSuccess+1, 10)) + + vectMissingBlks = TheBDM.missingBlockHashes() + LOGINFO('Blockfile corruption check: Missing blocks: %d', len(vectMissingBlks)) + if len(vectMissingBlks) > 0: + LOGINFO('Missing blocks: %d', len(vectMissingBlks)) + QMessageBox.critical(self, tr('Blockdata Error'), tr(""" + Armory has detected an error in the blockchain database + maintained by the third-party Bitcoin software (Bitcoin-Qt + or bitcoind). This error is not fatal, but may lead to + incorrect balances, inability to send coins, or application + instability. +

+ It is unlikely that the error affects your wallets, + but it is possible. If you experience crashing, + or see incorrect balances on any wallets, it is strongly + recommended you re-download the blockchain using: + "Help"\xe2\x86\x92"Factory Reset"."""), \ + QMessageBox.Ok) + + if self.getSettingOrSetDefault('NotifyBlkFinish',True): reply,remember = MsgBoxWithDNAA(MSGBOX.Info, \ 'Blockchain Loaded!', 'Blockchain loading is complete. ' @@ -1938,29 +2606,43 @@ def finishLoadBlockchain(self): 'under the "Transactions" tab. You can also send and ' 'receive bitcoins.', \ dnaaMsg='Do not show me this notification again ', yesStr='OK') - + if remember==True: self.writeSetting('NotifyBlkFinish',False) - else: - self.mainDisplayTabs.setCurrentIndex(self.MAINTABS.Transactions) - + self.mainDisplayTabs.setCurrentIndex(self.MAINTABS.Ledger) + + self.netMode = NETWORKMODE.Full self.settings.set('FailedLoadCount', 0) else: self.statusBar().showMessage('! Blockchain loading failed !', 10000) - - + + # This will force the table to refresh with new data self.setDashboardDetails() + self.updateAnnounceTab() # make sure satoshi version info is up to date + self.removeBootstrapDat() # if we got here, we're *really* done with it self.walletModel.reset() - - TimerStop('finishLoadBlockchain') + + qLen = self.delayedURIData['qLen'] + if qLen > 0: + #delayed URI parses, feed them back to the uri parser now + for i in range(0, qLen): + uriStr = self.delayedURIData[qLen-i-1] + self.delayedURIData['qLen'] = qLen -i -1 + self.uriLinkClicked(uriStr) + ############################################################################# + def removeBootstrapDat(self): + bfile = os.path.join(BTC_HOME_DIR, 'bootstrap.dat.old') + if os.path.exists(bfile): + os.remove(bfile) + ############################################################################# def checkMemoryPoolCorruption(self, mempoolname): - if not os.path.exists(mempoolname): + if not os.path.exists(mempoolname): return memfile = open(mempoolname, 'rb') @@ -1975,15 +2657,13 @@ def checkMemoryPoolCorruption(self, mempoolname): except: os.remove(mempoolname); LOGWARN('Memory pool file was corrupt. Deleted. (no further action is needed)') - - ############################################################################# def changeLedgerSorting(self, col, order): """ The direct sorting was implemented to avoid having to search for comment information for every ledger entry. Therefore, you can't sort by comments - without getting them first, which is the original problem to avoid. + without getting them first, which is the original problem to avoid. """ if col in (LEDGERCOLS.NumConf, LEDGERCOLS.DateStr, \ LEDGERCOLS.Comment, LEDGERCOLS.Amount, LEDGERCOLS.WltName): @@ -1991,16 +2671,13 @@ def changeLedgerSorting(self, col, order): self.sortLedgOrder = order self.createCombinedLedger() - ############################################################################# + @TimeThisFunction def createCombinedLedger(self, wltIDList=None, withZeroConf=True): """ Create a ledger to display on the main screen, that consists of ledger entries of any SUBSET of available wallets. """ - - TimerStart('createCombinedLedger') - start = RightNow() if wltIDList==None: # Create a list of [wltID, type] pairs @@ -2017,7 +2694,7 @@ def createCombinedLedger(self, wltIDList=None, withZeroConf=True): listWatching = [t[0] for t in filter(lambda x: x[1]==WLTTYPES.WatchOnly, typelist)] listCrypt = [t[0] for t in filter(lambda x: x[1]==WLTTYPES.Crypt, typelist)] listPlain = [t[0] for t in filter(lambda x: x[1]==WLTTYPES.Plain, typelist)] - + if currIdx==0: wltIDList = listOffline + listCrypt + listPlain elif currIdx==1: @@ -2028,12 +2705,11 @@ def createCombinedLedger(self, wltIDList=None, withZeroConf=True): wltIDList = self.walletIDList else: pass - #raise WalletExistsError, 'Bad combo-box selection: ' + str(currIdx) + #raise WalletExistsError('Bad combo-box selection: ' + str(currIdx)) self.writeSetting('LastFilterState', currIdx) - + if wltIDList==None: - TimerStop('createCombinedLedger') return self.combinedLedger = [] @@ -2073,7 +2749,7 @@ def createCombinedLedger(self, wltIDList=None, withZeroConf=True): self.lblLedgRange.setText('%d to %d' % (self.currLedgMin, self.currLedgMax)) self.lblLedgTotal.setText('(of %d)' % self.ledgerSize) - # Many MainWindow objects haven't been created yet... + # Many MainWindow objects haven't been created yet... # let's try to update them and fail silently if they don't exist try: if TheBDM.getBDMState() in ('Offline', 'Scanning'): @@ -2081,7 +2757,7 @@ def createCombinedLedger(self, wltIDList=None, withZeroConf=True): self.lblSpendFunds.setText( '-'*12 ) self.lblUnconfFunds.setText('-'*12 ) return - + uncolor = htmlColor('MoneyNeg') if unconfFunds>0 else htmlColor('Foreground') btccolor = htmlColor('DisableFG') if spendFunds==totalFunds else htmlColor('MoneyPos') lblcolor = htmlColor('DisableFG') if spendFunds==totalFunds else htmlColor('Foreground') @@ -2101,20 +2777,13 @@ def createCombinedLedger(self, wltIDList=None, withZeroConf=True): except AttributeError: raise - finally: - TimerStop('createCombinedLedger') - - - ############################################################################# + @TimeThisFunction def convertLedgerToTable(self, ledger): - - TimerStart('convertLedgerTbl') - table2D = [] datefmt = self.getPreferredDateFormat() - for wltID,le in ledger: + for wltID,le in ledger: row = [] wlt = self.walletMap[wltID] @@ -2129,75 +2798,56 @@ def convertLedgerToTable(self, ledger): #amt += self.getFeeForTx(le.getTxHash()) # If this was sent-to-self... we should display the actual specified - # value when the transaction was executed. This is pretty difficult + # value when the transaction was executed. This is pretty difficult # when both "recipient" and "change" are indistinguishable... but # They're actually not because we ALWAYS generate a new address to - # for change , which means the change address MUST have a higher + # for change , which means the change address MUST have a higher # chain index if le.isSentToSelf(): amt = determineSentToSelfAmt(le, wlt)[0] - - if le.getBlockNum() >= 0xffffffff: nConf = 0 # NumConf row.append(nConf) - # UnixTime (needed for sorting) row.append(le.getTxTime()) - # Date row.append(unixTimeToFormatStr(le.getTxTime(), datefmt)) - # TxDir (actually just the amt... use the sign of the amt to determine dir) row.append(coin2str(le.getValue(), maxZeros=2)) - # Wlt Name row.append(self.walletMap[wltID].labelName) - # Comment row.append(self.getCommentForLE(wltID, le)) - # Amount row.append(coin2str(amt, maxZeros=2)) - # Is this money mine? row.append( determineWalletType(wlt, self)[0]==WLTTYPES.WatchOnly) - # WltID row.append( wltID ) - # TxHash row.append( binary_to_hex(le.getTxHash() )) - # Is this a coinbase/generation transaction row.append( le.isCoinbase() ) - # Sent-to-self row.append( le.isSentToSelf() ) - # Tx was invalidated! (double=spend!) row.append( not le.isValid()) - # Finally, attach the row to the table table2D.append(row) - - TimerStop('convertLedgerTbl') - return table2D - + ############################################################################# + @TimeThisFunction def walletListChanged(self): - TimerStart('wltListChanged') self.walletModel.reset() self.populateLedgerComboBox() self.createCombinedLedger() - TimerStop('wltListChanged') ############################################################################# + @TimeThisFunction def populateLedgerComboBox(self): - TimerStart('populateLedgerCombo') self.comboWltSelect.clear() self.comboWltSelect.addItem( 'My Wallets' ) self.comboWltSelect.addItem( 'Offline Wallets' ) @@ -2209,8 +2859,6 @@ def populateLedgerComboBox(self): self.comboWltSelect.insertSeparator(4) comboIdx = self.getSettingOrSetDefault('LastFilterState', 0) self.comboWltSelect.setCurrentIndex(comboIdx) - TimerStop('populateLedgerCombo') - ############################################################################# def execDlgWalletDetails(self, index=None): @@ -2219,7 +2867,7 @@ def execDlgWalletDetails(self, index=None): 'You currently do not have any wallets. Would you like to ' 'create one, now?', QMessageBox.Yes | QMessageBox.No) if reply==QMessageBox.Yes: - self.createNewWallet(initLabel='Primary Wallet') + self.startWalletWizard() return if index==None: @@ -2233,14 +2881,14 @@ def execDlgWalletDetails(self, index=None): QMessageBox.Ok) return index = index[0] - + wlt = self.walletMap[self.walletIDList[index.row()]] dialog = DlgWalletDetails(wlt, self.usermode, self, self) dialog.exec_() #self.walletListChanged() - - - + + + ############################################################################# def updateTxCommentFromView(self, view): index = view.selectedIndexes()[0] @@ -2266,34 +2914,35 @@ def updateAddressCommentFromView(self, view, wlt): dialog = DlgSetComment(currComment, 'Address', self, self) if dialog.exec_(): newComment = str(dialog.edtComment.text()) - addr160 = addrStr_to_hash160(addrStr) + atype, addr160 = addrStr_to_hash160(addrStr) + if atype==P2SHBYTE: + LOGWARN('Setting comment for P2SH address: %s' % addrStr) wlt.setComment(addr160, newComment) ############################################################################# + @TimeThisFunction def getAddrCommentIfAvailAll(self, txHash): - TimerStart('getAddrCommentIfAvail') if not TheBDM.isInitialized(): - TimerStop('getAddrCommentIfAvail') return '' else: - + appendedComments = [] for wltID,wlt in self.walletMap.iteritems(): cmt = wlt.getAddrCommentIfAvail(txHash) if len(cmt)>0: appendedComments.append(cmt) - + return '; '.join(appendedComments) - + ############################################################################# def getCommentForLE(self, wltID, le): - # Smart comments for LedgerEntry objects: get any direct comments ... + # Smart comments for LedgerEntry objects: get any direct comments ... # if none, then grab the one for any associated addresses. - + return self.walletMap[wltID].getCommentForLE(le) """ txHash = le.getTxHash() @@ -2319,7 +2968,7 @@ def addWalletToApplication(self, newWallet, walletIsNew=True): if self.walletMap.has_key(newWltID): return - + self.walletMap[newWltID] = newWallet self.walletIndices[newWltID] = len(self.walletMap)-1 @@ -2330,8 +2979,9 @@ def addWalletToApplication(self, newWallet, walletIsNew=True): ledger = [] wlt = self.walletMap[newWltID] self.walletListChanged() + self.mainWnd = self + - ############################################################################# def removeWalletFromApplication(self, wltID): LOGINFO('removeWalletFromApplication') @@ -2353,78 +3003,9 @@ def removeWalletFromApplication(self, wltID): self.walletListChanged() - ############################################################################# - def createNewWallet(self, initLabel=''): - LOGINFO('createNewWallet') - dlg = DlgNewWallet(self, self, initLabel=initLabel) - if dlg.exec_(): - - if dlg.selectedImport: - self.execImportWallet() - return - - name = str(dlg.edtName.text()) - descr = str(dlg.edtDescr.toPlainText()) - kdfSec = dlg.kdfSec - kdfBytes = dlg.kdfBytes - - # If this will be encrypted, we'll need to get their passphrase - passwd = [] - if dlg.chkUseCrypto.isChecked(): - dlgPasswd = DlgChangePassphrase(self, self) - if dlgPasswd.exec_(): - passwd = SecureBinaryData(str(dlgPasswd.edtPasswd1.text())) - else: - return # no passphrase == abort new wallet - else: - return False - - newWallet = None - if passwd: - newWallet = PyBtcWallet().createNewWallet( \ - withEncrypt=True, \ - securePassphrase=passwd, \ - kdfTargSec=kdfSec, \ - kdfMaxMem=kdfBytes, \ - shortLabel=name, \ - longLabel=descr, \ - doRegisterWithBDM=False) - else: - newWallet = PyBtcWallet().createNewWallet( \ - withEncrypt=False, \ - shortLabel=name, \ - longLabel=descr, \ - doRegisterWithBDM=False) - - - # And we must unlock it before the first fillAddressPool call - if newWallet.useEncryption: - newWallet.unlock(securePassphrase=passwd) - - # We always want to fill the address pool, right away. - fillpool = lambda: newWallet.fillAddressPool(doRegister=False) - DlgExecLongProcess(fillpool, 'Creating Wallet...', self, self).exec_() - - # Reopening from file helps make sure everything is correct -- don't - # let the user use a wallet that triggers errors on reading it - wltpath = newWallet.walletPath - newWallet = None - newWallet = PyBtcWallet().readWalletFile(wltpath) - - - self.addWalletToApplication(newWallet, walletIsNew=True) - - if TheBDM.getBDMState() in ('Uninitialized', 'Offline'): - TheBDM.registerWallet(newWallet, isFresh=True, wait=False) - else: - self.newWalletList.append([newWallet, True]) - - # Prompt user to print paper backup if they requested it. - if dlg.chkPrintPaper.isChecked(): - OpenPaperBackupWindow('Single', self, self, newWallet, \ - tr("Create Paper Backup")) - + def RecoverWallet(self): + DlgWltRecoverWallet(self, self).promptWalletRecovery() ############################################################################# @@ -2432,7 +3013,7 @@ def createSweepAddrTx(self, a160ToSweepList, sweepTo160, forceZeroFee=False): """ This method takes a list of addresses (likely just created from private key data), finds all their unspent TxOuts, and creates a signed tx that - transfers 100% of the funds to the sweepTO160 address. It doesn't + transfers 100% of the funds to the sweepTO160 address. It doesn't actually execute the transaction, but it will return a broadcast-ready PyTx object that the user can confirm. TxFee is automatically calc'd and deducted from the output value, if necessary. @@ -2446,18 +3027,19 @@ def createSweepAddrTx(self, a160ToSweepList, sweepTo160, forceZeroFee=False): utxoList = getUnspentTxOutsForAddr160List(addr160List, 'Sweep', 0) if len(utxoList)==0: return [None, 0, 0] - + outValue = sumTxOutList(utxoList) inputSide = [] for utxo in utxoList: # The PyCreateAndSignTx method require PyTx and PyBtcAddress objects - CppPrevTx = TheBDM.getTxByHash(utxo.getTxHash()) + CppPrevTx = TheBDM.getTxByHash(utxo.getTxHash()) PyPrevTx = PyTx().unserialize(CppPrevTx.serialize()) addr160 = CheckHash160(utxo.getRecipientScrAddr()) inputSide.append([getAddr(addr160), PyPrevTx, utxo.getTxOutIndex()]) - minFee = calcMinSuggestedFees(utxoList, outValue, 0)[1] + # Try with zero fee and exactly one output + minFee = calcMinSuggestedFees(utxoList, outValue, 0, 1)[1] if minFee > 0 and \ not forceZeroFee and \ @@ -2469,13 +3051,14 @@ def createSweepAddrTx(self, a160ToSweepList, sweepTo160, forceZeroFee=False): return [None, outValue, minFee] outputSide = [] - outputSide.append( [PyBtcAddress().createFromPublicKeyHash160(sweepTo160), outValue] ) + outputSide.append( [PyBtcAddress().createFromPublicKeyHash160(sweepTo160), \ + outValue] ) pytx = PyCreateAndSignTx(inputSide, outputSide) return (pytx, outValue, minFee) - + ############################################################################# @@ -2529,7 +3112,7 @@ def confirmSweepScan(self, pybtcaddrList, targAddr160): elif TheBDM.getBDMState()=='BlockchainReady': msgConfirm += ( \ 'Would you like to start the scan operation right now?') - + msgConfirm += ('

Clicking "No" will abort the sweep operation') confirmed = QMessageBox.question(self, 'Confirm Rescan', msgConfirm, \ @@ -2550,12 +3133,12 @@ def confirmSweepScan(self, pybtcaddrList, targAddr160): def finishSweepScan(self): LOGINFO('finishSweepScan') sweepList, self.sweepAfterScanList = self.sweepAfterScanList,[] - + ####################################################################### # The createSweepTx method will return instantly because the blockchain # has already been rescanned, as described above finishedTx, outVal, fee = self.createSweepAddrTx(sweepList, self.sweepAfterScanTarg) - + gt1 = len(sweepList)>1 if finishedTx==None: @@ -2583,13 +3166,13 @@ def finishSweepScan(self): wltID = self.getWalletForAddr160(self.sweepAfterScanTarg) wlt = self.walletMap[wltID] - + # Finally, if we got here, we're ready to broadcast! if gt1: dispIn = '' else: dispIn = 'address %s' % sweepList[0].getAddrStr() - + dispOut = 'wallet "%s" (%s) ' % (wlt.labelName, wlt.uniqueIDB58) if DlgVerifySweep(dispIn, dispOut, outVal, fee).exec_(): self.broadcastTransaction(finishedTx, dryRun=False) @@ -2624,8 +3207,8 @@ def broadcastTransaction(self, pytx, dryRun=False): LOGINFO('Sending Tx, %s', binary_to_hex(newTxHash)) self.NetworkingFactory.sendTx(pytx) LOGINFO('Transaction sent to Satoshi client...!') - - + + def sendGetDataMsg(): msg = PyMessage('getdata') msg.payload.invList.append( [MSG_INV_TX, newTxHash] ) @@ -2666,12 +3249,12 @@ def checkForTxInBDM(): '

If the transaction did fail, please consider ' 'reporting this error the the Armory ' 'developers. From the main window, go to ' - '"File"-->"Export Log File" to make a copy of your ' + '"File"\xe2\x86\x92"Export Log File" to make a copy of your ' 'log file to send via email to support@bitcoinarmory.com. ' \ % (searchstr,searchstr[:8]), \ QMessageBox.Ok) - - self.mainDisplayTabs.setCurrentIndex(self.MAINTABS.Transactions) + + self.mainDisplayTabs.setCurrentIndex(self.MAINTABS.Ledger) reactor.callLater(4, sendGetDataMsg) reactor.callLater(5, checkForTxInBDM) @@ -2686,8 +3269,8 @@ def checkForTxInBDM(): #'received. Both issues are a problem with Armory that will be fixed ' #'with the next release.', QMessageBox.Ok) - - + + ############################################################################# def warnNoImportWhileScan(self): extraMsg = '' @@ -2700,14 +3283,16 @@ def warnNoImportWhileScan(self): 'Wallets and addresses cannot be imported while Armory is in ' 'the middle of an existing blockchain scan. Please wait for ' 'the scan to finish. ' + extraMsg, QMessageBox.Ok) - - - + + + ############################################################################# def execImportWallet(self): sdm = TheSDM.getSDMState() bdm = TheBDM.getBDMState() - if sdm in ['BitcoindInitializing','BitcoindSynchronizing'] or \ + if sdm in ['BitcoindInitializing', \ + 'BitcoindSynchronizing', \ + 'TorrentSynchronizing'] or \ bdm in ['Scanning']: QMessageBox.warning(self, tr('Scanning'), tr(""" Armory is currently in the middle of scanning the blockchain for @@ -2744,14 +3329,14 @@ def execGetImportWltName(self): shutil.copy(fn, newpath) newWlt = PyBtcWallet().readWalletFile(newpath) newWlt.fillAddressPool() - + self.addWalletToAppAndAskAboutRescan(newWlt) """ I think the addWalletToAppAndAskAboutRescan replaces this... if TheBDM.getBDMState() in ('Uninitialized', 'Offline'): self.addWalletToApplication(newWlt, walletIsNew=False) return - + if TheBDM.getBDMState()=='BlockchainReady': doRescanNow = QMessageBox.question(self, 'Rescan Needed', \ 'The wallet was imported successfully, but cannot be displayed ' @@ -2769,13 +3354,13 @@ def execGetImportWltName(self): 'The wallet was imported successfully, but its balance cannot ' 'be determined until Armory performs a "recovery scan" for the ' 'wallet. This scan potentially takes much longer than a regular ' - 'scan, and must be completed for all imported wallets. ' + 'scan, and must be completed for all imported wallets. ' '

' 'Armory is already in the middle of a scan and cannot be interrupted. ' 'Would you like to start the recovery scan when it is done?' '

' '
If you click "No," the wallet import will be aborted ' - 'and you must re-import the wallet when you ' + 'and you must re-import the wallet when you ' 'are able to wait for the recovery scan.', \ QMessageBox.Yes | QMessageBox.No) @@ -2790,7 +3375,7 @@ def execGetImportWltName(self): QMessageBox.warning(self, 'Import Failed', \ 'The wallet was not imported.', QMessageBox.Ok) - # The wallet cannot exist without also being on disk. + # The wallet cannot exist without also being on disk. # If the user aborted, we should remove the disk data. thepath = newWlt.getWalletPath() thepathBackup = newWlt.getWalletPath('backup') @@ -2809,14 +3394,14 @@ def execGetImportWltName(self): ############################################################################# def addWalletToAppAndAskAboutRescan(self, newWallet): LOGINFO('Raw import successful.') - - # If we are offline, then we can't assume there will ever be a + + # If we are offline, then we can't assume there will ever be a # rescan. Just add the wallet to the application if TheBDM.getBDMState() in ('Uninitialized', 'Offline'): TheBDM.registerWallet(newWallet.cppWallet) self.addWalletToApplication(newWallet, walletIsNew=False) return - + """ TODO: Temporarily removed recovery-rescan operations elif TheBDM.getBDMState()=='BlockchainReady': doRescanNow = QMessageBox.question(self, 'Rescan Needed', \ @@ -2847,28 +3432,28 @@ def addWalletToAppAndAskAboutRescan(self, newWallet): if TheBDM.getBDMState()=='BlockchainReady': doRescanNow = QMessageBox.question(self, tr('Rescan Needed'), \ - tr("""The wallet was restored successfully but its balance - cannot be displayed until the blockchain is rescanned. - Armory will need to go into offline mode for 5-20 minutes. + tr("""The wallet was restored successfully but its balance + cannot be displayed until the blockchain is rescanned. + Armory will need to go into offline mode for 5-20 minutes.

- Would you like to do the scan now? Clicking "No" will + Would you like to do the scan now? Clicking "No" will abort the restore/import operation."""), \ QMessageBox.Yes | QMessageBox.No) else: doRescanNow = QMessageBox.question(self, tr('Rescan Needed'), \ - tr("""The wallet was restored successfully but its balance - cannot be displayed until the blockchain is rescanned. - However, Armory is currently in the middle of a rescan + tr("""The wallet was restored successfully but its balance + cannot be displayed until the blockchain is rescanned. + However, Armory is currently in the middle of a rescan operation right now. Would you like to start a new scan as soon as this one is finished?

Clicking "No" will abort adding the wallet to Armory."""), \ QMessageBox.Yes | QMessageBox.No) - + if doRescanNow == QMessageBox.Yes: LOGINFO('User requested rescan after wallet restore') - #TheBDM.startWalletRecoveryScan(newWallet) + #TheBDM.startWalletRecoveryScan(newWallet) TheBDM.registerWallet(newWallet.cppWallet) self.startRescanBlockchain() self.setDashboardDetails() @@ -2878,7 +3463,7 @@ def addWalletToAppAndAskAboutRescan(self, newWallet): 'The wallet was not restored. To restore the wallet, reenter ' 'the "Restore Wallet" dialog again when you are able to wait ' 'for the rescan operation. ', QMessageBox.Ok) - # The wallet cannot exist without also being on disk. + # The wallet cannot exist without also being on disk. # If the user aborted, we should remove the disk data. thepath = newWallet.getWalletPath() thepathBackup = newWallet.getWalletPath('backup') @@ -2893,38 +3478,12 @@ def addWalletToAppAndAskAboutRescan(self, newWallet): ############################################################################# def digitalBackupWarning(self): reply = QMessageBox.warning(self, 'Be Careful!', tr(""" - WARNING: You are about to make an + WARNING: You are about to make an unencrypted backup of your wallet. It is highly recommended that you do not ever save unencrypted wallets to your regular hard drive. This feature is intended for saving to a USB key or other removable media."""), QMessageBox.Ok | QMessageBox.Cancel) return (reply==QMessageBox.Ok) - - - - ############################################################################# - def execMigrateSatoshi(self): - reply = MsgBoxCustom(MSGBOX.Question, 'Wallet Version Warning', \ - 'This wallet migration tool only works with regular Bitcoin wallets ' - 'produced using version 0.5.X and earlier. ' - 'You can determine the version by ' - 'opening the regular Bitcoin client, then choosing "Help"' - '-->"About Bitcoin-Qt" from the main menu. ' - '

' - 'If you have used your wallet with any version of the regular ' - 'Bitcoin client 0.6.0 or higher, this tool will fail. ' - 'In fact, it is highly recommended that you do not even attempt ' - 'to use the tool on such wallets until it is officially supported ' - 'by Armory.' - '

' - 'Has your wallet ever been opened in the 0.6.0+ Bitcoin-Qt client?', \ - yesStr='Yes, Abort!', noStr='No, Carry On!') - - if reply: - return - - DlgMigrateSatoshiWallet(self, self).exec_() - ############################################################################# @@ -2961,14 +3520,14 @@ def getUniqueWalletFilename(self, wltPath): fname='%s_%02d.wallet'%(base, newIndex) newIndex+=1 if newIndex==99: - raise WalletExistsError, ('Cannot find unique filename for wallet.' - 'Too many duplicates!') + raise WalletExistsError('Cannot find unique filename for wallet.' + 'Too many duplicates!') return fname - + ############################################################################# def addrViewDblClicked(self, index, wlt): - uacfv = lambda x: self.main.updateAddressCommentFromView(self.wltAddrView, self.wlt) + uacfv = lambda x: self.updateAddressCommentFromView(self.wltAddrView, self.wlt) ############################################################################# @@ -3005,10 +3564,10 @@ def showLedgerTx(self): ############################################################################# def showContextMenuLedger(self): menu = QMenu(self.ledgerView) - + if len(self.ledgerView.selectedIndexes())==0: return - + actViewTx = menu.addAction("View Details") actViewBlkChn = menu.addAction("View on www.blockchain.info") actComment = menu.addAction("Change Comment") @@ -3028,7 +3587,7 @@ def showContextMenuLedger(self): elif action==actViewBlkChn: try: webbrowser.open(blkchnURL) - except: + except: LOGEXCEPT('Failed to open webbrowser') QMessageBox.critical(self, 'Could not open browser', \ 'Armory encountered an error opening your web browser. To view ' @@ -3042,11 +3601,21 @@ def showContextMenuLedger(self): elif action==actComment: self.updateTxCommentFromView(self.ledgerView) elif action==actOpenWallet: - DlgWalletDetails(self.walletMap[wltID], self.usermode, self, self).exec_() + DlgWalletDetails(self.getSelectedWallet(), self.usermode, self, self).exec_() + ############################################################################# + def getSelectedWallet(self): + wltID = None + if len(self.walletMap) > 0: + wltID = self.walletMap.keys()[0] + wltSelect = self.walletsView.selectedIndexes() + if len(wltSelect) > 0: + row = wltSelect[0].row() + wltID = str(self.walletsView.model().index(row, WLTVIEWCOLS.ID).data().toString()) + # Starting the send dialog with or without a wallet + return None if wltID == None else self.walletMap[wltID] - ############################################################################# def clickSendBitcoins(self): if TheBDM.getBDMState() in ('Offline', 'Uninitialized'): QMessageBox.warning(self, 'Offline Mode', \ @@ -3067,7 +3636,6 @@ def clickSendBitcoins(self): QMessageBox.Ok) return - wltID = None selectionMade = True if len(self.walletMap)==0: reply = QMessageBox.information(self, 'No Wallets!', \ @@ -3075,48 +3643,31 @@ def clickSendBitcoins(self): 'receive some coins. Would you like to create a wallet?', \ QMessageBox.Yes | QMessageBox.No) if reply==QMessageBox.Yes: - self.createNewWallet(initLabel='Primary Wallet') - return - elif len(self.walletMap)==1: - wltID = self.walletMap.keys()[0] + self.startWalletWizard() else: - wltSelect = self.walletsView.selectedIndexes() - if len(wltSelect)>0: - row = wltSelect[0].row() - wltID = str(self.walletsView.model().index(row, WLTVIEWCOLS.ID).data().toString()) - dlg = DlgWalletSelect(self, self, 'Send from Wallet...', firstSelect=wltID, onlyMyWallets=False) - if dlg.exec_(): - wltID = dlg.selectedID - else: - selectionMade = False + DlgSendBitcoins(self.getSelectedWallet(), self, self).exec_() - if selectionMade: - wlt = self.walletMap[wltID] - wlttype = determineWalletType(wlt, self)[0] - dlgSend = DlgSendBitcoins(wlt, self, self) - dlgSend.exec_() - ############################################################################# def uriSendBitcoins(self, uriDict): # Because Bitcoin-Qt doesn't store the message= field we have to assume - # that the label field holds the Tx-info. So we concatenate them for + # that the label field holds the Tx-info. So we concatenate them for # the display message uri_has = lambda s: uriDict.has_key(s) haveLbl = uri_has('label') haveMsg = uri_has('message') - newMsg = '' + newMsg = '' if haveLbl and haveMsg: newMsg = uriDict['label'] + ': ' + uriDict['message'] elif not haveLbl and haveMsg: newMsg = uriDict['message'] elif haveLbl and not haveMsg: newMsg = uriDict['label'] - + descrStr = '' - descrStr = ('You just clicked on a "bitcoin:" link requesting bitcoins ' + descrStr = ('You just clicked on a "bitcoin:" link requesting bitcoins ' 'to be sent to the following address:
') descrStr += '
--Address:\t%s ' % uriDict['address'] @@ -3141,7 +3692,7 @@ def uriSendBitcoins(self, uriDict): descrStr += '
--Message:\t%s' % newMsg uriDict['message'] = newMsg - + if not uri_has('amount'): descrStr += ('

There is no amount specified in the link, so ' 'you can decide the amount after selecting a wallet to use ' @@ -3158,22 +3709,12 @@ def uriSendBitcoins(self, uriDict): 'currently have no wallets! Would you like to create a wallet ' 'now?', QMessageBox.Yes | QMessageBox.No) if reply==QMessageBox.Yes: - self.createNewWallet(initLabel='Primary Wallet') + self.startWalletWizard() return False - elif len(self.walletMap)>1: - dlg = DlgWalletSelect(self, self, 'Send from Wallet...', descrStr, \ - onlyMyWallets=True, atLeast=amt) - if not dlg.exec_(): - return False - selectedWalletID = dlg.selectedID else: - selectedWalletID = self.walletIDList[0] - - wlt = self.walletMap[selectedWalletID] - dlgSend = DlgSendBitcoins(wlt, self, self, uriDict) - dlgSend.exec_() + DlgSendBitcoins(self.getSelectedWallet(), self, self, uriDict).exec_() return True - + ############################################################################# def clickReceiveCoins(self): @@ -3186,7 +3727,7 @@ def clickReceiveCoins(self): 'store you bitcoins! Would you like to create a wallet now?', \ QMessageBox.Yes | QMessageBox.No) if reply==QMessageBox.Yes: - self.createNewWallet(initLabel='Primary Wallet') + self.startWalletWizard() return elif len(self.walletMap)==1: wltID = self.walletMap.keys()[0] @@ -3195,9 +3736,10 @@ def clickReceiveCoins(self): if len(wltSelect)>0: row = wltSelect[0].row() wltID = str(self.walletsView.model().index(row, WLTVIEWCOLS.ID).data().toString()) - dlg = DlgWalletSelect(self, self, 'Receive coins with wallet...', '', firstSelect=wltID, onlyMyWallets=False) + dlg = DlgWalletSelect(self, self, 'Receive coins with wallet...', '', \ + firstSelect=wltID, onlyMyWallets=False) if dlg.exec_(): - wltID = dlg.selectedID + wltID = dlg.selectedID else: selectionMade = False @@ -3214,7 +3756,7 @@ def sysTrayActivated(self, reason): if reason==QSystemTrayIcon.DoubleClick: self.bringArmoryToFront() - + ############################################################################# def bringArmoryToFront(self): @@ -3229,78 +3771,160 @@ def minimizeArmory(self): self.hide() self.sysTray.show() + ############################################################################# + def startWalletWizard(self): + walletWizard = WalletWizard(self, self) + walletWizard.exec_() + + ############################################################################# + def startTxWizard(self, prefill=None, onlyOfflineWallets=False): + txWizard = TxWizard(self, self, self.getSelectedWallet(), prefill, onlyOfflineWallets=onlyOfflineWallets) + txWizard.exec_() + ############################################################################# def exportLogFile(self): LOGDEBUG('exportLogFile') - extraStr = '' - if self.usermode in (USERMODE.Advanced, USERMODE.Expert): - extraStr = tr( """ -

Advanced tip: This log file is maintained at - the following location on your hard drive: -

%s

- Before sending the log file, you may edit it to remove information that - does not seem relevant for debugging purposes. Or, extract the error - messages from the log file and copy only those into a bug report email """) % \ - ARMORY_LOG_FILE - - #reply = QMessageBox.warning(self, 'Export Log File', \ - reply = MsgBoxCustom(MSGBOX.Warning, 'Privacy Warning', tr(""" - The log file contains information that may be considered sensitive - by some users. Log files should be protected the same - way you would protect a watching-only wallet, though it - usually contains much less information than that. + reply = QMessageBox.warning(self, tr('Bug Reporting'), tr(""" + As of version 0.91, Armory now includes a form for reporting + problems with the software. Please use + "Help"\xe2\x86\x92"Submit Bug Report" + to send a report directly to the Armory team, which will include + your log file automatically."""), QMessageBox.Ok | QMessageBox.Cancel) + + if not reply==QMessageBox.Ok: + return + + if self.logFilePrivacyWarning(wCancel=True): + self.saveCombinedLogFile() + + ############################################################################# + def getUserAgreeToPrivacy(self, getAgreement=False): + ptype = 'submitbug' if getAgreement else 'generic' + dlg = DlgPrivacyPolicy(self, self, ptype) + if not dlg.exec_(): + return False + + return dlg.chkUserAgrees.isChecked() + + ############################################################################# + def logFileTriplePrivacyWarning(self): + return MsgBoxCustom(MSGBOX.Warning, tr('Privacy Warning'), tr(""" + ATI Privacy Policy

- No private key data is ever written to the log file. - Some information about your wallets or balances may appear - in the log file, but only enough to help the Armory developers - track down bugs in the software. + You should review the Armory Technologies, Inc. privacy + policy before sending any data to ATI servers.

- Please do not send the log file to the Armory developers if you are not - comfortable with them seeing some of your addresses and transactions. - """) + extraStr, wCancel=True, yesStr='Export', noStr='Cancel') - - if reply: - - def getLastXBytesOfFile(filename, nBytes=500*1024): - if not os.path.exists(filename): - LOGERROR('File does not exist!') - return '' - - sz = os.path.getsize(filename) - with open(filename, 'rb') as fin: - if sz > nBytes: - fin.seek(sz - nBytes) - return fin.read() - - # TODO: Interleave the C++ log and the python log. That could be a lot of work! - defaultFn = 'armorylog_%s.txt' % unixTimeToFormatStr(RightNow(), '%Y%m%d_%H%M') - logfn = self.getFileSave(title='Export Log File', \ + Wallet Analysis Log Files +

+ The wallet analysis logs contain no personally-identifiable + information, only a record of errors and inconsistencies + found in your wallet file. No private keys or even public + keys are included. +

+ + Regular Log Files +

+ The regular log files do not contain any security-sensitive + information, but some users may consider the information to be + privacy-sensitive. The log files may identify some addresses + and transactions that are related to your wallets. It is always + recommended you include your log files with any request to the + Armory team, unless you are uncomfortable with the privacy + implications. +

+ + Watching-only Wallet +

+ A watching-only wallet is a copy of a regular wallet that does not + contain any signing keys. This allows the holder to see the balance + and transaction history of the wallet, but not spend any of the funds. +

+ You may be requested to submit a watching-only copy of your wallet + to Armory Technologies, Inc. to make sure that there is no + risk to the security of your funds. You should not even consider + sending your + watching-only wallet unless it was specifically requested by an + Armory representative.""") % PRIVACY_URL, yesStr="&Ok") + + + ############################################################################# + def logFilePrivacyWarning(self, wCancel=False): + return MsgBoxCustom(MSGBOX.Warning, tr('Privacy Warning'), tr(""" + ATI Privacy Policy +
+ You should review the Armory Technologies, Inc. privacy + policy before sending any data to ATI servers. +

+ + Armory log files do not contain any security-sensitive + information, but some users may consider the information to be + privacy-sensitive. The log files may identify some addresses + and transactions that are related to your wallets. +

+ + No signing-key data is ever written to the log file. + Only enough data is there to help the Armory developers + track down bugs in the software, but it may still be considered + sensitive information to some users. +

+ + Please do not send the log file to the Armory developers if you + are not comfortable with the privacy implications! However, if you + do not send the log file, it may be very difficult or impossible + for us to help you with your problem. + +

Advanced tip: You can use + "File"\xe2\x86\x92"Export Log File" from the main + window to save a copy of the log file that you can manually + review."""), wCancel=wCancel, yesStr="&Ok") + + + ############################################################################# + def saveCombinedLogFile(self, saveFile=None): + if saveFile is None: + # TODO: Interleave the C++ log and the python log. + # That could be a lot of work! + defaultFN = 'armorylog_%s.txt' % \ + unixTimeToFormatStr(RightNow(),'%Y%m%d_%H%M') + saveFile = self.getFileSave(title='Export Log File', \ ffilter=['Text Files (*.txt)'], \ - defaultFilename=defaultFn) + defaultFilename=defaultFN) - if len(unicode(logfn)) > 0: - pyFilename = ARMORY_LOG_FILE - cppFilename = os.path.join(ARMORY_HOME_DIR, 'armorycpplog.txt') - fout = open(logfn, 'wb') - fout.write(getLastXBytesOfFile(pyFilename, 256*1024)) - fout.write(getLastXBytesOfFile(cppFilename, 256*1024)) - fout.close() + def getLastBytesOfFile(filename, nBytes=500*1024): + if not os.path.exists(filename): + LOGERROR('File does not exist!') + return '' + + sz = os.path.getsize(filename) + with open(filename, 'rb') as fin: + if sz > nBytes: + fin.seek(sz - nBytes) + return fin.read() + + + if len(unicode(saveFile)) > 0: + fout = open(saveFile, 'wb') + fout.write(getLastBytesOfFile(ARMORY_LOG_FILE, 256*1024)) + fout.write(getLastBytesOfFile(ARMCPP_LOG_FILE, 256*1024)) + fout.close() + + LOGINFO('Log saved to %s', saveFile) + - LOGINFO('Log saved to %s', logfn) ############################################################################# def blinkTaskbar(self): self.activateWindow() - + ############################################################################# def lookForBitcoind(self): LOGDEBUG('lookForBitcoind') if satoshiIsAvailable(): return 'Running' - + self.setSatoshiPaths() try: @@ -3315,35 +3939,36 @@ def lookForBitcoind(self): return 'AllGood' ############################################################################# - def pressModeSwitchButton(self): - LOGDEBUG('pressModeSwitchButton') + def executeModeSwitch(self): + LOGDEBUG('executeModeSwitch') + if TheSDM.getSDMState() == 'BitcoindExeMissing': bitcoindStat = self.lookForBitcoind() if bitcoindStat=='Running': - result = QMessageBox.warning(self, 'Already running!', \ - 'The Bitcoin software appears to be installed now, but it ' - 'needs to be closed for Armory to work. Would you like Armory ' - 'to close it for you?', QMessageBox.Yes | QMessageBox.No) + result = QMessageBox.warning(self, tr('Already running!'), tr(""" + The Bitcoin software appears to be installed now, but it + needs to be closed for Armory to work. Would you like Armory + to close it for you?"""), QMessageBox.Yes | QMessageBox.No) if result==QMessageBox.Yes: self.closeExistingBitcoin() - self.startBitcoindIfNecessary() + self.startBitcoindIfNecessary() elif bitcoindStat=='StillMissing': - QMessageBox.warning(self, 'Still Missing', \ - 'The Bitcoin software still appears to be missing. If you ' - 'just installed it, then please adjust your settings to point ' - 'to the installation directory.', QMessageBox.Ok) - self.startBitcoindIfNecessary() - elif self.doManageSatoshi and not TheSDM.isRunningBitcoind(): + QMessageBox.warning(self, tr('Still Missing'), tr(""" + The Bitcoin software still appears to be missing. If you + just installed it, then please adjust your settings to point + to the installation directory."""), QMessageBox.Ok) + self.startBitcoindIfNecessary() + elif self.doAutoBitcoind and not TheSDM.isRunningBitcoind(): if satoshiIsAvailable(): - result = QMessageBox.warning(self, 'Still Running', \ - 'Bitcoin-Qt is still running. Armory cannot start until ' - 'it is closed. Do you want Armory to close it for you?', \ + result = QMessageBox.warning(self, tr('Still Running'), tr(""" + 'Bitcoin-Qt is still running. Armory cannot start until + 'it is closed. Do you want Armory to close it for you?"""), \ QMessageBox.Yes | QMessageBox.No) if result==QMessageBox.Yes: self.closeExistingBitcoin() - self.startBitcoindIfNecessary() + self.startBitcoindIfNecessary() else: - self.startBitcoindIfNecessary() + self.startBitcoindIfNecessary() elif TheBDM.getBDMState() == 'BlockchainReady' and TheBDM.isDirty(): #self.resetBdmBeforeScan() self.startRescanBlockchain() @@ -3357,30 +3982,29 @@ def pressModeSwitchButton(self): self.setDashboardDetails() - - + + ############################################################################# + @TimeThisFunction def resetBdmBeforeScan(self): - if TheBDM.getBDMState()=='Scanning': + if TheBDM.getBDMState()=='Scanning': LOGINFO('Aborting load') touchFile(os.path.join(ARMORY_HOME_DIR,'abortload.txt')) os.remove(os.path.join(ARMORY_HOME_DIR,'blkfiles.txt')) - TimerStart("resetBdmBeforeScan") TheBDM.Reset(wait=False) for wid,wlt in self.walletMap.iteritems(): TheBDM.registerWallet(wlt.cppWallet) - TimerStop("resetBdmBeforeScan") ############################################################################# - def SetupDashboard(self): - LOGDEBUG('SetupDashboard') + def setupDashboard(self): + LOGDEBUG('setupDashboard') self.lblBusy = QLabel('') if OS_WINDOWS: # Unfortunately, QMovie objects don't work in Windows with py2exe - # had to create my own little "Busy" icon and hook it up to the + # had to create my own little "Busy" icon and hook it up to the # heartbeat self.lblBusy.setPixmap(QPixmap(':/loadicon_0.png')) self.numHeartBeat = 0 @@ -3398,34 +4022,65 @@ def loadBarUpdate(): self.btnModeSwitch = QPushButton('') self.connect(self.btnModeSwitch, SIGNAL('clicked()'), \ - self.pressModeSwitchButton) + self.executeModeSwitch) - # Will switch this to array/matrix of widgets if I get more than 2 rows - self.lblDashModeSync = QRichLabel('',doWrap=False) - self.lblDashModeScan = QRichLabel('',doWrap=False) - self.lblDashModeSync.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) - self.lblDashModeScan.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) - self.barProgressSync = QProgressBar(self) - self.barProgressScan = QProgressBar(self) + # Will switch this to array/matrix of widgets if I get more than 2 rows + self.lblDashModeTorrent = QRichLabel('',doWrap=False) + self.lblDashModeSync = QRichLabel('',doWrap=False) + self.lblDashModeBuild = QRichLabel('',doWrap=False) + self.lblDashModeScan = QRichLabel('',doWrap=False) + + self.lblDashModeTorrent.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + self.lblDashModeSync.setAlignment( Qt.AlignLeft | Qt.AlignVCenter) + self.lblDashModeBuild.setAlignment( Qt.AlignLeft | Qt.AlignVCenter) + self.lblDashModeScan.setAlignment( Qt.AlignLeft | Qt.AlignVCenter) + + self.barProgressTorrent = QProgressBar(self) + self.barProgressSync = QProgressBar(self) + self.barProgressBuild = QProgressBar(self) + self.barProgressScan = QProgressBar(self) + + self.barProgressTorrent.setRange(0,100) self.barProgressSync.setRange(0,100) + self.barProgressBuild.setRange(0,100) self.barProgressScan.setRange(0,100) + + self.lblTorrentStats = QRichLabel('', hAlign=Qt.AlignHCenter) + twid = relaxedSizeStr(self,'99 seconds')[0] - self.lblTimeLeftSync = QRichLabel('') - self.lblTimeLeftScan = QRichLabel('') + self.lblTimeLeftTorrent = QRichLabel('') + self.lblTimeLeftSync = QRichLabel('') + self.lblTimeLeftBuild = QRichLabel('') + self.lblTimeLeftScan = QRichLabel('') + self.lblTimeLeftSync.setMinimumWidth(twid) self.lblTimeLeftScan.setMinimumWidth(twid) + self.lblStatsTorrent = QRichLabel('') + layoutDashMode = QGridLayout() - layoutDashMode.addWidget(self.lblDashModeSync, 0,0) - layoutDashMode.addWidget(self.barProgressSync, 0,1) - layoutDashMode.addWidget(self.lblTimeLeftSync, 0,2) - layoutDashMode.addWidget(self.lblDashModeScan, 1,0) - layoutDashMode.addWidget(self.barProgressScan, 1,1) - layoutDashMode.addWidget(self.lblTimeLeftScan, 1,2) - layoutDashMode.addWidget(self.lblBusy, 0,3, 2,1) - layoutDashMode.addWidget(self.btnModeSwitch, 0,3, 2,1) + layoutDashMode.addWidget(self.lblDashModeTorrent, 0,0) + layoutDashMode.addWidget(self.barProgressTorrent, 0,1) + layoutDashMode.addWidget(self.lblTimeLeftTorrent, 0,2) + layoutDashMode.addWidget(self.lblTorrentStats, 1,0) + + layoutDashMode.addWidget(self.lblDashModeSync, 2,0) + layoutDashMode.addWidget(self.barProgressSync, 2,1) + layoutDashMode.addWidget(self.lblTimeLeftSync, 2,2) + + layoutDashMode.addWidget(self.lblDashModeBuild, 3,0) + layoutDashMode.addWidget(self.barProgressBuild, 3,1) + layoutDashMode.addWidget(self.lblTimeLeftBuild, 3,2) + + layoutDashMode.addWidget(self.lblDashModeScan, 4,0) + layoutDashMode.addWidget(self.barProgressScan, 4,1) + layoutDashMode.addWidget(self.lblTimeLeftScan, 4,2) + + layoutDashMode.addWidget(self.lblBusy, 0,3, 5,1) + layoutDashMode.addWidget(self.btnModeSwitch, 0,3, 5,1) + self.frmDashModeSub = QFrame() self.frmDashModeSub.setFrameStyle(STYLE_SUNKEN) self.frmDashModeSub.setLayout(layoutDashMode) @@ -3433,7 +4088,7 @@ def loadBarUpdate(): self.frmDashModeSub, \ 'Stretch']) - + self.lblDashDescr1 = QRichLabel('') self.lblDashDescr2 = QRichLabel('') for lbl in [self.lblDashDescr1, self.lblDashDescr2]: @@ -3444,21 +4099,21 @@ def loadBarUpdate(): lbl.setPalette(qpal) lbl.setOpenExternalLinks(True) - # Set up an array of buttons in the middle of the dashboard, to be used + # Set up an array of buttons in the middle of the dashboard, to be used # to help the user install bitcoind. self.lblDashBtnDescr = QRichLabel('') self.lblDashBtnDescr.setOpenExternalLinks(True) BTN,LBL,TTIP = range(3) self.dashBtns = [[None]*3 for i in range(5)] self.dashBtns[DASHBTNS.Close ][BTN] = QPushButton('Close Bitcoin Process') - self.dashBtns[DASHBTNS.Install ][BTN] = QPushButton('Auto-Install Bitcoin') + self.dashBtns[DASHBTNS.Install ][BTN] = QPushButton('Download Bitcoin') self.dashBtns[DASHBTNS.Browse ][BTN] = QPushButton('Open www.bitcoin.org') self.dashBtns[DASHBTNS.Instruct][BTN] = QPushButton('Installation Instructions') self.dashBtns[DASHBTNS.Settings][BTN] = QPushButton('Change Settings') ##### - def openBitcoinOrg(): + def openBitcoinOrg(): webbrowser.open('http://www.bitcoin.org/en/download') @@ -3474,18 +4129,18 @@ def openInstruct(): - + self.connect(self.dashBtns[DASHBTNS.Close][BTN], SIGNAL('clicked()'), \ - self.closeExistingBitcoin) + self.closeExistingBitcoin) self.connect(self.dashBtns[DASHBTNS.Install][BTN], SIGNAL('clicked()'), \ - self.installSatoshiClient) + self.openDLSatoshi) self.connect(self.dashBtns[DASHBTNS.Browse][BTN], SIGNAL('clicked()'), \ openBitcoinOrg) self.connect(self.dashBtns[DASHBTNS.Settings][BTN], SIGNAL('clicked()'), \ self.openSettings) #self.connect(self.dashBtns[DASHBTNS.Instruct][BTN], SIGNAL('clicked()'), \ - #self.openInstructWindow) + #self.openInstructWindow) self.dashBtns[DASHBTNS.Close][LBL] = QRichLabel( \ 'Stop existing Bitcoin processes so that Armory can open its own') @@ -3539,15 +4194,15 @@ def openInstruct(): dist = platform.linux_distribution() if dist[0] in ['Ubuntu','LinuxMint'] or 'debian' in dist: self.dashBtns[DASHBTNS.Install][BTN].setEnabled(True) - self.dashBtns[DASHBTNS.Install][LBL] = QRichLabel( \ - 'Automatic installation for Ubuntu/Debian') - self.dashBtns[DASHBTNS.Install][TTIP] = self.createToolTipWidget( \ - 'Will download and install Bitcoin from trusted sources.') + self.dashBtns[DASHBTNS.Install][LBL] = QRichLabel( tr(""" + Download and Install Bitcoin Core for Ubuntu/Debian""")) + self.dashBtns[DASHBTNS.Install][TTIP] = self.createToolTipWidget( tr(""" + 'Will download and Bitcoin software and cryptographically verify it""")) elif OS_MACOSX: pass else: print 'Unrecognized OS!' - + self.frmDashMgmtButtons = QFrame() self.frmDashMgmtButtons.setFrameStyle(STYLE_SUNKEN) @@ -3559,10 +4214,10 @@ def openInstruct(): wMin = tightSizeNChar(self, 50)[0] self.dashBtns[r][c].setMinimumWidth(wMin) layoutButtons.addWidget(self.dashBtns[r][c], r+1,c) - + self.frmDashMgmtButtons.setLayout(layoutButtons) self.frmDashMidButtons = makeHorizFrame(['Stretch', \ - self.frmDashMgmtButtons, + self.frmDashMgmtButtons, 'Stretch']) dashLayout = QVBoxLayout() @@ -3580,6 +4235,366 @@ def openInstruct(): scrollLayout.addWidget(self.dashScrollArea) self.tabDashboard.setLayout(scrollLayout) + + + ############################################################################# + def setupAnnounceTab(self): + + self.lblAlertStr = QRichLabel(tr(""" + Announcements and alerts from Armory Technologies, + Inc."""), doWrap=False, hAlign=Qt.AlignHCenter) + + def checkUpd(): + lastUpdate = self.announceFetcher.getLastSuccessfulFetchTime() + self.explicitCheckAnnouncements(5) + lastUpdate2 = self.announceFetcher.getLastSuccessfulFetchTime() + if lastUpdate==lastUpdate2: + QMessageBox.warning(self, tr('Not Available'), tr(""" + Could not access the Armory + Technologies, Inc. announcement feeder. + Try again in a couple minutes.""") % \ + htmlColor('TextGreen'), QMessageBox.Ok) + else: + QMessageBox.warning(self, tr('Update'), tr(""" + Announcements are now up to date!"""), QMessageBox.Ok) + + + self.lblLastUpdated = QRichLabel('', doWrap=False) + self.btnCheckForUpdates = QPushButton(tr('Check for Updates')) + self.connect(self.btnCheckForUpdates, SIGNAL(CLICKED), checkUpd) + + + frmLastUpdate = makeHorizFrame(['Stretch', \ + self.lblLastUpdated, \ + self.btnCheckForUpdates, \ + 'Stretch']) + + self.icoArmorySWVersion = QLabel('') + self.lblArmorySWVersion = QRichLabel(tr(""" + No version information is available"""), doWrap=False) + self.icoSatoshiSWVersion = QLabel('') + self.lblSatoshiSWVersion = QRichLabel('', doWrap=False) + + self.btnSecureDLArmory = QPushButton(tr('Secure Downloader')) + self.btnSecureDLSatoshi = QPushButton(tr('Secure Downloader')) + self.btnSecureDLArmory.setVisible(False) + self.btnSecureDLSatoshi.setVisible(False) + self.connect(self.btnSecureDLArmory, SIGNAL(CLICKED), self.openDLArmory) + self.connect(self.btnSecureDLSatoshi, SIGNAL(CLICKED), self.openDLSatoshi) + + + frmVersions = QFrame() + layoutVersions = QGridLayout() + layoutVersions.addWidget(self.icoArmorySWVersion, 0,0) + layoutVersions.addWidget(self.lblArmorySWVersion, 0,1) + layoutVersions.addWidget(self.btnSecureDLArmory, 0,2) + layoutVersions.addWidget(self.icoSatoshiSWVersion, 1,0) + layoutVersions.addWidget(self.lblSatoshiSWVersion, 1,1) + layoutVersions.addWidget(self.btnSecureDLSatoshi, 1,2) + layoutVersions.setColumnStretch(0,0) + layoutVersions.setColumnStretch(1,1) + layoutVersions.setColumnStretch(2,0) + frmVersions.setLayout(layoutVersions) + frmVersions.setFrameStyle(STYLE_RAISED) + + lblVerHeader = QRichLabel(tr(""" + Software Version Updates:"""), doWrap=False, \ + hAlign=Qt.AlignHCenter) + lblTableHeader = QRichLabel(tr(""" + All Available Notifications:"""), doWrap=False, \ + hAlign=Qt.AlignHCenter) + + + # We need to generate popups when a widget is clicked, and be able + # change that particular widget's target, when the table is updated. + # Create one of these DlgGen objects for each of the 10 rows, simply + # update it's nid and notifyMap when the table is updated + class DlgGen(): + def setParams(self, parent, nid, notifyMap): + self.parent = parent + self.nid = nid + self.notifyMap = notifyMap + + def __call__(self): + return DlgNotificationWithDNAA(self.parent, self.parent, \ + self.nid, self.notifyMap, False).exec_() + + self.announceTableWidgets = \ + [[QLabel(''), QRichLabel(''), QLabelButton('+'), DlgGen()] \ + for i in range(10)] + + + + layoutTable = QGridLayout() + for i in range(10): + for j in range(3): + layoutTable.addWidget(self.announceTableWidgets[i][j], i,j) + self.connect(self.announceTableWidgets[i][2], SIGNAL(CLICKED), \ + self.announceTableWidgets[i][3]) + + layoutTable.setColumnStretch(0,0) + layoutTable.setColumnStretch(1,1) + layoutTable.setColumnStretch(2,0) + + frmTable = QFrame() + frmTable.setLayout(layoutTable) + frmTable.setFrameStyle(STYLE_SUNKEN) + + self.updateAnnounceTable() + + + frmEverything = makeVertFrame( [ self.lblAlertStr, + frmLastUpdate, + 'Space(30)', + lblTableHeader, + frmTable, + 'Space(30)', + lblVerHeader, + frmVersions, + 'Stretch']) + + frmEverything.setMinimumWidth(300) + frmEverything.setMaximumWidth(800) + + frmFinal = makeHorizFrame(['Stretch', frmEverything, 'Stretch']) + + self.announceScrollArea = QScrollArea() + self.announceScrollArea.setWidgetResizable(True) + self.announceScrollArea.setWidget(frmFinal) + scrollLayout = QVBoxLayout() + scrollLayout.addWidget(self.announceScrollArea) + self.tabAnnounce.setLayout(scrollLayout) + + self.announceIsSetup = True + + + ############################################################################# + def openDownloaderAll(self): + dl,cl = self.getDownloaderData() + if not dl is None and not cl is None: + UpgradeDownloaderDialog(self, self, None, dl, cl).exec_() + + ############################################################################# + def openDLArmory(self): + dl,cl = self.getDownloaderData() + if not dl is None and not cl is None: + UpgradeDownloaderDialog(self, self, 'Armory', dl, cl).exec_() + + ############################################################################# + def openDLSatoshi(self): + dl,cl = self.getDownloaderData() + if not dl is None and not cl is None: + UpgradeDownloaderDialog(self, self, 'Satoshi', dl, cl).exec_() + + + ############################################################################# + def getDownloaderData(self): + dl = self.announceFetcher.getAnnounceFile('downloads') + cl = self.announceFetcher.getAnnounceFile('changelog') + + dlObj = downloadLinkParser().parseDownloadList(dl) + clObj = changelogParser().parseChangelogText(cl) + + if dlObj is None or clObj is None: + QMessageBox.warning(self, tr('No Data'), tr(""" + The secure downloader has not received any download + data to display. Either the Armory + Technologies, Inc. announcement feeder is + down, or this computer cannot access the server.""") % \ + htmlColor('TextGreen'), QMessageBox.Ok) + return None,None + + lastUpdate = self.announceFetcher.getLastSuccessfulFetchTime() + sinceLastUpd = RightNow() - lastUpdate + if lastUpdate < RightNow()-1*WEEK: + QMessageBox.warning(self, tr('Old Data'), tr(""" + The last update retrieved from the Armory + Technologies, Inc. announcement feeder was %s + ago. The following downloads may not be the latest + available.""") % (htmlColor("TextGreen"), \ + secondsToHumanTime(sinceLastUpd)), QMessageBox.Ok) + + dl = self.announceFetcher.getAnnounceFile('downloads') + cl = self.announceFetcher.getAnnounceFile('changelog') + + return dl,cl + + + + ############################################################################# + def updateAnnounceTab(self, *args): + + if not self.announceIsSetup: + return + + iconArmory = ':/armory_icon_32x32.png' + iconSatoshi = ':/bitcoinlogo.png' + iconInfoFile = ':/MsgBox_info48.png' + iconGoodFile = ':/MsgBox_good48.png' + iconWarnFile = ':/MsgBox_warning48.png' + iconCritFile = ':/MsgBox_critical24.png' + + lastUpdate = self.announceFetcher.getLastSuccessfulFetchTime() + noAnnounce = (lastUpdate == 0) + + if noAnnounce: + self.lblLastUpdated.setText(tr("No announcement data was found!")) + self.btnSecureDLArmory.setVisible(False) + self.icoArmorySWVersion.setVisible(True) + self.lblArmorySWVersion.setText(tr(""" You are running Armory + version %s""") % getVersionString(BTCARMORY_VERSION)) + else: + updTimeStr = unixTimeToFormatStr(lastUpdate) + self.lblLastUpdated.setText(tr("Last Updated: %s") % updTimeStr) + + + verStrToInt = lambda s: getVersionInt(readVersionString(s)) + + # Notify of Armory updates + self.icoArmorySWVersion.setPixmap(QPixmap(iconArmory).scaled(24,24)) + self.icoSatoshiSWVersion.setPixmap(QPixmap(iconSatoshi).scaled(24,24)) + + try: + armCurrent = verStrToInt(self.armoryVersions[0]) + armLatest = verStrToInt(self.armoryVersions[1]) + if armCurrent >= armLatest: + dispIcon = QPixmap(iconArmory).scaled(24,24) + self.icoArmorySWVersion.setPixmap(dispIcon) + self.btnSecureDLArmory.setVisible(False) + self.lblArmorySWVersion.setText(tr(""" + You are using the latest version of Armory""")) + else: + dispIcon = QPixmap(iconWarnFile).scaled(24,24) + self.icoArmorySWVersion.setPixmap(dispIcon) + self.btnSecureDLArmory.setVisible(True) + self.lblArmorySWVersion.setText(tr(""" + There is a newer version of Armory available!""")) + self.btnSecureDLArmory.setVisible(True) + self.icoArmorySWVersion.setVisible(True) + except: + self.btnSecureDLArmory.setVisible(False) + self.lblArmorySWVersion.setText(tr(""" You are running Armory + version %s""") % getVersionString(BTCARMORY_VERSION)) + + + try: + satCurrStr,satLastStr = self.satoshiVersions + satCurrent = verStrToInt(satCurrStr) if satCurrStr else 0 + satLatest = verStrToInt(satLastStr) if satLastStr else 0 + + # Show CoreBTC updates + if satCurrent and satLatest: + if satCurrent >= satLatest: + dispIcon = QPixmap(iconGoodFile).scaled(24,24) + self.btnSecureDLSatoshi.setVisible(False) + self.icoSatoshiSWVersion.setPixmap(dispIcon) + self.lblSatoshiSWVersion.setText(tr(""" You are using + the latest version of core Bitcoin (%s)""") % satCurrStr) + else: + dispIcon = QPixmap(iconWarnFile).scaled(24,24) + self.btnSecureDLSatoshi.setVisible(True) + self.icoSatoshiSWVersion.setPixmap(dispIcon) + self.lblSatoshiSWVersion.setText(tr(""" + There is a newer version of the core Bitcoin software + available!""")) + elif satCurrent: + # satLatest is not available + dispIcon = QPixmap(iconGoodFile).scaled(24,24) + self.btnSecureDLSatoshi.setVisible(False) + self.icoSatoshiSWVersion.setPixmap(None) + self.lblSatoshiSWVersion.setText(tr(""" You are using + core Bitcoin version %s""") % satCurrStr) + elif satLatest: + # only satLatest is avail (maybe offline) + dispIcon = QPixmap(iconSatoshi).scaled(24,24) + self.btnSecureDLSatoshi.setVisible(True) + self.icoSatoshiSWVersion.setPixmap(dispIcon) + self.lblSatoshiSWVersion.setText(tr("""Core Bitcoin version + %s is available.""") % satLastStr) + else: + # only satLatest is avail (maybe offline) + dispIcon = QPixmap(iconSatoshi).scaled(24,24) + self.btnSecureDLSatoshi.setVisible(False) + self.icoSatoshiSWVersion.setPixmap(dispIcon) + self.lblSatoshiSWVersion.setText(tr("""No version information + is available for core Bitcoin""") ) + + + + + #self.btnSecureDLSatoshi.setVisible(False) + #if self.satoshiVersions[0]: + #self.lblSatoshiSWVersion.setText(tr(""" You are running + #core Bitcoin software version %s""") % self.satoshiVersions[0]) + #else: + #self.lblSatoshiSWVersion.setText(tr("""No information is + #available for the core Bitcoin software""")) + except: + LOGEXCEPT('Failed to process satoshi versions') + + + self.updateAnnounceTable() + + + ############################################################################# + def updateAnnounceTable(self): + + # Default: Make everything non-visible except first row, middle column + for i in range(10): + for j in range(3): + self.announceTableWidgets[i][j].setVisible(i==0 and j==1) + + if len(self.almostFullNotificationList)==0: + self.announceTableWidgets[0][1].setText(tr(""" + There are no announcements or alerts to display""")) + return + + + alertsForSorting = [] + for nid,nmap in self.almostFullNotificationList.iteritems(): + alertsForSorting.append([nid, int(nmap['PRIORITY'])]) + + sortedAlerts = sorted(alertsForSorting, key=lambda a: -a[1])[:10] + + i = 0 + for nid,priority in sortedAlerts: + if priority>=4096: + pixm = QPixmap(':/MsgBox_critical64.png') + elif priority>=3072: + pixm = QPixmap(':/MsgBox_warning48.png') + elif priority>=2048: + pixm = QPixmap(':/MsgBox_info48.png') + else: + pixm = QPixmap(':/MsgBox_info48.png') + + + shortDescr = self.almostFullNotificationList[nid]['SHORTDESCR'] + if priority>=4096: + shortDescr = '' + shortDescr + '' + shortDescr = shortDescr % htmlColor('TextWarn') + + self.announceTableWidgets[i][0].setPixmap(pixm.scaled(24,24)) + self.announceTableWidgets[i][1].setText(shortDescr) + self.announceTableWidgets[i][2].setVisible(True) + self.announceTableWidgets[i][3].setParams(self, nid, \ + self.almostFullNotificationList[nid]) + + for j in range(3): + self.announceTableWidgets[i][j].setVisible(True) + + i += 1 + + + + + + ############################################################################# + def explicitCheckAnnouncements(self, waitTime=3): + self.announceFetcher.fetchRightNow(waitTime) + self.processAnnounceData() + self.updateAnnounceTab() + + ############################################################################# def installSatoshiClient(self, closeWhenDone=False): @@ -3610,7 +4625,7 @@ def installSatoshiClient(self, closeWhenDone=False): 'download the installer yourself.') webbrowser.open('http://www.bitcoin.org/en/download') return - + print self.downloadDict['SATOSHI']['Windows'] theLink = self.downloadDict['SATOSHI']['Windows'][0] theHash = self.downloadDict['SATOSHI']['Windows'][1] @@ -3626,24 +4641,24 @@ def installSatoshiClient(self, closeWhenDone=False): 'to download and install Bitcoin-Qt manually.', QMessageBox.Ok) webbrowser.open('http://www.bitcoin.org/en/download') return - + installerPath = os.path.join(ARMORY_HOME_DIR, os.path.basename(theLink)) LOGINFO('Installer path: %s', installerPath) instFile = open(installerPath, 'wb') instFile.write(fileData) instFile.close() - + def startInstaller(): execAndWait('"'+installerPath+'"', useStartInfo=False) self.startBitcoindIfNecessary() - + DlgExecLongProcess(startInstaller, tr(""" - Please Complete Bitcoin Installation
(installer should + Please Complete Bitcoin Installation
(installer should have opened in your taskbar)"""), self, self).exec_() elif OS_MACOSX: LOGERROR('Cannot install on OSX') - + if closeWhenDone: self.closeForReal(None) @@ -3661,55 +4676,179 @@ def closeExistingBitcoin(self): 'Attempted to kill the running Bitcoin-Qt/bitcoind instance, ' 'but it was not found. ', QMessageBox.Ok) - ############################################################################# - def getPercentageFinished(self, maxblk, lastblk): - curr = EstimateCumulativeBlockchainSize(lastblk) - maxb = EstimateCumulativeBlockchainSize(maxblk) - return float(curr)/float(maxb) + ############################################################################# + def getPercentageFinished(self, maxblk, lastblk): + curr = EstimateCumulativeBlockchainSize(lastblk) + maxb = EstimateCumulativeBlockchainSize(maxblk) + return float(curr)/float(maxb) + + ############################################################################# + def updateSyncProgress(self): + + if TheTDM.getTDMState()=='Downloading': + + dlSpeed = TheTDM.getLastStats('downRate') + timeEst = TheTDM.getLastStats('timeEst') + fracDone = TheTDM.getLastStats('fracDone') + numSeeds = TheTDM.getLastStats('numSeeds') + numPeers = TheTDM.getLastStats('numPeers') + + self.barProgressTorrent.setVisible(True) + self.lblDashModeTorrent.setVisible(True) + self.lblTimeLeftTorrent.setVisible(True) + self.lblTorrentStats.setVisible(True) + self.barProgressTorrent.setFormat('%p%') + + self.lblDashModeSync.setVisible(True) + self.barProgressSync.setVisible(True) + self.barProgressSync.setValue(0) + self.lblTimeLeftSync.setVisible(True) + self.barProgressSync.setFormat('') + + self.lblDashModeBuild.setVisible(True) + self.barProgressBuild.setVisible(True) + self.barProgressBuild.setValue(0) + self.lblTimeLeftBuild.setVisible(True) + self.barProgressBuild.setFormat('') + + self.lblDashModeScan.setVisible(True) + self.barProgressScan.setVisible(True) + self.barProgressScan.setValue(0) + self.lblTimeLeftScan.setVisible(True) + self.barProgressScan.setFormat('') + + if not numSeeds: + self.barProgressTorrent.setValue(0) + self.lblTimeLeftTorrent.setText('') + self.lblTorrentStats.setText('') + + self.lblDashModeTorrent.setText(tr('Initializing Torrent Engine'), \ + size=4, bold=True, color='Foreground') + + self.lblTorrentStats.setVisible(False) + else: + self.lblDashModeTorrent.setText(tr('Downloading via Armory CDN'), \ + size=4, bold=True, color='Foreground') + + if fracDone: + self.barProgressTorrent.setValue(int(99.9*fracDone)) + + if timeEst: + self.lblTimeLeftTorrent.setText(secondsToHumanTime(timeEst)) + + self.lblTorrentStats.setText(tr(""" + Bootstrap Torrent: %s/sec from %d peers""") % \ + (bytesToHumanSize(dlSpeed), numSeeds+numPeers)) + + self.lblTorrentStats.setVisible(True) + + - ############################################################################# - def updateSyncProgress(self): + elif TheBDM.getBDMState()=='Scanning': + self.barProgressTorrent.setVisible(TheTDM.isStarted()) + self.lblDashModeTorrent.setVisible(TheTDM.isStarted()) + self.barProgressTorrent.setValue(100) + self.lblTimeLeftTorrent.setVisible(False) + self.lblTorrentStats.setVisible(False) + self.barProgressTorrent.setFormat('') + + self.lblDashModeSync.setVisible(self.doAutoBitcoind) + self.barProgressSync.setVisible(self.doAutoBitcoind) + self.barProgressSync.setValue(100) + self.lblTimeLeftSync.setVisible(False) + self.barProgressSync.setFormat('') + + self.lblDashModeBuild.setVisible(True) + self.barProgressBuild.setVisible(True) + self.lblTimeLeftBuild.setVisible(True) + + self.lblDashModeScan.setVisible(True) + self.barProgressScan.setVisible(True) + self.lblTimeLeftScan.setVisible(True) - if TheBDM.getBDMState()=='Scanning': # Scan time is super-simple to predict: it's pretty much linear # with the number of bytes remaining. + phase,pct,rate,tleft = TheBDM.predictLoadTime() if phase==1: - self.lblDashModeScan.setText( 'Building Databases', \ + self.lblDashModeBuild.setText( 'Building Databases', \ size=4, bold=True, color='Foreground') + self.lblDashModeScan.setText( 'Scan Transaction History', \ + size=4, bold=True, color='DisableFG') + self.barProgressBuild.setFormat('%p%') + self.barProgressScan.setFormat('') + elif phase==3: + self.lblDashModeBuild.setText( 'Build Databases', \ + size=4, bold=True, color='DisableFG') self.lblDashModeScan.setText( 'Scanning Transaction History', \ size=4, bold=True, color='Foreground') + self.lblTimeLeftBuild.setVisible(False) + self.barProgressBuild.setFormat('') + self.barProgressBuild.setValue(100) + self.barProgressScan.setFormat('%p%') elif phase==4: self.lblDashModeScan.setText( 'Global Blockchain Index', \ size=4, bold=True, color='Foreground') - self.barProgressSync.setValue(100) tleft15 = (int(tleft-1)/15 + 1)*15 if tleft < 2: - self.lblTimeLeftScan.setText('') - self.barProgressScan.setValue(1) + tstring = '' + pvalue = 100 else: - self.lblTimeLeftScan.setText(secondsToHumanTime(tleft15)) - self.barProgressScan.setValue(pct*100) + tstring = secondsToHumanTime(tleft15) + pvalue = pct*100 + + if phase==1: + self.lblTimeLeftBuild.setText(tstring) + self.barProgressBuild.setValue(pvalue) + elif phase==3: + self.lblTimeLeftScan.setText(tstring) + self.barProgressScan.setValue(pvalue) elif TheSDM.getSDMState() in ['BitcoindInitializing','BitcoindSynchronizing']: - ssdm = TheSDM.getSDMState() + + self.barProgressTorrent.setVisible(TheTDM.isStarted()) + self.lblDashModeTorrent.setVisible(TheTDM.isStarted()) + self.barProgressTorrent.setValue(100) + self.lblTimeLeftTorrent.setVisible(False) + self.lblTorrentStats.setVisible(False) + self.barProgressTorrent.setFormat('') + + self.lblDashModeSync.setVisible(True) + self.barProgressSync.setVisible(True) + self.lblTimeLeftSync.setVisible(True) + self.barProgressSync.setFormat('%p%') + + self.lblDashModeBuild.setVisible(True) + self.barProgressBuild.setVisible(True) + self.lblTimeLeftBuild.setVisible(False) + self.barProgressBuild.setValue(0) + self.barProgressBuild.setFormat('') + + self.lblDashModeScan.setVisible(True) + self.barProgressScan.setVisible(True) + self.lblTimeLeftScan.setVisible(False) + self.barProgressScan.setValue(0) + self.barProgressScan.setFormat('') + + ssdm = TheSDM.getSDMState() lastBlkNum = self.getSettingOrSetDefault('LastBlkRecv', 0) lastBlkTime = self.getSettingOrSetDefault('LastBlkRecvTime', 0) - + # Get data from SDM if it has it info = TheSDM.getTopBlockInfo() if len(info['tophash'])>0: lastBlkNum = info['numblks'] lastBlkTime = info['toptime'] - + # Use a reference point if we are starting from scratch - refBlock = max(231747, lastBlkNum) - refTime = max(1366171579, lastBlkTime) + refBlock = max(290746, lastBlkNum) + refTime = max(1394922889, lastBlkTime) + - - # Ten min/block is pretty accurate, even at genesis blk (about 1% slow) + # Ten min/block is pretty accurate, even from genesis (about 1% slow) + # And it gets better as we sync past the reference block above self.approxMaxBlock = refBlock + int((RightNow() - refTime) / (10*MINUTE)) self.approxBlkLeft = self.approxMaxBlock - lastBlkNum self.approxPctSoFar = self.getPercentageFinished(self.approxMaxBlock, \ @@ -3735,21 +4874,22 @@ def updateSyncProgress(self): else: timeRemain = None - + intPct = int(100*self.approxPctSoFar) strPct = '%d%%' % intPct - + + self.barProgressSync.setFormat('%p%') if ssdm == 'BitcoindReady': return (0,0,0.99) # because it's probably not completely done... self.lblTimeLeftSync.setText('Almost Done...') self.barProgressSync.setValue(99) elif ssdm == 'BitcoindSynchronizing': - self.barProgressSync.setValue(int(99.9*self.approxPctSoFar)) + sdmPercent = int(99.9*self.approxPctSoFar) if self.approxBlkLeft < 10000: if self.approxBlkLeft < 200: self.lblTimeLeftSync.setText('%d blocks' % self.approxBlkLeft) - else: + else: # If we're within 10k blocks, estimate based on blkspersec if info['blkspersec'] > 0: timeleft = int(self.approxBlkLeft/info['blkspersec']) @@ -3757,16 +4897,20 @@ def updateSyncProgress(self): else: # If we're more than 10k blocks behind... if timeRemain: - timeRemain = min(8*HOUR, timeRemain) + timeRemain = min(24*HOUR, timeRemain) self.lblTimeLeftSync.setText(secondsToHumanTime(timeRemain)) else: self.lblTimeLeftSync.setText('') elif ssdm == 'BitcoindInitializing': - self.barProgressSync.setValue(0) + sdmPercent = 0 self.barProgressSync.setFormat('') + self.barProgressBuild.setFormat('') + self.barProgressScan.setFormat('') else: LOGERROR('Should not predict sync info in non init/sync SDM state') return ('UNKNOWN','UNKNOWN', 'UNKNOWN') + + self.barProgressSync.setValue(sdmPercent) else: LOGWARN('Called updateSyncProgress while not sync\'ing') @@ -3774,7 +4918,7 @@ def updateSyncProgress(self): ############################################################################# def GetDashFunctionalityText(self, func): """ - Outsourcing all the verbose dashboard text to here, to de-clutter the + Outsourcing all the verbose dashboard text to here, to de-clutter the logic paths in the setDashboardDetails function """ LOGINFO('Switching Armory functional mode to "%s"', func) @@ -3827,12 +4971,12 @@ def GetDashFunctionalityText(self, func): '
  • Create transactions with watching-only wallets, ' 'to be signed by an offline wallets
  • ' '') - + ############################################################################# def GetDashStateText(self, mgmtMode, state): """ - Outsourcing all the verbose dashboard text to here, to de-clutter the + Outsourcing all the verbose dashboard text to here, to de-clutter the logic paths in the setDashboardDetails function """ LOGINFO('Switching Armory state text to Mgmt:%s, State:%s', mgmtMode, state) @@ -3840,28 +4984,28 @@ def GetDashStateText(self, mgmtMode, state): # A few states don't care which mgmtMode you are in... if state == 'NewUserInfo': return tr(""" - For more information about Armory, and even Bitcoin itself, you should - visit the frequently - asked questions page. If - you are experiencing problems using this software, please visit the - Armory - troubleshooting webpage. It will be updated frequently with - solutions to common problems. + For more information about Armory, and even Bitcoin itself, you should + visit the frequently + asked questions page. If + you are experiencing problems using this software, please visit the + Armory + troubleshooting webpage. It will be updated frequently with + solutions to common problems.

    - IMPORTANT: Make a backup of your wallet(s)! Paper - backups protect you forever against forgotten passwords, - hard-drive failure, and make it easy for your family to recover - your funds if something terrible happens to you. Each wallet - only needs to be backed up once, ever! Without it, you are at - risk of losing all of your Bitcoins! For more information, - visit the Armory + IMPORTANT: Make a backup of your wallet(s)! Paper + backups protect you forever against forgotten passwords, + hard-drive failure, and make it easy for your family to recover + your funds if something terrible happens to you. Each wallet + only needs to be backed up once, ever! Without it, you are at + risk of losing all of your Bitcoins! For more information, + visit the Armory Backups page.

    - To learn about improving your security through the use of offline - wallets, visit the - Armory - Quick Start Guide, and the - Offline + To learn about improving your security through the use of offline + wallets, visit the + Armory + Quick Start Guide, and the + Offline Wallet Tutorial.

    """) elif state == 'OnlineFull1': return ( \ @@ -3879,7 +5023,7 @@ def GetDashStateText(self, mgmtMode, state): 'Bitcoin-Qt window if it is synchronized. If not, it is ' 'recommended you close Armory and restart it only when you ' 'see that checkmark.' - '

    ' if not self.doManageSatoshi else '') + ( + '

    ' if not self.doAutoBitcoind else '') + ( 'Please backup your wallets! Armory wallets are ' '"deterministic", meaning they only need to be backed up ' 'one time (unless you have imported external addresses/keys). ' @@ -3929,7 +5073,7 @@ def GetDashStateText(self, mgmtMode, state): 'http://www.bitcoin.org.') # Branch the available display text based on which Satoshi-Management - # mode Armory is using. It probably wasn't necessary to branch the + # mode Armory is using. It probably wasn't necessary to branch the # the code like this, but it helped me organize the seemingly-endless # number of dashboard screens I need if mgmtMode.lower()=='user': @@ -3950,7 +5094,7 @@ def GetDashStateText(self, mgmtMode, state): bitconf = os.path.join(BTC_HOME_DIR, 'bitcoin.conf') return ( \ 'You are currently in offline mode because ' - 'Bitcoin-Qt is not running. To switch to online ' + 'Bitcoin-Qt is not running. To switch to online ' 'mode, start Bitcoin-Qt and let it synchronize with the network ' '-- you will see a green checkmark in the bottom-right corner when ' 'it is complete. If Bitcoin-Qt is already running and you believe ' @@ -4038,20 +5182,25 @@ def GetDashStateText(self, mgmtMode, state): 'to manage it yourself, please adjust your settings and ' 'restart Armory.') if state == 'InitializingLongTime': - return ( \ - 'To maximize your security, the Bitcoin engine is downloading ' - 'and verifying the global transaction ledger. This will take ' - 'several hours, but only needs to be done once! It is usually ' - 'best to leave it running over night for this initialization process. ' - 'Subsequent loads will only take a few minutes.' - '

    ' - 'While you wait, you can manage your wallets. Make new wallets, ' - 'make digital or paper backups, create Bitcoin addresses to receive ' - 'payments, ' - 'sign messages, and/or import private keys. You will always ' - 'receive Bitcoin payments regardless of whether you are online, ' - 'but you will have to verify that payment through another service ' - 'until Armory is finished this initialization.') + return tr(""" + To maximize your security, the Bitcoin engine is downloading + and verifying the global transaction ledger. This will take + several hours, but only needs to be done once! It is + usually best to leave it running over night for this + initialization process. Subsequent loads will only take a few + minutes. +

    + Please Note: Between Armory and the underlying Bitcoin + engine, you need to have 40-50 GB of spare disk space available + to hold the global transaction history. +

    + While you wait, you can manage your wallets. Make new wallets, + make digital or paper backups, create Bitcoin addresses to receive + payments, + sign messages, and/or import private keys. You will always + receive Bitcoin payments regardless of whether you are online, + but you will have to verify that payment through another service + until Armory is finished this initialization.""") if state == 'InitializingDoneSoon': return ( \ 'The software is downloading and processing the latest activity ' @@ -4128,43 +5277,42 @@ def GetDashStateText(self, mgmtMode, state): soutDisp = 'StdOut: %s' % soutHtml serrDisp = 'StdErr: %s' % serrHtml if len(sout)>0 or len(serr)>0: - return ( \ - 'There was an error starting the underlying Bitcoin engine. ' - 'This should not normally happen. Usually it occurs when you ' - 'have been using Bitcoin-Qt prior to using Armory, especially ' - 'if you have upgraded or downgraded Bitcoin-Qt recently (manually, ' - 'or through the Armory automatic installation). Output from ' - 'bitcoind:
    ' + - (soutDisp if len(sout)>0 else '') + + return (tr(""" + There was an error starting the underlying Bitcoin engine. + This should not normally happen. Usually it occurs when you + have been using Bitcoin-Qt prior to using Armory, especially + if you have upgraded or downgraded Bitcoin-Qt recently. + Output from bitcoind:
    """) + \ + (soutDisp if len(sout)>0 else '') + \ (serrDisp if len(serr)>0 else '') ) else: return ( tr(""" - There was an error starting the underlying Bitcoin engine. - This should not normally happen. Usually it occurs when you - have been using Bitcoin-Qt prior to using Armory, especially - if you have upgraded or downgraded Bitcoin-Qt recently (manually, - or through the Armory automatic installation). + There was an error starting the underlying Bitcoin engine. + This should not normally happen. Usually it occurs when you + have been using Bitcoin-Qt prior to using Armory, especially + if you have upgraded or downgraded Bitcoin-Qt recently.

    - Unfortunately, this error is so strange, Armory does not - recognize it. Please go to "Export Log File" from the "File" + Unfortunately, this error is so strange, Armory does not + recognize it. Please go to "Export Log File" from the "File" menu and email at as an attachment to - support@bitcoinarmory.com. We apologize for the + support@bitcoinarmory.com. We apologize for the inconvenience!""")) - + ############################################################################# + @TimeThisFunction def setDashboardDetails(self, INIT=False): """ We've dumped all the dashboard text into the above 2 methods in order to declutter this method. """ - - TimerStart('setDashboardDetails') onlineAvail = self.onlineModeIsPossible() sdmState = TheSDM.getSDMState() bdmState = TheBDM.getBDMState() + tdmState = TheTDM.getTDMState() + descr = '' descr1 = '' descr2 = '' @@ -4177,14 +5325,28 @@ def setSyncRowVisible(b): self.lblDashModeSync.setVisible(b) self.barProgressSync.setVisible(b) self.lblTimeLeftSync.setVisible(b) - + + + def setTorrentRowVisible(b): + self.lblDashModeTorrent.setVisible(b) + self.barProgressTorrent.setVisible(b) + self.lblTimeLeftTorrent.setVisible(b) + self.lblTorrentStats.setVisible(b) + + def setBuildRowVisible(b): + self.lblDashModeBuild.setVisible(b) + self.barProgressBuild.setVisible(b) + self.lblTimeLeftBuild.setVisible(b) + def setScanRowVisible(b): self.lblDashModeScan.setVisible(b) self.barProgressScan.setVisible(b) self.lblTimeLeftScan.setVisible(b) def setOnlyDashModeVisible(): + setTorrentRowVisible(False) setSyncRowVisible(False) + setBuildRowVisible(False) setScanRowVisible(False) self.lblBusy.setVisible(False) self.btnModeSwitch.setVisible(False) @@ -4206,15 +5368,18 @@ def setBtnFrameVisible(b, descr=''): setOnlyDashModeVisible() self.btnModeSwitch.setVisible(False) - if self.doManageSatoshi and not sdmState=='BitcoindReady': + # This keeps popping up for some reason! + self.lblTorrentStats.setVisible(False) + + if self.doAutoBitcoind and not sdmState=='BitcoindReady': # User is letting Armory manage the Satoshi client for them. - + if not sdmState==self.lastSDMState: self.lblBusy.setVisible(False) self.btnModeSwitch.setVisible(False) - # There's a whole bunch of stuff that has to be hidden/shown + # There's a whole bunch of stuff that has to be hidden/shown # depending on the state... set some reasonable defaults here setBtnFrameVisible(False) setBtnRowVisible(DASHBTNS.Install, False) @@ -4222,9 +5387,9 @@ def setBtnFrameVisible(b, descr=''): setBtnRowVisible(DASHBTNS.Instruct, False) setBtnRowVisible(DASHBTNS.Settings, True) setBtnRowVisible(DASHBTNS.Close, False) - + if not (self.forceOnline or self.internetAvail) or CLI_OPTIONS.offline: - self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Transactions, False) + self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Ledger, False) setOnlyDashModeVisible() self.lblDashModeSync.setText( 'Armory is offline', \ size=4, color='TextWarn', bold=True) @@ -4258,9 +5423,9 @@ def setBtnFrameVisible(b, descr=''): descr2 += self.GetDashFunctionalityText('Offline') self.lblDashDescr1.setText(descr1) self.lblDashDescr2.setText(descr2) - elif not TheSDM.isRunningBitcoind(): + elif not TheSDM.isRunningBitcoind() and not TheTDM.isRunning(): setOnlyDashModeVisible() - self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Transactions, False) + self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Ledger, False) self.lblDashModeSync.setText( 'Armory is offline', \ size=4, color='TextWarn', bold=True) # Bitcoind is not being managed, but we want it to be @@ -4340,7 +5505,7 @@ def setBtnFrameVisible(b, descr=''): self.notAvailErrorCount += 1 #if self.notAvailErrorCount < 5: #LOGERROR('Auto-mode-switch') - #self.pressModeSwitchButton() + #self.executeModeSwitch() descr1 += '' descr2 += self.GetDashFunctionalityText('Offline') self.lblDashDescr1.setText(descr1) @@ -4353,33 +5518,71 @@ def setBtnFrameVisible(b, descr=''): self.lblDashDescr2.setText(descr2) else: # online detected/forced, and TheSDM has already been started if sdmState in ['BitcoindWrongPassword', 'BitcoindNotAvailable']: - setOnlyDashModeVisible() - self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Transactions, False) + + extraTxt = '' + if not self.wasSynchronizing: + setOnlyDashModeVisible() + else: + extraTxt = tr(""" + Armory has lost connection to the + core Bitcoin software. If you did not do anything + that affects your network connection or the bitcoind + process, it will probably recover on its own in a + couple minutes

    """) + self.lblTimeLeftSync.setVisible(False) + self.barProgressSync.setFormat('') + + + self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Ledger, False) LOGINFO('Dashboard switched to auto-BadConnection') self.lblDashModeSync.setText( 'Armory is offline', \ size=4, color='TextWarn', bold=True) descr1 += self.GetDashStateText('Auto', 'OfflineBadConnection') descr2 += self.GetDashFunctionalityText('Offline') - self.lblDashDescr1.setText(descr1) + self.lblDashDescr1.setText(extraTxt + descr1) self.lblDashDescr2.setText(descr2) - elif sdmState in ['BitcoindInitializing', 'BitcoindSynchronizing']: + elif sdmState in ['BitcoindInitializing', \ + 'BitcoindSynchronizing', \ + 'TorrentSynchronizing']: + self.wasSynchronizing = True LOGINFO('Dashboard switched to auto-InitSync') - self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Transactions, False) + self.lblBusy.setVisible(True) + self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Ledger, False) self.updateSyncProgress() + + + # If torrent ever ran, leave it visible setSyncRowVisible(True) setScanRowVisible(True) - self.lblBusy.setVisible(True) + setTorrentRowVisible(TheTDM.isStarted()) - if sdmState=='BitcoindInitializing': + if TheTDM.isRunning(): + self.lblDashModeTorrent.setText('Downloading via Armory CDN', \ + size=4, bold=True, color='Foreground') + self.lblDashModeSync.setText( 'Synchronizing with Network', \ + size=4, bold=True, color='DisableFG') + self.lblTorrentStats.setVisible(True) + elif sdmState=='BitcoindInitializing': + self.lblDashModeTorrent.setText('Download via Armory CDN', \ + size=4, bold=True, color='DisableFG') self.lblDashModeSync.setText( 'Initializing Bitcoin Engine', \ size=4, bold=True, color='Foreground') + self.lblTorrentStats.setVisible(False) else: + self.lblDashModeTorrent.setText('Download via Armory CDN', \ + size=4, bold=True, color='DisableFG') self.lblDashModeSync.setText( 'Synchronizing with Network', \ size=4, bold=True, color='Foreground') + self.lblTorrentStats.setVisible(False) - self.lblDashModeScan.setText( 'Build Databases and Scan', \ + + self.lblDashModeBuild.setText( 'Build Databases', \ + size=4, bold=True, color='DisableFG') + self.lblDashModeScan.setText( 'Scan Transaction History', \ size=4, bold=True, color='DisableFG') - if self.approxBlkLeft > 1440: # more than 10 days + + # If more than 10 days behind, or still downloading torrent + if tdmState=='Downloading' or self.approxBlkLeft > 1440: descr1 += self.GetDashStateText('Auto', 'InitializingLongTime') descr2 += self.GetDashStateText('Auto', 'NewUserInfo') else: @@ -4391,7 +5594,7 @@ def setBtnFrameVisible(b, descr=''): 'Since version 0.88, Armory runs bitcoind in the ' 'background. You can switch back to ' 'the old way in the Settings dialog. ') - + descr2 += self.GetDashFunctionalityText('Offline') self.lblDashDescr1.setText(descr1) self.lblDashDescr2.setText(descr2) @@ -4401,7 +5604,7 @@ def setBtnFrameVisible(b, descr=''): if bdmState in ('Offline', 'Uninitialized'): if onlineAvail and not self.lastBDMState[1]==onlineAvail: LOGINFO('Dashboard switched to user-OfflineOnlinePoss') - self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Transactions, False) + self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Ledger, False) setOnlyDashModeVisible() self.lblBusy.setVisible(False) self.btnModeSwitch.setVisible(True) @@ -4412,14 +5615,14 @@ def setBtnFrameVisible(b, descr=''): descr += self.GetDashFunctionalityText('Offline') self.lblDashDescr1.setText(descr) elif not onlineAvail and not self.lastBDMState[1]==onlineAvail: - self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Transactions, False) + self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Ledger, False) setOnlyDashModeVisible() self.lblBusy.setVisible(False) self.btnModeSwitch.setVisible(False) self.btnModeSwitch.setEnabled(False) self.lblDashModeSync.setText( 'Armory is offline', \ size=4, color='TextWarn', bold=True) - + if not self.bitcoindIsAvailable(): if self.internetAvail: descr = self.GetDashStateText('User','OfflineNoSatoshi') @@ -4434,14 +5637,14 @@ def setBtnFrameVisible(b, descr=''): descr = self.GetDashStateText('User', 'OfflineNoInternet') elif not self.checkHaveBlockfiles(): descr = self.GetDashStateText('User', 'OfflineNoBlkFiles') - - descr += '

    ' + + descr += '

    ' descr += self.GetDashFunctionalityText('Offline') self.lblDashDescr1.setText(descr) - + elif bdmState == 'BlockchainReady': setOnlyDashModeVisible() - self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Transactions, True) + self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Ledger, True) self.lblBusy.setVisible(False) if self.netMode == NETWORKMODE.Disconnected: self.btnModeSwitch.setVisible(False) @@ -4453,7 +5656,7 @@ def setBtnFrameVisible(b, descr=''): LOGINFO('Dashboard switched to online-but-dirty mode') self.btnModeSwitch.setVisible(True) self.btnModeSwitch.setText('Rescan Now') - self.mainDisplayTabs.setCurrentIndex(self.MAINTABS.Dashboard) + self.mainDisplayTabs.setCurrentIndex(self.MAINTABS.Dash) self.lblDashModeSync.setText( 'Armory is online, but needs to rescan ' \ 'the blockchain', size=4, color='TextWarn', bold=True) if len(self.sweepAfterScanList) > 0: @@ -4465,12 +5668,12 @@ def setBtnFrameVisible(b, descr=''): LOGINFO('Dashboard switched to fully-online mode') self.btnModeSwitch.setVisible(False) self.lblDashModeSync.setText( 'Armory is online!', color='TextGreen', size=4, bold=True) - self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Transactions, True) + self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Ledger, True) descr = self.GetDashStateText('User', 'OnlineFull1') descr += self.GetDashFunctionalityText('Online') descr += self.GetDashStateText('User', 'OnlineFull2') self.lblDashDescr1.setText(descr) - #self.mainDisplayTabs.setCurrentIndex(self.MAINTABS.Dashboard) + #self.mainDisplayTabs.setCurrentIndex(self.MAINTABS.Dash) elif bdmState == 'Scanning': LOGINFO('Dashboard switched to "Scanning" mode') self.updateSyncProgress() @@ -4492,39 +5695,47 @@ def setBtnFrameVisible(b, descr=''): self.lblTimeLeftSync.setVisible(False) self.lblDashModeSync.setVisible(False) - if len(str(self.lblDashModeScan.text()).strip()) == 0: - self.lblDashModeScan.setText( 'Preparing Databases', \ + if len(str(self.lblDashModeBuild.text()).strip()) == 0: + self.lblDashModeBuild.setText( 'Preparing Databases', \ size=4, bold=True, color='Foreground') - self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Transactions, False) - + + if len(str(self.lblDashModeScan.text()).strip()) == 0: + self.lblDashModeScan.setText( 'Scan Transaction History', \ + size=4, bold=True, color='DisableFG') + + self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Ledger, False) + if len(self.walletMap)==0: descr = self.GetDashStateText('User','ScanNoWallets') else: descr = self.GetDashStateText('User','ScanWithWallets') - - descr += self.GetDashStateText('Auto', 'NewUserInfo') + + descr += self.GetDashStateText('Auto', 'NewUserInfo') descr += self.GetDashFunctionalityText('Scanning') + '
    ' self.lblDashDescr1.setText(descr) self.lblDashDescr2.setText('') - self.mainDisplayTabs.setCurrentIndex(self.MAINTABS.Dashboard) + self.mainDisplayTabs.setCurrentIndex(self.MAINTABS.Dash) else: LOGERROR('What the heck blockchain mode are we in? %s', bdmState) - + self.lastBDMState = [bdmState, onlineAvail] self.lastSDMState = sdmState - self.lblDashModeSync.setContentsMargins(50,5,50,5) - self.lblDashModeScan.setContentsMargins(50,5,50,5) + self.lblDashModeTorrent.setContentsMargins( 50,5,50,5) + self.lblDashModeSync.setContentsMargins( 50,5,50,5) + self.lblDashModeBuild.setContentsMargins(50,5,50,5) + self.lblDashModeScan.setContentsMargins( 50,5,50,5) vbar = self.dashScrollArea.verticalScrollBar() - vbar.setValue(vbar.minimum()) - - TimerStop('setDashboardDetails') - - + + # On Macs, this causes the main window scroll area to keep bouncing back + # to the top. Not setting the value seems to fix it. DR - 2014/02/12 + if not OS_MACOSX: + vbar.setValue(vbar.minimum()) + ############################################################################# def createToolTipWidget(self, tiptext, iconSz=2): """ - The is to signal to Qt that it should be interpretted as HTML/Rich - text even if no HTML tags are used. This appears to be necessary for Qt + The is to signal to Qt that it should be interpretted as HTML/Rich + text even if no HTML tags are used. This appears to be necessary for Qt to wrap the tooltip text """ fgColor = htmlColor('ToolTipQ') @@ -4532,68 +5743,43 @@ def createToolTipWidget(self, tiptext, iconSz=2): lbl.setToolTip('' + tiptext) lbl.setMaximumWidth(relaxedSizeStr(lbl, '(?)')[0]) def pressEv(ev): - DlgTooltip(self, lbl, tiptext).exec_() + QWhatsThis.showText(ev.globalPos(), tiptext, self) lbl.mousePressEvent = pressEv return lbl - ############################################################################# - def checkSatoshiVersion(self): - timeAlive = long(RightNow()) - self.bornOnTime - if not CLI_OPTIONS.skipVerCheck and \ - (timeAlive%900==0 or self.satoshiLatestVer==None): - try: - # Will eventually make a specially-signed file just for this - # kind of information. For now, it's all in the versions.txt - if not self.netMode==NETWORKMODE.Full or \ - not 'SATOSHI' in self.latestVer or \ - not 'SATOSHI' in self.downloadDict or \ - self.NetworkingFactory.proto==None: - return - - LOGDEBUG('Checking Satoshi Version') - self.checkForLatestVersion() - - self.satoshiLatestVer = self.latestVer['SATOSHI'] - self.satoshiLatestVer = readVersionString(self.satoshiLatestVer) - latestVerInt = getVersionInt(self.satoshiLatestVer) - peerVersion = self.NetworkingFactory.proto.peerInfo['subver'] - peerVersion = peerVersion.split(':')[-1][:-1] - peerVerInt = getVersionInt(readVersionString(peerVersion)) + ############################################################################# + @TimeThisFunction + def checkNewZeroConf(self): + while len(self.newZeroConfSinceLastUpdate)>0: + rawTx = self.newZeroConfSinceLastUpdate.pop() + for wltID in self.walletMap.keys(): + wlt = self.walletMap[wltID] + le = wlt.cppWallet.calcLedgerEntryForTxStr(rawTx) + if not le.getTxHash() == '\x00' * 32: + LOGDEBUG('ZerConf tx for wallet: %s. Adding to notify queue.' % wltID) + notifyIn = self.getSettingOrSetDefault('NotifyBtcIn', not OS_MACOSX) + notifyOut = self.getSettingOrSetDefault('NotifyBtcOut', not OS_MACOSX) + if (le.getValue() <= 0 and notifyOut) or (le.getValue() > 0 and notifyIn): + self.notifyQueue.append([wltID, le, False]) # notifiedAlready=False + self.createCombinedLedger() + self.walletModel.reset() - self.satoshiLatestVer = '0.0' + ############################################################################# + @TimeThisFunction + def newBlockSyncRescanZC(self, prevLedgSize): + didAffectUs = False + for wltID in self.walletMap.keys(): + self.walletMap[wltID].syncWithBlockchainLite() + TheBDM.rescanWalletZeroConf(self.walletMap[wltID].cppWallet) + newLedgerSize = len(self.walletMap[wltID].getTxLedger()) + didAffectUs = prevLedgSize[wltID] != newLedgerSize - LOGINFO('Satoshi Version: Curr: %d, Latest: %d', peerVerInt, latestVerInt) + return didAffectUs - if latestVerInt>peerVerInt and not self.satoshiVerWarnAlready: - LOGINFO('New version available!') - self.satoshiVerWarnAlready = True - doUpgrade = QMessageBox.warning(self, 'Updates Available', \ - 'There is a new version of the Bitcoin software available. ' - 'Newer version usually contain important security updates ' - 'so it is best to upgrade as soon as possible.' - '

    ' - 'Current Bitcoin Version: %s
    ' - 'Available Bitcoin Version: %s' - '

    ' - 'Would you like to upgrade the Bitcoin software?' % \ - (peerVersion, self.latestVer['SATOSHI']) , \ - QMessageBox.Yes | QMessageBox.No) - if doUpgrade==QMessageBox.Yes: - TheSDM.stopBitcoind() - self.setDashboardDetails() - self.installSatoshiClient(closeWhenDone=True) - - - return - except: - LOGEXCEPT('Error in checkSatoshiVersion') - - - - + ############################################################################# ############################################################################# def Heartbeat(self, nextBeatSec=1): """ @@ -4603,13 +5789,13 @@ def Heartbeat(self, nextBeatSec=1): """ # Special heartbeat functions are for special windows that may need - # to update every, say, every 0.1s + # to update every, say, every 0.1s # is all that matters at that moment, like a download progress window. # This is "special" because you are putting all other processing on # hold while this special window is active # IMPORTANT: Make sure that the special heartbeat function returns # a value below zero when it's done OR if it errors out! - # Otherwise, it should return the next heartbeat delay, + # Otherwise, it should return the next heartbeat delay, # which would probably be something like 0.1 for a rapidly # updating progress counter for fn in self.extraHeartbeatSpecial: @@ -4625,21 +5811,70 @@ def Heartbeat(self, nextBeatSec=1): self.extraHeartbeatSpecial = [] reactor.callLater(1, self.Heartbeat) return - + + # TorrentDownloadManager + # SatoshiDaemonManager + # BlockDataManager + tdmState = TheTDM.getTDMState() sdmState = TheSDM.getSDMState() bdmState = TheBDM.getBDMState() #print '(SDM, BDM) State = (%s, %s)' % (sdmState, bdmState) + self.processAnnounceData() + try: for func in self.extraHeartbeatAlways: - func() - + if isinstance(func, list): + fnc = func[0] + kargs = func[1] + keep_running = func[2] + if keep_running == False: + self.extraHeartbeatAlways.remove(func) + fnc(*kargs) + else: + func() + for idx,wltID in enumerate(self.walletIDList): self.walletMap[wltID].checkWalletLockTimeout() - - if self.doManageSatoshi: + + + + if self.doAutoBitcoind: + if TheTDM.isRunning(): + if tdmState=='Downloading': + self.updateSyncProgress() + + downRate = TheTDM.getLastStats('downRate') + self.torrentCircBuffer.append(downRate if downRate else 0) + + # Assumes 1 sec heartbeat + bufsz = len(self.torrentCircBuffer) + if bufsz > 5*MINUTE: + self.torrentCircBuffer = self.torrentCircBuffer[1:] + + if bufsz >= 4.99*MINUTE: + # If dlrate is below 30 kB/s, offer the user a way to skip it + avgDownRate = sum(self.torrentCircBuffer) / float(bufsz) + if avgDownRate < 30*KILOBYTE: + if (RightNow() - self.lastAskedUserStopTorrent) > 5*MINUTE: + self.lastAskedUserStopTorrent = RightNow() + reply = QMessageBox.warning(self, tr('Torrent'), tr(""" + Armory is attempting to use BitTorrent to speed up + the initial synchronization, but it appears to be + downloading slowly or not at all. +

    + If the torrent engine is not starting properly, + or is not downloading + at a reasonable speed for your internet connection, + you should disable it in + File\xe2\x86\x92Settings and then + restart Armory."""), QMessageBox.Ok) + + # For now, just show once then disable + self.lastAskedUserStopTorrent = UINT64_MAX + if sdmState in ['BitcoindInitializing','BitcoindSynchronizing']: self.updateSyncProgress() elif sdmState == 'BitcoindReady': @@ -4649,10 +5884,10 @@ def Heartbeat(self, nextBeatSec=1): elif bdmState == 'Offline': LOGERROR('Bitcoind is ready, but we are offline... ?') elif bdmState=='Scanning': - self.checkSatoshiVersion() self.updateSyncProgress() - if not sdmState==self.lastSDMState or not bdmState==self.lastBDMState[0]: + if not sdmState==self.lastSDMState or \ + not bdmState==self.lastBDMState[0]: self.setDashboardDetails() else: if bdmState in ('Offline','Uninitialized'): @@ -4662,7 +5897,6 @@ def Heartbeat(self, nextBeatSec=1): self.setDashboardDetails() return elif bdmState=='Scanning': - self.checkSatoshiVersion() self.updateSyncProgress() @@ -4674,7 +5908,7 @@ def Heartbeat(self, nextBeatSec=1): self.setDashboardDetails() self.dirtyLastTime = TheBDM.isDirty() - + if bdmState=='BlockchainReady': ##### @@ -4684,7 +5918,7 @@ def Heartbeat(self, nextBeatSec=1): self.finishLoadBlockchain() self.needUpdateAfterScan = False self.setDashboardDetails() - + ##### # If we just rescanned to sweep an address, need to finish it if len(self.sweepAfterScanList)>0: @@ -4709,34 +5943,14 @@ def Heartbeat(self, nextBeatSec=1): # Now we start the normal array of heartbeat operations - self.checkSatoshiVersion() # this actually only checks every 15 min newBlocks = TheBDM.readBlkFileUpdate(wait=True) self.currBlockNum = TheBDM.getTopBlockHeight() + if isinstance(self.currBlockNum, int): BDMcurrentBlock[0] = self.currBlockNum - ##### - # If we are getting lots of blocks, very rapidly, issue a warning - # We look at a rolling sum of the last 5 heartbeat updates (5s) if not newBlocks: newBlocks = 0 - self.detectNotSyncQ.insert(0, newBlocks) - self.detectNotSyncQ.pop() - blksInLast5sec = sum(self.detectNotSyncQ) - if( blksInLast5sec>20 ): - LOGERROR('Detected Bitcoin-Qt/bitcoind not synchronized') - LOGERROR('New blocks added in last 5 sec: %d', blksInLast5sec) - if self.noSyncWarnYet: - self.noSyncWarnYet = False - QMessageBox.warning(self,'Bitcoin-Qt is not synchronized', \ - 'Armory has detected that Bitcoin-Qt is not synchronized ' - 'with the bitcoin network yet, and Armory may not ' - 'work properly. If you experience any unusual behavior, it is ' - 'recommended that you close Armory and only restart it ' - 'when you see the green checkmark in the bottom-right ' - 'corner of the Bitcoin-Qt window.', QMessageBox.Ok) - return - - - + + # If we have new zero-conf transactions, scan them and update ledger if len(self.newZeroConfSinceLastUpdate)>0: self.newZeroConfSinceLastUpdate.reverse() @@ -4744,30 +5958,13 @@ def Heartbeat(self, nextBeatSec=1): wlt = self.walletMap[wltID] TheBDM.rescanWalletZeroConf(wlt.cppWallet, wait=True) - while len(self.newZeroConfSinceLastUpdate)>0: - TimerStart('CheckNewZeroConf') - # For each new tx, check each wallet - rawTx = self.newZeroConfSinceLastUpdate.pop() - for wltID in self.walletMap.keys(): - wlt = self.walletMap[wltID] - le = wlt.cppWallet.calcLedgerEntryForTxStr(rawTx) - if not le.getTxHash()=='\x00'*32: - LOGDEBUG('ZerConf tx for wallet: %s. Adding to notify queue.' % wltID) - notifyIn = self.getSettingOrSetDefault('NotifyBtcIn', not OS_MACOSX) - notifyOut = self.getSettingOrSetDefault('NotifyBtcOut', not OS_MACOSX) - if (le.getValue()<=0 and notifyOut) or (le.getValue()>0 and notifyIn): - self.notifyQueue.append([wltID, le, False]) # notifiedAlready=False - self.createCombinedLedger() - self.walletModel.reset() - TimerStop('CheckNewZeroConf') - + self.checkNewZeroConf() + # Trigger any notifications, if we have them... - TimerStart('doSystemTrayThing') self.doTheSystemTrayThing() - TimerStop('doSystemTrayThing') if newBlocks>0 and not TheBDM.isDirty(): - + # This says "after scan", but works when new blocks appear, too TheBDM.updateWalletsAfterScan(wait=True) @@ -4780,58 +5977,50 @@ def Heartbeat(self, nextBeatSec=1): LOGINFO('New Block! : %d', self.currBlockNum) didAffectUs = False - + # LITE sync means it won't rescan if addresses have been imported - TimerStart('newBlockSyncRescanZC') - for wltID in self.walletMap.keys(): - self.walletMap[wltID].syncWithBlockchainLite() - TheBDM.rescanWalletZeroConf(self.walletMap[wltID].cppWallet) - newLedgerSize = len(self.walletMap[wltID].getTxLedger()) - didAffectUs = (prevLedgSize[wltID] != newLedgerSize) - TimerStop('newBlockSyncRescanZC') - + didAffectUs = self.newBlockSyncRescanZC(prevLedgSize) + if didAffectUs: LOGINFO('New Block contained a transaction relevant to us!') self.walletListChanged() self.notifyOnSurpriseTx(self.currBlockNum-newBlocks, \ self.currBlockNum+1) - + self.createCombinedLedger() self.blkReceived = RightNow() self.writeSetting('LastBlkRecvTime', self.blkReceived) self.writeSetting('LastBlkRecv', self.currBlockNum) - + if self.netMode==NETWORKMODE.Full: LOGINFO('Current block number: %d', self.currBlockNum) self.lblArmoryStatus.setText(\ 'Connected (%s blocks) ' % \ (htmlColor('TextGreen'), self.currBlockNum)) - + # Update the wallet view to immediately reflect new balances - TimerStart('walletModelReset') self.walletModel.reset() - TimerStop('walletModelReset') - + blkRecvAgo = RightNow() - self.blkReceived #blkStampAgo = RightNow() - TheBDM.getTopBlockHeader().getTimestamp() self.lblArmoryStatus.setToolTip('Last block received is %s ago' % \ secondsToHumanTime(blkRecvAgo)) - - + + for func in self.extraHeartbeatOnline: func() - - + + except: LOGEXCEPT('Error in heartbeat function') print sys.exc_info() finally: reactor.callLater(nextBeatSec, self.Heartbeat) - + ############################################################################# def notifyOnSurpriseTx(self, blk0, blk1): - # We usually see transactions as zero-conf first, then they show up in + # We usually see transactions as zero-conf first, then they show up in # a block. It is a "surprise" when the first time we see it is in a block notifiedAlready = set([ n[1].getTxHash() for n in self.notifyQueue ]) for blk in range(blk0, blk1): @@ -4845,14 +6034,15 @@ def notifyOnSurpriseTx(self, blk0, blk1): self.notifyQueue.append([wltID, le, False]) else: pass - - + + ############################################################################# + @TimeThisFunction def doTheSystemTrayThing(self): """ I named this method as it is because this is not just "show a message." - I need to display all relevant transactions, in sequence that they were + I need to display all relevant transactions, in sequence that they were received. I will store them in self.notifyQueue, and this method will do nothing if it's empty. """ @@ -4860,7 +6050,7 @@ def doTheSystemTrayThing(self): RightNow()') dispLines.append( 'Wallet:\t"%s" (%s)' % (wlt.labelName, wltID)) @@ -4914,8 +6105,8 @@ def doTheSystemTrayThing(self): totalStr = coin2str( sum([other[i][1] for i in range(len(other))]), maxZeros=1) dispLines.append( 'Amount: \t%s BTC' % totalStr.strip()) if len(other)==1: - dispLines.append('Sent To:\t%s' % hash160_to_addrStr(other[0][0])) - addrComment = wlt.getComment(other[0][0]) + dispLines.append('Sent To:\t%s' % other[0][0]) + addrComment = wlt.getComment(addrStr_to_hash160(other[0][0])[1]) else: dispLines.append('') dispLines.append('From:\tWallet "%s" (%s)' % (wlt.labelName, wltID)) @@ -4924,14 +6115,16 @@ def doTheSystemTrayThing(self): '\n'.join(dispLines), \ QSystemTrayIcon.Information, \ 10000) + LOGINFO(title) + #LOGINFO('\n' + '\n'.join(dispLines)) #qsnd = QSound('drip.wav') #qsnd.play() self.notifyBlockedUntil = RightNow() + 5 return - - - + + + ############################################################################# def closeEvent(self, event=None): moc = self.getSettingOrSetDefault('MinimizeOrClose', 'DontKnow') @@ -4963,11 +6156,54 @@ def closeEvent(self, event=None): + ############################################################################# + def unpackLinuxTarGz(self, targzFile, changeSettings=True): + if targzFile is None: + return None + + if not os.path.exists(targzFile): + return None + + unpackDir = os.path.join(ARMORY_HOME_DIR, 'latestBitcoinInst') + unpackDir2 = os.path.join(ARMORY_HOME_DIR, 'latestBitcoinInstOld') + if os.path.exists(unpackDir): + if os.path.exists(unpackDir2): + shutil.rmtree(unpackDir2) + shutil.move(unpackDir, unpackDir2) + + os.mkdir(unpackDir) + + out,err = execAndWait('tar -zxf %s -C %s' % (targzFile, unpackDir), \ + timeout=5) + + LOGINFO('UNPACK STDOUT: "' + out + '"') + LOGINFO('UNPACK STDERR: "' + err + '"') + + + # There should only be one subdir + unpackDirChild = None + for fn in os.listdir(unpackDir): + unpackDirChild = os.path.join(unpackDir, fn) + + if unpackDirChild is None: + LOGERROR('There was apparently an error unpacking the file') + return None + + finalDir = os.path.abspath(unpackDirChild) + LOGWARN('Bitcoin Core unpacked into: %s', finalDir) + + if changeSettings: + self.settings.set('SatoshiExe', finalDir) + + return finalDir + + + ############################################################################# def closeForReal(self, event=None): ''' - Seriously, I could not figure out how to exit gracefully, so the next - best thing is to just hard-kill the app with a sys.exit() call. Oh well... + Unlike File->Quit or clicking the X on the window, which may actually + minimize Armory, this method is for *really* closing Armory ''' try: # Save the main window geometry in the settings file @@ -4982,26 +6218,10 @@ def closeForReal(self, event=None): LOGINFO('BDM is safe for clean shutdown') TheBDM.execCleanShutdown(wait=True) - # This will do nothing if bitcoind isn't running. + # This will do nothing if bitcoind isn't running. TheSDM.stopBitcoind() - - # Mostly for my own use, I'm curious how fast various things run - if CLI_OPTIONS.doDebug: - SaveTimingsCSV( os.path.join(ARMORY_HOME_DIR, 'timings.csv') ) - except: - # Don't want a strange error here interrupt shutdown - LOGEXCEPT('Strange error during shutdown') - - try: - if self.doHardReset: - rebuildFile = os.path.join(ARMORY_HOME_DIR, 'rebuild.txt') - touchFile(rebuildFile) - os.remove(self.settingsPath) - mempoolfile = os.path.join(ARMORY_HOME_DIR, 'mempool.bin') - if os.path.exists(mempoolfile): - os.remove(mempoolfile) except: - # Don't want a strange error here interrupt shutdown + # Don't want a strange error here interrupt shutdown LOGEXCEPT('Strange error during shutdown') @@ -5011,15 +6231,227 @@ def closeForReal(self, event=None): reactor.stop() if event: event.accept() + + + + ############################################################################# + def execTrigger(self, toSpawn): + super(ArmoryDialog, toSpawn).exec_() + + + ############################################################################# + def initTrigger(self, toInit): + if isinstance(toInit, DlgProgress): + toInit.setup(self) + toInit.status = 1 + + + ############################################################################# + def checkForNegImports(self): + negativeImports = [] + + for wlt in self.walletMap: + if self.walletMap[wlt].hasNegativeImports: + negativeImports.append(self.walletMap[wlt].uniqueIDB58) + + # If we detect any negative import + if len(negativeImports) > 0: + logDirs = [] + for wltID in negativeImports: + if not wltID in self.walletMap: + continue + + homedir = os.path.dirname(self.walletMap[wltID].walletPath) + wltlogdir = os.path.join(homedir, wltID) + if not os.path.exists(wltlogdir): + continue + + for subdirname in os.listdir(wltlogdir): + subdirpath = os.path.join(wltlogdir, subdirname) + logDirs.append([wltID, subdirpath]) + + + DlgInconsistentWltReport(self, self, logDirs).exec_() + + + ############################################################################# + def getAllRecoveryLogDirs(self, wltIDList): + self.logDirs = [] + for wltID in wltIDList: + if not wltID in self.walletMap: + continue + + homedir = os.path.dirname(self.walletMap[wltID].walletPath) + logdir = os.path.join(homedir, wltID) + if not os.path.exists(logdir): + continue + + self.logDirs.append([wltID, logdir]) + + return self.logDirs + + ############################################################################# + @AllowAsync + def CheckWalletConsistency(self, wallets, prgAt=None): + + if prgAt: + totalSize = 0 + walletSize = {} + for wlt in wallets: + statinfo = os.stat(wallets[wlt].walletPath) + walletSize[wlt] = statinfo.st_size + totalSize = totalSize + statinfo.st_size + + i=0 + dlgrdy = [0] + nerrors = 0 + + for wlt in wallets: + if prgAt: + prgAt[0] = i + f = 10000*walletSize[wlt]/totalSize + prgAt[1] = f + i = f +i + + self.wltCstStatus = WalletConsistencyCheck(wallets[wlt], prgAt) + if self.wltCstStatus[0] != 0: + self.WltCstError(wallets[wlt], self.wltCstStatus[1], dlgrdy) + while not dlgrdy[0]: + time.sleep(0.01) + nerrors = nerrors +1 + + prgAt[2] = 1 + + dlgrdy[0] = 0 + while prgAt[2] != 2: + time.sleep(0.1) + if nerrors == 0: + self.emit(SIGNAL('UWCS'), [1, 'All wallets are consistent', 10000, dlgrdy]) + self.emit(SIGNAL('checkForNegImports')) + else: + while not dlgrdy: + self.emit(SIGNAL('UWCS'), [1, 'Consistency Check Failed!', 0, dlgrdy]) + time.sleep(1) + + self.checkRdyForFix() + + + def checkRdyForFix(self): + #check BDM first + time.sleep(1) + self.dlgCptWlt.emit(SIGNAL('Show')) + while 1: + if TheBDM.getBDMState() == 'Scanning': + canFix = tr(""" + The wallet analysis tool will become available + as soon as Armory is done loading. You can close this + window and it will reappear when ready.""") + self.dlgCptWlt.UpdateCanFix([canFix]) + time.sleep(1) + elif TheBDM.getBDMState() == 'Offline' or \ + TheBDM.getBDMState() == 'Uninitialized': + TheSDM.setDisabled(True) + CLI_OPTIONS.offline = True + break + else: + break + + #check running dialogs + self.dlgCptWlt.emit(SIGNAL('Show')) + runningList = [] + while 1: + listchanged = 0 + canFix = [] + for dlg in runningList: + if dlg not in runningDialogsList: + runningList.remove(dlg) + listchanged = 1 + + for dlg in runningDialogsList: + if not isinstance(dlg, DlgCorruptWallet): + if dlg not in runningList: + runningList.append(dlg) + listchanged = 1 + + if len(runningList): + if listchanged: + canFix.append(tr(""" + The following windows need closed before you can + run the wallet analysis tool:""")) + canFix.extend([str(myobj.windowTitle()) for myobj in runningList]) + self.dlgCptWlt.UpdateCanFix(canFix) + time.sleep(0.2) + else: + break + + + canFix.append('Ready to analyze inconsistent wallets!') + self.dlgCptWlt.UpdateCanFix(canFix, True) + self.dlgCptWlt.exec_() + + def checkWallets(self): + nwallets = len(self.walletMap) + + if nwallets > 0: + self.prgAt = [0, 0, 0] + + self.pbarWalletProgress = QProgressBar() + self.pbarWalletProgress.setMaximum(10000) + self.pbarWalletProgress.setMaximumSize(300, 22) + self.pbarWalletProgress.setStyleSheet('text-align: center; margin-bottom: 2px; margin-left: 10px;') + self.pbarWalletProgress.setFormat('Wallet Consistency Check: %p%') + self.pbarWalletProgress.setValue(0) + self.statusBar().addWidget(self.pbarWalletProgress) + + self.connect(self, SIGNAL('UWCS'), self.UpdateWalletConsistencyStatus) + self.connect(self, SIGNAL('PWCE'), self.PromptWltCstError) + self.CheckWalletConsistency(self.walletMap, self.prgAt, async=True) + self.UpdateConsistencyCheckMessage(async = True) + #self.extraHeartbeatAlways.append(self.UpdateWalletConsistencyPBar) + + @AllowAsync + def UpdateConsistencyCheckMessage(self): + while self.prgAt[2] == 0: + self.emit(SIGNAL('UWCS'), [0, self.prgAt[0]]) + time.sleep(0.5) + + self.emit(SIGNAL('UWCS'), [2]) + self.prgAt[2] = 2 + + def UpdateWalletConsistencyStatus(self, msg): + if msg[0] == 0: + self.pbarWalletProgress.setValue(msg[1]) + elif msg[0] == 1: + self.statusBar().showMessage(msg[1], msg[2]) + msg[3][0] = 1 + else: + self.pbarWalletProgress.hide() + + def WltCstError(self, wlt, status, dlgrdy): + self.emit(SIGNAL('PWCE'), dlgrdy, wlt, status) + LOGERROR('Wallet consistency check failed! (%s)', wlt.uniqueIDB58) + + def PromptWltCstError(self, dlgrdy, wallet=None, status='', mode=None): + if not self.dlgCptWlt: + self.dlgCptWlt = DlgCorruptWallet(wallet, status, self, self) + dlgrdy[0] = 1 + else: + self.dlgCptWlt.addStatus(wallet, status) + + if not mode: + self.dlgCptWlt.show() + else: + self.dlgCptWlt.exec_() + ############################################ class ArmoryInstanceListener(Protocol): def connectionMade(self): LOGINFO('Another Armory instance just tried to open.') self.factory.func_conn_made() - + def dataReceived(self, data): LOGINFO('Received data from alternate Armory instance') self.factory.func_recv_data(data) @@ -5062,7 +6494,7 @@ def checkForAlreadyOpen(): ############################################ def checkForAlreadyOpenError(): LOGINFO('Already open error checking') - # Sometimes in Windows, Armory actually isn't open, because it holds + # Sometimes in Windows, Armory actually isn't open, because it holds # onto the socket even after it's closed. armoryExists = [] bitcoindExists = [] @@ -5079,7 +6511,7 @@ def checkForAlreadyOpenError(): if len(armoryExists)>0: LOGINFO('Not an error! Armory really is open') - return + return elif len(bitcoindExists)>0: # Strange condition where bitcoind doesn't get killed by Armory/guardian # (I've only seen this happen on windows, though) @@ -5088,7 +6520,7 @@ def checkForAlreadyOpenError(): killProcess(pid) time.sleep(0.5) raise - + ############################################ if 1: @@ -5125,7 +6557,7 @@ def endProgram(): reactor.threadpool.stop() QAPP.quit() os._exit(0) - + QAPP.connect(form, SIGNAL("lastWindowClosed()"), endProgram) reactor.addSystemEventTrigger('before', 'shutdown', endProgram) QAPP.setQuitOnLastWindowClosed(True) @@ -5133,3 +6565,5 @@ def endProgram(): os._exit(QAPP.exec_()) + + diff --git a/ArmorySetup.nsi b/ArmorySetup.nsi index 93c68227f..a88484031 100644 --- a/ArmorySetup.nsi +++ b/ArmorySetup.nsi @@ -52,7 +52,8 @@ Var StartMenuGroup !insertmacro MUI_LANGUAGE English # Installer attributes -OutFile ArmorySetup-${VERSION}-beta_win32.exe +# Default to -testing to match 90% of builds. Manually change actual releases +OutFile armory_${VERSION}-testing_winAll.exe InstallDir "$PROGRAMFILES\Armory" CRCCheck on XPStyle on diff --git a/BitTornado/BT1/Choker.py b/BitTornado/BT1/Choker.py new file mode 100644 index 000000000..be4017418 --- /dev/null +++ b/BitTornado/BT1/Choker.py @@ -0,0 +1,128 @@ +# Written by Bram Cohen +# see LICENSE.txt for license information + +from random import randrange, shuffle +from BitTornado.clock import clock +try: + True +except: + True = 1 + False = 0 + +class Choker: + def __init__(self, config, schedule, picker, done = lambda: False): + self.config = config + self.round_robin_period = config['round_robin_period'] + self.schedule = schedule + self.picker = picker + self.connections = [] + self.last_preferred = 0 + self.last_round_robin = clock() + self.done = done + self.super_seed = False + self.paused = False + schedule(self._round_robin, 5) + + def set_round_robin_period(self, x): + self.round_robin_period = x + + def _round_robin(self): + self.schedule(self._round_robin, 5) + if self.super_seed: + cons = range(len(self.connections)) + to_close = [] + count = self.config['min_uploads']-self.last_preferred + if count > 0: # optimization + shuffle(cons) + for c in cons: + i = self.picker.next_have(self.connections[c], count > 0) + if i is None: + continue + if i < 0: + to_close.append(self.connections[c]) + continue + self.connections[c].send_have(i) + count -= 1 + for c in to_close: + c.close() + if self.last_round_robin + self.round_robin_period < clock(): + self.last_round_robin = clock() + for i in xrange(1, len(self.connections)): + c = self.connections[i] + u = c.get_upload() + if u.is_choked() and u.is_interested(): + self.connections = self.connections[i:] + self.connections[:i] + break + self._rechoke() + + def _rechoke(self): + preferred = [] + maxuploads = self.config['max_uploads'] + if self.paused: + for c in self.connections: + c.get_upload().choke() + return + if maxuploads > 1: + for c in self.connections: + u = c.get_upload() + if not u.is_interested(): + continue + if self.done(): + r = u.get_rate() + else: + d = c.get_download() + r = d.get_rate() + if r < 1000 or d.is_snubbed(): + continue + preferred.append((-r, c)) + self.last_preferred = len(preferred) + preferred.sort() + del preferred[maxuploads-1:] + preferred = [x[1] for x in preferred] + count = len(preferred) + hit = False + to_unchoke = [] + for c in self.connections: + u = c.get_upload() + if c in preferred: + to_unchoke.append(u) + else: + if count < maxuploads or not hit: + to_unchoke.append(u) + if u.is_interested(): + count += 1 + hit = True + else: + u.choke() + for u in to_unchoke: + u.unchoke() + + def connection_made(self, connection, p = None): + if p is None: + p = randrange(-2, len(self.connections) + 1) + self.connections.insert(max(p, 0), connection) + self._rechoke() + + def connection_lost(self, connection): + self.connections.remove(connection) + self.picker.lost_peer(connection) + if connection.get_upload().is_interested() and not connection.get_upload().is_choked(): + self._rechoke() + + def interested(self, connection): + if not connection.get_upload().is_choked(): + self._rechoke() + + def not_interested(self, connection): + if not connection.get_upload().is_choked(): + self._rechoke() + + def set_super_seed(self): + while self.connections: # close all connections + self.connections[0].close() + self.picker.set_superseed() + self.super_seed = True + + def pause(self, flag): + self.paused = flag + self._rechoke() diff --git a/BitTornado/BT1/Connecter.py b/BitTornado/BT1/Connecter.py new file mode 100644 index 000000000..e668c02fc --- /dev/null +++ b/BitTornado/BT1/Connecter.py @@ -0,0 +1,288 @@ +# Written by Bram Cohen +# see LICENSE.txt for license information + +from BitTornado.bitfield import Bitfield +from BitTornado.clock import clock +from binascii import b2a_hex + +try: + True +except: + True = 1 + False = 0 + +DEBUG = False + +def toint(s): + return long(b2a_hex(s), 16) + +def tobinary(i): + return (chr(i >> 24) + chr((i >> 16) & 0xFF) + + chr((i >> 8) & 0xFF) + chr(i & 0xFF)) + +CHOKE = chr(0) +UNCHOKE = chr(1) +INTERESTED = chr(2) +NOT_INTERESTED = chr(3) +# index +HAVE = chr(4) +# index, bitfield +BITFIELD = chr(5) +# index, begin, length +REQUEST = chr(6) +# index, begin, piece +PIECE = chr(7) +# index, begin, piece +CANCEL = chr(8) + +class Connection: + def __init__(self, connection, connecter): + self.connection = connection + self.connecter = connecter + self.got_anything = False + self.next_upload = None + self.outqueue = [] + self.partial_message = None + self.download = None + self.send_choke_queued = False + self.just_unchoked = None + + def get_ip(self, real=False): + return self.connection.get_ip(real) + + def get_id(self): + return self.connection.get_id() + + def get_readable_id(self): + return self.connection.get_readable_id() + + def close(self): + if DEBUG: + print 'connection closed' + self.connection.close() + + def is_locally_initiated(self): + return self.connection.is_locally_initiated() + + def send_interested(self): + self._send_message(INTERESTED) + + def send_not_interested(self): + self._send_message(NOT_INTERESTED) + + def send_choke(self): + if self.partial_message: + self.send_choke_queued = True + else: + self._send_message(CHOKE) + self.upload.choke_sent() + self.just_unchoked = 0 + + def send_unchoke(self): + if self.send_choke_queued: + self.send_choke_queued = False + if DEBUG: + print 'CHOKE SUPPRESSED' + else: + self._send_message(UNCHOKE) + if ( self.partial_message or self.just_unchoked is None + or not self.upload.interested or self.download.active_requests ): + self.just_unchoked = 0 + else: + self.just_unchoked = clock() + + def send_request(self, index, begin, length): + self._send_message(REQUEST + tobinary(index) + + tobinary(begin) + tobinary(length)) + if DEBUG: + print 'sent request: '+str(index)+': '+str(begin)+'-'+str(begin+length) + + def send_cancel(self, index, begin, length): + self._send_message(CANCEL + tobinary(index) + + tobinary(begin) + tobinary(length)) + if DEBUG: + print 'sent cancel: '+str(index)+': '+str(begin)+'-'+str(begin+length) + + def send_bitfield(self, bitfield): + self._send_message(BITFIELD + bitfield) + + def send_have(self, index): + self._send_message(HAVE + tobinary(index)) + + def send_keepalive(self): + self._send_message('') + + def _send_message(self, s): + s = tobinary(len(s))+s + if self.partial_message: + self.outqueue.append(s) + else: + self.connection.send_message_raw(s) + + def send_partial(self, bytes): + if self.connection.closed: + return 0 + if self.partial_message is None: + s = self.upload.get_upload_chunk() + if s is None: + return 0 + index, begin, piece = s + self.partial_message = ''.join(( + tobinary(len(piece) + 9), PIECE, + tobinary(index), tobinary(begin), piece.tostring() )) + if DEBUG: + print 'sending chunk: '+str(index)+': '+str(begin)+'-'+str(begin+len(piece)) + + if bytes < len(self.partial_message): + self.connection.send_message_raw(self.partial_message[:bytes]) + self.partial_message = self.partial_message[bytes:] + return bytes + + q = [self.partial_message] + self.partial_message = None + if self.send_choke_queued: + self.send_choke_queued = False + self.outqueue.append(tobinary(1)+CHOKE) + self.upload.choke_sent() + self.just_unchoked = 0 + q.extend(self.outqueue) + self.outqueue = [] + q = ''.join(q) + self.connection.send_message_raw(q) + return len(q) + + def get_upload(self): + return self.upload + + def get_download(self): + return self.download + + def set_download(self, download): + self.download = download + + def backlogged(self): + return not self.connection.is_flushed() + + def got_request(self, i, p, l): + self.upload.got_request(i, p, l) + if self.just_unchoked: + self.connecter.ratelimiter.ping(clock() - self.just_unchoked) + self.just_unchoked = 0 + + + + +class Connecter: + def __init__(self, make_upload, downloader, choker, numpieces, + totalup, config, ratelimiter, sched = None): + self.downloader = downloader + self.make_upload = make_upload + self.choker = choker + self.numpieces = numpieces + self.config = config + self.ratelimiter = ratelimiter + self.rate_capped = False + self.sched = sched + self.totalup = totalup + self.rate_capped = False + self.connections = {} + self.external_connection_made = 0 + + def how_many_connections(self): + return len(self.connections) + + def connection_made(self, connection): + c = Connection(connection, self) + self.connections[connection] = c + c.upload = self.make_upload(c, self.ratelimiter, self.totalup) + c.download = self.downloader.make_download(c) + self.choker.connection_made(c) + return c + + def connection_lost(self, connection): + c = self.connections[connection] + del self.connections[connection] + if c.download: + c.download.disconnected() + self.choker.connection_lost(c) + + def connection_flushed(self, connection): + conn = self.connections[connection] + if conn.next_upload is None and (conn.partial_message is not None + or len(conn.upload.buffer) > 0): + self.ratelimiter.queue(conn) + + def got_piece(self, i): + for co in self.connections.values(): + co.send_have(i) + + def got_message(self, connection, message): + c = self.connections[connection] + t = message[0] + if t == BITFIELD and c.got_anything: + connection.close() + return + c.got_anything = True + if (t in [CHOKE, UNCHOKE, INTERESTED, NOT_INTERESTED] and + len(message) != 1): + connection.close() + return + if t == CHOKE: + c.download.got_choke() + elif t == UNCHOKE: + c.download.got_unchoke() + elif t == INTERESTED: + if not c.download.have.complete(): + c.upload.got_interested() + elif t == NOT_INTERESTED: + c.upload.got_not_interested() + elif t == HAVE: + if len(message) != 5: + connection.close() + return + i = toint(message[1:]) + if i >= self.numpieces: + connection.close() + return + if c.download.got_have(i): + c.upload.got_not_interested() + elif t == BITFIELD: + try: + b = Bitfield(self.numpieces, message[1:]) + except ValueError: + connection.close() + return + if c.download.got_have_bitfield(b): + c.upload.got_not_interested() + elif t == REQUEST: + if len(message) != 13: + connection.close() + return + i = toint(message[1:5]) + if i >= self.numpieces: + connection.close() + return + c.got_request(i, toint(message[5:9]), + toint(message[9:])) + elif t == CANCEL: + if len(message) != 13: + connection.close() + return + i = toint(message[1:5]) + if i >= self.numpieces: + connection.close() + return + c.upload.got_cancel(i, toint(message[5:9]), + toint(message[9:])) + elif t == PIECE: + if len(message) <= 9: + connection.close() + return + i = toint(message[1:5]) + if i >= self.numpieces: + connection.close() + return + if c.download.got_piece(i, toint(message[5:9]), message[9:]): + self.got_piece(i) + else: + connection.close() diff --git a/BitTornado/BT1/Downloader.py b/BitTornado/BT1/Downloader.py new file mode 100644 index 000000000..3b4c978a0 --- /dev/null +++ b/BitTornado/BT1/Downloader.py @@ -0,0 +1,594 @@ +# Written by Bram Cohen +# see LICENSE.txt for license information + +from BitTornado.CurrentRateMeasure import Measure +from BitTornado.bitfield import Bitfield +from random import shuffle +from BitTornado.clock import clock +try: + True +except: + True = 1 + False = 0 + +EXPIRE_TIME = 60 * 60 + +class PerIPStats: + def __init__(self, ip): + self.numgood = 0 + self.bad = {} + self.numconnections = 0 + self.lastdownload = None + self.peerid = None + +class BadDataGuard: + def __init__(self, download): + self.download = download + self.ip = download.ip + self.downloader = download.downloader + self.stats = self.downloader.perip[self.ip] + self.lastindex = None + + def failed(self, index, bump = False): + self.stats.bad.setdefault(index, 0) + self.downloader.gotbaddata[self.ip] = 1 + self.stats.bad[index] += 1 + if len(self.stats.bad) > 1: + if self.download is not None: + self.downloader.try_kick(self.download) + elif self.stats.numconnections == 1 and self.stats.lastdownload is not None: + self.downloader.try_kick(self.stats.lastdownload) + if len(self.stats.bad) >= 3 and len(self.stats.bad) > int(self.stats.numgood/30): + self.downloader.try_ban(self.ip) + elif bump: + self.downloader.picker.bump(index) + + def good(self, index): + # lastindex is a hack to only increase numgood by one for each good + # piece, however many chunks come from the connection(s) from this IP + if index != self.lastindex: + self.stats.numgood += 1 + self.lastindex = index + +class SingleDownload: + def __init__(self, downloader, connection): + self.downloader = downloader + self.connection = connection + self.choked = True + self.interested = False + self.active_requests = [] + self.measure = Measure(downloader.max_rate_period) + self.peermeasure = Measure(downloader.max_rate_period) + self.have = Bitfield(downloader.numpieces) + self.last = -1000 + self.last2 = -1000 + self.example_interest = None + self.backlog = 2 + self.ip = connection.get_ip() + self.guard = BadDataGuard(self) + + def _backlog(self, just_unchoked): + self.backlog = min( + 2+int(4*self.measure.get_rate()/self.downloader.chunksize), + (2*just_unchoked)+self.downloader.queue_limit() ) + if self.backlog > 50: + self.backlog = max(50, self.backlog * 0.075) + return self.backlog + + def disconnected(self): + self.downloader.lost_peer(self) + if self.have.complete(): + self.downloader.picker.lost_seed() + else: + for i in xrange(len(self.have)): + if self.have[i]: + self.downloader.picker.lost_have(i) + if self.have.complete() and self.downloader.storage.is_endgame(): + self.downloader.add_disconnected_seed(self.connection.get_readable_id()) + self._letgo() + self.guard.download = None + + def _letgo(self): + if self.downloader.queued_out.has_key(self): + del self.downloader.queued_out[self] + if not self.active_requests: + return + if self.downloader.endgamemode: + self.active_requests = [] + return + lost = {} + for index, begin, length in self.active_requests: + self.downloader.storage.request_lost(index, begin, length) + lost[index] = 1 + lost = lost.keys() + self.active_requests = [] + if self.downloader.paused: + return + ds = [d for d in self.downloader.downloads if not d.choked] + shuffle(ds) + for d in ds: + d._request_more() + for d in self.downloader.downloads: + if d.choked and not d.interested: + for l in lost: + if d.have[l] and self.downloader.storage.do_I_have_requests(l): + d.send_interested() + break + + def got_choke(self): + if not self.choked: + self.choked = True + self._letgo() + + def got_unchoke(self): + if self.choked: + self.choked = False + if self.interested: + self._request_more(new_unchoke = True) + self.last2 = clock() + + def is_choked(self): + return self.choked + + def is_interested(self): + return self.interested + + def send_interested(self): + if not self.interested: + self.interested = True + self.connection.send_interested() + if not self.choked: + self.last2 = clock() + + def send_not_interested(self): + if self.interested: + self.interested = False + self.connection.send_not_interested() + + def got_piece(self, index, begin, piece): + length = len(piece) + try: + self.active_requests.remove((index, begin, length)) + except ValueError: + self.downloader.discarded += length + return False + if self.downloader.endgamemode: + self.downloader.all_requests.remove((index, begin, length)) + self.last = clock() + self.last2 = clock() + self.measure.update_rate(length) + self.downloader.measurefunc(length) + if not self.downloader.storage.piece_came_in(index, begin, piece, self.guard): + self.downloader.piece_flunked(index) + return False + if self.downloader.storage.do_I_have(index): + self.downloader.picker.complete(index) + if self.downloader.endgamemode: + for d in self.downloader.downloads: + if d is not self: + if d.interested: + if d.choked: + assert not d.active_requests + d.fix_download_endgame() + else: + try: + d.active_requests.remove((index, begin, length)) + except ValueError: + continue + d.connection.send_cancel(index, begin, length) + d.fix_download_endgame() + else: + assert not d.active_requests + self._request_more() + self.downloader.check_complete(index) + return self.downloader.storage.do_I_have(index) + + def _request_more(self, new_unchoke = False): + assert not self.choked + if self.downloader.endgamemode: + self.fix_download_endgame(new_unchoke) + return + if self.downloader.paused: + return + if len(self.active_requests) >= self._backlog(new_unchoke): + if not (self.active_requests or self.backlog): + self.downloader.queued_out[self] = 1 + return + lost_interests = [] + while len(self.active_requests) < self.backlog: + interest = self.downloader.picker.next(self.have, + self.downloader.storage.do_I_have_requests, + self.downloader.too_many_partials()) + if interest is None: + break + self.example_interest = interest + self.send_interested() + loop = True + while len(self.active_requests) < self.backlog and loop: + begin, length = self.downloader.storage.new_request(interest) + self.downloader.picker.requested(interest) + self.active_requests.append((interest, begin, length)) + self.connection.send_request(interest, begin, length) + self.downloader.chunk_requested(length) + if not self.downloader.storage.do_I_have_requests(interest): + loop = False + lost_interests.append(interest) + if not self.active_requests: + self.send_not_interested() + if lost_interests: + for d in self.downloader.downloads: + if d.active_requests or not d.interested: + continue + if d.example_interest is not None and self.downloader.storage.do_I_have_requests(d.example_interest): + continue + for lost in lost_interests: + if d.have[lost]: + break + else: + continue + interest = self.downloader.picker.next(d.have, + self.downloader.storage.do_I_have_requests, + self.downloader.too_many_partials()) + if interest is None: + d.send_not_interested() + else: + d.example_interest = interest + if self.downloader.storage.is_endgame(): + self.downloader.start_endgame() + + + def fix_download_endgame(self, new_unchoke = False): + if self.downloader.paused: + return + if len(self.active_requests) >= self._backlog(new_unchoke): + if not (self.active_requests or self.backlog) and not self.choked: + self.downloader.queued_out[self] = 1 + return + want = [a for a in self.downloader.all_requests if self.have[a[0]] and a not in self.active_requests] + if not (self.active_requests or want): + self.send_not_interested() + return + if want: + self.send_interested() + if self.choked: + return + shuffle(want) + del want[self.backlog - len(self.active_requests):] + self.active_requests.extend(want) + for piece, begin, length in want: + self.connection.send_request(piece, begin, length) + self.downloader.chunk_requested(length) + + def got_have(self, index): + if index == self.downloader.numpieces-1: + self.downloader.totalmeasure.update_rate(self.downloader.storage.total_length-(self.downloader.numpieces-1)*self.downloader.storage.piece_length) + self.peermeasure.update_rate(self.downloader.storage.total_length-(self.downloader.numpieces-1)*self.downloader.storage.piece_length) + else: + self.downloader.totalmeasure.update_rate(self.downloader.storage.piece_length) + self.peermeasure.update_rate(self.downloader.storage.piece_length) + if not self.have[index]: + self.have[index] = True + self.downloader.picker.got_have(index) + if self.have.complete(): + self.downloader.picker.became_seed() + if self.downloader.storage.am_I_complete(): + self.downloader.add_disconnected_seed(self.connection.get_readable_id()) + self.connection.close() + elif self.downloader.endgamemode: + self.fix_download_endgame() + elif ( not self.downloader.paused + and not self.downloader.picker.is_blocked(index) + and self.downloader.storage.do_I_have_requests(index) ): + if not self.choked: + self._request_more() + else: + self.send_interested() + return self.have.complete() + + def _check_interests(self): + if self.interested or self.downloader.paused: + return + for i in xrange(len(self.have)): + if ( self.have[i] and not self.downloader.picker.is_blocked(i) + and ( self.downloader.endgamemode + or self.downloader.storage.do_I_have_requests(i) ) ): + self.send_interested() + return + + def got_have_bitfield(self, have): + if self.downloader.storage.am_I_complete() and have.complete(): + if self.downloader.super_seeding: + self.connection.send_bitfield(have.tostring()) # be nice, show you're a seed too + self.connection.close() + self.downloader.add_disconnected_seed(self.connection.get_readable_id()) + return False + self.have = have + if have.complete(): + self.downloader.picker.got_seed() + else: + for i in xrange(len(have)): + if have[i]: + self.downloader.picker.got_have(i) + if self.downloader.endgamemode and not self.downloader.paused: + for piece, begin, length in self.downloader.all_requests: + if self.have[piece]: + self.send_interested() + break + else: + self._check_interests() + return have.complete() + + def get_rate(self): + return self.measure.get_rate() + + def is_snubbed(self): + if ( self.interested and not self.choked + and clock() - self.last2 > self.downloader.snub_time ): + for index, begin, length in self.active_requests: + self.connection.send_cancel(index, begin, length) + self.got_choke() # treat it just like a choke + return clock() - self.last > self.downloader.snub_time + + +class Downloader: + def __init__(self, storage, picker, backlog, max_rate_period, + numpieces, chunksize, measurefunc, snub_time, + kickbans_ok, kickfunc, banfunc): + self.storage = storage + self.picker = picker + self.backlog = backlog + self.max_rate_period = max_rate_period + self.measurefunc = measurefunc + self.totalmeasure = Measure(max_rate_period*storage.piece_length/storage.request_size) + self.numpieces = numpieces + self.chunksize = chunksize + self.snub_time = snub_time + self.kickfunc = kickfunc + self.banfunc = banfunc + self.disconnectedseeds = {} + self.downloads = [] + self.perip = {} + self.gotbaddata = {} + self.kicked = {} + self.banned = {} + self.kickbans_ok = kickbans_ok + self.kickbans_halted = False + self.super_seeding = False + self.endgamemode = False + self.endgame_queued_pieces = [] + self.all_requests = [] + self.discarded = 0L +# self.download_rate = 25000 # 25K/s test rate + self.download_rate = 0 + self.bytes_requested = 0 + self.last_time = clock() + self.queued_out = {} + self.requeueing = False + self.paused = False + + def set_download_rate(self, rate): + self.download_rate = rate * 1000 + self.bytes_requested = 0 + + def queue_limit(self): + if not self.download_rate: + return 10e10 # that's a big queue! + t = clock() + self.bytes_requested -= (t - self.last_time) * self.download_rate + self.last_time = t + if not self.requeueing and self.queued_out and self.bytes_requested < 0: + self.requeueing = True + q = self.queued_out.keys() + shuffle(q) + self.queued_out = {} + for d in q: + d._request_more() + self.requeueing = False + if -self.bytes_requested > 5*self.download_rate: + self.bytes_requested = -5*self.download_rate + return max(int(-self.bytes_requested/self.chunksize),0) + + def chunk_requested(self, size): + self.bytes_requested += size + + external_data_received = chunk_requested + + def make_download(self, connection): + ip = connection.get_ip() + if self.perip.has_key(ip): + perip = self.perip[ip] + else: + perip = self.perip.setdefault(ip, PerIPStats(ip)) + perip.peerid = connection.get_readable_id() + perip.numconnections += 1 + d = SingleDownload(self, connection) + perip.lastdownload = d + self.downloads.append(d) + return d + + def piece_flunked(self, index): + if self.paused: + return + if self.endgamemode: + if self.downloads: + while self.storage.do_I_have_requests(index): + nb, nl = self.storage.new_request(index) + self.all_requests.append((index, nb, nl)) + for d in self.downloads: + d.fix_download_endgame() + return + self._reset_endgame() + return + ds = [d for d in self.downloads if not d.choked] + shuffle(ds) + for d in ds: + d._request_more() + ds = [d for d in self.downloads if not d.interested and d.have[index]] + for d in ds: + d.example_interest = index + d.send_interested() + + def has_downloaders(self): + return len(self.downloads) + + def lost_peer(self, download): + ip = download.ip + self.perip[ip].numconnections -= 1 + if self.perip[ip].lastdownload == download: + self.perip[ip].lastdownload = None + self.downloads.remove(download) + if self.endgamemode and not self.downloads: # all peers gone + self._reset_endgame() + + def _reset_endgame(self): + self.storage.reset_endgame(self.all_requests) + self.endgamemode = False + self.all_requests = [] + self.endgame_queued_pieces = [] + + + def add_disconnected_seed(self, id): +# if not self.disconnectedseeds.has_key(id): +# self.picker.seed_seen_recently() + self.disconnectedseeds[id]=clock() + +# def expire_disconnected_seeds(self): + + def num_disconnected_seeds(self): + # first expire old ones + expired = [] + for id,t in self.disconnectedseeds.items(): + if clock() - t > EXPIRE_TIME: #Expire old seeds after so long + expired.append(id) + for id in expired: +# self.picker.seed_disappeared() + del self.disconnectedseeds[id] + return len(self.disconnectedseeds) + # if this isn't called by a stats-gathering function + # it should be scheduled to run every minute or two. + + def _check_kicks_ok(self): + if len(self.gotbaddata) > 10: + self.kickbans_ok = False + self.kickbans_halted = True + return self.kickbans_ok and len(self.downloads) > 2 + + def try_kick(self, download): + if self._check_kicks_ok(): + download.guard.download = None + ip = download.ip + id = download.connection.get_readable_id() + self.kicked[ip] = id + self.perip[ip].peerid = id + self.kickfunc(download.connection) + + def try_ban(self, ip): + if self._check_kicks_ok(): + self.banfunc(ip) + self.banned[ip] = self.perip[ip].peerid + if self.kicked.has_key(ip): + del self.kicked[ip] + + def set_super_seed(self): + self.super_seeding = True + + def check_complete(self, index): + if self.endgamemode and not self.all_requests: + self.endgamemode = False + if self.endgame_queued_pieces and not self.endgamemode: + self.requeue_piece_download() + if self.storage.am_I_complete(): + assert not self.all_requests + assert not self.endgamemode + for d in [i for i in self.downloads if i.have.complete()]: + d.connection.send_have(index) # be nice, tell the other seed you completed + self.add_disconnected_seed(d.connection.get_readable_id()) + d.connection.close() + return True + return False + + def too_many_partials(self): + return len(self.storage.dirty) > (len(self.downloads)/2) + + + def cancel_piece_download(self, pieces): + if self.endgamemode: + if self.endgame_queued_pieces: + for piece in pieces: + try: + self.endgame_queued_pieces.remove(piece) + except: + pass + new_all_requests = [] + for index, nb, nl in self.all_requests: + if index in pieces: + self.storage.request_lost(index, nb, nl) + else: + new_all_requests.append((index, nb, nl)) + self.all_requests = new_all_requests + + for d in self.downloads: + hit = False + for index, nb, nl in d.active_requests: + if index in pieces: + hit = True + d.connection.send_cancel(index, nb, nl) + if not self.endgamemode: + self.storage.request_lost(index, nb, nl) + if hit: + d.active_requests = [ r for r in d.active_requests + if r[0] not in pieces ] + d._request_more() + if not self.endgamemode and d.choked: + d._check_interests() + + def requeue_piece_download(self, pieces = []): + if self.endgame_queued_pieces: + for piece in pieces: + if not piece in self.endgame_queued_pieces: + self.endgame_queued_pieces.append(piece) + pieces = self.endgame_queued_pieces + if self.endgamemode: + if self.all_requests: + self.endgame_queued_pieces = pieces + return + self.endgamemode = False + self.endgame_queued_pieces = None + + ds = [d for d in self.downloads] + shuffle(ds) + for d in ds: + if d.choked: + d._check_interests() + else: + d._request_more() + + def start_endgame(self): + assert not self.endgamemode + self.endgamemode = True + assert not self.all_requests + for d in self.downloads: + if d.active_requests: + assert d.interested and not d.choked + for request in d.active_requests: + assert not request in self.all_requests + self.all_requests.append(request) + for d in self.downloads: + d.fix_download_endgame() + + def pause(self, flag): + self.paused = flag + if flag: + for d in self.downloads: + for index, begin, length in d.active_requests: + d.connection.send_cancel(index, begin, length) + d._letgo() + d.send_not_interested() + if self.endgamemode: + self._reset_endgame() + else: + shuffle(self.downloads) + for d in self.downloads: + d._check_interests() + if d.interested and not d.choked: + d._request_more() diff --git a/BitTornado/BT1/DownloaderFeedback.py b/BitTornado/BT1/DownloaderFeedback.py new file mode 100644 index 000000000..836617457 --- /dev/null +++ b/BitTornado/BT1/DownloaderFeedback.py @@ -0,0 +1,155 @@ +# Written by Bram Cohen +# see LICENSE.txt for license information + +from cStringIO import StringIO +from urllib import quote +from threading import Event + +try: + True +except: + True = 1 + False = 0 + +class DownloaderFeedback: + def __init__(self, choker, httpdl, add_task, upfunc, downfunc, + ratemeasure, leftfunc, file_length, finflag, sp, statistics, + statusfunc = None, interval = None): + self.choker = choker + self.httpdl = httpdl + self.add_task = add_task + self.upfunc = upfunc + self.downfunc = downfunc + self.ratemeasure = ratemeasure + self.leftfunc = leftfunc + self.file_length = file_length + self.finflag = finflag + self.sp = sp + self.statistics = statistics + self.lastids = [] + self.spewdata = None + self.doneprocessing = Event() + self.doneprocessing.set() + if statusfunc: + self.autodisplay(statusfunc, interval) + + + def _rotate(self): + cs = self.choker.connections + for id in self.lastids: + for i in xrange(len(cs)): + if cs[i].get_id() == id: + return cs[i:] + cs[:i] + return cs + + def spews(self): + l = [] + cs = self._rotate() + self.lastids = [c.get_id() for c in cs] + for c in cs: + a = {} + a['id'] = c.get_readable_id() + a['ip'] = c.get_ip() + a['optimistic'] = (c is self.choker.connections[0]) + if c.is_locally_initiated(): + a['direction'] = 'L' + else: + a['direction'] = 'R' + u = c.get_upload() + a['uprate'] = int(u.measure.get_rate()) + a['uinterested'] = u.is_interested() + a['uchoked'] = u.is_choked() + d = c.get_download() + a['downrate'] = int(d.measure.get_rate()) + a['dinterested'] = d.is_interested() + a['dchoked'] = d.is_choked() + a['snubbed'] = d.is_snubbed() + a['utotal'] = d.connection.upload.measure.get_total() + a['dtotal'] = d.connection.download.measure.get_total() + if len(d.connection.download.have) > 0: + a['completed'] = float(len(d.connection.download.have)-d.connection.download.have.numfalse)/float(len(d.connection.download.have)) + else: + a['completed'] = 1.0 + a['speed'] = d.connection.download.peermeasure.get_rate() + + l.append(a) + + for dl in self.httpdl.get_downloads(): + if dl.goodseed: + a = {} + a['id'] = 'http seed' + a['ip'] = dl.baseurl + a['optimistic'] = False + a['direction'] = 'L' + a['uprate'] = 0 + a['uinterested'] = False + a['uchoked'] = False + a['downrate'] = int(dl.measure.get_rate()) + a['dinterested'] = True + a['dchoked'] = not dl.active + a['snubbed'] = not dl.active + a['utotal'] = None + a['dtotal'] = dl.measure.get_total() + a['completed'] = 1.0 + a['speed'] = None + + l.append(a) + + return l + + + def gather(self, displayfunc = None): + s = {'stats': self.statistics.update()} + if self.sp.isSet(): + s['spew'] = self.spews() + else: + s['spew'] = None + s['up'] = self.upfunc() + if self.finflag.isSet(): + s['done'] = self.file_length + return s + s['down'] = self.downfunc() + obtained, desired = self.leftfunc() + s['done'] = obtained + s['wanted'] = desired + if desired > 0: + s['frac'] = float(obtained)/desired + else: + s['frac'] = 1.0 + if desired == obtained: + s['time'] = 0 + else: + s['time'] = self.ratemeasure.get_time_left(desired-obtained) + return s + + + def display(self, displayfunc): + if not self.doneprocessing.isSet(): + return + self.doneprocessing.clear() + stats = self.gather() + if self.finflag.isSet(): + displayfunc(dpflag = self.doneprocessing, + upRate = stats['up'], + statistics = stats['stats'], spew = stats['spew']) + elif stats['time'] is not None: + displayfunc(dpflag = self.doneprocessing, + fractionDone = stats['frac'], sizeDone = stats['done'], + downRate = stats['down'], upRate = stats['up'], + statistics = stats['stats'], spew = stats['spew'], + timeEst = stats['time']) + else: + displayfunc(dpflag = self.doneprocessing, + fractionDone = stats['frac'], sizeDone = stats['done'], + downRate = stats['down'], upRate = stats['up'], + statistics = stats['stats'], spew = stats['spew']) + + + def autodisplay(self, displayfunc, interval): + self.displayfunc = displayfunc + self.interval = interval + self._autodisplay() + + def _autodisplay(self): + self.add_task(self._autodisplay, self.interval) + self.display(self.displayfunc) diff --git a/BitTornado/BT1/Encrypter.py b/BitTornado/BT1/Encrypter.py new file mode 100644 index 000000000..8dcce1f74 --- /dev/null +++ b/BitTornado/BT1/Encrypter.py @@ -0,0 +1,333 @@ +# Written by Bram Cohen +# see LICENSE.txt for license information + +from cStringIO import StringIO +from binascii import b2a_hex +from socket import error as socketerror +from urllib import quote +from traceback import print_exc +try: + True +except: + True = 1 + False = 0 + +MAX_INCOMPLETE = 8 + +protocol_name = 'BitTorrent protocol' +option_pattern = chr(0)*8 + +def toint(s): + return long(b2a_hex(s), 16) + +def tobinary(i): + return (chr(i >> 24) + chr((i >> 16) & 0xFF) + + chr((i >> 8) & 0xFF) + chr(i & 0xFF)) + +hexchars = '0123456789ABCDEF' +hexmap = [] +for i in xrange(256): + hexmap.append(hexchars[(i&0xF0)/16]+hexchars[i&0x0F]) + +def tohex(s): + r = [] + for c in s: + r.append(hexmap[ord(c)]) + return ''.join(r) + +def make_readable(s): + if not s: + return '' + if quote(s).find('%') >= 0: + return tohex(s) + return '"'+s+'"' + + +class IncompleteCounter: + def __init__(self): + self.c = 0 + def increment(self): + self.c += 1 + def decrement(self): + self.c -= 1 + def toomany(self): + return self.c >= MAX_INCOMPLETE + +incompletecounter = IncompleteCounter() + + +# header, reserved, download id, my id, [length, message] + +class Connection: + def __init__(self, Encoder, connection, id, ext_handshake=False): + self.Encoder = Encoder + self.connection = connection + self.connecter = Encoder.connecter + self.id = id + self.readable_id = make_readable(id) + self.locally_initiated = (id != None) + self.complete = False + self.keepalive = lambda: None + self.closed = False + self.buffer = StringIO() + if self.locally_initiated: + incompletecounter.increment() + if self.locally_initiated or ext_handshake: + self.connection.write(chr(len(protocol_name)) + protocol_name + + option_pattern + self.Encoder.download_id) + if ext_handshake: + self.Encoder.connecter.external_connection_made += 1 + self.connection.write(self.Encoder.my_id) + self.next_len, self.next_func = 20, self.read_peer_id + else: + self.next_len, self.next_func = 1, self.read_header_len + self.Encoder.raw_server.add_task(self._auto_close, 15) + + def get_ip(self, real=False): + return self.connection.get_ip(real) + + def get_id(self): + return self.id + + def get_readable_id(self): + return self.readable_id + + def is_locally_initiated(self): + return self.locally_initiated + + def is_flushed(self): + return self.connection.is_flushed() + + def read_header_len(self, s): + if ord(s) != len(protocol_name): + return None + return len(protocol_name), self.read_header + + def read_header(self, s): + if s != protocol_name: + return None + return 8, self.read_reserved + + def read_reserved(self, s): + return 20, self.read_download_id + + def read_download_id(self, s): + if s != self.Encoder.download_id: + return None + if not self.locally_initiated: + self.Encoder.connecter.external_connection_made += 1 + self.connection.write(chr(len(protocol_name)) + protocol_name + + option_pattern + self.Encoder.download_id + self.Encoder.my_id) + return 20, self.read_peer_id + + def read_peer_id(self, s): + if not self.id: + self.id = s + self.readable_id = make_readable(s) + else: + if s != self.id: + return None + self.complete = self.Encoder.got_id(self) + if not self.complete: + return None + if self.locally_initiated: + self.connection.write(self.Encoder.my_id) + incompletecounter.decrement() + c = self.Encoder.connecter.connection_made(self) + self.keepalive = c.send_keepalive + return 4, self.read_len + + def read_len(self, s): + l = toint(s) + if l > self.Encoder.max_len: + return None + return l, self.read_message + + def read_message(self, s): + if s != '': + self.connecter.got_message(self, s) + return 4, self.read_len + + def read_dead(self, s): + return None + + def _auto_close(self): + if not self.complete: + self.close() + + def close(self): + if not self.closed: + self.connection.close() + self.sever() + + def sever(self): + self.closed = True + del self.Encoder.connections[self.connection] + if self.complete: + self.connecter.connection_lost(self) + elif self.locally_initiated: + incompletecounter.decrement() + + def send_message_raw(self, message): + if not self.closed: + self.connection.write(message) + + def data_came_in(self, connection, s): + self.Encoder.measurefunc(len(s)) + while True: + if self.closed: + return + i = self.next_len - self.buffer.tell() + if i > len(s): + self.buffer.write(s) + return + self.buffer.write(s[:i]) + s = s[i:] + m = self.buffer.getvalue() + self.buffer.reset() + self.buffer.truncate() + try: + x = self.next_func(m) + except: + self.next_len, self.next_func = 1, self.read_dead + raise + if x is None: + self.close() + return + self.next_len, self.next_func = x + + def connection_flushed(self, connection): + if self.complete: + self.connecter.connection_flushed(self) + + def connection_lost(self, connection): + if self.Encoder.connections.has_key(connection): + self.sever() + + +class Encoder: + def __init__(self, connecter, raw_server, my_id, max_len, + schedulefunc, keepalive_delay, download_id, + measurefunc, config): + self.raw_server = raw_server + self.connecter = connecter + self.my_id = my_id + self.max_len = max_len + self.schedulefunc = schedulefunc + self.keepalive_delay = keepalive_delay + self.download_id = download_id + self.measurefunc = measurefunc + self.config = config + self.connections = {} + self.banned = {} + self.to_connect = [] + self.paused = False + if self.config['max_connections'] == 0: + self.max_connections = 2 ** 30 + else: + self.max_connections = self.config['max_connections'] + schedulefunc(self.send_keepalives, keepalive_delay) + + def send_keepalives(self): + self.schedulefunc(self.send_keepalives, self.keepalive_delay) + if self.paused: + return + for c in self.connections.values(): + c.keepalive() + + def start_connections(self, list): + if not self.to_connect: + self.raw_server.add_task(self._start_connection_from_queue) + self.to_connect = list + + def _start_connection_from_queue(self): + if self.connecter.external_connection_made: + max_initiate = self.config['max_initiate'] + else: + max_initiate = int(self.config['max_initiate']*1.5) + cons = len(self.connections) + if cons >= self.max_connections or cons >= max_initiate: + delay = 60 + elif self.paused or incompletecounter.toomany(): + delay = 1 + else: + delay = 0 + dns, id = self.to_connect.pop(0) + self.start_connection(dns, id) + if self.to_connect: + self.raw_server.add_task(self._start_connection_from_queue, delay) + + def start_connection(self, dns, id): + if ( self.paused + or len(self.connections) >= self.max_connections + or id == self.my_id + or self.banned.has_key(dns[0]) ): + return True + for v in self.connections.values(): + if v is None: + continue + if id and v.id == id: + return True + ip = v.get_ip(True) + if self.config['security'] and ip != 'unknown' and ip == dns[0]: + return True + try: + c = self.raw_server.start_connection(dns) + con = Connection(self, c, id) + self.connections[c] = con + c.set_handler(con) + except socketerror: + return False + return True + + def _start_connection(self, dns, id): + def foo(self=self, dns=dns, id=id): + self.start_connection(dns, id) + + self.schedulefunc(foo, 0) + + def got_id(self, connection): + if connection.id == self.my_id: + self.connecter.external_connection_made -= 1 + return False + ip = connection.get_ip(True) + if self.config['security'] and self.banned.has_key(ip): + return False + for v in self.connections.values(): + if connection is not v: + if connection.id == v.id: + return False + if self.config['security'] and ip != 'unknown' and ip == v.get_ip(True): + v.close() + return True + + def external_connection_made(self, connection): + if self.paused or len(self.connections) >= self.max_connections: + connection.close() + return False + con = Connection(self, connection, None) + self.connections[connection] = con + connection.set_handler(con) + return True + + def externally_handshaked_connection_made(self, connection, options, already_read): + if self.paused or len(self.connections) >= self.max_connections: + connection.close() + return False + con = Connection(self, connection, None, True) + self.connections[connection] = con + connection.set_handler(con) + if already_read: + con.data_came_in(con, already_read) + return True + + def close_all(self): + for c in self.connections.values(): + c.close() + self.connections = {} + + def ban(self, ip): + self.banned[ip] = 1 + + def pause(self, flag): + self.paused = flag diff --git a/BitTornado/BT1/FileSelector.py b/BitTornado/BT1/FileSelector.py new file mode 100644 index 000000000..79f096e32 --- /dev/null +++ b/BitTornado/BT1/FileSelector.py @@ -0,0 +1,245 @@ +# Written by John Hoffman +# see LICENSE.txt for license information + +from random import shuffle +from traceback import print_exc +try: + True +except: + True = 1 + False = 0 + + +class FileSelector: + def __init__(self, files, piece_length, bufferdir, + storage, storagewrapper, sched, failfunc): + self.files = files + self.storage = storage + self.storagewrapper = storagewrapper + self.sched = sched + self.failfunc = failfunc + self.downloader = None + self.picker = None + + storage.set_bufferdir(bufferdir) + + self.numfiles = len(files) + self.priority = [1] * self.numfiles + self.new_priority = None + self.new_partials = None + self.filepieces = [] + total = 0L + for file, length in files: + if not length: + self.filepieces.append(()) + else: + pieces = range( int(total/piece_length), + int((total+length-1)/piece_length)+1 ) + self.filepieces.append(tuple(pieces)) + total += length + self.numpieces = int((total+piece_length-1)/piece_length) + self.piece_priority = [1] * self.numpieces + + + + def init_priority(self, new_priority): + try: + assert len(new_priority) == self.numfiles + for v in new_priority: + assert type(v) in (type(0),type(0L)) + assert v >= -1 + assert v <= 2 + except: +# print_exc() + return False + try: + files_updated = False + for f in xrange(self.numfiles): + if new_priority[f] < 0: + self.storage.disable_file(f) + files_updated = True + if files_updated: + self.storage.reset_file_status() + self.new_priority = new_priority + except (IOError, OSError), e: + self.failfunc("can't open partial file for " + + self.files[f][0] + ': ' + str(e)) + return False + return True + + ''' + d['priority'] = [file #1 priority [,file #2 priority...] ] + a list of download priorities for each file. + Priority may be -1, 0, 1, 2. -1 = download disabled, + 0 = highest, 1 = normal, 2 = lowest. + Also see Storage.pickle and StorageWrapper.pickle for additional keys. + ''' + def unpickle(self, d): + if d.has_key('priority'): + if not self.init_priority(d['priority']): + return + pieces = self.storage.unpickle(d) + if not pieces: # don't bother, nothing restoreable + return + new_piece_priority = self._get_piece_priority_list(self.new_priority) + self.storagewrapper.reblock([i == -1 for i in new_piece_priority]) + self.new_partials = self.storagewrapper.unpickle(d, pieces) + + + def tie_in(self, picker, cancelfunc, requestmorefunc, rerequestfunc): + self.picker = picker + self.cancelfunc = cancelfunc + self.requestmorefunc = requestmorefunc + self.rerequestfunc = rerequestfunc + + if self.new_priority: + self.priority = self.new_priority + self.new_priority = None + self.new_piece_priority = self._set_piece_priority(self.priority) + + if self.new_partials: + shuffle(self.new_partials) + for p in self.new_partials: + self.picker.requested(p) + self.new_partials = None + + + def _set_files_disabled(self, old_priority, new_priority): + old_disabled = [p == -1 for p in old_priority] + new_disabled = [p == -1 for p in new_priority] + data_to_update = [] + for f in xrange(self.numfiles): + if new_disabled[f] != old_disabled[f]: + data_to_update.extend(self.storage.get_piece_update_list(f)) + buffer = [] + for piece, start, length in data_to_update: + if self.storagewrapper.has_data(piece): + data = self.storagewrapper.read_raw(piece, start, length) + if data is None: + return False + buffer.append((piece, start, data)) + + files_updated = False + try: + for f in xrange(self.numfiles): + if new_disabled[f] and not old_disabled[f]: + self.storage.disable_file(f) + files_updated = True + if old_disabled[f] and not new_disabled[f]: + self.storage.enable_file(f) + files_updated = True + except (IOError, OSError), e: + if new_disabled[f]: + msg = "can't open partial file for " + else: + msg = 'unable to open ' + self.failfunc(msg + self.files[f][0] + ': ' + str(e)) + return False + if files_updated: + self.storage.reset_file_status() + + changed_pieces = {} + for piece, start, data in buffer: + if not self.storagewrapper.write_raw(piece, start, data): + return False + data.release() + changed_pieces[piece] = 1 + if not self.storagewrapper.doublecheck_data(changed_pieces): + return False + + return True + + + def _get_piece_priority_list(self, file_priority_list): + l = [-1] * self.numpieces + for f in xrange(self.numfiles): + if file_priority_list[f] == -1: + continue + for i in self.filepieces[f]: + if l[i] == -1: + l[i] = file_priority_list[f] + continue + l[i] = min(l[i],file_priority_list[f]) + return l + + + def _set_piece_priority(self, new_priority): + was_complete = self.storagewrapper.am_I_complete() + new_piece_priority = self._get_piece_priority_list(new_priority) + pieces = range(self.numpieces) + shuffle(pieces) + new_blocked = [] + new_unblocked = [] + for piece in pieces: + self.picker.set_priority(piece,new_piece_priority[piece]) + o = self.piece_priority[piece] == -1 + n = new_piece_priority[piece] == -1 + if n and not o: + new_blocked.append(piece) + if o and not n: + new_unblocked.append(piece) + if new_blocked: + self.cancelfunc(new_blocked) + self.storagewrapper.reblock([i == -1 for i in new_piece_priority]) + if new_unblocked: + self.requestmorefunc(new_unblocked) + if was_complete and not self.storagewrapper.am_I_complete(): + self.rerequestfunc() + + return new_piece_priority + + + def set_priorities_now(self, new_priority = None): + if not new_priority: + new_priority = self.new_priority + self.new_priority = None # potential race condition + if not new_priority: + return + old_priority = self.priority + self.priority = new_priority + if not self._set_files_disabled(old_priority, new_priority): + return + self.piece_priority = self._set_piece_priority(new_priority) + + def set_priorities(self, new_priority): + self.new_priority = new_priority + self.sched(self.set_priorities_now) + + def set_priority(self, f, p): + new_priority = self.get_priorities() + new_priority[f] = p + self.set_priorities(new_priority) + + def get_priorities(self): + priority = self.new_priority + if not priority: + priority = self.priority # potential race condition + return [i for i in priority] + + def __setitem__(self, index, val): + self.set_priority(index, val) + + def __getitem__(self, index): + try: + return self.new_priority[index] + except: + return self.priority[index] + + + def finish(self): + for f in xrange(self.numfiles): + if self.priority[f] == -1: + self.storage.delete_file(f) + + def pickle(self): + d = {'priority': self.priority} + try: + s = self.storage.pickle() + sw = self.storagewrapper.pickle() + for k in s.keys(): + d[k] = s[k] + for k in sw.keys(): + d[k] = sw[k] + except (IOError, OSError): + pass + return d diff --git a/BitTornado/BT1/Filter.py b/BitTornado/BT1/Filter.py new file mode 100644 index 000000000..3506399f3 --- /dev/null +++ b/BitTornado/BT1/Filter.py @@ -0,0 +1,12 @@ +class Filter: + def __init__(self, callback): + self.callback = callback + + def check(self, ip, paramslist, headers): + + def params(key, default = None, l = paramslist): + if l.has_key(key): + return l[key][0] + return default + + return None diff --git a/BitTornado/BT1/HTTPDownloader.py b/BitTornado/BT1/HTTPDownloader.py new file mode 100644 index 000000000..922d3ffc0 --- /dev/null +++ b/BitTornado/BT1/HTTPDownloader.py @@ -0,0 +1,251 @@ +# Written by John Hoffman +# see LICENSE.txt for license information + +from BitTornado.CurrentRateMeasure import Measure +from random import randint +from urlparse import urlparse +from httplib import HTTPConnection +from urllib import quote +from threading import Thread +from BitTornado.__init__ import product_name,version_short +try: + True +except: + True = 1 + False = 0 + +EXPIRE_TIME = 60 * 60 + +VERSION = product_name+'/'+version_short + +class haveComplete: + def complete(self): + return True + def __getitem__(self, x): + return True +haveall = haveComplete() + +class SingleDownload: + def __init__(self, downloader, url): + self.downloader = downloader + self.baseurl = url + try: + (scheme, self.netloc, path, pars, query, fragment) = urlparse(url) + except: + self.downloader.errorfunc('cannot parse http seed address: '+url) + return + if scheme != 'http': + self.downloader.errorfunc('http seed url not http: '+url) + return + try: + self.connection = HTTPConnection(self.netloc) + except: + self.downloader.errorfunc('cannot connect to http seed: '+url) + return + self.seedurl = path + if pars: + self.seedurl += ';'+pars + self.seedurl += '?' + if query: + self.seedurl += query+'&' + self.seedurl += 'info_hash='+quote(self.downloader.infohash) + + self.measure = Measure(downloader.max_rate_period) + self.index = None + self.url = '' + self.requests = [] + self.request_size = 0 + self.endflag = False + self.error = None + self.retry_period = 30 + self._retry_period = None + self.errorcount = 0 + self.goodseed = False + self.active = False + self.cancelled = False + self.resched(randint(2,10)) + + def resched(self, len = None): + if len is None: + len = self.retry_period + if self.errorcount > 3: + len = len * (self.errorcount - 2) + self.downloader.rawserver.add_task(self.download, len) + + def _want(self, index): + if self.endflag: + return self.downloader.storage.do_I_have_requests(index) + else: + return self.downloader.storage.is_unstarted(index) + + def download(self): + self.cancelled = False + if self.downloader.picker.am_I_complete(): + self.downloader.downloads.remove(self) + return + self.index = self.downloader.picker.next(haveall, self._want) + if ( self.index is None and not self.endflag + and not self.downloader.peerdownloader.has_downloaders() ): + self.endflag = True + self.index = self.downloader.picker.next(haveall, self._want) + if self.index is None: + self.endflag = True + self.resched() + else: + self.url = ( self.seedurl+'&piece='+str(self.index) ) + self._get_requests() + if self.request_size < self.downloader.storage._piecelen(self.index): + self.url += '&ranges='+self._request_ranges() + rq = Thread(target = self._request) + rq.setDaemon(False) + rq.start() + self.active = True + + def _request(self): + import encodings.ascii + import encodings.punycode + import encodings.idna + + self.error = None + self.received_data = None + try: + self.connection.request('GET',self.url, None, + {'User-Agent': VERSION}) + r = self.connection.getresponse() + self.connection_status = r.status + self.received_data = r.read() + except Exception, e: + self.error = 'error accessing http seed: '+str(e) + try: + self.connection.close() + except: + pass + try: + self.connection = HTTPConnection(self.netloc) + except: + self.connection = None # will cause an exception and retry next cycle + self.downloader.rawserver.add_task(self.request_finished) + + def request_finished(self): + self.active = False + if self.error is not None: + if self.goodseed: + self.downloader.errorfunc(self.error) + self.errorcount += 1 + if self.received_data: + self.errorcount = 0 + if not self._got_data(): + self.received_data = None + if not self.received_data: + self._release_requests() + self.downloader.peerdownloader.piece_flunked(self.index) + if self._retry_period: + self.resched(self._retry_period) + self._retry_period = None + return + self.resched() + + def _got_data(self): + if self.connection_status == 503: # seed is busy + try: + self.retry_period = max(int(self.received_data),5) + except: + pass + return False + if self.connection_status != 200: + self.errorcount += 1 + return False + self._retry_period = 1 + if len(self.received_data) != self.request_size: + if self.goodseed: + self.downloader.errorfunc('corrupt data from http seed - redownloading') + return False + self.measure.update_rate(len(self.received_data)) + self.downloader.measurefunc(len(self.received_data)) + if self.cancelled: + return False + if not self._fulfill_requests(): + return False + if not self.goodseed: + self.goodseed = True + self.downloader.seedsfound += 1 + if self.downloader.storage.do_I_have(self.index): + self.downloader.picker.complete(self.index) + self.downloader.peerdownloader.check_complete(self.index) + self.downloader.gotpiecefunc(self.index) + return True + + def _get_requests(self): + self.requests = [] + self.request_size = 0L + while self.downloader.storage.do_I_have_requests(self.index): + r = self.downloader.storage.new_request(self.index) + self.requests.append(r) + self.request_size += r[1] + self.requests.sort() + + def _fulfill_requests(self): + start = 0L + success = True + while self.requests: + begin, length = self.requests.pop(0) + if not self.downloader.storage.piece_came_in(self.index, begin, + self.received_data[start:start+length]): + success = False + break + start += length + return success + + def _release_requests(self): + for begin, length in self.requests: + self.downloader.storage.request_lost(self.index, begin, length) + self.requests = [] + + def _request_ranges(self): + s = '' + begin, length = self.requests[0] + for begin1, length1 in self.requests[1:]: + if begin + length == begin1: + length += length1 + continue + else: + if s: + s += ',' + s += str(begin)+'-'+str(begin+length-1) + begin, length = begin1, length1 + if s: + s += ',' + s += str(begin)+'-'+str(begin+length-1) + return s + + +class HTTPDownloader: + def __init__(self, storage, picker, rawserver, + finflag, errorfunc, peerdownloader, + max_rate_period, infohash, measurefunc, gotpiecefunc): + self.storage = storage + self.picker = picker + self.rawserver = rawserver + self.finflag = finflag + self.errorfunc = errorfunc + self.peerdownloader = peerdownloader + self.infohash = infohash + self.max_rate_period = max_rate_period + self.gotpiecefunc = gotpiecefunc + self.measurefunc = measurefunc + self.downloads = [] + self.seedsfound = 0 + + def make_download(self, url): + self.downloads.append(SingleDownload(self, url)) + return self.downloads[-1] + + def get_downloads(self): + if self.finflag.isSet(): + return [] + return self.downloads + + def cancel_piece_download(self, pieces): + for d in self.downloads: + if d.active and d.index in pieces: + d.cancelled = True diff --git a/BitTornado/BT1/NatCheck.py b/BitTornado/BT1/NatCheck.py new file mode 100644 index 000000000..258d1cbbd --- /dev/null +++ b/BitTornado/BT1/NatCheck.py @@ -0,0 +1,95 @@ +# Written by Bram Cohen +# see LICENSE.txt for license information + +from cStringIO import StringIO +from socket import error as socketerror +from traceback import print_exc +try: + True +except: + True = 1 + False = 0 + +protocol_name = 'BitTorrent protocol' + +# header, reserved, download id, my id, [length, message] + +class NatCheck: + def __init__(self, resultfunc, downloadid, peerid, ip, port, rawserver): + self.resultfunc = resultfunc + self.downloadid = downloadid + self.peerid = peerid + self.ip = ip + self.port = port + self.closed = False + self.buffer = StringIO() + self.next_len = 1 + self.next_func = self.read_header_len + try: + self.connection = rawserver.start_connection((ip, port), self) + self.connection.write(chr(len(protocol_name)) + protocol_name + + (chr(0) * 8) + downloadid) + except socketerror: + self.answer(False) + except IOError: + self.answer(False) + + def answer(self, result): + self.closed = True + try: + self.connection.close() + except AttributeError: + pass + self.resultfunc(result, self.downloadid, self.peerid, self.ip, self.port) + + def read_header_len(self, s): + if ord(s) != len(protocol_name): + return None + return len(protocol_name), self.read_header + + def read_header(self, s): + if s != protocol_name: + return None + return 8, self.read_reserved + + def read_reserved(self, s): + return 20, self.read_download_id + + def read_download_id(self, s): + if s != self.downloadid: + return None + return 20, self.read_peer_id + + def read_peer_id(self, s): + if s != self.peerid: + return None + self.answer(True) + return None + + def data_came_in(self, connection, s): + while True: + if self.closed: + return + i = self.next_len - self.buffer.tell() + if i > len(s): + self.buffer.write(s) + return + self.buffer.write(s[:i]) + s = s[i:] + m = self.buffer.getvalue() + self.buffer.reset() + self.buffer.truncate() + x = self.next_func(m) + if x is None: + if not self.closed: + self.answer(False) + return + self.next_len, self.next_func = x + + def connection_lost(self, connection): + if not self.closed: + self.closed = True + self.resultfunc(False, self.downloadid, self.peerid, self.ip, self.port) + + def connection_flushed(self, connection): + pass diff --git a/BitTornado/BT1/PiecePicker.py b/BitTornado/BT1/PiecePicker.py new file mode 100644 index 000000000..83861f3bc --- /dev/null +++ b/BitTornado/BT1/PiecePicker.py @@ -0,0 +1,320 @@ +# Written by Bram Cohen +# see LICENSE.txt for license information + +from random import randrange, shuffle +from BitTornado.clock import clock +try: + True +except: + True = 1 + False = 0 + +class PiecePicker: + def __init__(self, numpieces, + rarest_first_cutoff = 1, rarest_first_priority_cutoff = 3, + priority_step = 20): + self.rarest_first_cutoff = rarest_first_cutoff + self.rarest_first_priority_cutoff = rarest_first_priority_cutoff + priority_step + self.priority_step = priority_step + self.cutoff = rarest_first_priority_cutoff + self.numpieces = numpieces + self.started = [] + self.totalcount = 0 + self.numhaves = [0] * numpieces + self.priority = [1] * numpieces + self.removed_partials = {} + self.crosscount = [numpieces] + self.crosscount2 = [numpieces] + self.has = [0] * numpieces + self.numgot = 0 + self.done = False + self.seed_connections = {} + self.past_ips = {} + self.seed_time = None + self.superseed = False + self.seeds_connected = 0 + self._init_interests() + + def _init_interests(self): + self.interests = [[] for x in xrange(self.priority_step)] + self.level_in_interests = [self.priority_step] * self.numpieces + interests = range(self.numpieces) + shuffle(interests) + self.pos_in_interests = [0] * self.numpieces + for i in xrange(self.numpieces): + self.pos_in_interests[interests[i]] = i + self.interests.append(interests) + + + def got_have(self, piece): + self.totalcount+=1 + numint = self.numhaves[piece] + self.numhaves[piece] += 1 + self.crosscount[numint] -= 1 + if numint+1==len(self.crosscount): + self.crosscount.append(0) + self.crosscount[numint+1] += 1 + if not self.done: + numintplus = numint+self.has[piece] + self.crosscount2[numintplus] -= 1 + if numintplus+1 == len(self.crosscount2): + self.crosscount2.append(0) + self.crosscount2[numintplus+1] += 1 + numint = self.level_in_interests[piece] + self.level_in_interests[piece] += 1 + if self.superseed: + self.seed_got_haves[piece] += 1 + numint = self.level_in_interests[piece] + self.level_in_interests[piece] += 1 + elif self.has[piece] or self.priority[piece] == -1: + return + if numint == len(self.interests) - 1: + self.interests.append([]) + self._shift_over(piece, self.interests[numint], self.interests[numint + 1]) + + def lost_have(self, piece): + self.totalcount-=1 + numint = self.numhaves[piece] + self.numhaves[piece] -= 1 + self.crosscount[numint] -= 1 + self.crosscount[numint-1] += 1 + if not self.done: + numintplus = numint+self.has[piece] + self.crosscount2[numintplus] -= 1 + self.crosscount2[numintplus-1] += 1 + numint = self.level_in_interests[piece] + self.level_in_interests[piece] -= 1 + if self.superseed: + numint = self.level_in_interests[piece] + self.level_in_interests[piece] -= 1 + elif self.has[piece] or self.priority[piece] == -1: + return + self._shift_over(piece, self.interests[numint], self.interests[numint - 1]) + + def _shift_over(self, piece, l1, l2): + assert self.superseed or (not self.has[piece] and self.priority[piece] >= 0) + parray = self.pos_in_interests + p = parray[piece] + assert l1[p] == piece + q = l1[-1] + l1[p] = q + parray[q] = p + del l1[-1] + newp = randrange(len(l2)+1) + if newp == len(l2): + parray[piece] = len(l2) + l2.append(piece) + else: + old = l2[newp] + parray[old] = len(l2) + l2.append(old) + l2[newp] = piece + parray[piece] = newp + + + def got_seed(self): + self.seeds_connected += 1 + self.cutoff = max(self.rarest_first_priority_cutoff-self.seeds_connected,0) + + def became_seed(self): + self.got_seed() + self.totalcount -= self.numpieces + self.numhaves = [i-1 for i in self.numhaves] + if self.superseed or not self.done: + self.level_in_interests = [i-1 for i in self.level_in_interests] + if self.interests: + del self.interests[0] + del self.crosscount[0] + if not self.done: + del self.crosscount2[0] + + def lost_seed(self): + self.seeds_connected -= 1 + self.cutoff = max(self.rarest_first_priority_cutoff-self.seeds_connected,0) + + + def requested(self, piece): + if piece not in self.started: + self.started.append(piece) + + def _remove_from_interests(self, piece, keep_partial = False): + l = self.interests[self.level_in_interests[piece]] + p = self.pos_in_interests[piece] + assert l[p] == piece + q = l[-1] + l[p] = q + self.pos_in_interests[q] = p + del l[-1] + try: + self.started.remove(piece) + if keep_partial: + self.removed_partials[piece] = 1 + except ValueError: + pass + + def complete(self, piece): + assert not self.has[piece] + self.has[piece] = 1 + self.numgot += 1 + if self.numgot == self.numpieces: + self.done = True + self.crosscount2 = self.crosscount + else: + numhaves = self.numhaves[piece] + self.crosscount2[numhaves] -= 1 + if numhaves+1 == len(self.crosscount2): + self.crosscount2.append(0) + self.crosscount2[numhaves+1] += 1 + self._remove_from_interests(piece) + + + def next(self, haves, wantfunc, complete_first = False): + cutoff = self.numgot < self.rarest_first_cutoff + complete_first = (complete_first or cutoff) and not haves.complete() + best = None + bestnum = 2 ** 30 + for i in self.started: + if haves[i] and wantfunc(i): + if self.level_in_interests[i] < bestnum: + best = i + bestnum = self.level_in_interests[i] + if best is not None: + if complete_first or (cutoff and len(self.interests) > self.cutoff): + return best + if haves.complete(): + r = [ (0, min(bestnum,len(self.interests))) ] + elif cutoff and len(self.interests) > self.cutoff: + r = [ (self.cutoff, min(bestnum,len(self.interests))), + (0, self.cutoff) ] + else: + r = [ (0, min(bestnum,len(self.interests))) ] + for lo,hi in r: + for i in xrange(lo,hi): + for j in self.interests[i]: + if haves[j] and wantfunc(j): + return j + if best is not None: + return best + return None + + + def am_I_complete(self): + return self.done + + def bump(self, piece): + l = self.interests[self.level_in_interests[piece]] + pos = self.pos_in_interests[piece] + del l[pos] + l.append(piece) + for i in range(pos,len(l)): + self.pos_in_interests[l[i]] = i + try: + self.started.remove(piece) + except: + pass + + def set_priority(self, piece, p): + if self.superseed: + return False # don't muck with this if you're a superseed + oldp = self.priority[piece] + if oldp == p: + return False + self.priority[piece] = p + if p == -1: + # when setting priority -1, + # make sure to cancel any downloads for this piece + if not self.has[piece]: + self._remove_from_interests(piece, True) + return True + if oldp == -1: + level = self.numhaves[piece] + (self.priority_step * p) + self.level_in_interests[piece] = level + if self.has[piece]: + return True + while len(self.interests) < level+1: + self.interests.append([]) + l2 = self.interests[level] + parray = self.pos_in_interests + newp = randrange(len(l2)+1) + if newp == len(l2): + parray[piece] = len(l2) + l2.append(piece) + else: + old = l2[newp] + parray[old] = len(l2) + l2.append(old) + l2[newp] = piece + parray[piece] = newp + if self.removed_partials.has_key(piece): + del self.removed_partials[piece] + self.started.append(piece) + # now go to downloader and try requesting more + return True + numint = self.level_in_interests[piece] + newint = numint + ((p - oldp) * self.priority_step) + self.level_in_interests[piece] = newint + if self.has[piece]: + return False + while len(self.interests) < newint+1: + self.interests.append([]) + self._shift_over(piece, self.interests[numint], self.interests[newint]) + return False + + def is_blocked(self, piece): + return self.priority[piece] < 0 + + + def set_superseed(self): + assert self.done + self.superseed = True + self.seed_got_haves = [0] * self.numpieces + self._init_interests() # assume everyone is disconnected + + def next_have(self, connection, looser_upload): + if self.seed_time is None: + self.seed_time = clock() + return None + if clock() < self.seed_time+10: # wait 10 seconds after seeing the first peers + return None # to give time to grab have lists + if not connection.upload.super_seeding: + return None + olddl = self.seed_connections.get(connection) + if olddl is None: + ip = connection.get_ip() + olddl = self.past_ips.get(ip) + if olddl is not None: # peer reconnected + self.seed_connections[connection] = olddl + if olddl is not None: + if looser_upload: + num = 1 # send a new have even if it hasn't spread that piece elsewhere + else: + num = 2 + if self.seed_got_haves[olddl] < num: + return None + if not connection.upload.was_ever_interested: # it never downloaded it? + connection.upload.skipped_count += 1 + if connection.upload.skipped_count >= 3: # probably another stealthed seed + return -1 # signal to close it + for tier in self.interests: + for piece in tier: + if not connection.download.have[piece]: + seedint = self.level_in_interests[piece] + self.level_in_interests[piece] += 1 # tweak it up one, so you don't duplicate effort + if seedint == len(self.interests) - 1: + self.interests.append([]) + self._shift_over(piece, + self.interests[seedint], self.interests[seedint + 1]) + self.seed_got_haves[piece] = 0 # reset this + self.seed_connections[connection] = piece + connection.upload.seed_have_list.append(piece) + return piece + return -1 # something screwy; terminate connection + + def lost_peer(self, connection): + olddl = self.seed_connections.get(connection) + if olddl is None: + return + del self.seed_connections[connection] + self.past_ips[connection.get_ip()] = olddl + if self.seed_got_haves[olddl] == 1: + self.seed_got_haves[olddl] = 0 diff --git a/BitTornado/BT1/Rerequester.py b/BitTornado/BT1/Rerequester.py new file mode 100644 index 000000000..b6aa3cf5c --- /dev/null +++ b/BitTornado/BT1/Rerequester.py @@ -0,0 +1,425 @@ +# Written by Bram Cohen +# modified for multitracker operation by John Hoffman +# see LICENSE.txt for license information + +from BitTornado.zurllib import urlopen, quote +from urlparse import urlparse, urlunparse +from socket import gethostbyname +from btformats import check_peers +from BitTornado.bencode import bdecode +from threading import Thread, Lock +from cStringIO import StringIO +from traceback import print_exc +from socket import error, gethostbyname +from random import shuffle +from sha import sha +from time import time +try: + from os import getpid +except ImportError: + def getpid(): + return 1 + +try: + True +except: + True = 1 + False = 0 + +mapbase64 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.-' +keys = {} +basekeydata = str(getpid()) + repr(time()) + 'tracker' + +def add_key(tracker): + key = '' + for i in sha(basekeydata+tracker).digest()[-6:]: + key += mapbase64[ord(i) & 0x3F] + keys[tracker] = key + +def get_key(tracker): + try: + return "&key="+keys[tracker] + except: + add_key(tracker) + return "&key="+keys[tracker] + +class fakeflag: + def __init__(self, state=False): + self.state = state + def wait(self): + pass + def isSet(self): + return self.state + +class Rerequester: + def __init__(self, trackerlist, interval, sched, howmany, minpeers, + connect, externalsched, amount_left, up, down, + port, ip, myid, infohash, timeout, errorfunc, excfunc, + maxpeers, doneflag, upratefunc, downratefunc, + unpauseflag = fakeflag(True), + seed_id = '', seededfunc = None, force_rapid_update = False ): + + self.excfunc = excfunc + newtrackerlist = [] + for tier in trackerlist: + if len(tier)>1: + shuffle(tier) + newtrackerlist += [tier] + self.trackerlist = newtrackerlist + self.lastsuccessful = '' + self.rejectedmessage = 'rejected by tracker - ' + + self.url = ('?info_hash=%s&peer_id=%s&port=%s' % + (quote(infohash), quote(myid), str(port))) + self.ip = ip + self.interval = interval + self.last = None + self.trackerid = None + self.announce_interval = 30 * 60 + self.sched = sched + self.howmany = howmany + self.minpeers = minpeers + self.connect = connect + self.externalsched = externalsched + self.amount_left = amount_left + self.up = up + self.down = down + self.timeout = timeout + self.errorfunc = errorfunc + self.maxpeers = maxpeers + self.doneflag = doneflag + self.upratefunc = upratefunc + self.downratefunc = downratefunc + self.unpauseflag = unpauseflag + if seed_id: + self.url += '&seed_id='+quote(seed_id) + self.seededfunc = seededfunc + if seededfunc: + self.url += '&check_seeded=1' + self.force_rapid_update = force_rapid_update + self.last_failed = True + self.never_succeeded = True + self.errorcodes = {} + self.lock = SuccessLock() + self.special = None + self.stopped = False + + def start(self): + self.sched(self.c, self.interval/2) + self.d(0) + + def c(self): + if self.stopped: + return + if not self.unpauseflag.isSet() and ( + self.howmany() < self.minpeers or self.force_rapid_update ): + self.announce(3, self._c) + else: + self._c() + + def _c(self): + self.sched(self.c, self.interval) + + def d(self, event = 3): + if self.stopped: + return + if not self.unpauseflag.isSet(): + self._d() + return + self.announce(event, self._d) + + def _d(self): + if self.never_succeeded: + self.sched(self.d, 60) # retry in 60 seconds + elif self.force_rapid_update: + return + else: + self.sched(self.d, self.announce_interval) + + + def hit(self, event = 3): + if not self.unpauseflag.isSet() and ( + self.howmany() < self.minpeers or self.force_rapid_update ): + self.announce(event) + + def announce(self, event = 3, callback = lambda: None, specialurl = None): + + if specialurl is not None: + s = self.url+'&uploaded=0&downloaded=0&left=1' # don't add to statistics + if self.howmany() >= self.maxpeers: + s += '&numwant=0' + else: + s += '&no_peer_id=1&compact=1' + self.last_failed = True # force true, so will display an error + self.special = specialurl + self.rerequest(s, callback) + return + + else: + s = ('%s&uploaded=%s&downloaded=%s&left=%s' % + (self.url, str(self.up()), str(self.down()), + str(self.amount_left()))) + if self.last is not None: + s += '&last=' + quote(str(self.last)) + if self.trackerid is not None: + s += '&trackerid=' + quote(str(self.trackerid)) + if self.howmany() >= self.maxpeers: + s += '&numwant=0' + else: + s += '&no_peer_id=1&compact=1' + if event != 3: + s += '&event=' + ['started', 'completed', 'stopped'][event] + if event == 2: + self.stopped = True + self.rerequest(s, callback) + + + def snoop(self, peers, callback = lambda: None): # tracker call support + self.rerequest(self.url + +'&event=stopped&port=0&uploaded=0&downloaded=0&left=1&tracker=1&numwant=' + +str(peers), callback) + + + def rerequest(self, s, callback): + if not self.lock.isfinished(): # still waiting for prior cycle to complete?? + def retry(self = self, s = s, callback = callback): + self.rerequest(s, callback) + self.sched(retry,5) # retry in 5 seconds + return + self.lock.reset() + rq = Thread(target = self._rerequest, args = [s, callback]) + rq.setDaemon(False) + rq.start() + + def _rerequest(self, s, callback): + try: + def fail (self = self, callback = callback): + self._fail(callback) + if self.ip: + try: + s += '&ip=' + gethostbyname(self.ip) + except: + self.errorcodes['troublecode'] = 'unable to resolve: '+self.ip + self.externalsched(fail) + self.errorcodes = {} + if self.special is None: + for t in range(len(self.trackerlist)): + for tr in range(len(self.trackerlist[t])): + tracker = self.trackerlist[t][tr] + if self.rerequest_single(tracker, s, callback): + if not self.last_failed and tr != 0: + del self.trackerlist[t][tr] + self.trackerlist[t] = [tracker] + self.trackerlist[t] + return + else: + tracker = self.special + self.special = None + if self.rerequest_single(tracker, s, callback): + return + # no success from any tracker + self.externalsched(fail) + except: + self.exception(callback) + + + def _fail(self, callback): + if ( (self.upratefunc() < 100 and self.downratefunc() < 100) + or not self.amount_left() ): + for f in ['rejected', 'bad_data', 'troublecode']: + if self.errorcodes.has_key(f): + r = self.errorcodes[f] + break + else: + r = 'Problem connecting to tracker - unspecified error' + self.errorfunc(r) + + self.last_failed = True + self.lock.give_up() + self.externalsched(callback) + + + def rerequest_single(self, t, s, callback): + l = self.lock.set() + rq = Thread(target = self._rerequest_single, args = [t, s+get_key(t), l, callback]) + rq.setDaemon(False) + rq.start() + self.lock.wait() + if self.lock.success: + self.lastsuccessful = t + self.last_failed = False + self.never_succeeded = False + return True + if not self.last_failed and self.lastsuccessful == t: + # if the last tracker hit was successful, and you've just tried the tracker + # you'd contacted before, don't go any further, just fail silently. + self.last_failed = True + self.externalsched(callback) + self.lock.give_up() + return True + return False # returns true if it wants rerequest() to exit + + + def _rerequest_single(self, t, s, l, callback): + try: + closer = [None] + def timedout(self = self, l = l, closer = closer): + if self.lock.trip(l): + self.errorcodes['troublecode'] = 'Problem connecting to tracker - timeout exceeded' + self.lock.unwait(l) + try: + closer[0]() + except: + pass + + self.externalsched(timedout, self.timeout) + + err = None + try: + h = urlopen(t+s) + closer[0] = h.close + data = h.read() + except (IOError, error), e: + err = 'Problem connecting to tracker - ' + str(e) + except: + err = 'Problem connecting to tracker' + try: + h.close() + except: + pass + if err: + if self.lock.trip(l): + self.errorcodes['troublecode'] = err + self.lock.unwait(l) + return + + if data == '': + if self.lock.trip(l): + self.errorcodes['troublecode'] = 'no data from tracker' + self.lock.unwait(l) + return + + try: + r = bdecode(data, sloppy=1) + check_peers(r) + except ValueError, e: + if self.lock.trip(l): + self.errorcodes['bad_data'] = 'bad data from tracker - ' + str(e) + self.lock.unwait(l) + return + + if r.has_key('failure reason'): + if self.lock.trip(l): + self.errorcodes['rejected'] = self.rejectedmessage + r['failure reason'] + self.lock.unwait(l) + return + + if self.lock.trip(l, True): # success! + self.lock.unwait(l) + else: + callback = lambda: None # attempt timed out, don't do a callback + + # even if the attempt timed out, go ahead and process data + def add(self = self, r = r, callback = callback): + self.postrequest(r, callback) + self.externalsched(add) + except: + self.exception(callback) + + + def postrequest(self, r, callback): + if r.has_key('warning message'): + self.errorfunc('warning from tracker - ' + r['warning message']) + self.announce_interval = r.get('interval', self.announce_interval) + self.interval = r.get('min interval', self.interval) + self.trackerid = r.get('tracker id', self.trackerid) + self.last = r.get('last') +# ps = len(r['peers']) + self.howmany() + p = r['peers'] + peers = [] + if type(p) == type(''): + for x in xrange(0, len(p), 6): + ip = '.'.join([str(ord(i)) for i in p[x:x+4]]) + port = (ord(p[x+4]) << 8) | ord(p[x+5]) + peers.append(((ip, port), 0)) + else: + for x in p: + peers.append(((x['ip'].strip(), x['port']), x.get('peer id',0))) + ps = len(peers) + self.howmany() + if ps < self.maxpeers: + if self.doneflag.isSet(): + if r.get('num peers', 1000) - r.get('done peers', 0) > ps * 1.2: + self.last = None + else: + if r.get('num peers', 1000) > ps * 1.2: + self.last = None + if self.seededfunc and r.get('seeded'): + self.seededfunc() + elif peers: + shuffle(peers) + self.connect(peers) + callback() + + def exception(self, callback): + data = StringIO() + print_exc(file = data) + def r(s = data.getvalue(), callback = callback): + if self.excfunc: + self.excfunc(s) + else: + print s + callback() + self.externalsched(r) + + +class SuccessLock: + def __init__(self): + self.lock = Lock() + self.pause = Lock() + self.code = 0L + self.success = False + self.finished = True + + def reset(self): + self.success = False + self.finished = False + + def set(self): + self.lock.acquire() + if not self.pause.locked(): + self.pause.acquire() + self.first = True + self.code += 1L + self.lock.release() + return self.code + + def trip(self, code, s = False): + self.lock.acquire() + try: + if code == self.code and not self.finished: + r = self.first + self.first = False + if s: + self.finished = True + self.success = True + return r + finally: + self.lock.release() + + def give_up(self): + self.lock.acquire() + self.success = False + self.finished = True + self.lock.release() + + def wait(self): + self.pause.acquire() + + def unwait(self, code): + if code == self.code and self.pause.locked(): + self.pause.release() + + def isfinished(self): + self.lock.acquire() + x = self.finished + self.lock.release() + return x diff --git a/BitTornado/BT1/Statistics.py b/BitTornado/BT1/Statistics.py new file mode 100644 index 000000000..499907781 --- /dev/null +++ b/BitTornado/BT1/Statistics.py @@ -0,0 +1,177 @@ +# Written by Edward Keyes +# see LICENSE.txt for license information + +from threading import Event +try: + True +except: + True = 1 + False = 0 + +class Statistics_Response: + pass # empty class + + +class Statistics: + def __init__(self, upmeasure, downmeasure, connecter, httpdl, + ratelimiter, rerequest_lastfailed, fdatflag): + self.upmeasure = upmeasure + self.downmeasure = downmeasure + self.connecter = connecter + self.httpdl = httpdl + self.ratelimiter = ratelimiter + self.downloader = connecter.downloader + self.picker = connecter.downloader.picker + self.storage = connecter.downloader.storage + self.torrentmeasure = connecter.downloader.totalmeasure + self.rerequest_lastfailed = rerequest_lastfailed + self.fdatflag = fdatflag + self.fdatactive = False + self.piecescomplete = None + self.placesopen = None + self.storage_totalpieces = len(self.storage.hashes) + + + def set_dirstats(self, files, piece_length): + self.piecescomplete = 0 + self.placesopen = 0 + self.filelistupdated = Event() + self.filelistupdated.set() + frange = xrange(len(files)) + self.filepieces = [[] for x in frange] + self.filepieces2 = [[] for x in frange] + self.fileamtdone = [0.0 for x in frange] + self.filecomplete = [False for x in frange] + self.fileinplace = [False for x in frange] + start = 0L + for i in frange: + l = files[i][1] + if l == 0: + self.fileamtdone[i] = 1.0 + self.filecomplete[i] = True + self.fileinplace[i] = True + else: + fp = self.filepieces[i] + fp2 = self.filepieces2[i] + for piece in range(int(start/piece_length), + int((start+l-1)/piece_length)+1): + fp.append(piece) + fp2.append(piece) + start += l + + + def update(self): + s = Statistics_Response() + s.upTotal = self.upmeasure.get_total() + s.downTotal = self.downmeasure.get_total() + s.last_failed = self.rerequest_lastfailed() + s.external_connection_made = self.connecter.external_connection_made + if s.downTotal > 0: + s.shareRating = float(s.upTotal)/s.downTotal + elif s.upTotal == 0: + s.shareRating = 0.0 + else: + s.shareRating = -1.0 + s.torrentRate = self.torrentmeasure.get_rate() + s.torrentTotal = self.torrentmeasure.get_total() + s.numSeeds = self.picker.seeds_connected + s.numOldSeeds = self.downloader.num_disconnected_seeds() + s.numPeers = len(self.downloader.downloads)-s.numSeeds + s.numCopies = 0.0 + for i in self.picker.crosscount: + if i==0: + s.numCopies+=1 + else: + s.numCopies+=1-float(i)/self.picker.numpieces + break + if self.picker.done: + s.numCopies2 = s.numCopies + 1 + else: + s.numCopies2 = 0.0 + for i in self.picker.crosscount2: + if i==0: + s.numCopies2+=1 + else: + s.numCopies2+=1-float(i)/self.picker.numpieces + break + s.discarded = self.downloader.discarded + s.numSeeds += self.httpdl.seedsfound + s.numOldSeeds += self.httpdl.seedsfound + if s.numPeers == 0 or self.picker.numpieces == 0: + s.percentDone = 0.0 + else: + s.percentDone = 100.0*(float(self.picker.totalcount)/self.picker.numpieces)/s.numPeers + + s.backgroundallocating = self.storage.bgalloc_active + s.storage_totalpieces = len(self.storage.hashes) + s.storage_active = len(self.storage.stat_active) + s.storage_new = len(self.storage.stat_new) + s.storage_dirty = len(self.storage.dirty) + numdownloaded = self.storage.stat_numdownloaded + s.storage_justdownloaded = numdownloaded + s.storage_numcomplete = self.storage.stat_numfound + numdownloaded + s.storage_numflunked = self.storage.stat_numflunked + s.storage_isendgame = self.downloader.endgamemode + + s.peers_kicked = self.downloader.kicked.items() + s.peers_banned = self.downloader.banned.items() + + try: + s.upRate = int(self.ratelimiter.upload_rate/1000) + assert s.upRate < 5000 + except: + s.upRate = 0 + s.upSlots = self.ratelimiter.slots + + if self.piecescomplete is None: # not a multi-file torrent + return s + + if self.fdatflag.isSet(): + if not self.fdatactive: + self.fdatactive = True + else: + self.fdatactive = False + + if self.piecescomplete != self.picker.numgot: + for i in xrange(len(self.filecomplete)): + if self.filecomplete[i]: + continue + oldlist = self.filepieces[i] + newlist = [ piece + for piece in oldlist + if not self.storage.have[piece] ] + if len(newlist) != len(oldlist): + self.filepieces[i] = newlist + self.fileamtdone[i] = ( + (len(self.filepieces2[i])-len(newlist)) + /float(len(self.filepieces2[i])) ) + if not newlist: + self.filecomplete[i] = True + self.filelistupdated.set() + + self.piecescomplete = self.picker.numgot + + if ( self.filelistupdated.isSet() + or self.placesopen != len(self.storage.places) ): + for i in xrange(len(self.filecomplete)): + if not self.filecomplete[i] or self.fileinplace[i]: + continue + while self.filepieces2[i]: + piece = self.filepieces2[i][-1] + if self.storage.places[piece] != piece: + break + del self.filepieces2[i][-1] + if not self.filepieces2[i]: + self.fileinplace[i] = True + self.storage.set_file_readonly(i) + self.filelistupdated.set() + + self.placesopen = len(self.storage.places) + + s.fileamtdone = self.fileamtdone + s.filecomplete = self.filecomplete + s.fileinplace = self.fileinplace + s.filelistupdated = self.filelistupdated + + return s + diff --git a/BitTornado/BT1/Storage.py b/BitTornado/BT1/Storage.py new file mode 100644 index 000000000..1ee9b5c81 --- /dev/null +++ b/BitTornado/BT1/Storage.py @@ -0,0 +1,584 @@ +# Written by Bram Cohen +# see LICENSE.txt for license information + +from BitTornado.piecebuffer import BufferPool +from threading import Lock +from time import time, strftime, localtime +import os +from os.path import exists, getsize, getmtime, basename +from traceback import print_exc +try: + from os import fsync +except ImportError: + fsync = lambda x: None +from bisect import bisect + +try: + True +except: + True = 1 + False = 0 + +DEBUG = False + +MAXREADSIZE = 32768 +MAXLOCKSIZE = 1000000000L +MAXLOCKRANGE = 3999999999L # only lock first 4 gig of file + +_pool = BufferPool() +PieceBuffer = _pool.new + +def dummy_status(fractionDone = None, activity = None): + pass + +class Storage: + def __init__(self, files, piece_length, doneflag, config, + disabled_files = None): + # can raise IOError and ValueError + self.files = files + self.piece_length = piece_length + self.doneflag = doneflag + self.disabled = [False] * len(files) + self.file_ranges = [] + self.disabled_ranges = [] + self.working_ranges = [] + numfiles = 0 + total = 0l + so_far = 0l + self.handles = {} + self.whandles = {} + self.tops = {} + self.sizes = {} + self.mtimes = {} + if config.get('lock_files', True): + self.lock_file, self.unlock_file = self._lock_file, self._unlock_file + else: + self.lock_file, self.unlock_file = lambda x1,x2: None, lambda x1,x2: None + self.lock_while_reading = config.get('lock_while_reading', False) + self.lock = Lock() + + if not disabled_files: + disabled_files = [False] * len(files) + + for i in xrange(len(files)): + file, length = files[i] + if doneflag.isSet(): # bail out if doneflag is set + return + self.disabled_ranges.append(None) + if length == 0: + self.file_ranges.append(None) + self.working_ranges.append([]) + else: + range = (total, total + length, 0, file) + self.file_ranges.append(range) + self.working_ranges.append([range]) + numfiles += 1 + total += length + if disabled_files[i]: + l = 0 + else: + if exists(file): + l = getsize(file) + if l > length: + h = open(file, 'rb+') + h.truncate(length) + h.flush() + h.close() + l = length + else: + l = 0 + h = open(file, 'wb+') + h.flush() + h.close() + self.mtimes[file] = getmtime(file) + self.tops[file] = l + self.sizes[file] = length + so_far += l + + self.total_length = total + self._reset_ranges() + + self.max_files_open = config['max_files_open'] + if self.max_files_open > 0 and numfiles > self.max_files_open: + self.handlebuffer = [] + else: + self.handlebuffer = None + + + if os.name == 'nt': + def _lock_file(self, name, f): + import msvcrt + for p in range(0, min(self.sizes[name],MAXLOCKRANGE), MAXLOCKSIZE): + f.seek(p) + msvcrt.locking(f.fileno(), msvcrt.LK_LOCK, + min(MAXLOCKSIZE,self.sizes[name]-p)) + + def _unlock_file(self, name, f): + import msvcrt + for p in range(0, min(self.sizes[name],MAXLOCKRANGE), MAXLOCKSIZE): + f.seek(p) + msvcrt.locking(f.fileno(), msvcrt.LK_UNLCK, + min(MAXLOCKSIZE,self.sizes[name]-p)) + + elif os.name == 'posix': + def _lock_file(self, name, f): + import fcntl + fcntl.flock(f.fileno(), fcntl.LOCK_EX) + + def _unlock_file(self, name, f): + import fcntl + fcntl.flock(f.fileno(), fcntl.LOCK_UN) + + else: + def _lock_file(self, name, f): + pass + def _unlock_file(self, name, f): + pass + + + def was_preallocated(self, pos, length): + for file, begin, end in self._intervals(pos, length): + if self.tops.get(file, 0) < end: + return False + return True + + + def _sync(self, file): + self._close(file) + if self.handlebuffer: + self.handlebuffer.remove(file) + + def sync(self): + # may raise IOError or OSError + for file in self.whandles.keys(): + self._sync(file) + + + def set_readonly(self, f=None): + if f is None: + self.sync() + return + file = self.files[f][0] + if self.whandles.has_key(file): + self._sync(file) + + + def get_total_length(self): + return self.total_length + + + def _open(self, file, mode): + if self.mtimes.has_key(file): + try: + if self.handlebuffer is not None: + assert getsize(file) == self.tops[file] + newmtime = getmtime(file) + oldmtime = self.mtimes[file] + assert newmtime <= oldmtime+1 + assert newmtime >= oldmtime-1 + except: + if DEBUG: + print ( file+' modified: ' + +strftime('(%x %X)',localtime(self.mtimes[file])) + +strftime(' != (%x %X) ?',localtime(getmtime(file))) ) + raise IOError('modified during download') + try: + return open(file, mode) + except: + if DEBUG: + print_exc() + raise + + + def _close(self, file): + f = self.handles[file] + del self.handles[file] + if self.whandles.has_key(file): + del self.whandles[file] + f.flush() + self.unlock_file(file, f) + f.close() + self.tops[file] = getsize(file) + self.mtimes[file] = getmtime(file) + else: + if self.lock_while_reading: + self.unlock_file(file, f) + f.close() + + + def _close_file(self, file): + if not self.handles.has_key(file): + return + self._close(file) + if self.handlebuffer: + self.handlebuffer.remove(file) + + + def _get_file_handle(self, file, for_write): + if self.handles.has_key(file): + if for_write and not self.whandles.has_key(file): + self._close(file) + try: + f = self._open(file, 'rb+') + self.handles[file] = f + self.whandles[file] = 1 + self.lock_file(file, f) + except (IOError, OSError), e: + if DEBUG: + print_exc() + raise IOError('unable to reopen '+file+': '+str(e)) + + if self.handlebuffer: + if self.handlebuffer[-1] != file: + self.handlebuffer.remove(file) + self.handlebuffer.append(file) + elif self.handlebuffer is not None: + self.handlebuffer.append(file) + else: + try: + if for_write: + f = self._open(file, 'rb+') + self.handles[file] = f + self.whandles[file] = 1 + self.lock_file(file, f) + else: + f = self._open(file, 'rb') + self.handles[file] = f + if self.lock_while_reading: + self.lock_file(file, f) + except (IOError, OSError), e: + if DEBUG: + print_exc() + raise IOError('unable to open '+file+': '+str(e)) + + if self.handlebuffer is not None: + self.handlebuffer.append(file) + if len(self.handlebuffer) > self.max_files_open: + self._close(self.handlebuffer.pop(0)) + + return self.handles[file] + + + def _reset_ranges(self): + self.ranges = [] + for l in self.working_ranges: + self.ranges.extend(l) + self.begins = [i[0] for i in self.ranges] + + def _intervals(self, pos, amount): + r = [] + stop = pos + amount + p = bisect(self.begins, pos) - 1 + while p < len(self.ranges): + begin, end, offset, file = self.ranges[p] + if begin >= stop: + break + r.append(( file, + offset + max(pos, begin) - begin, + offset + min(end, stop) - begin )) + p += 1 + return r + + + def read(self, pos, amount, flush_first = False): + r = PieceBuffer() + for file, pos, end in self._intervals(pos, amount): + if DEBUG: + print 'reading '+file+' from '+str(pos)+' to '+str(end) + self.lock.acquire() + h = self._get_file_handle(file, False) + if flush_first and self.whandles.has_key(file): + h.flush() + fsync(h) + h.seek(pos) + while pos < end: + length = min(end-pos, MAXREADSIZE) + data = h.read(length) + if len(data) != length: + raise IOError('error reading data from '+file) + r.append(data) + pos += length + self.lock.release() + return r + + def write(self, pos, s): + # might raise an IOError + total = 0 + for file, begin, end in self._intervals(pos, len(s)): + if DEBUG: + print 'writing '+file+' from '+str(pos)+' to '+str(end) + self.lock.acquire() + h = self._get_file_handle(file, True) + h.seek(begin) + h.write(s[total: total + end - begin]) + self.lock.release() + total += end - begin + + def top_off(self): + for begin, end, offset, file in self.ranges: + l = offset + end - begin + if l > self.tops.get(file, 0): + self.lock.acquire() + h = self._get_file_handle(file, True) + h.seek(l-1) + h.write(chr(0xFF)) + self.lock.release() + + def flush(self): + # may raise IOError or OSError + for file in self.whandles.keys(): + self.lock.acquire() + self.handles[file].flush() + self.lock.release() + + def close(self): + for file, f in self.handles.items(): + try: + self.unlock_file(file, f) + except: + pass + try: + f.close() + except: + pass + self.handles = {} + self.whandles = {} + self.handlebuffer = None + + + def _get_disabled_ranges(self, f): + if not self.file_ranges[f]: + return ((),(),()) + r = self.disabled_ranges[f] + if r: + return r + start, end, offset, file = self.file_ranges[f] + if DEBUG: + print 'calculating disabled range for '+self.files[f][0] + print 'bytes: '+str(start)+'-'+str(end) + print 'file spans pieces '+str(int(start/self.piece_length))+'-'+str(int((end-1)/self.piece_length)+1) + pieces = range( int(start/self.piece_length), + int((end-1)/self.piece_length)+1 ) + offset = 0 + disabled_files = [] + if len(pieces) == 1: + if ( start % self.piece_length == 0 + and end % self.piece_length == 0 ): # happens to be a single, + # perfect piece + working_range = [(start, end, offset, file)] + update_pieces = [] + else: + midfile = os.path.join(self.bufferdir,str(f)) + working_range = [(start, end, 0, midfile)] + disabled_files.append((midfile, start, end)) + length = end - start + self.sizes[midfile] = length + piece = pieces[0] + update_pieces = [(piece, start-(piece*self.piece_length), length)] + else: + update_pieces = [] + if start % self.piece_length != 0: # doesn't begin on an even piece boundary + end_b = pieces[1]*self.piece_length + startfile = os.path.join(self.bufferdir,str(f)+'b') + working_range_b = [ ( start, end_b, 0, startfile ) ] + disabled_files.append((startfile, start, end_b)) + length = end_b - start + self.sizes[startfile] = length + offset = length + piece = pieces.pop(0) + update_pieces.append((piece, start-(piece*self.piece_length), length)) + else: + working_range_b = [] + if f != len(self.files)-1 and end % self.piece_length != 0: + # doesn't end on an even piece boundary + start_e = pieces[-1] * self.piece_length + endfile = os.path.join(self.bufferdir,str(f)+'e') + working_range_e = [ ( start_e, end, 0, endfile ) ] + disabled_files.append((endfile, start_e, end)) + length = end - start_e + self.sizes[endfile] = length + piece = pieces.pop(-1) + update_pieces.append((piece, 0, length)) + else: + working_range_e = [] + if pieces: + working_range_m = [ ( pieces[0]*self.piece_length, + (pieces[-1]+1)*self.piece_length, + offset, file ) ] + else: + working_range_m = [] + working_range = working_range_b + working_range_m + working_range_e + + if DEBUG: + print str(working_range) + print str(update_pieces) + r = (tuple(working_range), tuple(update_pieces), tuple(disabled_files)) + self.disabled_ranges[f] = r + return r + + + def set_bufferdir(self, dir): + self.bufferdir = dir + + def enable_file(self, f): + if not self.disabled[f]: + return + self.disabled[f] = False + r = self.file_ranges[f] + if not r: + return + file = r[3] + if not exists(file): + h = open(file, 'wb+') + h.flush() + h.close() + if not self.tops.has_key(file): + self.tops[file] = getsize(file) + if not self.mtimes.has_key(file): + self.mtimes[file] = getmtime(file) + self.working_ranges[f] = [r] + + def disable_file(self, f): + if self.disabled[f]: + return + self.disabled[f] = True + r = self._get_disabled_ranges(f) + if not r: + return + for file, begin, end in r[2]: + if not os.path.isdir(self.bufferdir): + os.makedirs(self.bufferdir) + if not exists(file): + h = open(file, 'wb+') + h.flush() + h.close() + if not self.tops.has_key(file): + self.tops[file] = getsize(file) + if not self.mtimes.has_key(file): + self.mtimes[file] = getmtime(file) + self.working_ranges[f] = r[0] + + reset_file_status = _reset_ranges + + + def get_piece_update_list(self, f): + return self._get_disabled_ranges(f)[1] + + + def delete_file(self, f): + try: + os.remove(self.files[f][0]) + except: + pass + + + ''' + Pickled data format: + + d['files'] = [ file #, size, mtime {, file #, size, mtime...} ] + file # in torrent, and the size and last modification + time for those files. Missing files are either empty + or disabled. + d['partial files'] = [ name, size, mtime... ] + Names, sizes and last modification times of files containing + partial piece data. Filenames go by the following convention: + {file #, 0-based}{nothing, "b" or "e"} + eg: "0e" "3" "4b" "4e" + Where "b" specifies the partial data for the first piece in + the file, "e" the last piece, and no letter signifying that + the file is disabled but is smaller than one piece, and that + all the data is cached inside so adjacent files may be + verified. + ''' + def pickle(self): + files = [] + pfiles = [] + for i in xrange(len(self.files)): + if not self.files[i][1]: # length == 0 + continue + if self.disabled[i]: + for file, start, end in self._get_disabled_ranges(i)[2]: + pfiles.extend([basename(file),getsize(file),int(getmtime(file))]) + continue + file = self.files[i][0] + files.extend([i,getsize(file),int(getmtime(file))]) + return {'files': files, 'partial files': pfiles} + + + def unpickle(self, data): + # assume all previously-disabled files have already been disabled + try: + files = {} + pfiles = {} + l = data['files'] + assert len(l) % 3 == 0 + l = [l[x:x+3] for x in xrange(0,len(l),3)] + for f, size, mtime in l: + files[f] = (size, mtime) + l = data.get('partial files',[]) + assert len(l) % 3 == 0 + l = [l[x:x+3] for x in xrange(0,len(l),3)] + for file, size, mtime in l: + pfiles[file] = (size, mtime) + + valid_pieces = {} + for i in xrange(len(self.files)): + if self.disabled[i]: + continue + r = self.file_ranges[i] + if not r: + continue + start, end, offset, file =r + if DEBUG: + print 'adding '+file + for p in xrange( int(start/self.piece_length), + int((end-1)/self.piece_length)+1 ): + valid_pieces[p] = 1 + + if DEBUG: + print valid_pieces.keys() + + def test(old, size, mtime): + oldsize, oldmtime = old + if size != oldsize: + return False + if mtime > oldmtime+1: + return False + if mtime < oldmtime-1: + return False + return True + + for i in xrange(len(self.files)): + if self.disabled[i]: + for file, start, end in self._get_disabled_ranges(i)[2]: + f1 = basename(file) + if ( not pfiles.has_key(f1) + or not test(pfiles[f1],getsize(file),getmtime(file)) ): + if DEBUG: + print 'removing '+file + for p in xrange( int(start/self.piece_length), + int((end-1)/self.piece_length)+1 ): + if valid_pieces.has_key(p): + del valid_pieces[p] + continue + file, size = self.files[i] + if not size: + continue + if ( not files.has_key(i) + or not test(files[i],getsize(file),getmtime(file)) ): + start, end, offset, file = self.file_ranges[i] + if DEBUG: + print 'removing '+file + for p in xrange( int(start/self.piece_length), + int((end-1)/self.piece_length)+1 ): + if valid_pieces.has_key(p): + del valid_pieces[p] + except: + if DEBUG: + print_exc() + return [] + + if DEBUG: + print valid_pieces.keys() + return valid_pieces.keys() + diff --git a/BitTornado/BT1/StorageWrapper.py b/BitTornado/BT1/StorageWrapper.py new file mode 100644 index 000000000..8dfe6b1b1 --- /dev/null +++ b/BitTornado/BT1/StorageWrapper.py @@ -0,0 +1,1045 @@ +# Written by Bram Cohen +# see LICENSE.txt for license information + +from BitTornado.bitfield import Bitfield +from sha import sha +from BitTornado.clock import clock +from traceback import print_exc +from random import randrange +try: + True +except: + True = 1 + False = 0 +try: + from bisect import insort +except: + def insort(l, item): + l.append(item) + l.sort() + +DEBUG = False + +STATS_INTERVAL = 0.2 + +def dummy_status(fractionDone = None, activity = None): + pass + +class Olist: + def __init__(self, l = []): + self.d = {} + for i in l: + self.d[i] = 1 + def __len__(self): + return len(self.d) + def includes(self, i): + return self.d.has_key(i) + def add(self, i): + self.d[i] = 1 + def extend(self, l): + for i in l: + self.d[i] = 1 + def pop(self, n=0): + # assert self.d + k = self.d.keys() + if n == 0: + i = min(k) + elif n == -1: + i = max(k) + else: + k.sort() + i = k[n] + del self.d[i] + return i + def remove(self, i): + if self.d.has_key(i): + del self.d[i] + +class fakeflag: + def __init__(self, state=False): + self.state = state + def wait(self): + pass + def isSet(self): + return self.state + + +class StorageWrapper: + def __init__(self, storage, request_size, hashes, + piece_size, finished, failed, + statusfunc = dummy_status, flag = fakeflag(), check_hashes = True, + data_flunked = lambda x: None, backfunc = None, + config = {}, unpauseflag = fakeflag(True) ): + self.storage = storage + self.request_size = long(request_size) + self.hashes = hashes + self.piece_size = long(piece_size) + self.piece_length = long(piece_size) + self.finished = finished + self.failed = failed + self.statusfunc = statusfunc + self.flag = flag + self.check_hashes = check_hashes + self.data_flunked = data_flunked + self.backfunc = backfunc + self.config = config + self.unpauseflag = unpauseflag + + self.alloc_type = config.get('alloc_type','normal') + self.double_check = config.get('double_check', 0) + self.triple_check = config.get('triple_check', 0) + if self.triple_check: + self.double_check = True + self.bgalloc_enabled = False + self.bgalloc_active = False + self.total_length = storage.get_total_length() + self.amount_left = self.total_length + if self.total_length <= self.piece_size * (len(hashes) - 1): + raise ValueError, 'bad data in responsefile - total too small' + if self.total_length > self.piece_size * len(hashes): + raise ValueError, 'bad data in responsefile - total too big' + self.numactive = [0] * len(hashes) + self.inactive_requests = [1] * len(hashes) + self.amount_inactive = self.total_length + self.amount_obtained = 0 + self.amount_desired = self.total_length + self.have = Bitfield(len(hashes)) + self.have_cloaked_data = None + self.blocked = [False] * len(hashes) + self.blocked_holes = [] + self.blocked_movein = Olist() + self.blocked_moveout = Olist() + self.waschecked = [False] * len(hashes) + self.places = {} + self.holes = [] + self.stat_active = {} + self.stat_new = {} + self.dirty = {} + self.stat_numflunked = 0 + self.stat_numdownloaded = 0 + self.stat_numfound = 0 + self.download_history = {} + self.failed_pieces = {} + self.out_of_place = 0 + self.write_buf_max = config['write_buffer_size']*1048576L + self.write_buf_size = 0L + self.write_buf = {} # structure: piece: [(start, data), ...] + self.write_buf_list = [] + + self.initialize_tasks = [ + ['checking existing data', 0, self.init_hashcheck, self.hashcheckfunc], + ['moving data', 1, self.init_movedata, self.movedatafunc], + ['allocating disk space', 1, self.init_alloc, self.allocfunc] ] + + self.backfunc(self._bgalloc,0.1) + self.backfunc(self._bgsync,max(self.config['auto_flush']*60,60)) + + def _bgsync(self): + if self.config['auto_flush']: + self.sync() + self.backfunc(self._bgsync,max(self.config['auto_flush']*60,60)) + + + def old_style_init(self): + while self.initialize_tasks: + msg, done, init, next = self.initialize_tasks.pop(0) + if init(): + self.statusfunc(activity = msg, fractionDone = done) + t = clock() + STATS_INTERVAL + x = 0 + while x is not None: + if t < clock(): + t = clock() + STATS_INTERVAL + self.statusfunc(fractionDone = x) + self.unpauseflag.wait() + if self.flag.isSet(): + return False + x = next() + + self.statusfunc(fractionDone = 0) + return True + + + def initialize(self, donefunc, statusfunc = None): + self.initialize_done = donefunc + if statusfunc is None: + statusfunc = self.statusfunc + self.initialize_status = statusfunc + self.initialize_next = None + + self.backfunc(self._initialize) + + def _initialize(self): + if not self.unpauseflag.isSet(): + self.backfunc(self._initialize, 1) + return + + if self.initialize_next: + x = self.initialize_next() + if x is None: + self.initialize_next = None + else: + self.initialize_status(fractionDone = x) + else: + if not self.initialize_tasks: + self.initialize_done() + return + msg, done, init, next = self.initialize_tasks.pop(0) + if init(): + self.initialize_status(activity = msg, fractionDone = done) + self.initialize_next = next + + self.backfunc(self._initialize) + + + def init_hashcheck(self): + if self.flag.isSet(): + return False + self.check_list = [] + if len(self.hashes) == 0 or self.amount_left == 0: + self.check_total = 0 + self.finished() + return False + + self.check_targets = {} + got = {} + for p,v in self.places.items(): + assert not got.has_key(v) + got[v] = 1 + for i in xrange(len(self.hashes)): + if self.places.has_key(i): # restored from pickled + self.check_targets[self.hashes[i]] = [] + if self.places[i] == i: + continue + else: + assert not got.has_key(i) + self.out_of_place += 1 + if got.has_key(i): + continue + if self._waspre(i): + if self.blocked[i]: + self.places[i] = i + else: + self.check_list.append(i) + continue + if not self.check_hashes: + self.failed('told file complete on start-up, but data is missing') + return False + self.holes.append(i) + if self.blocked[i] or self.check_targets.has_key(self.hashes[i]): + self.check_targets[self.hashes[i]] = [] # in case of a hash collision, discard + else: + self.check_targets[self.hashes[i]] = [i] + self.check_total = len(self.check_list) + self.check_numchecked = 0.0 + self.lastlen = self._piecelen(len(self.hashes) - 1) + self.numchecked = 0.0 + return self.check_total > 0 + + def _markgot(self, piece, pos): + if DEBUG: + print str(piece)+' at '+str(pos) + self.places[piece] = pos + self.have[piece] = True + len = self._piecelen(piece) + self.amount_obtained += len + self.amount_left -= len + self.amount_inactive -= len + self.inactive_requests[piece] = None + self.waschecked[piece] = self.check_hashes + self.stat_numfound += 1 + + def hashcheckfunc(self): + if self.flag.isSet(): + return None + if not self.check_list: + return None + + i = self.check_list.pop(0) + if not self.check_hashes: + self._markgot(i, i) + else: + d1 = self.read_raw(i,0,self.lastlen) + if d1 is None: + return None + sh = sha(d1[:]) + d1.release() + sp = sh.digest() + d2 = self.read_raw(i,self.lastlen,self._piecelen(i)-self.lastlen) + if d2 is None: + return None + sh.update(d2[:]) + d2.release() + s = sh.digest() + if s == self.hashes[i]: + self._markgot(i, i) + elif ( self.check_targets.get(s) + and self._piecelen(i) == self._piecelen(self.check_targets[s][-1]) ): + self._markgot(self.check_targets[s].pop(), i) + self.out_of_place += 1 + elif ( not self.have[-1] and sp == self.hashes[-1] + and (i == len(self.hashes) - 1 + or not self._waspre(len(self.hashes) - 1)) ): + self._markgot(len(self.hashes) - 1, i) + self.out_of_place += 1 + else: + self.places[i] = i + self.numchecked += 1 + if self.amount_left == 0: + self.finished() + return (self.numchecked / self.check_total) + + + def init_movedata(self): + if self.flag.isSet(): + return False + if self.alloc_type != 'sparse': + return False + self.storage.top_off() # sets file lengths to their final size + self.movelist = [] + if self.out_of_place == 0: + for i in self.holes: + self.places[i] = i + self.holes = [] + return False + self.tomove = float(self.out_of_place) + for i in xrange(len(self.hashes)): + if not self.places.has_key(i): + self.places[i] = i + elif self.places[i] != i: + self.movelist.append(i) + self.holes = [] + return True + + def movedatafunc(self): + if self.flag.isSet(): + return None + if not self.movelist: + return None + i = self.movelist.pop(0) + old = self.read_raw(self.places[i], 0, self._piecelen(i)) + if old is None: + return None + if not self.write_raw(i, 0, old): + return None + if self.double_check and self.have[i]: + if self.triple_check: + old.release() + old = self.read_raw( i, 0, self._piecelen(i), + flush_first = True ) + if old is None: + return None + if sha(old[:]).digest() != self.hashes[i]: + self.failed('download corrupted; please restart and resume') + return None + old.release() + + self.places[i] = i + self.tomove -= 1 + return (self.tomove / self.out_of_place) + + + def init_alloc(self): + if self.flag.isSet(): + return False + if not self.holes: + return False + self.numholes = float(len(self.holes)) + self.alloc_buf = chr(0xFF) * self.piece_size + if self.alloc_type == 'pre-allocate': + self.bgalloc_enabled = True + return True + if self.alloc_type == 'background': + self.bgalloc_enabled = True + if self.blocked_moveout: + return True + return False + + + def _allocfunc(self): + while self.holes: + n = self.holes.pop(0) + if self.blocked[n]: # assume not self.blocked[index] + if not self.blocked_movein: + self.blocked_holes.append(n) + continue + if not self.places.has_key(n): + b = self.blocked_movein.pop(0) + oldpos = self._move_piece(b, n) + self.places[oldpos] = oldpos + return None + if self.places.has_key(n): + oldpos = self._move_piece(n, n) + self.places[oldpos] = oldpos + return None + return n + return None + + def allocfunc(self): + if self.flag.isSet(): + return None + + if self.blocked_moveout: + self.bgalloc_active = True + n = self._allocfunc() + if n is not None: + if self.blocked_moveout.includes(n): + self.blocked_moveout.remove(n) + b = n + else: + b = self.blocked_moveout.pop(0) + oldpos = self._move_piece(b,n) + self.places[oldpos] = oldpos + return len(self.holes) / self.numholes + + if self.holes and self.bgalloc_enabled: + self.bgalloc_active = True + n = self._allocfunc() + if n is not None: + self.write_raw(n, 0, self.alloc_buf[:self._piecelen(n)]) + self.places[n] = n + return len(self.holes) / self.numholes + + self.bgalloc_active = False + return None + + def bgalloc(self): + if self.bgalloc_enabled: + if not self.holes and not self.blocked_moveout and self.backfunc: + self.backfunc(self.storage.flush) + # force a flush whenever the "finish allocation" button is hit + self.bgalloc_enabled = True + return False + + def _bgalloc(self): + self.allocfunc() + if self.config.get('alloc_rate',0) < 0.1: + self.config['alloc_rate'] = 0.1 + self.backfunc( self._bgalloc, + float(self.piece_size)/(self.config['alloc_rate']*1048576) ) + + + def _waspre(self, piece): + return self.storage.was_preallocated(piece * self.piece_size, self._piecelen(piece)) + + def _piecelen(self, piece): + if piece < len(self.hashes) - 1: + return self.piece_size + else: + return self.total_length - (piece * self.piece_size) + + def get_amount_left(self): + return self.amount_left + + def do_I_have_anything(self): + return self.amount_left < self.total_length + + def _make_inactive(self, index): + length = self._piecelen(index) + l = [] + x = 0 + while x + self.request_size < length: + l.append((x, self.request_size)) + x += self.request_size + l.append((x, length - x)) + self.inactive_requests[index] = l + + def is_endgame(self): + return not self.amount_inactive + + def am_I_complete(self): + return self.amount_obtained == self.amount_desired + + def reset_endgame(self, requestlist): + for index, begin, length in requestlist: + self.request_lost(index, begin, length) + + def get_have_list(self): + return self.have.tostring() + + def get_have_list_cloaked(self): + if self.have_cloaked_data is None: + newhave = Bitfield(copyfrom = self.have) + unhaves = [] + n = min(randrange(2,5),len(self.hashes)) # between 2-4 unless torrent is small + while len(unhaves) < n: + unhave = randrange(min(32,len(self.hashes))) # all in first 4 bytes + if not unhave in unhaves: + unhaves.append(unhave) + newhave[unhave] = False + self.have_cloaked_data = (newhave.tostring(), unhaves) + return self.have_cloaked_data + + def do_I_have(self, index): + return self.have[index] + + def do_I_have_requests(self, index): + return not not self.inactive_requests[index] + + def is_unstarted(self, index): + return ( not self.have[index] and not self.numactive[index] + and not self.dirty.has_key(index) ) + + def get_hash(self, index): + return self.hashes[index] + + def get_stats(self): + return self.amount_obtained, self.amount_desired + + def new_request(self, index): + # returns (begin, length) + if self.inactive_requests[index] == 1: + self._make_inactive(index) + self.numactive[index] += 1 + self.stat_active[index] = 1 + if not self.dirty.has_key(index): + self.stat_new[index] = 1 + rs = self.inactive_requests[index] +# r = min(rs) +# rs.remove(r) + r = rs.pop(0) + self.amount_inactive -= r[1] + return r + + + def write_raw(self, index, begin, data): + try: + self.storage.write(self.piece_size * index + begin, data) + return True + except IOError, e: + self.failed('IO Error: ' + str(e)) + return False + + + def _write_to_buffer(self, piece, start, data): + if not self.write_buf_max: + return self.write_raw(self.places[piece], start, data) + self.write_buf_size += len(data) + while self.write_buf_size > self.write_buf_max: + old = self.write_buf_list.pop(0) + if not self._flush_buffer(old, True): + return False + if self.write_buf.has_key(piece): + self.write_buf_list.remove(piece) + else: + self.write_buf[piece] = [] + self.write_buf_list.append(piece) + self.write_buf[piece].append((start,data)) + return True + + def _flush_buffer(self, piece, popped = False): + if not self.write_buf.has_key(piece): + return True + if not popped: + self.write_buf_list.remove(piece) + l = self.write_buf[piece] + del self.write_buf[piece] + l.sort() + for start, data in l: + self.write_buf_size -= len(data) + if not self.write_raw(self.places[piece], start, data): + return False + return True + + def sync(self): + spots = {} + for p in self.write_buf_list: + spots[self.places[p]] = p + l = spots.keys() + l.sort() + for i in l: + try: + self._flush_buffer(spots[i]) + except: + pass + try: + self.storage.sync() + except IOError, e: + self.failed('IO Error: ' + str(e)) + except OSError, e: + self.failed('OS Error: ' + str(e)) + + + def _move_piece(self, index, newpos): + oldpos = self.places[index] + if DEBUG: + print 'moving '+str(index)+' from '+str(oldpos)+' to '+str(newpos) + assert oldpos != index + assert oldpos != newpos + assert index == newpos or not self.places.has_key(newpos) + old = self.read_raw(oldpos, 0, self._piecelen(index)) + if old is None: + return -1 + if not self.write_raw(newpos, 0, old): + return -1 + self.places[index] = newpos + if self.have[index] and ( + self.triple_check or (self.double_check and index == newpos) ): + if self.triple_check: + old.release() + old = self.read_raw(newpos, 0, self._piecelen(index), + flush_first = True) + if old is None: + return -1 + if sha(old[:]).digest() != self.hashes[index]: + self.failed('download corrupted; please restart and resume') + return -1 + old.release() + + if self.blocked[index]: + self.blocked_moveout.remove(index) + if self.blocked[newpos]: + self.blocked_movein.remove(index) + else: + self.blocked_movein.add(index) + else: + self.blocked_movein.remove(index) + if self.blocked[newpos]: + self.blocked_moveout.add(index) + else: + self.blocked_moveout.remove(index) + + return oldpos + + def _clear_space(self, index): + h = self.holes.pop(0) + n = h + if self.blocked[n]: # assume not self.blocked[index] + if not self.blocked_movein: + self.blocked_holes.append(n) + return True # repeat + if not self.places.has_key(n): + b = self.blocked_movein.pop(0) + oldpos = self._move_piece(b, n) + if oldpos < 0: + return False + n = oldpos + if self.places.has_key(n): + oldpos = self._move_piece(n, n) + if oldpos < 0: + return False + n = oldpos + if index == n or index in self.holes: + if n == h: + self.write_raw(n, 0, self.alloc_buf[:self._piecelen(n)]) + self.places[index] = n + if self.blocked[n]: + # because n may be a spot cleared 10 lines above, it's possible + # for it to be blocked. While that spot could be left cleared + # and a new spot allocated, this condition might occur several + # times in a row, resulting in a significant amount of disk I/O, + # delaying the operation of the engine. Rather than do this, + # queue the piece to be moved out again, which will be performed + # by the background allocator, with which data movement is + # automatically limited. + self.blocked_moveout.add(index) + return False + for p, v in self.places.items(): + if v == index: + break + else: + self.failed('download corrupted; please restart and resume') + return False + self._move_piece(p, n) + self.places[index] = index + return False + + + def piece_came_in(self, index, begin, piece, source = None): + assert not self.have[index] + + if not self.places.has_key(index): + while self._clear_space(index): + pass + if DEBUG: + print 'new place for '+str(index)+' at '+str(self.places[index]) + if self.flag.isSet(): + return + + if self.failed_pieces.has_key(index): + old = self.read_raw(self.places[index], begin, len(piece)) + if old is None: + return True + if old[:].tostring() != piece: + try: + self.failed_pieces[index][self.download_history[index][begin]] = 1 + except: + self.failed_pieces[index][None] = 1 + old.release() + self.download_history.setdefault(index,{})[begin] = source + + if not self._write_to_buffer(index, begin, piece): + return True + + self.amount_obtained += len(piece) + self.dirty.setdefault(index,[]).append((begin, len(piece))) + self.numactive[index] -= 1 + assert self.numactive[index] >= 0 + if not self.numactive[index]: + del self.stat_active[index] + if self.stat_new.has_key(index): + del self.stat_new[index] + + if self.inactive_requests[index] or self.numactive[index]: + return True + + del self.dirty[index] + if not self._flush_buffer(index): + return True + length = self._piecelen(index) + data = self.read_raw(self.places[index], 0, length, + flush_first = self.triple_check) + if data is None: + return True + hash = sha(data[:]).digest() + data.release() + if hash != self.hashes[index]: + + self.amount_obtained -= length + self.data_flunked(length, index) + self.inactive_requests[index] = 1 + self.amount_inactive += length + self.stat_numflunked += 1 + + self.failed_pieces[index] = {} + allsenders = {} + for d in self.download_history[index].values(): + allsenders[d] = 1 + if len(allsenders) == 1: + culprit = allsenders.keys()[0] + if culprit is not None: + culprit.failed(index, bump = True) + del self.failed_pieces[index] # found the culprit already + + return False + + self.have[index] = True + self.inactive_requests[index] = None + self.waschecked[index] = True + self.amount_left -= length + self.stat_numdownloaded += 1 + + for d in self.download_history[index].values(): + if d is not None: + d.good(index) + del self.download_history[index] + if self.failed_pieces.has_key(index): + for d in self.failed_pieces[index].keys(): + if d is not None: + d.failed(index) + del self.failed_pieces[index] + + if self.amount_left == 0: + self.finished() + return True + + + def request_lost(self, index, begin, length): + assert not (begin, length) in self.inactive_requests[index] + insort(self.inactive_requests[index], (begin, length)) + self.amount_inactive += length + self.numactive[index] -= 1 + if not self.numactive[index]: + del self.stat_active[index] + if self.stat_new.has_key(index): + del self.stat_new[index] + + + def get_piece(self, index, begin, length): + if not self.have[index]: + return None + data = None + if not self.waschecked[index]: + data = self.read_raw(self.places[index], 0, self._piecelen(index)) + if data is None: + return None + if sha(data[:]).digest() != self.hashes[index]: + self.failed('told file complete on start-up, but piece failed hash check') + return None + self.waschecked[index] = True + if length == -1 and begin == 0: + return data # optimization + if length == -1: + if begin > self._piecelen(index): + return None + length = self._piecelen(index)-begin + if begin == 0: + return self.read_raw(self.places[index], 0, length) + elif begin + length > self._piecelen(index): + return None + if data is not None: + s = data[begin:begin+length] + data.release() + return s + data = self.read_raw(self.places[index], begin, length) + if data is None: + return None + s = data.getarray() + data.release() + return s + + def read_raw(self, piece, begin, length, flush_first = False): + try: + return self.storage.read(self.piece_size * piece + begin, + length, flush_first) + except IOError, e: + self.failed('IO Error: ' + str(e)) + return None + + + def set_file_readonly(self, n): + try: + self.storage.set_readonly(n) + except IOError, e: + self.failed('IO Error: ' + str(e)) + except OSError, e: + self.failed('OS Error: ' + str(e)) + + + def has_data(self, index): + return index not in self.holes and index not in self.blocked_holes + + def doublecheck_data(self, pieces_to_check): + if not self.double_check: + return + sources = [] + for p,v in self.places.items(): + if pieces_to_check.has_key(v): + sources.append(p) + assert len(sources) == len(pieces_to_check) + sources.sort() + for index in sources: + if self.have[index]: + piece = self.read_raw(self.places[index],0,self._piecelen(index), + flush_first = True ) + if piece is None: + return False + if sha(piece[:]).digest() != self.hashes[index]: + self.failed('download corrupted; please restart and resume') + return False + piece.release() + return True + + + def reblock(self, new_blocked): + # assume downloads have already been canceled and chunks made inactive + for i in xrange(len(new_blocked)): + if new_blocked[i] and not self.blocked[i]: + length = self._piecelen(i) + self.amount_desired -= length + if self.have[i]: + self.amount_obtained -= length + continue + if self.inactive_requests[i] == 1: + self.amount_inactive -= length + continue + inactive = 0 + for nb, nl in self.inactive_requests[i]: + inactive += nl + self.amount_inactive -= inactive + self.amount_obtained -= length - inactive + + if self.blocked[i] and not new_blocked[i]: + length = self._piecelen(i) + self.amount_desired += length + if self.have[i]: + self.amount_obtained += length + continue + if self.inactive_requests[i] == 1: + self.amount_inactive += length + continue + inactive = 0 + for nb, nl in self.inactive_requests[i]: + inactive += nl + self.amount_inactive += inactive + self.amount_obtained += length - inactive + + self.blocked = new_blocked + + self.blocked_movein = Olist() + self.blocked_moveout = Olist() + for p,v in self.places.items(): + if p != v: + if self.blocked[p] and not self.blocked[v]: + self.blocked_movein.add(p) + elif self.blocked[v] and not self.blocked[p]: + self.blocked_moveout.add(p) + + self.holes.extend(self.blocked_holes) # reset holes list + self.holes.sort() + self.blocked_holes = [] + + + ''' + Pickled data format: + + d['pieces'] = either a string containing a bitfield of complete pieces, + or the numeric value "1" signifying a seed. If it is + a seed, d['places'] and d['partials'] should be empty + and needn't even exist. + d['partials'] = [ piece, [ offset, length... ]... ] + a list of partial data that had been previously + downloaded, plus the given offsets. Adjacent partials + are merged so as to save space, and so that if the + request size changes then new requests can be + calculated more efficiently. + d['places'] = [ piece, place, {,piece, place ...} ] + the piece index, and the place it's stored. + If d['pieces'] specifies a complete piece or d['partials'] + specifies a set of partials for a piece which has no + entry in d['places'], it can be assumed that + place[index] = index. A place specified with no + corresponding data in d['pieces'] or d['partials'] + indicates allocated space with no valid data, and is + reserved so it doesn't need to be hash-checked. + ''' + def pickle(self): + if self.have.complete(): + return {'pieces': 1} + pieces = Bitfield(len(self.hashes)) + places = [] + partials = [] + for p in xrange(len(self.hashes)): + if self.blocked[p] or not self.places.has_key(p): + continue + h = self.have[p] + pieces[p] = h + pp = self.dirty.get(p) + if not h and not pp: # no data + places.extend([self.places[p],self.places[p]]) + elif self.places[p] != p: + places.extend([p, self.places[p]]) + if h or not pp: + continue + pp.sort() + r = [] + while len(pp) > 1: + if pp[0][0]+pp[0][1] == pp[1][0]: + pp[0] = list(pp[0]) + pp[0][1] += pp[1][1] + del pp[1] + else: + r.extend(pp[0]) + del pp[0] + r.extend(pp[0]) + partials.extend([p,r]) + return {'pieces': pieces.tostring(), 'places': places, 'partials': partials} + + + def unpickle(self, data, valid_places): + got = {} + places = {} + dirty = {} + download_history = {} + stat_active = {} + stat_numfound = self.stat_numfound + amount_obtained = self.amount_obtained + amount_inactive = self.amount_inactive + amount_left = self.amount_left + inactive_requests = [x for x in self.inactive_requests] + restored_partials = [] + + try: + if data['pieces'] == 1: # a seed + assert not data.get('places',None) + assert not data.get('partials',None) + have = Bitfield(len(self.hashes)) + for i in xrange(len(self.hashes)): + have[i] = True + assert have.complete() + _places = [] + _partials = [] + else: + have = Bitfield(len(self.hashes), data['pieces']) + _places = data['places'] + assert len(_places) % 2 == 0 + _places = [_places[x:x+2] for x in xrange(0,len(_places),2)] + _partials = data['partials'] + assert len(_partials) % 2 == 0 + _partials = [_partials[x:x+2] for x in xrange(0,len(_partials),2)] + + for index, place in _places: + if place not in valid_places: + continue + assert not got.has_key(index) + assert not got.has_key(place) + places[index] = place + got[index] = 1 + got[place] = 1 + + for index in xrange(len(self.hashes)): + if have[index]: + if not places.has_key(index): + if index not in valid_places: + have[index] = False + continue + assert not got.has_key(index) + places[index] = index + got[index] = 1 + length = self._piecelen(index) + amount_obtained += length + stat_numfound += 1 + amount_inactive -= length + amount_left -= length + inactive_requests[index] = None + + for index, plist in _partials: + assert not dirty.has_key(index) + assert not have[index] + if not places.has_key(index): + if index not in valid_places: + continue + assert not got.has_key(index) + places[index] = index + got[index] = 1 + assert len(plist) % 2 == 0 + plist = [plist[x:x+2] for x in xrange(0,len(plist),2)] + dirty[index] = plist + stat_active[index] = 1 + download_history[index] = {} + # invert given partials + length = self._piecelen(index) + l = [] + if plist[0][0] > 0: + l.append((0,plist[0][0])) + for i in xrange(len(plist)-1): + end = plist[i][0]+plist[i][1] + assert not end > plist[i+1][0] + l.append((end,plist[i+1][0]-end)) + end = plist[-1][0]+plist[-1][1] + assert not end > length + if end < length: + l.append((end,length-end)) + # split them to request_size + ll = [] + amount_obtained += length + amount_inactive -= length + for nb, nl in l: + while nl > 0: + r = min(nl,self.request_size) + ll.append((nb,r)) + amount_inactive += r + amount_obtained -= r + nb += self.request_size + nl -= self.request_size + inactive_requests[index] = ll + restored_partials.append(index) + + assert amount_obtained + amount_inactive == self.amount_desired + except: +# print_exc() + return [] # invalid data, discard everything + + self.have = have + self.places = places + self.dirty = dirty + self.download_history = download_history + self.stat_active = stat_active + self.stat_numfound = stat_numfound + self.amount_obtained = amount_obtained + self.amount_inactive = amount_inactive + self.amount_left = amount_left + self.inactive_requests = inactive_requests + + return restored_partials + diff --git a/BitTornado/BT1/StreamCheck.py b/BitTornado/BT1/StreamCheck.py new file mode 100644 index 000000000..7e57c7ca8 --- /dev/null +++ b/BitTornado/BT1/StreamCheck.py @@ -0,0 +1,135 @@ +# Written by Bram Cohen +# see LICENSE.txt for license information + +from cStringIO import StringIO +from binascii import b2a_hex +from socket import error as socketerror +from urllib import quote +from traceback import print_exc +import Connecter +try: + True +except: + True = 1 + False = 0 + +DEBUG = False + + +protocol_name = 'BitTorrent protocol' +option_pattern = chr(0)*8 + +def toint(s): + return long(b2a_hex(s), 16) + +def tobinary(i): + return (chr(i >> 24) + chr((i >> 16) & 0xFF) + + chr((i >> 8) & 0xFF) + chr(i & 0xFF)) + +hexchars = '0123456789ABCDEF' +hexmap = [] +for i in xrange(256): + hexmap.append(hexchars[(i&0xF0)/16]+hexchars[i&0x0F]) + +def tohex(s): + r = [] + for c in s: + r.append(hexmap[ord(c)]) + return ''.join(r) + +def make_readable(s): + if not s: + return '' + if quote(s).find('%') >= 0: + return tohex(s) + return '"'+s+'"' + +def toint(s): + return long(b2a_hex(s), 16) + +# header, reserved, download id, my id, [length, message] + +streamno = 0 + + +class StreamCheck: + def __init__(self): + global streamno + self.no = streamno + streamno += 1 + self.buffer = StringIO() + self.next_len, self.next_func = 1, self.read_header_len + + def read_header_len(self, s): + if ord(s) != len(protocol_name): + print self.no, 'BAD HEADER LENGTH' + return len(protocol_name), self.read_header + + def read_header(self, s): + if s != protocol_name: + print self.no, 'BAD HEADER' + return 8, self.read_reserved + + def read_reserved(self, s): + return 20, self.read_download_id + + def read_download_id(self, s): + if DEBUG: + print self.no, 'download ID ' + tohex(s) + return 20, self.read_peer_id + + def read_peer_id(self, s): + if DEBUG: + print self.no, 'peer ID' + make_readable(s) + return 4, self.read_len + + def read_len(self, s): + l = toint(s) + if l > 2 ** 23: + print self.no, 'BAD LENGTH: '+str(l)+' ('+s+')' + return l, self.read_message + + def read_message(self, s): + if not s: + return 4, self.read_len + m = s[0] + if ord(m) > 8: + print self.no, 'BAD MESSAGE: '+str(ord(m)) + if m == Connecter.REQUEST: + if len(s) != 13: + print self.no, 'BAD REQUEST SIZE: '+str(len(s)) + return 4, self.read_len + index = toint(s[1:5]) + begin = toint(s[5:9]) + length = toint(s[9:]) + print self.no, 'Request: '+str(index)+': '+str(begin)+'-'+str(begin)+'+'+str(length) + elif m == Connecter.CANCEL: + if len(s) != 13: + print self.no, 'BAD CANCEL SIZE: '+str(len(s)) + return 4, self.read_len + index = toint(s[1:5]) + begin = toint(s[5:9]) + length = toint(s[9:]) + print self.no, 'Cancel: '+str(index)+': '+str(begin)+'-'+str(begin)+'+'+str(length) + elif m == Connecter.PIECE: + index = toint(s[1:5]) + begin = toint(s[5:9]) + length = len(s)-9 + print self.no, 'Piece: '+str(index)+': '+str(begin)+'-'+str(begin)+'+'+str(length) + else: + print self.no, 'Message '+str(ord(m))+' (length '+str(len(s))+')' + return 4, self.read_len + + def write(self, s): + while True: + i = self.next_len - self.buffer.tell() + if i > len(s): + self.buffer.write(s) + return + self.buffer.write(s[:i]) + s = s[i:] + m = self.buffer.getvalue() + self.buffer.reset() + self.buffer.truncate() + x = self.next_func(m) + self.next_len, self.next_func = x diff --git a/BitTornado/BT1/T2T.py b/BitTornado/BT1/T2T.py new file mode 100644 index 000000000..f4ba2276e --- /dev/null +++ b/BitTornado/BT1/T2T.py @@ -0,0 +1,193 @@ +# Written by John Hoffman +# see LICENSE.txt for license information + +from Rerequester import Rerequester +from urllib import quote +from threading import Event +from random import randrange +from string import lower +import sys +import __init__ +try: + True +except: + True = 1 + False = 0 + +DEBUG = True + + +def excfunc(x): + print x + +class T2TConnection: + def __init__(self, myid, tracker, hash, interval, peers, timeout, + rawserver, disallow, isdisallowed): + self.tracker = tracker + self.interval = interval + self.hash = hash + self.operatinginterval = interval + self.peers = peers + self.rawserver = rawserver + self.disallow = disallow + self.isdisallowed = isdisallowed + self.active = True + self.busy = False + self.errors = 0 + self.rejected = 0 + self.trackererror = False + self.peerlists = [] + + self.rerequester = Rerequester([[tracker]], interval, + rawserver.add_task, lambda: 0, peers, self.addtolist, + rawserver.add_task, lambda: 1, 0, 0, 0, '', + myid, hash, timeout, self.errorfunc, excfunc, peers, Event(), + lambda: 0, lambda: 0) + + if self.isactive(): + rawserver.add_task(self.refresh, randrange(int(self.interval/10), self.interval)) + # stagger announces + + def isactive(self): + if self.isdisallowed(self.tracker): # whoops! + self.deactivate() + return self.active + + def deactivate(self): + self.active = False + + def refresh(self): + if not self.isactive(): + return + self.lastsuccessful = True + self.newpeerdata = [] + if DEBUG: + print 'contacting %s for info_hash=%s' % (self.tracker, quote(self.hash)) + self.rerequester.snoop(self.peers, self.callback) + + def callback(self): + self.busy = False + if self.lastsuccessful: + self.errors = 0 + self.rejected = 0 + if self.rerequester.announce_interval > (3*self.interval): + # I think I'm stripping from a regular tracker; boost the number of peers requested + self.peers = int(self.peers * (self.rerequester.announce_interval / self.interval)) + self.operatinginterval = self.rerequester.announce_interval + if DEBUG: + print ("%s with info_hash=%s returned %d peers" % + (self.tracker, quote(self.hash), len(self.newpeerdata))) + self.peerlists.append(self.newpeerdata) + self.peerlists = self.peerlists[-10:] # keep up to the last 10 announces + if self.isactive(): + self.rawserver.add_task(self.refresh, self.operatinginterval) + + def addtolist(self, peers): + for peer in peers: + self.newpeerdata.append((peer[1],peer[0][0],peer[0][1])) + + def errorfunc(self, r): + self.lastsuccessful = False + if DEBUG: + print "%s with info_hash=%s gives error: '%s'" % (self.tracker, quote(self.hash), r) + if r == self.rerequester.rejectedmessage + 'disallowed': # whoops! + if DEBUG: + print ' -- disallowed - deactivating' + self.deactivate() + self.disallow(self.tracker) # signal other torrents on this tracker + return + if lower(r[:8]) == 'rejected': # tracker rejected this particular torrent + self.rejected += 1 + if self.rejected == 3: # rejected 3 times + if DEBUG: + print ' -- rejected 3 times - deactivating' + self.deactivate() + return + self.errors += 1 + if self.errors >= 3: # three or more errors in a row + self.operatinginterval += self.interval # lengthen the interval + if DEBUG: + print ' -- lengthening interval to '+str(self.operatinginterval)+' seconds' + + def harvest(self): + x = [] + for list in self.peerlists: + x += list + self.peerlists = [] + return x + + +class T2TList: + def __init__(self, enabled, trackerid, interval, maxpeers, timeout, rawserver): + self.enabled = enabled + self.trackerid = trackerid + self.interval = interval + self.maxpeers = maxpeers + self.timeout = timeout + self.rawserver = rawserver + self.list = {} + self.torrents = {} + self.disallowed = {} + self.oldtorrents = [] + + def parse(self, allowed_list): + if not self.enabled: + return + + # step 1: Create a new list with all tracker/torrent combinations in allowed_dir + newlist = {} + for hash, data in allowed_list.items(): + if data.has_key('announce-list'): + for tier in data['announce-list']: + for tracker in tier: + self.disallowed.setdefault(tracker, False) + newlist.setdefault(tracker, {}) + newlist[tracker][hash] = None # placeholder + + # step 2: Go through and copy old data to the new list. + # if the new list has no place for it, then it's old, so deactivate it + for tracker, hashdata in self.list.items(): + for hash, t2t in hashdata.items(): + if not newlist.has_key(tracker) or not newlist[tracker].has_key(hash): + t2t.deactivate() # this connection is no longer current + self.oldtorrents += [t2t] + # keep it referenced in case a thread comes along and tries to access. + else: + newlist[tracker][hash] = t2t + if not newlist.has_key(tracker): + self.disallowed[tracker] = False # reset when no torrents on it left + + self.list = newlist + newtorrents = {} + + # step 3: If there are any entries that haven't been initialized yet, do so. + # At the same time, copy all entries onto the by-torrent list. + for tracker, hashdata in newlist.items(): + for hash, t2t in hashdata.items(): + if t2t is None: + hashdata[hash] = T2TConnection(self.trackerid, tracker, hash, + self.interval, self.maxpeers, self.timeout, + self.rawserver, self._disallow, self._isdisallowed) + newtorrents.setdefault(hash,[]) + newtorrents[hash] += [hashdata[hash]] + + self.torrents = newtorrents + + # structures: + # list = {tracker: {hash: T2TConnection, ...}, ...} + # torrents = {hash: [T2TConnection, ...]} + # disallowed = {tracker: flag, ...} + # oldtorrents = [T2TConnection, ...] + + def _disallow(self,tracker): + self.disallowed[tracker] = True + + def _isdisallowed(self,tracker): + return self.disallowed[tracker] + + def harvest(self,hash): + harvest = [] + if self.enabled: + for t2t in self.torrents[hash]: + harvest += t2t.harvest() + return harvest diff --git a/BitTornado/BT1/Uploader.py b/BitTornado/BT1/Uploader.py new file mode 100644 index 000000000..f9cddb9c7 --- /dev/null +++ b/BitTornado/BT1/Uploader.py @@ -0,0 +1,145 @@ +# Written by Bram Cohen +# see LICENSE.txt for license information + +from BitTornado.CurrentRateMeasure import Measure + +try: + True +except: + True = 1 + False = 0 + +class Upload: + def __init__(self, connection, ratelimiter, totalup, choker, storage, + picker, config): + self.connection = connection + self.ratelimiter = ratelimiter + self.totalup = totalup + self.choker = choker + self.storage = storage + self.picker = picker + self.config = config + self.max_slice_length = config['max_slice_length'] + self.choked = True + self.cleared = True + self.interested = False + self.super_seeding = False + self.buffer = [] + self.measure = Measure(config['max_rate_period'], config['upload_rate_fudge']) + self.was_ever_interested = False + if storage.get_amount_left() == 0: + if choker.super_seed: + self.super_seeding = True # flag, and don't send bitfield + self.seed_have_list = [] # set from piecepicker + self.skipped_count = 0 + else: + if config['breakup_seed_bitfield']: + bitfield, msgs = storage.get_have_list_cloaked() + connection.send_bitfield(bitfield) + for have in msgs: + connection.send_have(have) + else: + connection.send_bitfield(storage.get_have_list()) + else: + if storage.do_I_have_anything(): + connection.send_bitfield(storage.get_have_list()) + self.piecedl = None + self.piecebuf = None + + def got_not_interested(self): + if self.interested: + self.interested = False + del self.buffer[:] + self.piecedl = None + if self.piecebuf: + self.piecebuf.release() + self.piecebuf = None + self.choker.not_interested(self.connection) + + def got_interested(self): + if not self.interested: + self.interested = True + self.was_ever_interested = True + self.choker.interested(self.connection) + + def get_upload_chunk(self): + if self.choked or not self.buffer: + return None + index, begin, length = self.buffer.pop(0) + if self.config['buffer_reads']: + if index != self.piecedl: + if self.piecebuf: + self.piecebuf.release() + self.piecedl = index + self.piecebuf = self.storage.get_piece(index, 0, -1) + try: + piece = self.piecebuf[begin:begin+length] + assert len(piece) == length + except: # fails if storage.get_piece returns None or if out of range + self.connection.close() + return None + else: + if self.piecebuf: + self.piecebuf.release() + self.piecedl = None + piece = self.storage.get_piece(index, begin, length) + if piece is None: + self.connection.close() + return None + self.measure.update_rate(len(piece)) + self.totalup.update_rate(len(piece)) + return (index, begin, piece) + + def got_request(self, index, begin, length): + if ( (self.super_seeding and not index in self.seed_have_list) + or not self.interested or length > self.max_slice_length ): + self.connection.close() + return + if not self.cleared: + self.buffer.append((index, begin, length)) + if not self.choked and self.connection.next_upload is None: + self.ratelimiter.queue(self.connection) + + + def got_cancel(self, index, begin, length): + try: + self.buffer.remove((index, begin, length)) + except ValueError: + pass + + def choke(self): + if not self.choked: + self.choked = True + self.connection.send_choke() + self.piecedl = None + if self.piecebuf: + self.piecebuf.release() + self.piecebuf = None + + def choke_sent(self): + del self.buffer[:] + self.cleared = True + + def unchoke(self): + if self.choked: + self.choked = False + self.cleared = False + self.connection.send_unchoke() + + def disconnected(self): + if self.piecebuf: + self.piecebuf.release() + self.piecebuf = None + + def is_choked(self): + return self.choked + + def is_interested(self): + return self.interested + + def has_queries(self): + return not self.choked and len(self.buffer) > 0 + + def get_rate(self): + return self.measure.get_rate() + diff --git a/BitTornado/BT1/__init__.py b/BitTornado/BT1/__init__.py new file mode 100644 index 000000000..f4572339b --- /dev/null +++ b/BitTornado/BT1/__init__.py @@ -0,0 +1 @@ +# placeholder \ No newline at end of file diff --git a/BitTornado/BT1/btformats.py b/BitTornado/BT1/btformats.py new file mode 100644 index 000000000..9e6089915 --- /dev/null +++ b/BitTornado/BT1/btformats.py @@ -0,0 +1,100 @@ +# Written by Bram Cohen +# see LICENSE.txt for license information + +from types import StringType, LongType, IntType, ListType, DictType +from re import compile + +reg = compile(r'^[^/\\.~][^/\\]*$') + +ints = (LongType, IntType) + +def check_info(info): + if type(info) != DictType: + raise ValueError, 'bad metainfo - not a dictionary' + pieces = info.get('pieces') + if type(pieces) != StringType or len(pieces) % 20 != 0: + raise ValueError, 'bad metainfo - bad pieces key' + piecelength = info.get('piece length') + if type(piecelength) not in ints or piecelength <= 0: + raise ValueError, 'bad metainfo - illegal piece length' + name = info.get('name') + if type(name) != StringType: + raise ValueError, 'bad metainfo - bad name' + if not reg.match(name): + raise ValueError, 'name %s disallowed for security reasons' % name + if info.has_key('files') == info.has_key('length'): + raise ValueError, 'single/multiple file mix' + if info.has_key('length'): + length = info.get('length') + if type(length) not in ints or length < 0: + raise ValueError, 'bad metainfo - bad length' + else: + files = info.get('files') + if type(files) != ListType: + raise ValueError + for f in files: + if type(f) != DictType: + raise ValueError, 'bad metainfo - bad file value' + length = f.get('length') + if type(length) not in ints or length < 0: + raise ValueError, 'bad metainfo - bad length' + path = f.get('path') + if type(path) != ListType or path == []: + raise ValueError, 'bad metainfo - bad path' + for p in path: + if type(p) != StringType: + raise ValueError, 'bad metainfo - bad path dir' + if not reg.match(p): + raise ValueError, 'path %s disallowed for security reasons' % p + for i in xrange(len(files)): + for j in xrange(i): + if files[i]['path'] == files[j]['path']: + raise ValueError, 'bad metainfo - duplicate path' + +def check_message(message): + if type(message) != DictType: + raise ValueError + check_info(message.get('info')) + if type(message.get('announce')) != StringType: + raise ValueError + +def check_peers(message): + if type(message) != DictType: + raise ValueError + if message.has_key('failure reason'): + if type(message['failure reason']) != StringType: + raise ValueError + return + peers = message.get('peers') + if type(peers) == ListType: + for p in peers: + if type(p) != DictType: + raise ValueError + if type(p.get('ip')) != StringType: + raise ValueError + port = p.get('port') + if type(port) not in ints or p <= 0: + raise ValueError + if p.has_key('peer id'): + id = p['peer id'] + if type(id) != StringType or len(id) != 20: + raise ValueError + elif type(peers) != StringType or len(peers) % 6 != 0: + raise ValueError + interval = message.get('interval', 1) + if type(interval) not in ints or interval <= 0: + raise ValueError + minint = message.get('min interval', 1) + if type(minint) not in ints or minint <= 0: + raise ValueError + if type(message.get('tracker id', '')) != StringType: + raise ValueError + npeers = message.get('num peers', 0) + if type(npeers) not in ints or npeers < 0: + raise ValueError + dpeers = message.get('done peers', 0) + if type(dpeers) not in ints or dpeers < 0: + raise ValueError + last = message.get('last', 0) + if type(last) not in ints or last < 0: + raise ValueError diff --git a/BitTornado/BT1/fakeopen.py b/BitTornado/BT1/fakeopen.py new file mode 100644 index 000000000..e5064922b --- /dev/null +++ b/BitTornado/BT1/fakeopen.py @@ -0,0 +1,89 @@ +# Written by Bram Cohen +# see LICENSE.txt for license information + +from string import join + +class FakeHandle: + def __init__(self, name, fakeopen): + self.name = name + self.fakeopen = fakeopen + self.pos = 0 + + def flush(self): + pass + + def close(self): + pass + + def seek(self, pos): + self.pos = pos + + def read(self, amount = None): + old = self.pos + f = self.fakeopen.files[self.name] + if self.pos >= len(f): + return '' + if amount is None: + self.pos = len(f) + return join(f[old:], '') + else: + self.pos = min(len(f), old + amount) + return join(f[old:self.pos], '') + + def write(self, s): + f = self.fakeopen.files[self.name] + while len(f) < self.pos: + f.append(chr(0)) + self.fakeopen.files[self.name][self.pos : self.pos + len(s)] = list(s) + self.pos += len(s) + +class FakeOpen: + def __init__(self, initial = {}): + self.files = {} + for key, value in initial.items(): + self.files[key] = list(value) + + def open(self, filename, mode): + """currently treats everything as rw - doesn't support append""" + self.files.setdefault(filename, []) + return FakeHandle(filename, self) + + def exists(self, file): + return self.files.has_key(file) + + def getsize(self, file): + return len(self.files[file]) + +def test_normal(): + f = FakeOpen({'f1': 'abcde'}) + assert f.exists('f1') + assert not f.exists('f2') + assert f.getsize('f1') == 5 + h = f.open('f1', 'rw') + assert h.read(3) == 'abc' + assert h.read(1) == 'd' + assert h.read() == 'e' + assert h.read(2) == '' + h.write('fpq') + h.seek(4) + assert h.read(2) == 'ef' + h.write('ghij') + h.seek(0) + assert h.read() == 'abcdefghij' + h.seek(2) + h.write('p') + h.write('q') + assert h.read(1) == 'e' + h.seek(1) + assert h.read(5) == 'bpqef' + + h2 = f.open('f2', 'rw') + assert h2.read() == '' + h2.write('mnop') + h2.seek(1) + assert h2.read() == 'nop' + + assert f.exists('f1') + assert f.exists('f2') + assert f.getsize('f1') == 10 + assert f.getsize('f2') == 4 diff --git a/BitTornado/BT1/makemetafile.py b/BitTornado/BT1/makemetafile.py new file mode 100644 index 000000000..ddb412d25 --- /dev/null +++ b/BitTornado/BT1/makemetafile.py @@ -0,0 +1,263 @@ +# Written by Bram Cohen +# multitracker extensions by John Hoffman +# see LICENSE.txt for license information + +from os.path import getsize, split, join, abspath, isdir +from os import listdir +from sha import sha +from copy import copy +from string import strip +from BitTornado.bencode import bencode +from btformats import check_info +from threading import Event +from time import time +from traceback import print_exc +try: + from sys import getfilesystemencoding + ENCODING = getfilesystemencoding() +except: + from sys import getdefaultencoding + ENCODING = getdefaultencoding() + +defaults = [ + ('announce_list', '', + 'a list of announce URLs - explained below'), + ('httpseeds', '', + 'a list of http seed URLs - explained below'), + ('piece_size_pow2', 0, + "which power of 2 to set the piece size to (0 = automatic)"), + ('comment', '', + "optional human-readable comment to put in .torrent"), + ('filesystem_encoding', '', + "optional specification for filesystem encoding " + + "(set automatically in recent Python versions)"), + ('target', '', + "optional target file for the torrent") + ] + +default_piece_len_exp = 18 + +ignore = ['core', 'CVS'] + +def print_announcelist_details(): + print (' announce_list = optional list of redundant/backup tracker URLs, in the format:') + print (' url[,url...][|url[,url...]...]') + print (' where URLs separated by commas are all tried first') + print (' before the next group of URLs separated by the pipe is checked.') + print (" If none is given, it is assumed you don't want one in the metafile.") + print (' If announce_list is given, clients which support it') + print (' will ignore the value.') + print (' Examples:') + print (' http://tracker1.com|http://tracker2.com|http://tracker3.com') + print (' (tries trackers 1-3 in order)') + print (' http://tracker1.com,http://tracker2.com,http://tracker3.com') + print (' (tries trackers 1-3 in a randomly selected order)') + print (' http://tracker1.com|http://backup1.com,http://backup2.com') + print (' (tries tracker 1 first, then tries between the 2 backups randomly)') + print ('') + print (' httpseeds = optional list of http-seed URLs, in the format:') + print (' url[|url...]') + +def make_meta_file(file, url, params = {}, flag = Event(), + progress = lambda x: None, progress_percent = 1): + if params.has_key('piece_size_pow2'): + piece_len_exp = params['piece_size_pow2'] + else: + piece_len_exp = default_piece_len_exp + if params.has_key('target') and params['target'] != '': + f = params['target'] + else: + a, b = split(file) + if b == '': + f = a + '.torrent' + else: + f = join(a, b + '.torrent') + + if piece_len_exp == 0: # automatic + size = calcsize(file) + if size > 8L*1024*1024*1024: # > 8 gig = + piece_len_exp = 21 # 2 meg pieces + elif size > 2*1024*1024*1024: # > 2 gig = + piece_len_exp = 20 # 1 meg pieces + elif size > 512*1024*1024: # > 512M = + piece_len_exp = 19 # 512K pieces + elif size > 64*1024*1024: # > 64M = + piece_len_exp = 18 # 256K pieces + elif size > 16*1024*1024: # > 16M = + piece_len_exp = 17 # 128K pieces + elif size > 4*1024*1024: # > 4M = + piece_len_exp = 16 # 64K pieces + else: # < 4M = + piece_len_exp = 15 # 32K pieces + piece_length = 2 ** piece_len_exp + + encoding = None + if params.has_key('filesystem_encoding'): + encoding = params['filesystem_encoding'] + if not encoding: + encoding = ENCODING + if not encoding: + encoding = 'ascii' + + info = makeinfo(file, piece_length, encoding, flag, progress, progress_percent) + if flag.isSet(): + return + check_info(info) + h = open(f, 'wb') + data = {'info': info, 'announce': strip(url), 'creation date': long(time())} + + if params.has_key('comment') and params['comment']: + data['comment'] = params['comment'] + + if params.has_key('real_announce_list'): # shortcut for progs calling in from outside + data['announce-list'] = params['real_announce_list'] + elif params.has_key('announce_list') and params['announce_list']: + l = [] + for tier in params['announce_list'].split('|'): + l.append(tier.split(',')) + data['announce-list'] = l + + if params.has_key('real_httpseeds'): # shortcut for progs calling in from outside + data['httpseeds'] = params['real_httpseeds'] + elif params.has_key('httpseeds') and params['httpseeds']: + data['httpseeds'] = params['httpseeds'].split('|') + + h.write(bencode(data)) + h.close() + +def calcsize(file): + if not isdir(file): + return getsize(file) + total = 0L + for s in subfiles(abspath(file)): + total += getsize(s[1]) + return total + + +def uniconvertl(l, e): + r = [] + try: + for s in l: + r.append(uniconvert(s, e)) + except UnicodeError: + raise UnicodeError('bad filename: '+join(l)) + return r + +def uniconvert(s, e): + try: + s = unicode(s,e) + except UnicodeError: + raise UnicodeError('bad filename: '+s) + return s.encode('utf-8') + +def makeinfo(file, piece_length, encoding, flag, progress, progress_percent=1): + file = abspath(file) + if isdir(file): + subs = subfiles(file) + subs.sort() + pieces = [] + sh = sha() + done = 0L + fs = [] + totalsize = 0.0 + totalhashed = 0L + for p, f in subs: + totalsize += getsize(f) + + for p, f in subs: + pos = 0L + size = getsize(f) + fs.append({'length': size, 'path': uniconvertl(p, encoding)}) + h = open(f, 'rb') + while pos < size: + a = min(size - pos, piece_length - done) + sh.update(h.read(a)) + if flag.isSet(): + return + done += a + pos += a + totalhashed += a + + if done == piece_length: + pieces.append(sh.digest()) + done = 0 + sh = sha() + if progress_percent: + progress(totalhashed / totalsize) + else: + progress(a) + h.close() + if done > 0: + pieces.append(sh.digest()) + return {'pieces': ''.join(pieces), + 'piece length': piece_length, 'files': fs, + 'name': uniconvert(split(file)[1], encoding) } + else: + size = getsize(file) + pieces = [] + p = 0L + h = open(file, 'rb') + while p < size: + x = h.read(min(piece_length, size - p)) + if flag.isSet(): + return + pieces.append(sha(x).digest()) + p += piece_length + if p > size: + p = size + if progress_percent: + progress(float(p) / size) + else: + progress(min(piece_length, size - p)) + h.close() + return {'pieces': ''.join(pieces), + 'piece length': piece_length, 'length': size, + 'name': uniconvert(split(file)[1], encoding) } + +def subfiles(d): + r = [] + stack = [([], d)] + while len(stack) > 0: + p, n = stack.pop() + if isdir(n): + for s in listdir(n): + if s not in ignore and s[:1] != '.': + stack.append((copy(p) + [s], join(n, s))) + else: + r.append((p, n)) + return r + + +def completedir(dir, url, params = {}, flag = Event(), + vc = lambda x: None, fc = lambda x: None): + files = listdir(dir) + files.sort() + ext = '.torrent' + if params.has_key('target'): + target = params['target'] + else: + target = '' + + togen = [] + for f in files: + if f[-len(ext):] != ext and (f + ext) not in files: + togen.append(join(dir, f)) + + total = 0 + for i in togen: + total += calcsize(i) + + subtotal = [0] + def callback(x, subtotal = subtotal, total = total, vc = vc): + subtotal[0] += x + vc(float(subtotal[0]) / total) + for i in togen: + fc(i) + try: + t = split(i)[-1] + if t not in ignore and t[0] != '.': + if target != '': + params['target'] = join(target,t+ext) + make_meta_file(i, url, params, flag, progress = callback, progress_percent = 0) + except ValueError: + print_exc() diff --git a/BitTornado/BT1/track.py b/BitTornado/BT1/track.py new file mode 100644 index 000000000..625750b8a --- /dev/null +++ b/BitTornado/BT1/track.py @@ -0,0 +1,1067 @@ +# Written by Bram Cohen +# see LICENSE.txt for license information + +from BitTornado.parseargs import parseargs, formatDefinitions +from BitTornado.RawServer import RawServer, autodetect_ipv6, autodetect_socket_style +from BitTornado.HTTPHandler import HTTPHandler, months, weekdays +from BitTornado.parsedir import parsedir +from NatCheck import NatCheck +from T2T import T2TList +from BitTornado.subnetparse import IP_List, ipv6_to_ipv4, to_ipv4, is_valid_ip, is_ipv4 +from BitTornado.iprangeparse import IP_List as IP_Range_List +from BitTornado.torrentlistparse import parsetorrentlist +from threading import Event, Thread +from BitTornado.bencode import bencode, bdecode, Bencached +from BitTornado.zurllib import urlopen, quote, unquote +from Filter import Filter +from urlparse import urlparse +from os import rename, getpid +from os.path import exists, isfile +from cStringIO import StringIO +from traceback import print_exc +from time import time, gmtime, strftime, localtime +from BitTornado.clock import clock +from random import shuffle, seed, randrange +from sha import sha +from types import StringType, IntType, LongType, ListType, DictType +from binascii import b2a_hex, a2b_hex, a2b_base64 +from string import lower +import sys, os +import signal +import re +import BitTornado.__init__ +from BitTornado.__init__ import version, createPeerID +try: + True +except: + True = 1 + False = 0 + +defaults = [ + ('port', 80, "Port to listen on."), + ('dfile', None, 'file to store recent downloader info in'), + ('bind', '', 'comma-separated list of ips/hostnames to bind to locally'), +# ('ipv6_enabled', autodetect_ipv6(), + ('ipv6_enabled', 0, + 'allow the client to connect to peers via IPv6'), + ('ipv6_binds_v4', autodetect_socket_style(), + 'set if an IPv6 server socket will also field IPv4 connections'), + ('socket_timeout', 15, 'timeout for closing connections'), + ('save_dfile_interval', 5 * 60, 'seconds between saving dfile'), + ('timeout_downloaders_interval', 45 * 60, 'seconds between expiring downloaders'), + ('reannounce_interval', 30 * 60, 'seconds downloaders should wait between reannouncements'), + ('response_size', 50, 'number of peers to send in an info message'), + ('timeout_check_interval', 5, + 'time to wait between checking if any connections have timed out'), + ('nat_check', 3, + "how many times to check if a downloader is behind a NAT (0 = don't check)"), + ('log_nat_checks', 0, + "whether to add entries to the log for nat-check results"), + ('min_time_between_log_flushes', 3.0, + 'minimum time it must have been since the last flush to do another one'), + ('min_time_between_cache_refreshes', 600.0, + 'minimum time in seconds before a cache is considered stale and is flushed'), + ('allowed_dir', '', 'only allow downloads for .torrents in this dir'), + ('allowed_list', '', 'only allow downloads for hashes in this list (hex format, one per line)'), + ('allowed_controls', 0, 'allow special keys in torrents in the allowed_dir to affect tracker access'), + ('multitracker_enabled', 0, 'whether to enable multitracker operation'), + ('multitracker_allowed', 'autodetect', 'whether to allow incoming tracker announces (can be none, autodetect or all)'), + ('multitracker_reannounce_interval', 2 * 60, 'seconds between outgoing tracker announces'), + ('multitracker_maxpeers', 20, 'number of peers to get in a tracker announce'), + ('aggregate_forward', '', 'format: [,] - if set, forwards all non-multitracker to this url with this optional password'), + ('aggregator', '0', 'whether to act as a data aggregator rather than a tracker. If enabled, may be 1, or ; ' + + 'if password is set, then an incoming password is required for access'), + ('hupmonitor', 0, 'whether to reopen the log file upon receipt of HUP signal'), + ('http_timeout', 60, + 'number of seconds to wait before assuming that an http connection has timed out'), + ('parse_dir_interval', 60, 'seconds between reloading of allowed_dir or allowed_file ' + + 'and allowed_ips and banned_ips lists'), + ('show_infopage', 1, "whether to display an info page when the tracker's root dir is loaded"), + ('infopage_redirect', '', 'a URL to redirect the info page to'), + ('show_names', 1, 'whether to display names from allowed dir'), + ('favicon', '', 'file containing x-icon data to return when browser requests favicon.ico'), + ('allowed_ips', '', 'only allow connections from IPs specified in the given file; '+ + 'file contains subnet data in the format: aa.bb.cc.dd/len'), + ('banned_ips', '', "don't allow connections from IPs specified in the given file; "+ + 'file contains IP range data in the format: xxx:xxx:ip1-ip2'), + ('only_local_override_ip', 2, "ignore the ip GET parameter from machines which aren't on local network IPs " + + "(0 = never, 1 = always, 2 = ignore if NAT checking is not enabled)"), + ('logfile', '', 'file to write the tracker logs, use - for stdout (default)'), + ('allow_get', 0, 'use with allowed_dir; adds a /file?hash={hash} url that allows users to download the torrent file'), + ('keep_dead', 0, 'keep dead torrents after they expire (so they still show up on your /scrape and web page)'), + ('scrape_allowed', 'full', 'scrape access allowed (can be none, specific or full)'), + ('dedicated_seed_id', '', 'allows tracker to monitor dedicated seed(s) and flag torrents as seeded'), + ] + +def statefiletemplate(x): + if type(x) != DictType: + raise ValueError + for cname, cinfo in x.items(): + if cname == 'peers': + for y in cinfo.values(): # The 'peers' key is a dictionary of SHA hashes (torrent ids) + if type(y) != DictType: # ... for the active torrents, and each is a dictionary + raise ValueError + for id, info in y.items(): # ... of client ids interested in that torrent + if (len(id) != 20): + raise ValueError + if type(info) != DictType: # ... each of which is also a dictionary + raise ValueError # ... which has an IP, a Port, and a Bytes Left count for that client for that torrent + if type(info.get('ip', '')) != StringType: + raise ValueError + port = info.get('port') + if type(port) not in (IntType,LongType) or port < 0: + raise ValueError + left = info.get('left') + if type(left) not in (IntType,LongType) or left < 0: + raise ValueError + elif cname == 'completed': + if (type(cinfo) != DictType): # The 'completed' key is a dictionary of SHA hashes (torrent ids) + raise ValueError # ... for keeping track of the total completions per torrent + for y in cinfo.values(): # ... each torrent has an integer value + if type(y) not in (IntType,LongType): + raise ValueError # ... for the number of reported completions for that torrent + elif cname == 'allowed': + if (type(cinfo) != DictType): # a list of info_hashes and included data + raise ValueError + if x.has_key('allowed_dir_files'): + adlist = [z[1] for z in x['allowed_dir_files'].values()] + for y in cinfo.keys(): # and each should have a corresponding key here + if not y in adlist: + raise ValueError + elif cname == 'allowed_dir_files': + if (type(cinfo) != DictType): # a list of files, their attributes and info hashes + raise ValueError + dirkeys = {} + for y in cinfo.values(): # each entry should have a corresponding info_hash + if not y[1]: + continue + if not x['allowed'].has_key(y[1]): + raise ValueError + if dirkeys.has_key(y[1]): # and each should have a unique info_hash + raise ValueError + dirkeys[y[1]] = 1 + + +alas = 'your file may exist elsewhere in the universe\nbut alas, not here\n' + +local_IPs = IP_List() +local_IPs.set_intranet_addresses() + + +def isotime(secs = None): + if secs == None: + secs = time() + return strftime('%Y-%m-%d %H:%M UTC', gmtime(secs)) + +http_via_filter = re.compile(' for ([0-9.]+)\Z') + +def _get_forwarded_ip(headers): + header = headers.get('x-forwarded-for') + if header: + try: + x,y = header.split(',') + except: + return header + if is_valid_ip(x) and not local_IPs.includes(x): + return x + return y + header = headers.get('client-ip') + if header: + return header + header = headers.get('via') + if header: + x = http_via_filter.search(header) + try: + return x.group(1) + except: + pass + header = headers.get('from') + #if header: + # return header + #return None + return header + +def get_forwarded_ip(headers): + x = _get_forwarded_ip(headers) + if not is_valid_ip(x) or local_IPs.includes(x): + return None + return x + +def compact_peer_info(ip, port): + try: + s = ( ''.join([chr(int(i)) for i in ip.split('.')]) + + chr((port & 0xFF00) >> 8) + chr(port & 0xFF) ) + if len(s) != 6: + raise ValueError + except: + s = '' # not a valid IP, must be a domain name + return s + +class Tracker: + def __init__(self, config, rawserver): + self.config = config + self.response_size = config['response_size'] + self.dfile = config['dfile'] + self.natcheck = config['nat_check'] + favicon = config['favicon'] + self.parse_dir_interval = config['parse_dir_interval'] + self.favicon = None + if favicon: + try: + h = open(favicon,'r') + self.favicon = h.read() + h.close() + except: + print "**warning** specified favicon file -- %s -- does not exist." % favicon + self.rawserver = rawserver + self.cached = {} # format: infohash: [[time1, l1, s1], [time2, l2, s2], [time3, l3, s3]] + self.cached_t = {} # format: infohash: [time, cache] + self.times = {} + self.state = {} + self.seedcount = {} + + self.allowed_IPs = None + self.banned_IPs = None + if config['allowed_ips'] or config['banned_ips']: + self.allowed_ip_mtime = 0 + self.banned_ip_mtime = 0 + self.read_ip_lists() + + self.only_local_override_ip = config['only_local_override_ip'] + if self.only_local_override_ip == 2: + self.only_local_override_ip = not config['nat_check'] + + if exists(self.dfile): + try: + h = open(self.dfile, 'rb') + ds = h.read() + h.close() + tempstate = bdecode(ds) + if not tempstate.has_key('peers'): + tempstate = {'peers': tempstate} + statefiletemplate(tempstate) + self.state = tempstate + except: + print '**warning** statefile '+self.dfile+' corrupt; resetting' + self.downloads = self.state.setdefault('peers', {}) + self.completed = self.state.setdefault('completed', {}) + + self.becache = {} # format: infohash: [[l1, s1], [l2, s2], [l3, s3]] + for infohash, ds in self.downloads.items(): + self.seedcount[infohash] = 0 + for x,y in ds.items(): + ip = y['ip'] + if ( (self.allowed_IPs and not self.allowed_IPs.includes(ip)) + or (self.banned_IPs and self.banned_IPs.includes(ip)) ): + del ds[x] + continue + if not y['left']: + self.seedcount[infohash] += 1 + if y.get('nat',-1): + continue + gip = y.get('given_ip') + if is_valid_ip(gip) and ( + not self.only_local_override_ip or local_IPs.includes(ip) ): + ip = gip + self.natcheckOK(infohash,x,ip,y['port'],y['left']) + + for x in self.downloads.keys(): + self.times[x] = {} + for y in self.downloads[x].keys(): + self.times[x][y] = 0 + + self.trackerid = createPeerID('-T-') + seed(self.trackerid) + + self.reannounce_interval = config['reannounce_interval'] + self.save_dfile_interval = config['save_dfile_interval'] + self.show_names = config['show_names'] + rawserver.add_task(self.save_state, self.save_dfile_interval) + self.prevtime = clock() + self.timeout_downloaders_interval = config['timeout_downloaders_interval'] + rawserver.add_task(self.expire_downloaders, self.timeout_downloaders_interval) + self.logfile = None + self.log = None + if (config['logfile']) and (config['logfile'] != '-'): + try: + self.logfile = config['logfile'] + self.log = open(self.logfile,'a') + sys.stdout = self.log + print "# Log Started: ", isotime() + except: + print "**warning** could not redirect stdout to log file: ", sys.exc_info()[0] + + if config['hupmonitor']: + def huphandler(signum, frame, self = self): + try: + self.log.close () + self.log = open(self.logfile,'a') + sys.stdout = self.log + print "# Log reopened: ", isotime() + except: + print "**warning** could not reopen logfile" + + signal.signal(signal.SIGHUP, huphandler) + + self.allow_get = config['allow_get'] + + self.t2tlist = T2TList(config['multitracker_enabled'], self.trackerid, + config['multitracker_reannounce_interval'], + config['multitracker_maxpeers'], config['http_timeout'], + self.rawserver) + + if config['allowed_list']: + if config['allowed_dir']: + print '**warning** allowed_dir and allowed_list options cannot be used together' + print '**warning** disregarding allowed_dir' + config['allowed_dir'] = '' + self.allowed = self.state.setdefault('allowed_list',{}) + self.allowed_list_mtime = 0 + self.parse_allowed() + self.remove_from_state('allowed','allowed_dir_files') + if config['multitracker_allowed'] == 'autodetect': + config['multitracker_allowed'] = 'none' + config['allowed_controls'] = 0 + + elif config['allowed_dir']: + self.allowed = self.state.setdefault('allowed',{}) + self.allowed_dir_files = self.state.setdefault('allowed_dir_files',{}) + self.allowed_dir_blocked = {} + self.parse_allowed() + self.remove_from_state('allowed_list') + + else: + self.allowed = None + self.remove_from_state('allowed','allowed_dir_files', 'allowed_list') + if config['multitracker_allowed'] == 'autodetect': + config['multitracker_allowed'] = 'none' + config['allowed_controls'] = 0 + + self.uq_broken = unquote('+') != ' ' + self.keep_dead = config['keep_dead'] + self.Filter = Filter(rawserver.add_task) + + aggregator = config['aggregator'] + if aggregator == '0': + self.is_aggregator = False + self.aggregator_key = None + else: + self.is_aggregator = True + if aggregator == '1': + self.aggregator_key = None + else: + self.aggregator_key = aggregator + self.natcheck = False + + send = config['aggregate_forward'] + if not send: + self.aggregate_forward = None + else: + try: + self.aggregate_forward, self.aggregate_password = send.split(',') + except: + self.aggregate_forward = send + self.aggregate_password = None + + self.dedicated_seed_id = config['dedicated_seed_id'] + self.is_seeded = {} + + self.cachetime = 0 + self.cachetimeupdate() + + def cachetimeupdate(self): + self.cachetime += 1 # raw clock, but more efficient for cache + self.rawserver.add_task(self.cachetimeupdate,1) + + def aggregate_senddata(self, query): + url = self.aggregate_forward+'?'+query + if self.aggregate_password is not None: + url += '&password='+self.aggregate_password + rq = Thread(target = self._aggregate_senddata, args = [url]) + rq.setDaemon(False) + rq.start() + + def _aggregate_senddata(self, url): # just send, don't attempt to error check, + try: # discard any returned data + h = urlopen(url) + h.read() + h.close() + except: + return + + + def get_infopage(self): + try: + if not self.config['show_infopage']: + return (404, 'Not Found', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, alas) + red = self.config['infopage_redirect'] + if red: + return (302, 'Found', {'Content-Type': 'text/html', 'Location': red}, + 'Click Here') + + s = StringIO() + s.write('\n' \ + 'BitTorrent download info\n') + if self.favicon is not None: + s.write('\n') + s.write('\n\n' \ + '

    BitTorrent download info

    \n'\ + '
      \n' + '
    • tracker version: %s
    • \n' \ + '
    • server time: %s
    • \n' \ + '
    \n' % (version, isotime())) + if self.config['allowed_dir']: + if self.show_names: + names = [ (self.allowed[hash]['name'],hash) + for hash in self.allowed.keys() ] + else: + names = [ (None,hash) + for hash in self.allowed.keys() ] + else: + names = [ (None,hash) for hash in self.downloads.keys() ] + if not names: + s.write('

    not tracking any files yet...

    \n') + else: + names.sort() + tn = 0 + tc = 0 + td = 0 + tt = 0 # Total transferred + ts = 0 # Total size + nf = 0 # Number of files displayed + if self.config['allowed_dir'] and self.show_names: + s.write('\n' \ + '\n') + else: + s.write('
    info hashtorrent namesizecompletedownloadingdownloadedtransferred
    \n' \ + '\n') + for name,hash in names: + l = self.downloads[hash] + n = self.completed.get(hash, 0) + tn = tn + n + c = self.seedcount[hash] + tc = tc + c + d = len(l) - c + td = td + d + if self.config['allowed_dir'] and self.show_names: + if self.allowed.has_key(hash): + nf = nf + 1 + sz = self.allowed[hash]['length'] # size + ts = ts + sz + szt = sz * n # Transferred for this torrent + tt = tt + szt + if self.allow_get == 1: + linkname = '' + name + '' + else: + linkname = name + s.write('\n' \ + % (b2a_hex(hash), linkname, size_format(sz), c, d, n, size_format(szt))) + else: + s.write('\n' \ + % (b2a_hex(hash), c, d, n)) + ttn = 0 + for i in self.completed.values(): + ttn = ttn + i + if self.config['allowed_dir'] and self.show_names: + s.write('\n' + % (nf, size_format(ts), tc, td, tn, ttn, size_format(tt))) + else: + s.write('\n' + % (nf, tc, td, tn, ttn)) + s.write('
    info hashcompletedownloadingdownloaded
    %s%s%s%i%i%i%s
    %s%i%i%i
    %i files%s%i%i%i/%i%s
    %i files%i%i%i/%i
    \n' \ + '
      \n' \ + '
    • info hash: SHA1 hash of the "info" section of the metainfo (*.torrent)
    • \n' \ + '
    • complete: number of connected clients with the complete file
    • \n' \ + '
    • downloading: number of connected clients still downloading
    • \n' \ + '
    • downloaded: reported complete downloads (total: current/all)
    • \n' \ + '
    • transferred: torrent size * total downloaded (does not include partial transfers)
    • \n' \ + '
    \n') + + s.write('\n' \ + '\n') + return (200, 'OK', {'Content-Type': 'text/html; charset=iso-8859-1'}, s.getvalue()) + except: + print_exc() + return (500, 'Internal Server Error', {'Content-Type': 'text/html; charset=iso-8859-1'}, 'Server Error') + + + def scrapedata(self, hash, return_name = True): + l = self.downloads[hash] + n = self.completed.get(hash, 0) + c = self.seedcount[hash] + d = len(l) - c + f = {'complete': c, 'incomplete': d, 'downloaded': n} + if return_name and self.show_names and self.config['allowed_dir']: + f['name'] = self.allowed[hash]['name'] + return (f) + + def get_scrape(self, paramslist): + fs = {} + if paramslist.has_key('info_hash'): + if self.config['scrape_allowed'] not in ['specific', 'full']: + return (400, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, + bencode({'failure reason': + 'specific scrape function is not available with this tracker.'})) + for hash in paramslist['info_hash']: + if self.allowed is not None: + if self.allowed.has_key(hash): + fs[hash] = self.scrapedata(hash) + else: + if self.downloads.has_key(hash): + fs[hash] = self.scrapedata(hash) + else: + if self.config['scrape_allowed'] != 'full': + return (400, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, + bencode({'failure reason': + 'full scrape function is not available with this tracker.'})) + if self.allowed is not None: + keys = self.allowed.keys() + else: + keys = self.downloads.keys() + for hash in keys: + fs[hash] = self.scrapedata(hash) + + return (200, 'OK', {'Content-Type': 'text/plain'}, bencode({'files': fs})) + + + def get_file(self, hash): + if not self.allow_get: + return (400, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, + 'get function is not available with this tracker.') + if not self.allowed.has_key(hash): + return (404, 'Not Found', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, alas) + fname = self.allowed[hash]['file'] + fpath = self.allowed[hash]['path'] + return (200, 'OK', {'Content-Type': 'application/x-bittorrent', + 'Content-Disposition': 'attachment; filename=' + fname}, + open(fpath, 'rb').read()) + + + def check_allowed(self, infohash, paramslist): + if ( self.aggregator_key is not None + and not ( paramslist.has_key('password') + and paramslist['password'][0] == self.aggregator_key ) ): + return (200, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, + bencode({'failure reason': + 'Requested download is not authorized for use with this tracker.'})) + + if self.allowed is not None: + if not self.allowed.has_key(infohash): + return (200, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, + bencode({'failure reason': + 'Requested download is not authorized for use with this tracker.'})) + if self.config['allowed_controls']: + if self.allowed[infohash].has_key('failure reason'): + return (200, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, + bencode({'failure reason': self.allowed[infohash]['failure reason']})) + + if paramslist.has_key('tracker'): + if ( self.config['multitracker_allowed'] == 'none' or # turned off + paramslist['peer_id'][0] == self.trackerid ): # oops! contacted myself + return (200, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, + bencode({'failure reason': 'disallowed'})) + + if ( self.config['multitracker_allowed'] == 'autodetect' + and not self.allowed[infohash].has_key('announce-list') ): + return (200, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, + bencode({'failure reason': + 'Requested download is not authorized for multitracker use.'})) + + return None + + + def add_data(self, infohash, event, ip, paramslist): + peers = self.downloads.setdefault(infohash, {}) + ts = self.times.setdefault(infohash, {}) + self.completed.setdefault(infohash, 0) + self.seedcount.setdefault(infohash, 0) + + def params(key, default = None, l = paramslist): + if l.has_key(key): + return l[key][0] + return default + + myid = params('peer_id','') + if len(myid) != 20: + raise ValueError, 'id not of length 20' + if event not in ['started', 'completed', 'stopped', 'snooped', None]: + raise ValueError, 'invalid event' + port = long(params('port','')) + if port < 0 or port > 65535: + raise ValueError, 'invalid port' + left = long(params('left','')) + if left < 0: + raise ValueError, 'invalid amount left' + uploaded = long(params('uploaded','')) + downloaded = long(params('downloaded','')) + + peer = peers.get(myid) + islocal = local_IPs.includes(ip) + mykey = params('key') + if peer: + auth = peer.get('key',-1) == mykey or peer.get('ip') == ip + + gip = params('ip') + if is_valid_ip(gip) and (islocal or not self.only_local_override_ip): + ip1 = gip + else: + ip1 = ip + + if params('numwant') is not None: + rsize = min(int(params('numwant')),self.response_size) + else: + rsize = self.response_size + + if event == 'stopped': + if peer: + if auth: + self.delete_peer(infohash,myid) + + elif not peer: + ts[myid] = clock() + peer = {'ip': ip, 'port': port, 'left': left} + if mykey: + peer['key'] = mykey + if gip: + peer['given ip'] = gip + if port: + if not self.natcheck or islocal: + peer['nat'] = 0 + self.natcheckOK(infohash,myid,ip1,port,left) + else: + NatCheck(self.connectback_result,infohash,myid,ip1,port,self.rawserver) + else: + peer['nat'] = 2**30 + if event == 'completed': + self.completed[infohash] += 1 + if not left: + self.seedcount[infohash] += 1 + + peers[myid] = peer + + else: + if not auth: + return rsize # return w/o changing stats + + ts[myid] = clock() + if not left and peer['left']: + self.completed[infohash] += 1 + self.seedcount[infohash] += 1 + if not peer.get('nat', -1): + for bc in self.becache[infohash]: + bc[1][myid] = bc[0][myid] + del bc[0][myid] + elif left and not peer['left']: + self.completed[infohash] -= 1 + self.seedcount[infohash] -= 1 + if not peer.get('nat', -1): + for bc in self.becache[infohash]: + bc[0][myid] = bc[1][myid] + del bc[1][myid] + peer['left'] = left + + if port: + recheck = False + if ip != peer['ip']: + peer['ip'] = ip + recheck = True + if gip != peer.get('given ip'): + if gip: + peer['given ip'] = gip + elif peer.has_key('given ip'): + del peer['given ip'] + recheck = True + + natted = peer.get('nat', -1) + if recheck: + if natted == 0: + l = self.becache[infohash] + y = not peer['left'] + for x in l: + del x[y][myid] + if natted >= 0: + del peer['nat'] # restart NAT testing + if natted and natted < self.natcheck: + recheck = True + + if recheck: + if not self.natcheck or islocal: + peer['nat'] = 0 + self.natcheckOK(infohash,myid,ip1,port,left) + else: + NatCheck(self.connectback_result,infohash,myid,ip1,port,self.rawserver) + + return rsize + + + def peerlist(self, infohash, stopped, tracker, is_seed, return_type, rsize): + data = {} # return data + seeds = self.seedcount[infohash] + data['complete'] = seeds + data['incomplete'] = len(self.downloads[infohash]) - seeds + + if ( self.config['allowed_controls'] + and self.allowed[infohash].has_key('warning message') ): + data['warning message'] = self.allowed[infohash]['warning message'] + + if tracker: + data['interval'] = self.config['multitracker_reannounce_interval'] + if not rsize: + return data + cache = self.cached_t.setdefault(infohash, None) + if ( not cache or len(cache[1]) < rsize + or cache[0] + self.config['min_time_between_cache_refreshes'] < clock() ): + bc = self.becache.setdefault(infohash,[[{}, {}], [{}, {}], [{}, {}]]) + cache = [ clock(), bc[0][0].values() + bc[0][1].values() ] + self.cached_t[infohash] = cache + shuffle(cache[1]) + cache = cache[1] + + data['peers'] = cache[-rsize:] + del cache[-rsize:] + return data + + data['interval'] = self.reannounce_interval + if stopped or not rsize: # save some bandwidth + data['peers'] = [] + return data + + bc = self.becache.setdefault(infohash,[[{}, {}], [{}, {}], [{}, {}]]) + len_l = len(bc[0][0]) + len_s = len(bc[0][1]) + if not (len_l+len_s): # caches are empty! + data['peers'] = [] + return data + l_get_size = int(float(rsize)*(len_l)/(len_l+len_s)) + cache = self.cached.setdefault(infohash,[None,None,None])[return_type] + if cache and ( not cache[1] + or (is_seed and len(cache[1]) < rsize) + or len(cache[1]) < l_get_size + or cache[0]+self.config['min_time_between_cache_refreshes'] < self.cachetime ): + cache = None + if not cache: + peers = self.downloads[infohash] + vv = [[],[],[]] + for key, ip, port in self.t2tlist.harvest(infohash): # empty if disabled + if not peers.has_key(key): + vv[0].append({'ip': ip, 'port': port, 'peer id': key}) + vv[1].append({'ip': ip, 'port': port}) + vv[2].append(compact_peer_info(ip, port)) + cache = [ self.cachetime, + bc[return_type][0].values()+vv[return_type], + bc[return_type][1].values() ] + shuffle(cache[1]) + shuffle(cache[2]) + self.cached[infohash][return_type] = cache + for rr in xrange(len(self.cached[infohash])): + if rr != return_type: + try: + self.cached[infohash][rr][1].extend(vv[rr]) + except: + pass + if len(cache[1]) < l_get_size: + peerdata = cache[1] + if not is_seed: + peerdata.extend(cache[2]) + cache[1] = [] + cache[2] = [] + else: + if not is_seed: + peerdata = cache[2][l_get_size-rsize:] + del cache[2][l_get_size-rsize:] + rsize -= len(peerdata) + else: + peerdata = [] + if rsize: + peerdata.extend(cache[1][-rsize:]) + del cache[1][-rsize:] + if return_type == 2: + peerdata = ''.join(peerdata) + data['peers'] = peerdata + return data + + + def get(self, connection, path, headers): + real_ip = connection.get_ip() + ip = real_ip + if is_ipv4(ip): + ipv4 = True + else: + try: + ip = ipv6_to_ipv4(ip) + ipv4 = True + except ValueError: + ipv4 = False + + if ( (self.allowed_IPs and not self.allowed_IPs.includes(ip)) + or (self.banned_IPs and self.banned_IPs.includes(ip)) ): + return (400, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, + bencode({'failure reason': + 'your IP is not allowed on this tracker'})) + + nip = get_forwarded_ip(headers) + if nip and not self.only_local_override_ip: + ip = nip + try: + ip = to_ipv4(ip) + ipv4 = True + except ValueError: + ipv4 = False + + paramslist = {} + def params(key, default = None, l = paramslist): + if l.has_key(key): + return l[key][0] + return default + + try: + (scheme, netloc, path, pars, query, fragment) = urlparse(path) + if self.uq_broken == 1: + path = path.replace('+',' ') + query = query.replace('+',' ') + path = unquote(path)[1:] + for s in query.split('&'): + if s: + i = s.index('=') + kw = unquote(s[:i]) + paramslist.setdefault(kw, []) + paramslist[kw] += [unquote(s[i+1:])] + + if path == '' or path == 'index.html': + return self.get_infopage() + if (path == 'file'): + return self.get_file(params('info_hash')) + if path == 'favicon.ico' and self.favicon is not None: + return (200, 'OK', {'Content-Type' : 'image/x-icon'}, self.favicon) + + # automated access from here on + + if path in ('scrape', 'scrape.php', 'tracker.php/scrape'): + return self.get_scrape(paramslist) + + if not path in ('announce', 'announce.php', 'tracker.php/announce'): + return (404, 'Not Found', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, alas) + + # main tracker function + + filtered = self.Filter.check(real_ip, paramslist, headers) + if filtered: + return (400, 'Not Authorized', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, + bencode({'failure reason': filtered})) + + infohash = params('info_hash') + if not infohash: + raise ValueError, 'no info hash' + + notallowed = self.check_allowed(infohash, paramslist) + if notallowed: + return notallowed + + event = params('event') + + rsize = self.add_data(infohash, event, ip, paramslist) + + except ValueError, e: + return (400, 'Bad Request', {'Content-Type': 'text/plain'}, + 'you sent me garbage - ' + str(e)) + + if self.aggregate_forward and not paramslist.has_key('tracker'): + self.aggregate_senddata(query) + + if self.is_aggregator: # don't return peer data here + return (200, 'OK', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, + bencode({'response': 'OK'})) + + if params('compact') and ipv4: + return_type = 2 + elif params('no_peer_id'): + return_type = 1 + else: + return_type = 0 + + data = self.peerlist(infohash, event=='stopped', + params('tracker'), not params('left'), + return_type, rsize) + + if paramslist.has_key('scrape'): # deprecated + data['scrape'] = self.scrapedata(infohash, False) + + if self.dedicated_seed_id: + if params('seed_id') == self.dedicated_seed_id and params('left') == 0: + self.is_seeded[infohash] = True + if params('check_seeded') and self.is_seeded.get(infohash): + data['seeded'] = 1 + + return (200, 'OK', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, bencode(data)) + + + def natcheckOK(self, infohash, peerid, ip, port, not_seed): + bc = self.becache.setdefault(infohash,[[{}, {}], [{}, {}], [{}, {}]]) + bc[0][not not_seed][peerid] = Bencached(bencode({'ip': ip, 'port': port, + 'peer id': peerid})) + bc[1][not not_seed][peerid] = Bencached(bencode({'ip': ip, 'port': port})) + bc[2][not not_seed][peerid] = compact_peer_info(ip, port) + + + def natchecklog(self, peerid, ip, port, result): + year, month, day, hour, minute, second, a, b, c = localtime(time()) + print '%s - %s [%02d/%3s/%04d:%02d:%02d:%02d] "!natcheck-%s:%i" %i 0 - -' % ( + ip, quote(peerid), day, months[month], year, hour, minute, second, + ip, port, result) + + def connectback_result(self, result, downloadid, peerid, ip, port): + record = self.downloads.get(downloadid, {}).get(peerid) + if ( record is None + or (record['ip'] != ip and record.get('given ip') != ip) + or record['port'] != port ): + if self.config['log_nat_checks']: + self.natchecklog(peerid, ip, port, 404) + return + if self.config['log_nat_checks']: + if result: + x = 200 + else: + x = 503 + self.natchecklog(peerid, ip, port, x) + if not record.has_key('nat'): + record['nat'] = int(not result) + if result: + self.natcheckOK(downloadid,peerid,ip,port,record['left']) + elif result and record['nat']: + record['nat'] = 0 + self.natcheckOK(downloadid,peerid,ip,port,record['left']) + elif not result: + record['nat'] += 1 + + + def remove_from_state(self, *l): + for s in l: + try: + del self.state[s] + except: + pass + + def save_state(self): + self.rawserver.add_task(self.save_state, self.save_dfile_interval) + h = open(self.dfile, 'wb') + h.write(bencode(self.state)) + h.close() + + + def parse_allowed(self): + self.rawserver.add_task(self.parse_allowed, self.parse_dir_interval) + + if self.config['allowed_dir']: + r = parsedir( self.config['allowed_dir'], self.allowed, + self.allowed_dir_files, self.allowed_dir_blocked, + [".torrent"] ) + ( self.allowed, self.allowed_dir_files, self.allowed_dir_blocked, + added, garbage2 ) = r + + self.state['allowed'] = self.allowed + self.state['allowed_dir_files'] = self.allowed_dir_files + + self.t2tlist.parse(self.allowed) + + else: + f = self.config['allowed_list'] + if self.allowed_list_mtime == os.path.getmtime(f): + return + try: + r = parsetorrentlist(f, self.allowed) + (self.allowed, added, garbage2) = r + self.state['allowed_list'] = self.allowed + except (IOError, OSError): + print '**warning** unable to read allowed torrent list' + return + self.allowed_list_mtime = os.path.getmtime(f) + + for infohash in added.keys(): + self.downloads.setdefault(infohash, {}) + self.completed.setdefault(infohash, 0) + self.seedcount.setdefault(infohash, 0) + + + def read_ip_lists(self): + self.rawserver.add_task(self.read_ip_lists,self.parse_dir_interval) + + f = self.config['allowed_ips'] + if f and self.allowed_ip_mtime != os.path.getmtime(f): + self.allowed_IPs = IP_List() + try: + self.allowed_IPs.read_fieldlist(f) + self.allowed_ip_mtime = os.path.getmtime(f) + except (IOError, OSError): + print '**warning** unable to read allowed_IP list' + + f = self.config['banned_ips'] + if f and self.banned_ip_mtime != os.path.getmtime(f): + self.banned_IPs = IP_Range_List() + try: + self.banned_IPs.read_rangelist(f) + self.banned_ip_mtime = os.path.getmtime(f) + except (IOError, OSError): + print '**warning** unable to read banned_IP list' + + + def delete_peer(self, infohash, peerid): + dls = self.downloads[infohash] + peer = dls[peerid] + if not peer['left']: + self.seedcount[infohash] -= 1 + if not peer.get('nat',-1): + l = self.becache[infohash] + y = not peer['left'] + for x in l: + del x[y][peerid] + del self.times[infohash][peerid] + del dls[peerid] + + def expire_downloaders(self): + for x in self.times.keys(): + for myid, t in self.times[x].items(): + if t < self.prevtime: + self.delete_peer(x,myid) + self.prevtime = clock() + if (self.keep_dead != 1): + for key, value in self.downloads.items(): + if len(value) == 0 and ( + self.allowed is None or not self.allowed.has_key(key) ): + del self.times[key] + del self.downloads[key] + del self.seedcount[key] + self.rawserver.add_task(self.expire_downloaders, self.timeout_downloaders_interval) + + +def track(args): + if len(args) == 0: + print formatDefinitions(defaults, 80) + return + try: + config, files = parseargs(args, defaults, 0, 0) + except ValueError, e: + print 'error: ' + str(e) + print 'run with no arguments for parameter explanations' + return + r = RawServer(Event(), config['timeout_check_interval'], + config['socket_timeout'], ipv6_enable = config['ipv6_enabled']) + t = Tracker(config, r) + r.bind(config['port'], config['bind'], + reuse = True, ipv6_socket_style = config['ipv6_binds_v4']) + r.listen_forever(HTTPHandler(t.get, config['min_time_between_log_flushes'])) + t.save_state() + print '# Shutting down: ' + isotime() + +def size_format(s): + if (s < 1024): + r = str(s) + 'B' + elif (s < 1048576): + r = str(int(s/1024)) + 'KiB' + elif (s < 1073741824L): + r = str(int(s/1048576)) + 'MiB' + elif (s < 1099511627776L): + r = str(int((s/1073741824.0)*100.0)/100.0) + 'GiB' + else: + r = str(int((s/1099511627776.0)*100.0)/100.0) + 'TiB' + return(r) + diff --git a/BitTornado/ConfigDir.py b/BitTornado/ConfigDir.py new file mode 100644 index 000000000..ecd8f581b --- /dev/null +++ b/BitTornado/ConfigDir.py @@ -0,0 +1,385 @@ +#written by John Hoffman + +from inifile import ini_write, ini_read +from bencode import bencode, bdecode +from types import IntType, LongType, StringType, FloatType +from CreateIcons import GetIcons, CreateIcon +from parseargs import defaultargs +from __init__ import product_name, version_short +import sys,os +from time import time, strftime + +try: + True +except: + True = 1 + False = 0 + +try: + realpath = os.path.realpath +except: + realpath = lambda x:x +OLDICONPATH = os.path.abspath(os.path.dirname(realpath(sys.argv[0]))) + +DIRNAME = '.'+product_name + +hexchars = '0123456789abcdef' +hexmap = [] +revmap = {} +for i in xrange(256): + x = hexchars[(i&0xF0)/16]+hexchars[i&0x0F] + hexmap.append(x) + revmap[x] = chr(i) + +def tohex(s): + r = [] + for c in s: + r.append(hexmap[ord(c)]) + return ''.join(r) + +def unhex(s): + r = [ revmap[s[x:x+2]] for x in xrange(0, len(s), 2) ] + return ''.join(r) + +def copyfile(oldpath, newpath): # simple file copy, all in RAM + try: + f = open(oldpath,'rb') + r = f.read() + success = True + except: + success = False + try: + f.close() + except: + pass + if not success: + return False + try: + f = open(newpath,'wb') + f.write(r) + except: + success = False + try: + f.close() + except: + pass + return success + + +class ConfigDir: + + ###### INITIALIZATION TASKS ###### + + def __init__(self, dir_root): + """ + Modified by ACR, for Armory-specific download + """ + config_ext = '.armorydl' + + self.dir_root = dir_root + + if not os.path.isdir(self.dir_root): + os.mkdir(self.dir_root, 0700) # exception if failed + + self.dir_icons = os.path.join(dir_root,'icons') + if not os.path.isdir(self.dir_icons): + os.mkdir(self.dir_icons) + + for icon in GetIcons(): + i = os.path.join(self.dir_icons,icon) + if not os.path.exists(i): + if not copyfile(os.path.join(OLDICONPATH,icon),i): + CreateIcon(icon,self.dir_icons) + + self.dir_torrentcache = os.path.join(dir_root,'torrentcache') + if not os.path.isdir(self.dir_torrentcache): + os.mkdir(self.dir_torrentcache) + + self.dir_datacache = os.path.join(dir_root,'datacache') + if not os.path.isdir(self.dir_datacache): + os.mkdir(self.dir_datacache) + + self.dir_piececache = os.path.join(dir_root,'piececache') + if not os.path.isdir(self.dir_piececache): + os.mkdir(self.dir_piececache) + + self.configfile = os.path.join(dir_root,'config'+config_ext+'.ini') + self.statefile = os.path.join(dir_root,'state'+config_ext) + + self.TorrentDataBuffer = {} + + + ###### CONFIG HANDLING ###### + + def setDefaults(self, defaults, ignore=[]): + self.config = defaultargs(defaults) + for k in ignore: + if self.config.has_key(k): + del self.config[k] + + def checkConfig(self): + return os.path.exists(self.configfile) + + def loadConfig(self): + try: + r = ini_read(self.configfile)[''] + except: + return self.config + l = self.config.keys() + for k,v in r.items(): + if self.config.has_key(k): + t = type(self.config[k]) + try: + if t == StringType: + self.config[k] = v + elif t == IntType or t == LongType: + self.config[k] = long(v) + elif t == FloatType: + self.config[k] = float(v) + l.remove(k) + except: + pass + if l: # new default values since last save + self.saveConfig() + return self.config + + def saveConfig(self, new_config = None): + if new_config: + for k,v in new_config.items(): + if self.config.has_key(k): + self.config[k] = v + try: + ini_write( self.configfile, self.config, + 'Generated by '+product_name+'/'+version_short+'\n' + + strftime('%x %X') ) + return True + except: + return False + + def getConfig(self): + return self.config + + + ###### STATE HANDLING ###### + + def getState(self): + try: + f = open(self.statefile,'rb') + r = f.read() + except: + r = None + try: + f.close() + except: + pass + try: + r = bdecode(r) + except: + r = None + return r + + def saveState(self, state): + try: + f = open(self.statefile,'wb') + f.write(bencode(state)) + success = True + except: + success = False + try: + f.close() + except: + pass + return success + + + ###### TORRENT HANDLING ###### + + def getTorrents(self): + d = {} + for f in os.listdir(self.dir_torrentcache): + f = os.path.basename(f) + try: + f, garbage = f.split('.') + except: + pass + d[unhex(f)] = 1 + return d.keys() + + def getTorrentVariations(self, t): + t = tohex(t) + d = [] + for f in os.listdir(self.dir_torrentcache): + f = os.path.basename(f) + if f[:len(t)] == t: + try: + garbage, ver = f.split('.') + except: + ver = '0' + d.append(int(ver)) + d.sort() + return d + + def getTorrent(self, t, v = -1): + t = tohex(t) + if v == -1: + v = max(self.getTorrentVariations(t)) # potential exception + if v: + t += '.'+str(v) + try: + f = open(os.path.join(self.dir_torrentcache,t),'rb') + r = bdecode(f.read()) + except: + r = None + try: + f.close() + except: + pass + return r + + def writeTorrent(self, data, t, v = -1): + t = tohex(t) + if v == -1: + try: + v = max(self.getTorrentVariations(t))+1 + except: + v = 0 + if v: + t += '.'+str(v) + try: + f = open(os.path.join(self.dir_torrentcache,t),'wb') + f.write(bencode(data)) + except: + v = None + try: + f.close() + except: + pass + return v + + + ###### TORRENT DATA HANDLING ###### + + def getTorrentData(self, t): + if self.TorrentDataBuffer.has_key(t): + return self.TorrentDataBuffer[t] + t = os.path.join(self.dir_datacache,tohex(t)) + if not os.path.exists(t): + return None + try: + f = open(t,'rb') + r = bdecode(f.read()) + except: + r = None + try: + f.close() + except: + pass + self.TorrentDataBuffer[t] = r + return r + + def writeTorrentData(self, t, data): + self.TorrentDataBuffer[t] = data + try: + f = open(os.path.join(self.dir_datacache,tohex(t)),'wb') + f.write(bencode(data)) + success = True + except: + success = False + try: + f.close() + except: + pass + if not success: + self.deleteTorrentData(t) + return success + + def deleteTorrentData(self, t): + try: + os.remove(os.path.join(self.dir_datacache,tohex(t))) + except: + pass + + def getPieceDir(self, t): + return os.path.join(self.dir_piececache,tohex(t)) + + + ###### EXPIRATION HANDLING ###### + + def deleteOldCacheData(self, days, still_active = [], delete_torrents = False): + if not days: + return + exptime = time() - (days*24*3600) + names = {} + times = {} + + for f in os.listdir(self.dir_torrentcache): + p = os.path.join(self.dir_torrentcache,f) + f = os.path.basename(f) + try: + f, garbage = f.split('.') + except: + pass + try: + f = unhex(f) + assert len(f) == 20 + except: + continue + if delete_torrents: + names.setdefault(f,[]).append(p) + try: + t = os.path.getmtime(p) + except: + t = time() + times.setdefault(f,[]).append(t) + + for f in os.listdir(self.dir_datacache): + p = os.path.join(self.dir_datacache,f) + try: + f = unhex(os.path.basename(f)) + assert len(f) == 20 + except: + continue + names.setdefault(f,[]).append(p) + try: + t = os.path.getmtime(p) + except: + t = time() + times.setdefault(f,[]).append(t) + + for f in os.listdir(self.dir_piececache): + p = os.path.join(self.dir_piececache,f) + try: + f = unhex(os.path.basename(f)) + assert len(f) == 20 + except: + continue + for f2 in os.listdir(p): + p2 = os.path.join(p,f2) + names.setdefault(f,[]).append(p2) + try: + t = os.path.getmtime(p2) + except: + t = time() + times.setdefault(f,[]).append(t) + names.setdefault(f,[]).append(p) + + for k,v in times.items(): + if max(v) < exptime and not k in still_active: + for f in names[k]: + try: + os.remove(f) + except: + try: + os.removedirs(f) + except: + pass + + + def deleteOldTorrents(self, days, still_active = []): + self.deleteOldCacheData(days, still_active, True) + + + ###### OTHER ###### + + def getIconDir(self): + return self.dir_icons diff --git a/BitTornado/ConfigReader.py b/BitTornado/ConfigReader.py new file mode 100644 index 000000000..ae3f38f78 --- /dev/null +++ b/BitTornado/ConfigReader.py @@ -0,0 +1,1068 @@ +#written by John Hoffman + +from ConnChoice import * +from wxPython.wx import * +from types import IntType, FloatType, StringType +from download_bt1 import defaults +from ConfigDir import ConfigDir +import sys,os +import socket +from parseargs import defaultargs + +try: + True +except: + True = 1 + False = 0 + +try: + wxFULL_REPAINT_ON_RESIZE +except: + wxFULL_REPAINT_ON_RESIZE = 0 # fix for wx pre-2.5 + +if (sys.platform == 'win32'): + _FONT = 9 +else: + _FONT = 10 + +def HexToColor(s): + r,g,b = s.split(' ') + return wxColour(red=int(r,16), green=int(g,16), blue=int(b,16)) + +def hex2(c): + h = hex(c)[2:] + if len(h) == 1: + h = '0'+h + return h +def ColorToHex(c): + return hex2(c.Red()) + ' ' + hex2(c.Green()) + ' ' + hex2(c.Blue()) + +ratesettingslist = [] +for x in connChoices: + if not x.has_key('super-seed'): + ratesettingslist.append(x['name']) + + +configFileDefaults = [ + #args only available for the gui client + ('win32_taskbar_icon', 1, + "whether to iconize do system try or not on win32"), + ('gui_stretchwindow', 0, + "whether to stretch the download status window to fit the torrent name"), + ('gui_displaystats', 1, + "whether to display statistics on peers and seeds"), + ('gui_displaymiscstats', 1, + "whether to display miscellaneous other statistics"), + ('gui_ratesettingsdefault', ratesettingslist[0], + "the default setting for maximum upload rate and users"), + ('gui_ratesettingsmode', 'full', + "what rate setting controls to display; options are 'none', 'basic', and 'full'"), + ('gui_forcegreenonfirewall', 0, + "forces the status icon to be green even if the client seems to be firewalled"), + ('gui_default_savedir', '', + "default save directory"), + ('last_saved', '', # hidden; not set in config + "where the last torrent was saved"), + ('gui_font', _FONT, + "the font size to use"), + ('gui_saveas_ask', -1, + "whether to ask where to download to (0 = never, 1 = always, -1 = automatic resume"), +] + +def setwxconfigfiledefaults(): + CHECKINGCOLOR = ColorToHex(wxSystemSettings_GetColour(wxSYS_COLOUR_3DSHADOW)) + DOWNLOADCOLOR = ColorToHex(wxSystemSettings_GetColour(wxSYS_COLOUR_ACTIVECAPTION)) + + configFileDefaults.extend([ + ('gui_checkingcolor', CHECKINGCOLOR, + "progress bar checking color"), + ('gui_downloadcolor', DOWNLOADCOLOR, + "progress bar downloading color"), + ('gui_seedingcolor', '00 FF 00', + "progress bar seeding color"), + ]) + +defaultsToIgnore = ['responsefile', 'url', 'priority'] + + +class configReader: + + def __init__(self): + self.configfile = wxConfig("BitTorrent",style=wxCONFIG_USE_LOCAL_FILE) + self.configMenuBox = None + self.advancedMenuBox = None + self._configReset = True # run reset for the first time + + setwxconfigfiledefaults() + + defaults.extend(configFileDefaults) + self.defaults = defaultargs(defaults) + + self.configDir = ConfigDir('gui') + self.configDir.setDefaults(defaults,defaultsToIgnore) + if self.configDir.checkConfig(): + self.config = self.configDir.loadConfig() + else: + self.config = self.configDir.getConfig() + self.importOldGUIConfig() + self.configDir.saveConfig() + + updated = False # make all config default changes here + + if self.config['gui_ratesettingsdefault'] not in ratesettingslist: + self.config['gui_ratesettingsdefault'] = ( + self.defaults['gui_ratesettingsdefault'] ) + updated = True + if self.config['ipv6_enabled'] and ( + sys.version_info < (2,3) or not socket.has_ipv6 ): + self.config['ipv6_enabled'] = 0 + updated = True + for c in ['gui_checkingcolor','gui_downloadcolor','gui_seedingcolor']: + try: + HexToColor(self.config[c]) + except: + self.config[c] = self.defaults[c] + updated = True + + if updated: + self.configDir.saveConfig() + + self.configDir.deleteOldCacheData(self.config['expire_cache_data']) + + + def importOldGUIConfig(self): + oldconfig = wxConfig("BitTorrent",style=wxCONFIG_USE_LOCAL_FILE) + cont, s, i = oldconfig.GetFirstEntry() + if not cont: + oldconfig.DeleteAll() + return False + while cont: # import old config data + if self.config.has_key(s): + t = oldconfig.GetEntryType(s) + try: + if t == 1: + assert type(self.config[s]) == type('') + self.config[s] = oldconfig.Read(s) + elif t == 2 or t == 3: + assert type(self.config[s]) == type(1) + self.config[s] = int(oldconfig.ReadInt(s)) + elif t == 4: + assert type(self.config[s]) == type(1.0) + self.config[s] = oldconfig.ReadFloat(s) + except: + pass + cont, s, i = oldconfig.GetNextEntry(i) + +# oldconfig.DeleteAll() + return True + + + def resetConfigDefaults(self): + for p,v in self.defaults.items(): + if not p in defaultsToIgnore: + self.config[p] = v + self.configDir.saveConfig() + + def writeConfigFile(self): + self.configDir.saveConfig() + + def WriteLastSaved(self, l): + self.config['last_saved'] = l + self.configDir.saveConfig() + + + def getcheckingcolor(self): + return HexToColor(self.config['gui_checkingcolor']) + def getdownloadcolor(self): + return HexToColor(self.config['gui_downloadcolor']) + def getseedingcolor(self): + return HexToColor(self.config['gui_seedingcolor']) + + def configReset(self): + r = self._configReset + self._configReset = False + return r + + def getConfigDir(self): + return self.configDir + + def getIconDir(self): + return self.configDir.getIconDir() + + def getTorrentData(self,t): + return self.configDir.getTorrentData(t) + + def setColorIcon(self, xxicon, xxiconptr, xxcolor): + idata = wxMemoryDC() + idata.SelectObject(xxicon) + idata.SetBrush(wxBrush(xxcolor,wxSOLID)) + idata.DrawRectangle(0,0,16,16) + idata.SelectObject(wxNullBitmap) + xxiconptr.Refresh() + + + def getColorFromUser(self, parent, colInit): + data = wxColourData() + if colInit.Ok(): + data.SetColour(colInit) + data.SetCustomColour(0, self.checkingcolor) + data.SetCustomColour(1, self.downloadcolor) + data.SetCustomColour(2, self.seedingcolor) + dlg = wxColourDialog(parent,data) + if not dlg.ShowModal(): + return colInit + return dlg.GetColourData().GetColour() + + + def configMenu(self, parent): + self.parent = parent + try: + self.FONT = self.config['gui_font'] + self.default_font = wxFont(self.FONT, wxDEFAULT, wxNORMAL, wxNORMAL, False) + self.checkingcolor = HexToColor(self.config['gui_checkingcolor']) + self.downloadcolor = HexToColor(self.config['gui_downloadcolor']) + self.seedingcolor = HexToColor(self.config['gui_seedingcolor']) + + if (self.configMenuBox is not None): + try: + self.configMenuBox.Close() + except wxPyDeadObjectError, e: + self.configMenuBox = None + + self.configMenuBox = wxFrame(None, -1, 'BitTorrent Preferences', size = (1,1), + style = wxDEFAULT_FRAME_STYLE|wxFULL_REPAINT_ON_RESIZE) + if (sys.platform == 'win32'): + self.icon = self.parent.icon + self.configMenuBox.SetIcon(self.icon) + + panel = wxPanel(self.configMenuBox, -1) + self.panel = panel + + def StaticText(text, font = self.FONT, underline = False, color = None, panel = panel): + x = wxStaticText(panel, -1, text, style = wxALIGN_LEFT) + x.SetFont(wxFont(font, wxDEFAULT, wxNORMAL, wxNORMAL, underline)) + if color is not None: + x.SetForegroundColour(color) + return x + + colsizer = wxFlexGridSizer(cols = 1, vgap = 8) + + self.gui_stretchwindow_checkbox = wxCheckBox(panel, -1, "Stretch window to fit torrent name *") + self.gui_stretchwindow_checkbox.SetFont(self.default_font) + self.gui_stretchwindow_checkbox.SetValue(self.config['gui_stretchwindow']) + + self.gui_displaystats_checkbox = wxCheckBox(panel, -1, "Display peer and seed statistics") + self.gui_displaystats_checkbox.SetFont(self.default_font) + self.gui_displaystats_checkbox.SetValue(self.config['gui_displaystats']) + + self.gui_displaymiscstats_checkbox = wxCheckBox(panel, -1, "Display miscellaneous other statistics") + self.gui_displaymiscstats_checkbox.SetFont(self.default_font) + self.gui_displaymiscstats_checkbox.SetValue(self.config['gui_displaymiscstats']) + + self.security_checkbox = wxCheckBox(panel, -1, "Don't allow multiple connections from the same IP") + self.security_checkbox.SetFont(self.default_font) + self.security_checkbox.SetValue(self.config['security']) + + self.autokick_checkbox = wxCheckBox(panel, -1, "Kick/ban clients that send you bad data *") + self.autokick_checkbox.SetFont(self.default_font) + self.autokick_checkbox.SetValue(self.config['auto_kick']) + + self.buffering_checkbox = wxCheckBox(panel, -1, "Enable read/write buffering *") + self.buffering_checkbox.SetFont(self.default_font) + self.buffering_checkbox.SetValue(self.config['buffer_reads']) + + self.breakup_checkbox = wxCheckBox(panel, -1, "Break-up seed bitfield to foil ISP manipulation") + self.breakup_checkbox.SetFont(self.default_font) + self.breakup_checkbox.SetValue(self.config['breakup_seed_bitfield']) + + self.autoflush_checkbox = wxCheckBox(panel, -1, "Flush data to disk every 5 minutes") + self.autoflush_checkbox.SetFont(self.default_font) + self.autoflush_checkbox.SetValue(self.config['auto_flush']) + + if sys.version_info >= (2,3) and socket.has_ipv6: + self.ipv6enabled_checkbox = wxCheckBox(panel, -1, "Initiate and receive connections via IPv6 *") + self.ipv6enabled_checkbox.SetFont(self.default_font) + self.ipv6enabled_checkbox.SetValue(self.config['ipv6_enabled']) + + self.gui_forcegreenonfirewall_checkbox = wxCheckBox(panel, -1, + "Force icon to display green when firewalled") + self.gui_forcegreenonfirewall_checkbox.SetFont(self.default_font) + self.gui_forcegreenonfirewall_checkbox.SetValue(self.config['gui_forcegreenonfirewall']) + + + self.minport_data = wxSpinCtrl(panel, -1, '', (-1,-1), (self.FONT*8, -1)) + self.minport_data.SetFont(self.default_font) + self.minport_data.SetRange(1,65535) + self.minport_data.SetValue(self.config['minport']) + + self.maxport_data = wxSpinCtrl(panel, -1, '', (-1,-1), (self.FONT*8, -1)) + self.maxport_data.SetFont(self.default_font) + self.maxport_data.SetRange(1,65535) + self.maxport_data.SetValue(self.config['maxport']) + + self.randomport_checkbox = wxCheckBox(panel, -1, "randomize") + self.randomport_checkbox.SetFont(self.default_font) + self.randomport_checkbox.SetValue(self.config['random_port']) + + self.gui_font_data = wxSpinCtrl(panel, -1, '', (-1,-1), (self.FONT*5, -1)) + self.gui_font_data.SetFont(self.default_font) + self.gui_font_data.SetRange(8,16) + self.gui_font_data.SetValue(self.config['gui_font']) + + self.gui_ratesettingsdefault_data=wxChoice(panel, -1, choices = ratesettingslist) + self.gui_ratesettingsdefault_data.SetFont(self.default_font) + self.gui_ratesettingsdefault_data.SetStringSelection(self.config['gui_ratesettingsdefault']) + + self.maxdownload_data = wxSpinCtrl(panel, -1, '', (-1,-1), (self.FONT*7, -1)) + self.maxdownload_data.SetFont(self.default_font) + self.maxdownload_data.SetRange(0,5000) + self.maxdownload_data.SetValue(self.config['max_download_rate']) + + self.gui_ratesettingsmode_data=wxRadioBox(panel, -1, 'Rate Settings Mode', + choices = [ 'none', 'basic', 'full' ] ) + self.gui_ratesettingsmode_data.SetFont(self.default_font) + self.gui_ratesettingsmode_data.SetStringSelection(self.config['gui_ratesettingsmode']) + + if (sys.platform == 'win32'): + self.win32_taskbar_icon_checkbox = wxCheckBox(panel, -1, "Minimize to system tray") + self.win32_taskbar_icon_checkbox.SetFont(self.default_font) + self.win32_taskbar_icon_checkbox.SetValue(self.config['win32_taskbar_icon']) + +# self.upnp_checkbox = wxCheckBox(panel, -1, "Enable automatic UPnP port forwarding") +# self.upnp_checkbox.SetFont(self.default_font) +# self.upnp_checkbox.SetValue(self.config['upnp_nat_access']) + self.upnp_data=wxChoice(panel, -1, + choices = ['disabled', 'type 1 (fast)', 'type 2 (slow)']) + self.upnp_data.SetFont(self.default_font) + self.upnp_data.SetSelection(self.config['upnp_nat_access']) + + self.gui_default_savedir_ctrl = wxTextCtrl(parent = panel, id = -1, + value = self.config['gui_default_savedir'], + size = (26*self.FONT, -1), style = wxTE_PROCESS_TAB) + self.gui_default_savedir_ctrl.SetFont(self.default_font) + + self.gui_savemode_data=wxRadioBox(panel, -1, 'Ask where to save: *', + choices = [ 'always', 'never', 'auto-resume' ] ) + self.gui_savemode_data.SetFont(self.default_font) + self.gui_savemode_data.SetSelection(1-self.config['gui_saveas_ask']) + + self.checkingcolor_icon = wxEmptyBitmap(16,16) + self.checkingcolor_iconptr = wxStaticBitmap(panel, -1, self.checkingcolor_icon) + self.setColorIcon(self.checkingcolor_icon, self.checkingcolor_iconptr, self.checkingcolor) + + self.downloadcolor_icon = wxEmptyBitmap(16,16) + self.downloadcolor_iconptr = wxStaticBitmap(panel, -1, self.downloadcolor_icon) + self.setColorIcon(self.downloadcolor_icon, self.downloadcolor_iconptr, self.downloadcolor) + + self.seedingcolor_icon = wxEmptyBitmap(16,16) + self.seedingcolor_iconptr = wxStaticBitmap(panel, -1, self.seedingcolor_icon) + self.setColorIcon(self.seedingcolor_icon, self.downloadcolor_iconptr, self.seedingcolor) + + rowsizer = wxFlexGridSizer(cols = 2, hgap = 20) + + block12sizer = wxFlexGridSizer(cols = 1, vgap = 7) + + block1sizer = wxFlexGridSizer(cols = 1, vgap = 2) + if (sys.platform == 'win32'): + block1sizer.Add(self.win32_taskbar_icon_checkbox) +# block1sizer.Add(self.upnp_checkbox) + block1sizer.Add(self.gui_stretchwindow_checkbox) + block1sizer.Add(self.gui_displaystats_checkbox) + block1sizer.Add(self.gui_displaymiscstats_checkbox) + block1sizer.Add(self.security_checkbox) + block1sizer.Add(self.autokick_checkbox) + block1sizer.Add(self.buffering_checkbox) + block1sizer.Add(self.breakup_checkbox) + block1sizer.Add(self.autoflush_checkbox) + if sys.version_info >= (2,3) and socket.has_ipv6: + block1sizer.Add(self.ipv6enabled_checkbox) + block1sizer.Add(self.gui_forcegreenonfirewall_checkbox) + + block12sizer.Add(block1sizer) + + colorsizer = wxStaticBoxSizer(wxStaticBox(panel, -1, "Gauge Colors:"), wxVERTICAL) + colorsizer1 = wxFlexGridSizer(cols = 7) + colorsizer1.Add(StaticText(' Checking: '), 1, wxALIGN_BOTTOM) + colorsizer1.Add(self.checkingcolor_iconptr, 1, wxALIGN_BOTTOM) + colorsizer1.Add(StaticText(' Downloading: '), 1, wxALIGN_BOTTOM) + colorsizer1.Add(self.downloadcolor_iconptr, 1, wxALIGN_BOTTOM) + colorsizer1.Add(StaticText(' Seeding: '), 1, wxALIGN_BOTTOM) + colorsizer1.Add(self.seedingcolor_iconptr, 1, wxALIGN_BOTTOM) + colorsizer1.Add(StaticText(' ')) + minsize = self.checkingcolor_iconptr.GetBestSize() + minsize.SetHeight(minsize.GetHeight()+5) + colorsizer1.SetMinSize(minsize) + colorsizer.Add(colorsizer1) + + block12sizer.Add(colorsizer, 1, wxALIGN_LEFT) + + rowsizer.Add(block12sizer) + + block3sizer = wxFlexGridSizer(cols = 1) + + portsettingsSizer = wxStaticBoxSizer(wxStaticBox(panel, -1, "Port Range:*"), wxVERTICAL) + portsettingsSizer1 = wxGridSizer(cols = 2, vgap = 1) + portsettingsSizer1.Add(StaticText('From: '), 1, wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT) + portsettingsSizer1.Add(self.minport_data, 1, wxALIGN_BOTTOM) + portsettingsSizer1.Add(StaticText('To: '), 1, wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT) + portsettingsSizer1.Add(self.maxport_data, 1, wxALIGN_BOTTOM) + portsettingsSizer.Add(portsettingsSizer1) + portsettingsSizer.Add(self.randomport_checkbox, 1, wxALIGN_CENTER) + block3sizer.Add(portsettingsSizer, 1, wxALIGN_CENTER) + block3sizer.Add(StaticText(' ')) + block3sizer.Add(self.gui_ratesettingsmode_data, 1, wxALIGN_CENTER) + block3sizer.Add(StaticText(' ')) + ratesettingsSizer = wxFlexGridSizer(cols = 1, vgap = 2) + ratesettingsSizer.Add(StaticText('Default Rate Setting: *'), 1, wxALIGN_CENTER) + ratesettingsSizer.Add(self.gui_ratesettingsdefault_data, 1, wxALIGN_CENTER) + block3sizer.Add(ratesettingsSizer, 1, wxALIGN_CENTER) + if (sys.platform == 'win32'): + block3sizer.Add(StaticText(' ')) + upnpSizer = wxFlexGridSizer(cols = 1, vgap = 2) + upnpSizer.Add(StaticText('UPnP Port Forwarding: *'), 1, wxALIGN_CENTER) + upnpSizer.Add(self.upnp_data, 1, wxALIGN_CENTER) + block3sizer.Add(upnpSizer, 1, wxALIGN_CENTER) + + rowsizer.Add(block3sizer) + colsizer.Add(rowsizer) + + block4sizer = wxFlexGridSizer(cols = 3, hgap = 15) + savepathsizer = wxFlexGridSizer(cols = 2, vgap = 1) + savepathsizer.Add(StaticText('Default Save Path: *')) + savepathsizer.Add(StaticText(' ')) + savepathsizer.Add(self.gui_default_savedir_ctrl, 1, wxEXPAND) + savepathButton = wxButton(panel, -1, '...', size = (18,18)) +# savepathButton.SetFont(self.default_font) + savepathsizer.Add(savepathButton, 0, wxALIGN_CENTER) + savepathsizer.Add(self.gui_savemode_data, 0, wxALIGN_CENTER) + block4sizer.Add(savepathsizer, -1, wxALIGN_BOTTOM) + + fontsizer = wxFlexGridSizer(cols = 1, vgap = 2) + fontsizer.Add(StaticText('')) + fontsizer.Add(StaticText('Font: *'), 1, wxALIGN_CENTER) + fontsizer.Add(self.gui_font_data, 1, wxALIGN_CENTER) + block4sizer.Add(fontsizer, 1, wxALIGN_CENTER_VERTICAL) + + dratesettingsSizer = wxFlexGridSizer(cols = 1, vgap = 2) + dratesettingsSizer.Add(StaticText('Default Max'), 1, wxALIGN_CENTER) + dratesettingsSizer.Add(StaticText('Download Rate'), 1, wxALIGN_CENTER) + dratesettingsSizer.Add(StaticText('(kB/s): *'), 1, wxALIGN_CENTER) + dratesettingsSizer.Add(self.maxdownload_data, 1, wxALIGN_CENTER) + dratesettingsSizer.Add(StaticText('(0 = disabled)'), 1, wxALIGN_CENTER) + + block4sizer.Add(dratesettingsSizer, 1, wxALIGN_CENTER_VERTICAL) + + colsizer.Add(block4sizer, 0, wxALIGN_CENTER) +# colsizer.Add(StaticText(' ')) + + savesizer = wxGridSizer(cols = 4, hgap = 10) + saveButton = wxButton(panel, -1, 'Save') +# saveButton.SetFont(self.default_font) + savesizer.Add(saveButton, 0, wxALIGN_CENTER) + + cancelButton = wxButton(panel, -1, 'Cancel') +# cancelButton.SetFont(self.default_font) + savesizer.Add(cancelButton, 0, wxALIGN_CENTER) + + defaultsButton = wxButton(panel, -1, 'Revert to Defaults') +# defaultsButton.SetFont(self.default_font) + savesizer.Add(defaultsButton, 0, wxALIGN_CENTER) + + advancedButton = wxButton(panel, -1, 'Advanced...') +# advancedButton.SetFont(self.default_font) + savesizer.Add(advancedButton, 0, wxALIGN_CENTER) + colsizer.Add(savesizer, 1, wxALIGN_CENTER) + + resizewarningtext=StaticText('* These settings will not take effect until the next time you start BitTorrent', self.FONT-2) + colsizer.Add(resizewarningtext, 1, wxALIGN_CENTER) + + border = wxBoxSizer(wxHORIZONTAL) + border.Add(colsizer, 1, wxEXPAND | wxALL, 4) + + panel.SetSizer(border) + panel.SetAutoLayout(True) + + self.advancedConfig = {} + + def setDefaults(evt, self = self): + try: + self.minport_data.SetValue(self.defaults['minport']) + self.maxport_data.SetValue(self.defaults['maxport']) + self.randomport_checkbox.SetValue(self.defaults['random_port']) + self.gui_stretchwindow_checkbox.SetValue(self.defaults['gui_stretchwindow']) + self.gui_displaystats_checkbox.SetValue(self.defaults['gui_displaystats']) + self.gui_displaymiscstats_checkbox.SetValue(self.defaults['gui_displaymiscstats']) + self.security_checkbox.SetValue(self.defaults['security']) + self.autokick_checkbox.SetValue(self.defaults['auto_kick']) + self.buffering_checkbox.SetValue(self.defaults['buffer_reads']) + self.breakup_checkbox.SetValue(self.defaults['breakup_seed_bitfield']) + self.autoflush_checkbox.SetValue(self.defaults['auto_flush']) + if sys.version_info >= (2,3) and socket.has_ipv6: + self.ipv6enabled_checkbox.SetValue(self.defaults['ipv6_enabled']) + self.gui_forcegreenonfirewall_checkbox.SetValue(self.defaults['gui_forcegreenonfirewall']) + self.gui_font_data.SetValue(self.defaults['gui_font']) + self.gui_ratesettingsdefault_data.SetStringSelection(self.defaults['gui_ratesettingsdefault']) + self.maxdownload_data.SetValue(self.defaults['max_download_rate']) + self.gui_ratesettingsmode_data.SetStringSelection(self.defaults['gui_ratesettingsmode']) + self.gui_default_savedir_ctrl.SetValue(self.defaults['gui_default_savedir']) + self.gui_savemode_data.SetSelection(1-self.defaults['gui_saveas_ask']) + + self.checkingcolor = HexToColor(self.defaults['gui_checkingcolor']) + self.setColorIcon(self.checkingcolor_icon, self.checkingcolor_iconptr, self.checkingcolor) + self.downloadcolor = HexToColor(self.defaults['gui_downloadcolor']) + self.setColorIcon(self.downloadcolor_icon, self.downloadcolor_iconptr, self.downloadcolor) + self.seedingcolor = HexToColor(self.defaults['gui_seedingcolor']) + self.setColorIcon(self.seedingcolor_icon, self.seedingcolor_iconptr, self.seedingcolor) + + if (sys.platform == 'win32'): + self.win32_taskbar_icon_checkbox.SetValue(self.defaults['win32_taskbar_icon']) +# self.upnp_checkbox.SetValue(self.defaults['upnp_nat_access']) + self.upnp_data.SetSelection(self.defaults['upnp_nat_access']) + + # reset advanced too + self.advancedConfig = {} + for key in ['ip', 'bind', 'min_peers', 'max_initiate', 'display_interval', + 'alloc_type', 'alloc_rate', 'max_files_open', 'max_connections', 'super_seeder', + 'ipv6_binds_v4', 'double_check', 'triple_check', 'lock_files', 'lock_while_reading', + 'expire_cache_data']: + self.advancedConfig[key] = self.defaults[key] + self.CloseAdvanced() + except: + self.parent.exception() + + + def saveConfigs(evt, self = self): + try: + self.config['gui_stretchwindow']=int(self.gui_stretchwindow_checkbox.GetValue()) + self.config['gui_displaystats']=int(self.gui_displaystats_checkbox.GetValue()) + self.config['gui_displaymiscstats']=int(self.gui_displaymiscstats_checkbox.GetValue()) + self.config['security']=int(self.security_checkbox.GetValue()) + self.config['auto_kick']=int(self.autokick_checkbox.GetValue()) + buffering=int(self.buffering_checkbox.GetValue()) + self.config['buffer_reads']=buffering + if buffering: + self.config['write_buffer_size']=self.defaults['write_buffer_size'] + else: + self.config['write_buffer_size']=0 + self.config['breakup_seed_bitfield']=int(self.breakup_checkbox.GetValue()) + if self.autoflush_checkbox.GetValue(): + self.config['auto_flush']=5 + else: + self.config['auto_flush']=0 + if sys.version_info >= (2,3) and socket.has_ipv6: + self.config['ipv6_enabled']=int(self.ipv6enabled_checkbox.GetValue()) + self.config['gui_forcegreenonfirewall']=int(self.gui_forcegreenonfirewall_checkbox.GetValue()) + self.config['minport']=self.minport_data.GetValue() + self.config['maxport']=self.maxport_data.GetValue() + self.config['random_port']=int(self.randomport_checkbox.GetValue()) + self.config['gui_font']=self.gui_font_data.GetValue() + self.config['gui_ratesettingsdefault']=self.gui_ratesettingsdefault_data.GetStringSelection() + self.config['max_download_rate']=self.maxdownload_data.GetValue() + self.config['gui_ratesettingsmode']=self.gui_ratesettingsmode_data.GetStringSelection() + self.config['gui_default_savedir']=self.gui_default_savedir_ctrl.GetValue() + self.config['gui_saveas_ask']=1-self.gui_savemode_data.GetSelection() + self.config['gui_checkingcolor']=ColorToHex(self.checkingcolor) + self.config['gui_downloadcolor']=ColorToHex(self.downloadcolor) + self.config['gui_seedingcolor']=ColorToHex(self.seedingcolor) + + if (sys.platform == 'win32'): + self.config['win32_taskbar_icon']=int(self.win32_taskbar_icon_checkbox.GetValue()) +# self.config['upnp_nat_access']=int(self.upnp_checkbox.GetValue()) + self.config['upnp_nat_access']=self.upnp_data.GetSelection() + + if self.advancedConfig: + for key,val in self.advancedConfig.items(): + self.config[key] = val + + self.writeConfigFile() + self._configReset = True + self.Close() + except: + self.parent.exception() + + def cancelConfigs(evt, self = self): + self.Close() + + def savepath_set(evt, self = self): + try: + d = self.gui_default_savedir_ctrl.GetValue() + if d == '': + d = self.config['last_saved'] + dl = wxDirDialog(self.panel, 'Choose a default directory to save to', + d, style = wxDD_DEFAULT_STYLE | wxDD_NEW_DIR_BUTTON) + if dl.ShowModal() == wxID_OK: + self.gui_default_savedir_ctrl.SetValue(dl.GetPath()) + except: + self.parent.exception() + + def checkingcoloricon_set(evt, self = self): + try: + newcolor = self.getColorFromUser(self.panel,self.checkingcolor) + self.setColorIcon(self.checkingcolor_icon, self.checkingcolor_iconptr, newcolor) + self.checkingcolor = newcolor + except: + self.parent.exception() + + def downloadcoloricon_set(evt, self = self): + try: + newcolor = self.getColorFromUser(self.panel,self.downloadcolor) + self.setColorIcon(self.downloadcolor_icon, self.downloadcolor_iconptr, newcolor) + self.downloadcolor = newcolor + except: + self.parent.exception() + + def seedingcoloricon_set(evt, self = self): + try: + newcolor = self.getColorFromUser(self.panel,self.seedingcolor) + self.setColorIcon(self.seedingcolor_icon, self.seedingcolor_iconptr, newcolor) + self.seedingcolor = newcolor + except: + self.parent.exception() + + EVT_BUTTON(self.configMenuBox, saveButton.GetId(), saveConfigs) + EVT_BUTTON(self.configMenuBox, cancelButton.GetId(), cancelConfigs) + EVT_BUTTON(self.configMenuBox, defaultsButton.GetId(), setDefaults) + EVT_BUTTON(self.configMenuBox, advancedButton.GetId(), self.advancedMenu) + EVT_BUTTON(self.configMenuBox, savepathButton.GetId(), savepath_set) + EVT_LEFT_DOWN(self.checkingcolor_iconptr, checkingcoloricon_set) + EVT_LEFT_DOWN(self.downloadcolor_iconptr, downloadcoloricon_set) + EVT_LEFT_DOWN(self.seedingcolor_iconptr, seedingcoloricon_set) + + self.configMenuBox.Show () + border.Fit(panel) + self.configMenuBox.Fit() + except: + self.parent.exception() + + + def Close(self): + self.CloseAdvanced() + if self.configMenuBox is not None: + try: + self.configMenuBox.Close () + except wxPyDeadObjectError, e: + pass + self.configMenuBox = None + + def advancedMenu(self, event = None): + try: + if not self.advancedConfig: + for key in ['ip', 'bind', 'min_peers', 'max_initiate', 'display_interval', + 'alloc_type', 'alloc_rate', 'max_files_open', 'max_connections', 'super_seeder', + 'ipv6_binds_v4', 'double_check', 'triple_check', 'lock_files', 'lock_while_reading', + 'expire_cache_data']: + self.advancedConfig[key] = self.config[key] + + if (self.advancedMenuBox is not None): + try: + self.advancedMenuBox.Close () + except wxPyDeadObjectError, e: + self.advancedMenuBox = None + + self.advancedMenuBox = wxFrame(None, -1, 'BitTorrent Advanced Preferences', size = (1,1), + style = wxDEFAULT_FRAME_STYLE|wxFULL_REPAINT_ON_RESIZE) + if (sys.platform == 'win32'): + self.advancedMenuBox.SetIcon(self.icon) + + panel = wxPanel(self.advancedMenuBox, -1) +# self.panel = panel + + def StaticText(text, font = self.FONT, underline = False, color = None, panel = panel): + x = wxStaticText(panel, -1, text, style = wxALIGN_LEFT) + x.SetFont(wxFont(font, wxDEFAULT, wxNORMAL, wxNORMAL, underline)) + if color is not None: + x.SetForegroundColour(color) + return x + + colsizer = wxFlexGridSizer(cols = 1, hgap = 13, vgap = 13) + warningtext = StaticText('CHANGE THESE SETTINGS AT YOUR OWN RISK', self.FONT+4, True, 'Red') + colsizer.Add(warningtext, 1, wxALIGN_CENTER) + + self.ip_data = wxTextCtrl(parent = panel, id = -1, + value = self.advancedConfig['ip'], + size = (self.FONT*13, int(self.FONT*2.2)), style = wxTE_PROCESS_TAB) + self.ip_data.SetFont(self.default_font) + + self.bind_data = wxTextCtrl(parent = panel, id = -1, + value = self.advancedConfig['bind'], + size = (self.FONT*13, int(self.FONT*2.2)), style = wxTE_PROCESS_TAB) + self.bind_data.SetFont(self.default_font) + + if sys.version_info >= (2,3) and socket.has_ipv6: + self.ipv6bindsv4_data=wxChoice(panel, -1, + choices = ['separate sockets', 'single socket']) + self.ipv6bindsv4_data.SetFont(self.default_font) + self.ipv6bindsv4_data.SetSelection(self.advancedConfig['ipv6_binds_v4']) + + self.minpeers_data = wxSpinCtrl(panel, -1, '', (-1,-1), (self.FONT*7, -1)) + self.minpeers_data.SetFont(self.default_font) + self.minpeers_data.SetRange(10,100) + self.minpeers_data.SetValue(self.advancedConfig['min_peers']) + # max_initiate = 2*minpeers + + self.displayinterval_data = wxSpinCtrl(panel, -1, '', (-1,-1), (self.FONT*7, -1)) + self.displayinterval_data.SetFont(self.default_font) + self.displayinterval_data.SetRange(100,2000) + self.displayinterval_data.SetValue(int(self.advancedConfig['display_interval']*1000)) + + self.alloctype_data=wxChoice(panel, -1, + choices = ['normal', 'background', 'pre-allocate', 'sparse']) + self.alloctype_data.SetFont(self.default_font) + self.alloctype_data.SetStringSelection(self.advancedConfig['alloc_type']) + + self.allocrate_data = wxSpinCtrl(panel, -1, '', (-1,-1), (self.FONT*7,-1)) + self.allocrate_data.SetFont(self.default_font) + self.allocrate_data.SetRange(1,100) + self.allocrate_data.SetValue(int(self.advancedConfig['alloc_rate'])) + + self.locking_data=wxChoice(panel, -1, + choices = ['no locking', 'lock while writing', 'lock always']) + self.locking_data.SetFont(self.default_font) + if self.advancedConfig['lock_files']: + if self.advancedConfig['lock_while_reading']: + self.locking_data.SetSelection(2) + else: + self.locking_data.SetSelection(1) + else: + self.locking_data.SetSelection(0) + + self.doublecheck_data=wxChoice(panel, -1, + choices = ['no extra checking', 'double-check', 'triple-check']) + self.doublecheck_data.SetFont(self.default_font) + if self.advancedConfig['double_check']: + if self.advancedConfig['triple_check']: + self.doublecheck_data.SetSelection(2) + else: + self.doublecheck_data.SetSelection(1) + else: + self.doublecheck_data.SetSelection(0) + + self.maxfilesopen_choices = ['50', '100', '200', 'no limit '] + self.maxfilesopen_data=wxChoice(panel, -1, choices = self.maxfilesopen_choices) + self.maxfilesopen_data.SetFont(self.default_font) + setval = self.advancedConfig['max_files_open'] + if setval == 0: + setval = 'no limit ' + else: + setval = str(setval) + if not setval in self.maxfilesopen_choices: + setval = self.maxfilesopen_choices[0] + self.maxfilesopen_data.SetStringSelection(setval) + + self.maxconnections_choices = ['no limit ', '20', '30', '40', '50', '60', '100', '200'] + self.maxconnections_data=wxChoice(panel, -1, choices = self.maxconnections_choices) + self.maxconnections_data.SetFont(self.default_font) + setval = self.advancedConfig['max_connections'] + if setval == 0: + setval = 'no limit ' + else: + setval = str(setval) + if not setval in self.maxconnections_choices: + setval = self.maxconnections_choices[0] + self.maxconnections_data.SetStringSelection(setval) + + self.superseeder_data=wxChoice(panel, -1, + choices = ['normal', 'super-seed']) + self.superseeder_data.SetFont(self.default_font) + self.superseeder_data.SetSelection(self.advancedConfig['super_seeder']) + + self.expirecache_choices = ['never ', '3', '5', '7', '10', '15', '30', '60', '90'] + self.expirecache_data=wxChoice(panel, -1, choices = self.expirecache_choices) + setval = self.advancedConfig['expire_cache_data'] + if setval == 0: + setval = 'never ' + else: + setval = str(setval) + if not setval in self.expirecache_choices: + setval = self.expirecache_choices[0] + self.expirecache_data.SetFont(self.default_font) + self.expirecache_data.SetStringSelection(setval) + + + twocolsizer = wxFlexGridSizer(cols = 2, hgap = 20) + datasizer = wxFlexGridSizer(cols = 2, vgap = 2) + datasizer.Add(StaticText('Local IP: '), 1, wxALIGN_CENTER_VERTICAL) + datasizer.Add(self.ip_data) + datasizer.Add(StaticText('IP to bind to: '), 1, wxALIGN_CENTER_VERTICAL) + datasizer.Add(self.bind_data) + if sys.version_info >= (2,3) and socket.has_ipv6: + datasizer.Add(StaticText('IPv6 socket handling: '), 1, wxALIGN_CENTER_VERTICAL) + datasizer.Add(self.ipv6bindsv4_data) + datasizer.Add(StaticText('Minimum number of peers: '), 1, wxALIGN_CENTER_VERTICAL) + datasizer.Add(self.minpeers_data) + datasizer.Add(StaticText('Display interval (ms): '), 1, wxALIGN_CENTER_VERTICAL) + datasizer.Add(self.displayinterval_data) + datasizer.Add(StaticText('Disk allocation type:'), 1, wxALIGN_CENTER_VERTICAL) + datasizer.Add(self.alloctype_data) + datasizer.Add(StaticText('Allocation rate (MiB/s):'), 1, wxALIGN_CENTER_VERTICAL) + datasizer.Add(self.allocrate_data) + datasizer.Add(StaticText('File locking:'), 1, wxALIGN_CENTER_VERTICAL) + datasizer.Add(self.locking_data) + datasizer.Add(StaticText('Extra data checking:'), 1, wxALIGN_CENTER_VERTICAL) + datasizer.Add(self.doublecheck_data) + datasizer.Add(StaticText('Max files open:'), 1, wxALIGN_CENTER_VERTICAL) + datasizer.Add(self.maxfilesopen_data) + datasizer.Add(StaticText('Max peer connections:'), 1, wxALIGN_CENTER_VERTICAL) + datasizer.Add(self.maxconnections_data) + datasizer.Add(StaticText('Default seeding mode:'), 1, wxALIGN_CENTER_VERTICAL) + datasizer.Add(self.superseeder_data) + datasizer.Add(StaticText('Expire resume data(days):'), 1, wxALIGN_CENTER_VERTICAL) + datasizer.Add(self.expirecache_data) + + twocolsizer.Add(datasizer) + + infosizer = wxFlexGridSizer(cols = 1) + self.hinttext = StaticText('', self.FONT, False, 'Blue') + infosizer.Add(self.hinttext, 1, wxALIGN_LEFT|wxALIGN_CENTER_VERTICAL) + infosizer.SetMinSize((180,100)) + twocolsizer.Add(infosizer, 1, wxEXPAND) + + colsizer.Add(twocolsizer) + + savesizer = wxGridSizer(cols = 3, hgap = 20) + okButton = wxButton(panel, -1, 'OK') +# okButton.SetFont(self.default_font) + savesizer.Add(okButton, 0, wxALIGN_CENTER) + + cancelButton = wxButton(panel, -1, 'Cancel') +# cancelButton.SetFont(self.default_font) + savesizer.Add(cancelButton, 0, wxALIGN_CENTER) + + defaultsButton = wxButton(panel, -1, 'Revert to Defaults') +# defaultsButton.SetFont(self.default_font) + savesizer.Add(defaultsButton, 0, wxALIGN_CENTER) + colsizer.Add(savesizer, 1, wxALIGN_CENTER) + + resizewarningtext=StaticText('None of these settings will take effect until the next time you start BitTorrent', self.FONT-2) + colsizer.Add(resizewarningtext, 1, wxALIGN_CENTER) + + border = wxBoxSizer(wxHORIZONTAL) + border.Add(colsizer, 1, wxEXPAND | wxALL, 4) + + panel.SetSizer(border) + panel.SetAutoLayout(True) + + def setDefaults(evt, self = self): + try: + self.ip_data.SetValue(self.defaults['ip']) + self.bind_data.SetValue(self.defaults['bind']) + if sys.version_info >= (2,3) and socket.has_ipv6: + self.ipv6bindsv4_data.SetSelection(self.defaults['ipv6_binds_v4']) + self.minpeers_data.SetValue(self.defaults['min_peers']) + self.displayinterval_data.SetValue(int(self.defaults['display_interval']*1000)) + self.alloctype_data.SetStringSelection(self.defaults['alloc_type']) + self.allocrate_data.SetValue(int(self.defaults['alloc_rate'])) + if self.defaults['lock_files']: + if self.defaults['lock_while_reading']: + self.locking_data.SetSelection(2) + else: + self.locking_data.SetSelection(1) + else: + self.locking_data.SetSelection(0) + if self.defaults['double_check']: + if self.defaults['triple_check']: + self.doublecheck_data.SetSelection(2) + else: + self.doublecheck_data.SetSelection(1) + else: + self.doublecheck_data.SetSelection(0) + setval = self.defaults['max_files_open'] + if setval == 0: + setval = 'no limit ' + else: + setval = str(setval) + if not setval in self.maxfilesopen_choices: + setval = self.maxfilesopen_choices[0] + self.maxfilesopen_data.SetStringSelection(setval) + setval = self.defaults['max_connections'] + if setval == 0: + setval = 'no limit ' + else: + setval = str(setval) + if not setval in self.maxconnections_choices: + setval = self.maxconnections_choices[0] + self.maxconnections_data.SetStringSelection(setval) + self.superseeder_data.SetSelection(int(self.defaults['super_seeder'])) + setval = self.defaults['expire_cache_data'] + if setval == 0: + setval = 'never ' + else: + setval = str(setval) + if not setval in self.expirecache_choices: + setval = self.expirecache_choices[0] + self.expirecache_data.SetStringSelection(setval) + except: + self.parent.exception() + + def saveConfigs(evt, self = self): + try: + self.advancedConfig['ip'] = self.ip_data.GetValue() + self.advancedConfig['bind'] = self.bind_data.GetValue() + if sys.version_info >= (2,3) and socket.has_ipv6: + self.advancedConfig['ipv6_binds_v4'] = self.ipv6bindsv4_data.GetSelection() + self.advancedConfig['min_peers'] = self.minpeers_data.GetValue() + self.advancedConfig['display_interval'] = float(self.displayinterval_data.GetValue())/1000 + self.advancedConfig['alloc_type'] = self.alloctype_data.GetStringSelection() + self.advancedConfig['alloc_rate'] = float(self.allocrate_data.GetValue()) + self.advancedConfig['lock_files'] = int(self.locking_data.GetSelection() >= 1) + self.advancedConfig['lock_while_reading'] = int(self.locking_data.GetSelection() > 1) + self.advancedConfig['double_check'] = int(self.doublecheck_data.GetSelection() >= 1) + self.advancedConfig['triple_check'] = int(self.doublecheck_data.GetSelection() > 1) + try: + self.advancedConfig['max_files_open'] = int(self.maxfilesopen_data.GetStringSelection()) + except: # if it ain't a number, it must be "no limit" + self.advancedConfig['max_files_open'] = 0 + try: + self.advancedConfig['max_connections'] = int(self.maxconnections_data.GetStringSelection()) + self.advancedConfig['max_initiate'] = min( + 2*self.advancedConfig['min_peers'], self.advancedConfig['max_connections']) + except: # if it ain't a number, it must be "no limit" + self.advancedConfig['max_connections'] = 0 + self.advancedConfig['max_initiate'] = 2*self.advancedConfig['min_peers'] + self.advancedConfig['super_seeder']=int(self.superseeder_data.GetSelection()) + try: + self.advancedConfig['expire_cache_data'] = int(self.expirecache_data.GetStringSelection()) + except: + self.advancedConfig['expire_cache_data'] = 0 + self.advancedMenuBox.Close() + except: + self.parent.exception() + + def cancelConfigs(evt, self = self): + self.advancedMenuBox.Close() + + def ip_hint(evt, self = self): + self.hinttext.SetLabel('\n\n\nThe IP reported to the tracker.\n' + + 'unless the tracker is on the\n' + + 'same intranet as this client,\n' + + 'the tracker will autodetect the\n' + + "client's IP and ignore this\n" + + "value.") + + def bind_hint(evt, self = self): + self.hinttext.SetLabel('\n\n\nThe IP the client will bind to.\n' + + 'Only useful if your machine is\n' + + 'directly handling multiple IPs.\n' + + "If you don't know what this is,\n" + + "leave it blank.") + + def ipv6bindsv4_hint(evt, self = self): + self.hinttext.SetLabel('\n\n\nCertain operating systems will\n' + + 'open IPv4 protocol connections on\n' + + 'an IPv6 socket; others require you\n' + + "to open two sockets on the same\n" + + "port, one IPv4 and one IPv6.") + + def minpeers_hint(evt, self = self): + self.hinttext.SetLabel('\n\n\nThe minimum number of peers the\n' + + 'client tries to stay connected\n' + + 'with. Do not set this higher\n' + + 'unless you have a very fast\n' + + "connection and a lot of system\n" + + "resources.") + + def displayinterval_hint(evt, self = self): + self.hinttext.SetLabel('\n\n\nHow often to update the\n' + + 'graphical display, in 1/1000s\n' + + 'of a second. Setting this too low\n' + + "will strain your computer's\n" + + "processor and video access.") + + def alloctype_hint(evt, self = self): + self.hinttext.SetLabel('\n\nHow to allocate disk space.\n' + + 'normal allocates space as data is\n' + + 'received, background also adds\n' + + "space in the background, pre-\n" + + "allocate reserves up front, and\n" + + 'sparse is only for filesystems\n' + + 'that support it by default.') + + def allocrate_hint(evt, self = self): + self.hinttext.SetLabel('\n\n\nAt what rate to allocate disk\n' + + 'space when allocating in the\n' + + 'background. Set this too high on a\n' + + "slow filesystem and your download\n" + + "will slow to a crawl.") + + def locking_hint(evt, self = self): + self.hinttext.SetLabel('\n\n\n\nFile locking prevents other\n' + + 'programs (including other instances\n' + + 'of BitTorrent) from accessing files\n' + + "you are downloading.") + + def doublecheck_hint(evt, self = self): + self.hinttext.SetLabel('\n\n\nHow much extra checking to do\n' + + 'making sure no data is corrupted.\n' + + 'Double-check mode uses more CPU,\n' + + "while triple-check mode increases\n" + + "disk accesses.") + + def maxfilesopen_hint(evt, self = self): + self.hinttext.SetLabel('\n\n\nThe maximum number of files to\n' + + 'keep open at the same time. Zero\n' + + 'means no limit. Please note that\n' + + "if this option is in effect,\n" + + "files are not guaranteed to be\n" + + "locked.") + + def maxconnections_hint(evt, self = self): + self.hinttext.SetLabel('\n\nSome operating systems, most\n' + + 'notably Windows 9x/ME combined\n' + + 'with certain network drivers,\n' + + "cannot handle more than a certain\n" + + "number of open ports. If the\n" + + "client freezes, try setting this\n" + + "to 60 or below.") + + def superseeder_hint(evt, self = self): + self.hinttext.SetLabel('\n\nThe "super-seed" method allows\n' + + 'a single source to more efficiently\n' + + 'seed a large torrent, but is not\n' + + "necessary in a well-seeded torrent,\n" + + "and causes problems with statistics.\n" + + "Unless you routinely seed torrents\n" + + "you can enable this by selecting\n" + + '"SUPER-SEED" for connection type.\n' + + '(once enabled it does not turn off.)') + + def expirecache_hint(evt, self = self): + self.hinttext.SetLabel('\n\nThe client stores temporary data\n' + + 'in order to handle downloading only\n' + + 'specific files from the torrent and\n' + + "so it can resume downloads more\n" + + "quickly. This sets how long the\n" + + "client will keep this data before\n" + + "deleting it to free disk space.") + + EVT_BUTTON(self.advancedMenuBox, okButton.GetId(), saveConfigs) + EVT_BUTTON(self.advancedMenuBox, cancelButton.GetId(), cancelConfigs) + EVT_BUTTON(self.advancedMenuBox, defaultsButton.GetId(), setDefaults) + EVT_ENTER_WINDOW(self.ip_data, ip_hint) + EVT_ENTER_WINDOW(self.bind_data, bind_hint) + if sys.version_info >= (2,3) and socket.has_ipv6: + EVT_ENTER_WINDOW(self.ipv6bindsv4_data, ipv6bindsv4_hint) + EVT_ENTER_WINDOW(self.minpeers_data, minpeers_hint) + EVT_ENTER_WINDOW(self.displayinterval_data, displayinterval_hint) + EVT_ENTER_WINDOW(self.alloctype_data, alloctype_hint) + EVT_ENTER_WINDOW(self.allocrate_data, allocrate_hint) + EVT_ENTER_WINDOW(self.locking_data, locking_hint) + EVT_ENTER_WINDOW(self.doublecheck_data, doublecheck_hint) + EVT_ENTER_WINDOW(self.maxfilesopen_data, maxfilesopen_hint) + EVT_ENTER_WINDOW(self.maxconnections_data, maxconnections_hint) + EVT_ENTER_WINDOW(self.superseeder_data, superseeder_hint) + EVT_ENTER_WINDOW(self.expirecache_data, expirecache_hint) + + self.advancedMenuBox.Show () + border.Fit(panel) + self.advancedMenuBox.Fit() + except: + self.parent.exception() + + + def CloseAdvanced(self): + if self.advancedMenuBox is not None: + try: + self.advancedMenuBox.Close() + except wxPyDeadObjectError, e: + self.advancedMenuBox = None + diff --git a/BitTornado/ConnChoice.py b/BitTornado/ConnChoice.py new file mode 100644 index 000000000..6a086d5a2 --- /dev/null +++ b/BitTornado/ConnChoice.py @@ -0,0 +1,31 @@ +connChoices=( + {'name':'automatic', + 'rate':{'min':0, 'max':5000, 'def': 0}, + 'conn':{'min':0, 'max':100, 'def': 0}, + 'automatic':1}, + {'name':'unlimited', + 'rate':{'min':0, 'max':5000, 'def': 0, 'div': 50}, + 'conn':{'min':4, 'max':100, 'def': 4}}, + {'name':'dialup/isdn', + 'rate':{'min':3, 'max': 8, 'def': 5}, + 'conn':{'min':2, 'max': 3, 'def': 2}, + 'initiate': 12}, + {'name':'dsl/cable slow', + 'rate':{'min':10, 'max': 48, 'def': 13}, + 'conn':{'min':4, 'max': 20, 'def': 4}}, + {'name':'dsl/cable fast', + 'rate':{'min':20, 'max': 100, 'def': 40}, + 'conn':{'min':4, 'max': 30, 'def': 6}}, + {'name':'T1', + 'rate':{'min':100, 'max': 300, 'def':150}, + 'conn':{'min':4, 'max': 40, 'def':10}}, + {'name':'T3+', + 'rate':{'min':400, 'max':2000, 'def':500}, + 'conn':{'min':4, 'max':100, 'def':20}}, + {'name':'seeder', + 'rate':{'min':0, 'max':5000, 'def':0, 'div': 50}, + 'conn':{'min':1, 'max':100, 'def':1}}, + {'name':'SUPER-SEED', 'super-seed':1} + ) + +connChoiceList = map(lambda x:x['name'], connChoices) diff --git a/BitTornado/CreateIcons.py b/BitTornado/CreateIcons.py new file mode 100644 index 000000000..a61fc3d49 --- /dev/null +++ b/BitTornado/CreateIcons.py @@ -0,0 +1,105 @@ +# Generated from bt_MakeCreateIcons - 05/10/04 22:15:33 +# T-0.3.0 (BitTornado) + +from binascii import a2b_base64 +from zlib import decompress +from os.path import join + +icons = { + "icon_bt.ico": + "eJyt1K+OFEEQx/FaQTh5GDRZhSQpiUHwCrxCBYXFrjyJLXeXEARPsZqUPMm+" + + "AlmP+PGtngoLDji69zMz2zt/qqtr1mxHv7621d4+MnvK/jl66Bl2drV+e7Wz" + + "S/v12A7rY4fDtuvOwfF4tOPXo52/fLLz+WwpWd6nqRXHKXux39sTrtnjNd7g" + + "PW7wGSd860f880kffjvJ2QYS1Zcw4AjcoaA5yRFIFDQXOgKJguZmjkCioB4T" + + "Y2CqxpTXA7sHEgVNEC8RSBQ0gfk7xtknCupgk3EEEgXlNgFHIFHQTMoRSBQ0" + + "E+1ouicKmsk7AomCJiGOQKKgSZIjkChoEucIJAqaZDoCiYImwb4iydULmqQ7" + + "AomC1kLcEQ/jSBQ0i+MIJAqaBXMEElVdi9siOgKJgmZhfWWlVjTddXW/FtsR" + + "SBQ0BeAIJAqaonAEEgVNoTgCiYKmeByBREHaqiVWRtSRrAJzBBIFTdE5AomC" + + "phBPpxPP57dVkDfrTl063nUVnWe383fZx9tb3uN+o7U+BLDtuvcQm8d/27Y/" + + "jO3o5/ay+YPv/+f6y30e1OyB7QcsGWFj", + "icon_done.ico": + "eJyt1K2OVEEQhuEaQbJyMWgyCklSEoPgFvYWKigsduRKbLndhCC4itGk5Erm" + + "Fsh4xMdbfSoMOGDpnuf89Jyf6uqaMdvRr69ttbdPzJ6xf4Eeeo6dXa3vXu/s" + + "0n49tsP62OGw7bpzcDwe7fj1aOcvn+x8PltKlg9pasVxyl7u9/aUe/Z4gxu8" + + "xy0+44Rv/Yp/vujDbxc520Ci+hYGHIF7FDQXOQKJguZGRyBR0DzMEUgU1GNi" + + "DEzVmPJ6YfdAoqAJ4hUCiYImMH/HOPtEQR1sMo5AoqDcJuAIJAqaSTkCiYJm" + + "oh1N90RBM3lHIFHQJMQRSBQ0SXIEEgVN4hyBREGTTEcgUdAk2FckuXpBk3RH" + + "IFHQWoh74mEciYJmcRyBREGzYI5AoqprcVtERyBR0Cysr6zUiqa7rh7WYjsC" + + "iYKmAByBREFTFI5AoqApFEcgUdAUjyOQKEhbtcTKiDqSVWCOQKKgKTpHIFHQ" + + "FOLpdOL9fLcK8nY9qUvHu66i8+x2/i77eHfH77h/0VofAth23Xuoz/+2bX8Y" + + "29HP7WXzB+f/5/7Lcx7V7JHtB9dPG3I=", + "black.ico": + "eJzt1zsOgkAYReFLLCztjJ2UlpLY485kOS7DpbgESwqTcQZDghjxZwAfyfl0" + + "LIieGzUWSom/pan840rHnbSUtPHHX9Je9+tAh2ybNe8TZZ/vk8ajJ4zl6JVJ" + + "+xFx+0R03Djx1/2B8bcT9L/bt0+4Wq+4se8e/VTfMvGqb4n3nYiIGz+lvt9s" + + "9EpE2T4xJN4xNFYWU6t+JWXuXDFzTom7SodSyi/S+iwtwjlJ80KaNY/C34rW" + + "aT8nvK5uhF7ohn7Yqfb87kffLAAAAAAAAAAAAAAAAAAAGMUNy7dADg==", + "blue.ico": + "eJzt10EOwUAYhuGv6cLSTux06QD2dTM9jmM4iiNYdiEZ81cIFTWddtDkfbQW" + + "De8XogtS5h9FIf+81H4jLSSt/ekvaavrdaCDez4SZV+PpPHoicBy9ErSfkQ8" + + "fCI6Hjgx6f7A+McJ+r/t95i46xMP7bf8Uz9o4k0/XMT338voP5shK0MkjXcM" + + "YSqam6Qunatyf7Nk7iztaqk8SaujNLfzIM0qKX88ZX8rWmf7Nfa+W8N61rW+" + + "7TR7fverHxYAAAAAAAAAAAAAAAAAAIziApVZ444=", + "green.ico": + "eJzt1zEOgjAAheFHGBzdjJuMHsAdbybxNB7Do3gERwaT2mJIBCOWlqok/yc4" + + "EP1fNDIoZfZRFLLPa5120krS1p72kvZ6XAeGHLtHouzrkTQePOFZDl5J2g+I" + + "+08Exz0nZt2PjH+coP/bvveEaY2L+/VN13/1PSbe9v0FfP+jTP6ziVmJkTQ+" + + "MISZaO6SujSmyu3dkpmbdKil8iptLtLSnWdpUUn58yn3t6J39l/j3tc2XM91" + + "Xd/tNHt296sfFgAAAAAAAAAAAAAAAAAATOIOVLEoDg==", + "red.ico": + "eJzt10EOwUAYhuGv6cLSTux06QD2dTOO4xiO4giWXUjG/BVCRTuddtDkfbQW" + + "De8XogtS5h9FIf+81GEjLSSt/ekvaavbdaCVez0SZd+PpPHoicBy9ErSfkQ8" + + "fCI6Hjgx6f7AeOcE/d/2QyceesaD+g1/1u+e+NwPF/H99zL6z2bIyhBJ4y1D" + + "mIb6LqlK5/a5v1syd5F2lVSepdVJmtt5lGZ7KX8+ZX8rGmfzNfa+e8N61rW+" + + "7dR7fverHxYAAAAAAAAAAAAAAAAAAIziCpgs444=", + "white.ico": + "eJzt1zsOgkAYReFLKCztjJ2ULsAed6bLcRnuwYTaJVhSmIwzGBLEiD8D+EjO" + + "p2NB9NyosVBK/C3L5B+XOmykhaS1P/6StrpfBzoUp6J5nyj7fJ80Hj1hLEev" + + "TNqPiNsnouPGib/uD4y/naD/3b59wtV6xY199+in+paJV31LvO9ERNz4KfX9" + + "ZqNXIsr2iSHxjqGxspha9Sspc+f2qXNK3FXalVJ+kVZnaR7OUZrtpbR5FP5W" + + "tE77OeF1dSP0Qjf0w06153c/+mYBAAAAAAAAAAAAAAAAAMAobj//I7s=", + "yellow.ico": + "eJzt1zsOgkAYReFLKCztjJ2ULsAedybLcRkuxSVYUpiM82M0ihGHgVFJzidY" + + "ED03vgqlzN+KQv5+qf1GWkha+9Nf0lbX60AX556ORNnXI2k8eiKwHL2StB8R" + + "D5+IjgdOTLo/MP5xgv5v+8ETd/3iYf2W/+oHTLzth4t4/3sZ/WszZGWIpPGO" + + "IUxE8yupS+eq3H9smTtLu1oqT9LqKM3tPEizSsofT9nfitbZfow979awnnWt" + + "bzvNnt/96osFAAAAAAAAAAAAAAAAAACjuABhjmIs", + "black1.ico": + "eJzt0zEOgkAUANEhFpZSGTstTWzkVt5Cj8ZROAIHMNGPWBCFDYgxMZkHn2Iz" + + "G5YCyOLKc+K54XSANbCPiSV2tOt/qjgW3XtSnN41FH/Qv29Jx/P7qefp7W8P" + + "4z85HQ+9JRG/7BpTft31DPUKyiVcFjEZzQ/TTtdzrWnKmCr6evv780qSJEmS" + + "JEmSJEmSJEmSpPnunVFDcA==", + "green1.ico": + "eJzt0zEKwkAQRuEXLCyTSuy0DHgxb6F4shzFI+QAgpkkFoombowIwvt2Z4vh" + + "X5gtFrJYRUGca/Y7WAFlVLTY0vf/1elxTwqP3xoKf5B/vjIenp+fOs+r/LWT" + + "/uQ34aGpUqQnv+1ygDqHagnHRVRG+2H6unfrtZkq6hz5evP7eSVJkiRJkiRJ" + + "kiRJkiRJ0nwNoWQ+AA==", + "yellow1.ico": + "eJzt0zEKwkAQRuEXLCxNJXZaCl7MW8Sj5SgeIQcQ4oS1UDTJxkhAeN/ubDH8" + + "C7PFQhGrLIlzx/kEW+AYFS0OpP6/atuXPSk8fKsv/EX+/cpweH5+6jyf8kn+" + + "k0fCfVPlyE/+2q2CZgP1Gi6rqILuw6R69uh1mTrqGvlmv/y8kiRJkiRJkiRJ" + + "kiRJkiRpvjsp9L8k", + "alloc.gif": + "eJxz93SzsEw0YRBh+M4ABi0MS3ue///P8H8UjIIRBhR/sjAyMDAx6IAyAihP" + + "MHAcYWDlkPHYsOBgM4ewVsyJDQsPNzEoebF8CHjo0smjH3dmRsDjI33C7Dw3" + + "MiYuOtjNyDShRSNwyemJguJJKhaGS32nGka61Vg2NJyYKRd+bY+nwtMzjbqV" + + "Qh84gxMCJgnlL4vJuqJyaa5NfFLNLsNVV2a7syacfVWkHd4bv7RN1ltM7ejm" + + "tMtNZ19Oyb02p8C3aqr3dr2GbXl/7fZyOej5rW653WZ7MzzHZV+v7O2/EZM+" + + "Pt45kbX6ScWHNWfOilo3n5thucXv8org1XF3DRQYrAEWiVY3" +} + +def GetIcons(): + return icons.keys() + +def CreateIcon(icon, savedir): + try: + f = open(join(savedir,icon),"wb") + f.write(decompress(a2b_base64(icons[icon]))) + success = 1 + except: + success = 0 + try: + f.close() + except: + pass + return success diff --git a/BitTornado/CurrentRateMeasure.py b/BitTornado/CurrentRateMeasure.py new file mode 100644 index 000000000..ff828aaa2 --- /dev/null +++ b/BitTornado/CurrentRateMeasure.py @@ -0,0 +1,37 @@ +# Written by Bram Cohen +# see LICENSE.txt for license information + +from clock import clock + +class Measure: + def __init__(self, max_rate_period, fudge = 1): + self.max_rate_period = max_rate_period + self.ratesince = clock() - fudge + self.last = self.ratesince + self.rate = 0.0 + self.total = 0l + + def update_rate(self, amount): + self.total += amount + t = clock() + self.rate = (self.rate * (self.last - self.ratesince) + + amount) / (t - self.ratesince + 0.0001) + self.last = t + if self.ratesince < t - self.max_rate_period: + self.ratesince = t - self.max_rate_period + + def get_rate(self): + self.update_rate(0) + return self.rate + + def get_rate_noupdate(self): + return self.rate + + def time_until_rate(self, newrate): + if self.rate <= newrate: + return 0 + t = clock() - self.ratesince + return ((self.rate * t) / newrate) - t + + def get_total(self): + return self.total \ No newline at end of file diff --git a/BitTornado/HTTPHandler.py b/BitTornado/HTTPHandler.py new file mode 100644 index 000000000..0aa72cd1a --- /dev/null +++ b/BitTornado/HTTPHandler.py @@ -0,0 +1,167 @@ +# Written by Bram Cohen +# see LICENSE.txt for license information + +from cStringIO import StringIO +from sys import stdout +import time +from clock import clock +from gzip import GzipFile +try: + True +except: + True = 1 + False = 0 + +DEBUG = False + +weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + +months = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + +class HTTPConnection: + def __init__(self, handler, connection): + self.handler = handler + self.connection = connection + self.buf = '' + self.closed = False + self.done = False + self.donereading = False + self.next_func = self.read_type + + def get_ip(self): + return self.connection.get_ip() + + def data_came_in(self, data): + if self.donereading or self.next_func is None: + return True + self.buf += data + while True: + try: + i = self.buf.index('\n') + except ValueError: + return True + val = self.buf[:i] + self.buf = self.buf[i+1:] + self.next_func = self.next_func(val) + if self.donereading: + return True + if self.next_func is None or self.closed: + return False + + def read_type(self, data): + self.header = data.strip() + words = data.split() + if len(words) == 3: + self.command, self.path, garbage = words + self.pre1 = False + elif len(words) == 2: + self.command, self.path = words + self.pre1 = True + if self.command != 'GET': + return None + else: + return None + if self.command not in ('HEAD', 'GET'): + return None + self.headers = {} + return self.read_header + + def read_header(self, data): + data = data.strip() + if data == '': + self.donereading = True + if self.headers.get('accept-encoding','').find('gzip') > -1: + self.encoding = 'gzip' + else: + self.encoding = 'identity' + r = self.handler.getfunc(self, self.path, self.headers) + if r is not None: + self.answer(r) + return None + try: + i = data.index(':') + except ValueError: + return None + self.headers[data[:i].strip().lower()] = data[i+1:].strip() + if DEBUG: + print data[:i].strip() + ": " + data[i+1:].strip() + return self.read_header + + def answer(self, (responsecode, responsestring, headers, data)): + if self.closed: + return + if self.encoding == 'gzip': + compressed = StringIO() + gz = GzipFile(fileobj = compressed, mode = 'wb', compresslevel = 9) + gz.write(data) + gz.close() + cdata = compressed.getvalue() + if len(cdata) >= len(data): + self.encoding = 'identity' + else: + if DEBUG: + print "Compressed: %i Uncompressed: %i\n" % (len(cdata),len(data)) + data = cdata + headers['Content-Encoding'] = 'gzip' + + # i'm abusing the identd field here, but this should be ok + if self.encoding == 'identity': + ident = '-' + else: + ident = self.encoding + self.handler.log( self.connection.get_ip(), ident, '-', + self.header, responsecode, len(data), + self.headers.get('referer','-'), + self.headers.get('user-agent','-') ) + self.done = True + r = StringIO() + r.write('HTTP/1.0 ' + str(responsecode) + ' ' + + responsestring + '\r\n') + if not self.pre1: + headers['Content-Length'] = len(data) + for key, value in headers.items(): + r.write(key + ': ' + str(value) + '\r\n') + r.write('\r\n') + if self.command != 'HEAD': + r.write(data) + self.connection.write(r.getvalue()) + if self.connection.is_flushed(): + self.connection.shutdown(1) + +class HTTPHandler: + def __init__(self, getfunc, minflush): + self.connections = {} + self.getfunc = getfunc + self.minflush = minflush + self.lastflush = clock() + + def external_connection_made(self, connection): + self.connections[connection] = HTTPConnection(self, connection) + + def connection_flushed(self, connection): + if self.connections[connection].done: + connection.shutdown(1) + + def connection_lost(self, connection): + ec = self.connections[connection] + ec.closed = True + del ec.connection + del ec.next_func + del self.connections[connection] + + def data_came_in(self, connection, data): + c = self.connections[connection] + if not c.data_came_in(data) and not c.closed: + c.connection.shutdown(1) + + def log(self, ip, ident, username, header, + responsecode, length, referrer, useragent): + year, month, day, hour, minute, second, a, b, c = time.localtime(time.time()) + print '%s %s %s [%02d/%3s/%04d:%02d:%02d:%02d] "%s" %i %i "%s" "%s"' % ( + ip, ident, username, day, months[month], year, hour, + minute, second, header, responsecode, length, referrer, useragent) + t = clock() + if t - self.lastflush > self.minflush: + self.lastflush = t + stdout.flush() diff --git a/BitTornado/PSYCO.py b/BitTornado/PSYCO.py new file mode 100644 index 000000000..36c7bd997 --- /dev/null +++ b/BitTornado/PSYCO.py @@ -0,0 +1,5 @@ +# edit this file to enable/disable Psyco +# psyco = 1 -- enabled +# psyco = 0 -- disabled + +psyco = 0 diff --git a/BitTornado/RateLimiter.py b/BitTornado/RateLimiter.py new file mode 100644 index 000000000..94cd9bfd8 --- /dev/null +++ b/BitTornado/RateLimiter.py @@ -0,0 +1,153 @@ +# Written by Bram Cohen +# see LICENSE.txt for license information + +from traceback import print_exc +from binascii import b2a_hex +from clock import clock +from CurrentRateMeasure import Measure +from cStringIO import StringIO +from math import sqrt + +try: + True +except: + True = 1 + False = 0 +try: + sum([1]) +except: + sum = lambda a: reduce(lambda x,y: x+y, a, 0) + +DEBUG = False + +MAX_RATE_PERIOD = 20.0 +MAX_RATE = 10e10 +PING_BOUNDARY = 1.2 +PING_SAMPLES = 7 +PING_DISCARDS = 1 +PING_THRESHHOLD = 5 +PING_DELAY = 5 # cycles 'til first upward adjustment +PING_DELAY_NEXT = 2 # 'til next +ADJUST_UP = 1.05 +ADJUST_DOWN = 0.95 +UP_DELAY_FIRST = 5 +UP_DELAY_NEXT = 2 +SLOTS_STARTING = 6 +SLOTS_FACTOR = 1.66/1000 + +class RateLimiter: + def __init__(self, sched, unitsize, slotsfunc = lambda x: None): + self.sched = sched + self.last = None + self.unitsize = unitsize + self.slotsfunc = slotsfunc + self.measure = Measure(MAX_RATE_PERIOD) + self.autoadjust = False + self.upload_rate = MAX_RATE * 1000 + self.slots = SLOTS_STARTING # garbage if not automatic + + def set_upload_rate(self, rate): + # rate = -1 # test automatic + if rate < 0: + if self.autoadjust: + return + self.autoadjust = True + self.autoadjustup = 0 + self.pings = [] + rate = MAX_RATE + self.slots = SLOTS_STARTING + self.slotsfunc(self.slots) + else: + self.autoadjust = False + if not rate: + rate = MAX_RATE + self.upload_rate = rate * 1000 + self.lasttime = clock() + self.bytes_sent = 0 + + def queue(self, conn): + assert conn.next_upload is None + if self.last is None: + self.last = conn + conn.next_upload = conn + self.try_send(True) + else: + conn.next_upload = self.last.next_upload + self.last.next_upload = conn + self.last = conn + + def try_send(self, check_time = False): + t = clock() + self.bytes_sent -= (t - self.lasttime) * self.upload_rate + self.lasttime = t + if check_time: + self.bytes_sent = max(self.bytes_sent, 0) + cur = self.last.next_upload + while self.bytes_sent <= 0: + bytes = cur.send_partial(self.unitsize) + self.bytes_sent += bytes + self.measure.update_rate(bytes) + if bytes == 0 or cur.backlogged(): + if self.last is cur: + self.last = None + cur.next_upload = None + break + else: + self.last.next_upload = cur.next_upload + cur.next_upload = None + cur = self.last.next_upload + else: + self.last = cur + cur = cur.next_upload + else: + self.sched(self.try_send, self.bytes_sent / self.upload_rate) + + def adjust_sent(self, bytes): + self.bytes_sent = min(self.bytes_sent+bytes, self.upload_rate*3) + self.measure.update_rate(bytes) + + + def ping(self, delay): + if DEBUG: + print delay + if not self.autoadjust: + return + self.pings.append(delay > PING_BOUNDARY) + if len(self.pings) < PING_SAMPLES+PING_DISCARDS: + return + if DEBUG: + print 'cycle' + pings = sum(self.pings[PING_DISCARDS:]) + del self.pings[:] + if pings >= PING_THRESHHOLD: # assume flooded + if self.upload_rate == MAX_RATE: + self.upload_rate = self.measure.get_rate()*ADJUST_DOWN + else: + self.upload_rate = min(self.upload_rate, + self.measure.get_rate()*1.1) + self.upload_rate = max(int(self.upload_rate*ADJUST_DOWN),2) + self.slots = int(sqrt(self.upload_rate*SLOTS_FACTOR)) + self.slotsfunc(self.slots) + if DEBUG: + print 'adjust down to '+str(self.upload_rate) + self.lasttime = clock() + self.bytes_sent = 0 + self.autoadjustup = UP_DELAY_FIRST + else: # not flooded + if self.upload_rate == MAX_RATE: + return + self.autoadjustup -= 1 + if self.autoadjustup: + return + self.upload_rate = int(self.upload_rate*ADJUST_UP) + self.slots = int(sqrt(self.upload_rate*SLOTS_FACTOR)) + self.slotsfunc(self.slots) + if DEBUG: + print 'adjust up to '+str(self.upload_rate) + self.lasttime = clock() + self.bytes_sent = 0 + self.autoadjustup = UP_DELAY_NEXT + + + + diff --git a/BitTornado/RateMeasure.py b/BitTornado/RateMeasure.py new file mode 100644 index 000000000..d1f8e37ac --- /dev/null +++ b/BitTornado/RateMeasure.py @@ -0,0 +1,75 @@ +# Written by Bram Cohen +# see LICENSE.txt for license information + +from clock import clock +try: + True +except: + True = 1 + False = 0 + +FACTOR = 0.999 + +class RateMeasure: + def __init__(self): + self.last = None + self.time = 1.0 + self.got = 0.0 + self.remaining = None + self.broke = False + self.got_anything = False + self.last_checked = None + self.rate = 0 + self.lastten = False + + def data_came_in(self, amount): + if not self.got_anything: + self.got_anything = True + self.last = clock() + return + self.update(amount) + + def data_rejected(self, amount): + pass + + def get_time_left(self, left): + t = clock() + if not self.got_anything: + return None + if t - self.last > 15: + self.update(0) + try: + remaining = left/self.rate + if not self.lastten and remaining <= 10: + self.lastten = True + if self.lastten: + return remaining + delta = max(remaining/20,2) + if self.remaining is None: + self.remaining = remaining + elif abs(self.remaining-remaining) > delta: + self.remaining = remaining + else: + self.remaining -= t - self.last_checked + except ZeroDivisionError: + self.remaining = None + if self.remaining is not None and self.remaining < 0.1: + self.remaining = 0.1 + self.last_checked = t + return self.remaining + + def update(self, amount): + t = clock() + t1 = int(t) + l1 = int(self.last) + for i in xrange(l1,t1): + self.time *= FACTOR + self.got *= FACTOR + self.got += amount + if t - self.last < 20: + self.time += t - self.last + self.last = t + try: + self.rate = self.got / self.time + except ZeroDivisionError: + pass diff --git a/BitTornado/RawServer.py b/BitTornado/RawServer.py new file mode 100644 index 000000000..bbf1cb1df --- /dev/null +++ b/BitTornado/RawServer.py @@ -0,0 +1,195 @@ +# Written by Bram Cohen +# see LICENSE.txt for license information + +from bisect import insort +from SocketHandler import SocketHandler, UPnP_ERROR +import socket +from cStringIO import StringIO +from traceback import print_exc +from select import error +from threading import Thread, Event +from time import sleep +from clock import clock +import sys +try: + True +except: + True = 1 + False = 0 + + +def autodetect_ipv6(): + try: + assert sys.version_info >= (2,3) + assert socket.has_ipv6 + socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + except: + return 0 + return 1 + +def autodetect_socket_style(): + if sys.platform.find('linux') < 0: + return 1 + else: + try: + f = open('/proc/sys/net/ipv6/bindv6only','r') + dual_socket_style = int(f.read()) + f.close() + return int(not dual_socket_style) + except: + return 0 + + +READSIZE = 100000 + +class RawServer: + def __init__(self, doneflag, timeout_check_interval, timeout, noisy = True, + ipv6_enable = True, failfunc = lambda x: None, errorfunc = None, + sockethandler = None, excflag = Event()): + self.timeout_check_interval = timeout_check_interval + self.timeout = timeout + self.servers = {} + self.single_sockets = {} + self.dead_from_write = [] + self.doneflag = doneflag + self.noisy = noisy + self.failfunc = failfunc + self.errorfunc = errorfunc + self.exccount = 0 + self.funcs = [] + self.externally_added = [] + self.finished = Event() + self.tasks_to_kill = [] + self.excflag = excflag + + if sockethandler is None: + sockethandler = SocketHandler(timeout, ipv6_enable, READSIZE) + self.sockethandler = sockethandler + self.add_task(self.scan_for_timeouts, timeout_check_interval) + + def get_exception_flag(self): + return self.excflag + + def _add_task(self, func, delay, id = None): + assert float(delay) >= 0 + insort(self.funcs, (clock() + delay, func, id)) + + def add_task(self, func, delay = 0, id = None): + assert float(delay) >= 0 + self.externally_added.append((func, delay, id)) + + def scan_for_timeouts(self): + self.add_task(self.scan_for_timeouts, self.timeout_check_interval) + self.sockethandler.scan_for_timeouts() + + def bind(self, port, bind = '', reuse = False, + ipv6_socket_style = 1, upnp = False): + self.sockethandler.bind(port, bind, reuse, ipv6_socket_style, upnp) + + def find_and_bind(self, minport, maxport, bind = '', reuse = False, + ipv6_socket_style = 1, upnp = 0, randomizer = False): + return self.sockethandler.find_and_bind(minport, maxport, bind, reuse, + ipv6_socket_style, upnp, randomizer) + + def start_connection_raw(self, dns, socktype, handler = None): + return self.sockethandler.start_connection_raw(dns, socktype, handler) + + def start_connection(self, dns, handler = None, randomize = False): + return self.sockethandler.start_connection(dns, handler, randomize) + + def get_stats(self): + return self.sockethandler.get_stats() + + def pop_external(self): + while self.externally_added: + (a, b, c) = self.externally_added.pop(0) + self._add_task(a, b, c) + + + def listen_forever(self, handler): + self.sockethandler.set_handler(handler) + try: + while not self.doneflag.isSet(): + try: + self.pop_external() + self._kill_tasks() + if self.funcs: + period = self.funcs[0][0] + 0.001 - clock() + else: + period = 2 ** 30 + if period < 0: + period = 0 + events = self.sockethandler.do_poll(period) + if self.doneflag.isSet(): + return + while self.funcs and self.funcs[0][0] <= clock(): + garbage1, func, id = self.funcs.pop(0) + if id in self.tasks_to_kill: + pass + try: +# print func.func_name + func() + except (SystemError, MemoryError), e: + self.failfunc(str(e)) + return + except KeyboardInterrupt: +# self.exception(True) + return + except: + if self.noisy: + self.exception() + self.sockethandler.close_dead() + self.sockethandler.handle_events(events) + if self.doneflag.isSet(): + return + self.sockethandler.close_dead() + except (SystemError, MemoryError), e: + self.failfunc(str(e)) + return + except error: + if self.doneflag.isSet(): + return + except KeyboardInterrupt: +# self.exception(True) + return + except: + self.exception() + if self.exccount > 10: + return + finally: +# self.sockethandler.shutdown() + self.finished.set() + + def is_finished(self): + return self.finished.isSet() + + def wait_until_finished(self): + self.finished.wait() + + def _kill_tasks(self): + if self.tasks_to_kill: + new_funcs = [] + for (t, func, id) in self.funcs: + if id not in self.tasks_to_kill: + new_funcs.append((t, func, id)) + self.funcs = new_funcs + self.tasks_to_kill = [] + + def kill_tasks(self, id): + self.tasks_to_kill.append(id) + + def exception(self, kbint = False): + if not kbint: + self.excflag.set() + self.exccount += 1 + if self.errorfunc is None: + print_exc() + else: + data = StringIO() + print_exc(file = data) +# print data.getvalue() # report exception here too + if not kbint: # don't report here if it's a keyboard interrupt + self.errorfunc(data.getvalue()) + + def shutdown(self): + self.sockethandler.shutdown() diff --git a/BitTornado/ServerPortHandler.py b/BitTornado/ServerPortHandler.py new file mode 100644 index 000000000..90d42b5a7 --- /dev/null +++ b/BitTornado/ServerPortHandler.py @@ -0,0 +1,188 @@ +# Written by John Hoffman +# see LICENSE.txt for license information + +from cStringIO import StringIO +#from RawServer import RawServer +try: + True +except: + True = 1 + False = 0 + +from BT1.Encrypter import protocol_name + +default_task_id = [] + +class SingleRawServer: + def __init__(self, info_hash, multihandler, doneflag, protocol): + self.info_hash = info_hash + self.doneflag = doneflag + self.protocol = protocol + self.multihandler = multihandler + self.rawserver = multihandler.rawserver + self.finished = False + self.running = False + self.handler = None + self.taskqueue = [] + + def shutdown(self): + if not self.finished: + self.multihandler.shutdown_torrent(self.info_hash) + + def _shutdown(self): + if not self.finished: + self.finished = True + self.running = False + self.rawserver.kill_tasks(self.info_hash) + if self.handler: + self.handler.close_all() + + def _external_connection_made(self, c, options, already_read): + if self.running: + c.set_handler(self.handler) + self.handler.externally_handshaked_connection_made( + c, options, already_read) + + ### RawServer functions ### + + def add_task(self, func, delay=0, id = default_task_id): + if id is default_task_id: + id = self.info_hash + if not self.finished: + self.rawserver.add_task(func, delay, id) + +# def bind(self, port, bind = '', reuse = False): +# pass # not handled here + + def start_connection(self, dns, handler = None): + if not handler: + handler = self.handler + c = self.rawserver.start_connection(dns, handler) + return c + +# def listen_forever(self, handler): +# pass # don't call with this + + def start_listening(self, handler): + self.handler = handler + self.running = True + return self.shutdown # obviously, doesn't listen forever + + def is_finished(self): + return self.finished + + def get_exception_flag(self): + return self.rawserver.get_exception_flag() + + +class NewSocketHandler: # hand a new socket off where it belongs + def __init__(self, multihandler, connection): + self.multihandler = multihandler + self.connection = connection + connection.set_handler(self) + self.closed = False + self.buffer = StringIO() + self.complete = False + self.next_len, self.next_func = 1, self.read_header_len + self.multihandler.rawserver.add_task(self._auto_close, 15) + + def _auto_close(self): + if not self.complete: + self.close() + + def close(self): + if not self.closed: + self.connection.close() + self.closed = True + + +# header format: +# connection.write(chr(len(protocol_name)) + protocol_name + +# (chr(0) * 8) + self.encrypter.download_id + self.encrypter.my_id) + + # copied from Encrypter and modified + + def read_header_len(self, s): + l = ord(s) + return l, self.read_header + + def read_header(self, s): + self.protocol = s + return 8, self.read_reserved + + def read_reserved(self, s): + self.options = s + return 20, self.read_download_id + + def read_download_id(self, s): + if self.multihandler.singlerawservers.has_key(s): + if self.multihandler.singlerawservers[s].protocol == self.protocol: + return True + return None + + def read_dead(self, s): + return None + + def data_came_in(self, garbage, s): + while True: + if self.closed: + return + i = self.next_len - self.buffer.tell() + if i > len(s): + self.buffer.write(s) + return + self.buffer.write(s[:i]) + s = s[i:] + m = self.buffer.getvalue() + self.buffer.reset() + self.buffer.truncate() + try: + x = self.next_func(m) + except: + self.next_len, self.next_func = 1, self.read_dead + raise + if x is None: + self.close() + return + if x == True: # ready to process + self.multihandler.singlerawservers[m]._external_connection_made( + self.connection, self.options, s) + self.complete = True + return + self.next_len, self.next_func = x + + def connection_flushed(self, ss): + pass + + def connection_lost(self, ss): + self.closed = True + +class MultiHandler: + def __init__(self, rawserver, doneflag): + self.rawserver = rawserver + self.masterdoneflag = doneflag + self.singlerawservers = {} + self.connections = {} + self.taskqueues = {} + + def newRawServer(self, info_hash, doneflag, protocol=protocol_name): + new = SingleRawServer(info_hash, self, doneflag, protocol) + self.singlerawservers[info_hash] = new + return new + + def shutdown_torrent(self, info_hash): + self.singlerawservers[info_hash]._shutdown() + del self.singlerawservers[info_hash] + + def listen_forever(self): + self.rawserver.listen_forever(self) + for srs in self.singlerawservers.values(): + srs.finished = True + srs.running = False + srs.doneflag.set() + + ### RawServer handler functions ### + # be wary of name collisions + + def external_connection_made(self, ss): + NewSocketHandler(self, ss) diff --git a/BitTornado/SocketHandler.py b/BitTornado/SocketHandler.py new file mode 100644 index 000000000..7a4372d7d --- /dev/null +++ b/BitTornado/SocketHandler.py @@ -0,0 +1,375 @@ +# Written by Bram Cohen +# see LICENSE.txt for license information + +import socket +from errno import EWOULDBLOCK, ECONNREFUSED, EHOSTUNREACH +try: + from select import poll, error, POLLIN, POLLOUT, POLLERR, POLLHUP + timemult = 1000 +except ImportError: + from selectpoll import poll, error, POLLIN, POLLOUT, POLLERR, POLLHUP + timemult = 1 +from time import sleep +from clock import clock +import sys +from random import shuffle, randrange +from natpunch import UPnP_open_port, UPnP_close_port +# from BT1.StreamCheck import StreamCheck +# import inspect +try: + True +except: + True = 1 + False = 0 + +all = POLLIN | POLLOUT + +UPnP_ERROR = "unable to forward port via UPnP" + +class SingleSocket: + def __init__(self, socket_handler, sock, handler, ip = None): + self.socket_handler = socket_handler + self.socket = sock + self.handler = handler + self.buffer = [] + self.last_hit = clock() + self.fileno = sock.fileno() + self.connected = False + self.skipped = 0 +# self.check = StreamCheck() + try: + self.ip = self.socket.getpeername()[0] + except: + if ip is None: + self.ip = 'unknown' + else: + self.ip = ip + + def get_ip(self, real=False): + if real: + try: + self.ip = self.socket.getpeername()[0] + except: + pass + return self.ip + + def close(self): + ''' + for x in xrange(5,0,-1): + try: + f = inspect.currentframe(x).f_code + print (f.co_filename,f.co_firstlineno,f.co_name) + del f + except: + pass + print '' + ''' + assert self.socket + self.connected = False + sock = self.socket + self.socket = None + self.buffer = [] + del self.socket_handler.single_sockets[self.fileno] + self.socket_handler.poll.unregister(sock) + sock.close() + + def shutdown(self, val): + self.socket.shutdown(val) + + def is_flushed(self): + return not self.buffer + + def write(self, s): +# self.check.write(s) + assert self.socket is not None + self.buffer.append(s) + if len(self.buffer) == 1: + self.try_write() + + def try_write(self): + if self.connected: + dead = False + try: + while self.buffer: + buf = self.buffer[0] + amount = self.socket.send(buf) + if amount == 0: + self.skipped += 1 + break + self.skipped = 0 + if amount != len(buf): + self.buffer[0] = buf[amount:] + break + del self.buffer[0] + except socket.error, e: + try: + dead = e[0] != EWOULDBLOCK + except: + dead = True + self.skipped += 1 + if self.skipped >= 3: + dead = True + if dead: + self.socket_handler.dead_from_write.append(self) + return + if self.buffer: + self.socket_handler.poll.register(self.socket, all) + else: + self.socket_handler.poll.register(self.socket, POLLIN) + + def set_handler(self, handler): + self.handler = handler + +class SocketHandler: + def __init__(self, timeout, ipv6_enable, readsize = 100000): + self.timeout = timeout + self.ipv6_enable = ipv6_enable + self.readsize = readsize + self.poll = poll() + # {socket: SingleSocket} + self.single_sockets = {} + self.dead_from_write = [] + self.max_connects = 1000 + self.port_forwarded = None + self.servers = {} + + def scan_for_timeouts(self): + t = clock() - self.timeout + tokill = [] + for s in self.single_sockets.values(): + if s.last_hit < t: + tokill.append(s) + for k in tokill: + if k.socket is not None: + self._close_socket(k) + + def bind(self, port, bind = '', reuse = False, ipv6_socket_style = 1, upnp = 0): + port = int(port) + addrinfos = [] + self.servers = {} + self.interfaces = [] + # if bind != "" thread it as a comma seperated list and bind to all + # addresses (can be ips or hostnames) else bind to default ipv6 and + # ipv4 address + if bind: + if self.ipv6_enable: + socktype = socket.AF_UNSPEC + else: + socktype = socket.AF_INET + bind = bind.split(',') + for addr in bind: + if sys.version_info < (2,2): + addrinfos.append((socket.AF_INET, None, None, None, (addr, port))) + else: + addrinfos.extend(socket.getaddrinfo(addr, port, + socktype, socket.SOCK_STREAM)) + else: + if self.ipv6_enable: + addrinfos.append([socket.AF_INET6, None, None, None, ('', port)]) + if not addrinfos or ipv6_socket_style != 0: + addrinfos.append([socket.AF_INET, None, None, None, ('', port)]) + for addrinfo in addrinfos: + try: + server = socket.socket(addrinfo[0], socket.SOCK_STREAM) + if reuse: + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.setblocking(0) + server.bind(addrinfo[4]) + self.servers[server.fileno()] = server + if bind: + self.interfaces.append(server.getsockname()[0]) + server.listen(64) + self.poll.register(server, POLLIN) + except socket.error, e: + for server in self.servers.values(): + try: + server.close() + except: + pass + if self.ipv6_enable and ipv6_socket_style == 0 and self.servers: + raise socket.error('blocked port (may require ipv6_binds_v4 to be set)') + raise socket.error(str(e)) + if not self.servers: + raise socket.error('unable to open server port') + if upnp: + if not UPnP_open_port(port): + for server in self.servers.values(): + try: + server.close() + except: + pass + self.servers = None + self.interfaces = None + raise socket.error(UPnP_ERROR) + self.port_forwarded = port + self.port = port + + def find_and_bind(self, minport, maxport, bind = '', reuse = False, + ipv6_socket_style = 1, upnp = 0, randomizer = False): + e = 'maxport less than minport - no ports to check' + if maxport-minport < 50 or not randomizer: + portrange = range(minport, maxport+1) + if randomizer: + shuffle(portrange) + portrange = portrange[:20] # check a maximum of 20 ports + else: + portrange = [] + while len(portrange) < 20: + listen_port = randrange(minport, maxport+1) + if not listen_port in portrange: + portrange.append(listen_port) + for listen_port in portrange: + try: + self.bind(listen_port, bind, + ipv6_socket_style = ipv6_socket_style, upnp = upnp) + return listen_port + except socket.error, e: + pass + raise socket.error(str(e)) + + + def set_handler(self, handler): + self.handler = handler + + + def start_connection_raw(self, dns, socktype = socket.AF_INET, handler = None): + if handler is None: + handler = self.handler + sock = socket.socket(socktype, socket.SOCK_STREAM) + sock.setblocking(0) + try: + sock.connect_ex(dns) + except socket.error: + raise + except Exception, e: + raise socket.error(str(e)) + self.poll.register(sock, POLLIN) + s = SingleSocket(self, sock, handler, dns[0]) + self.single_sockets[sock.fileno()] = s + return s + + + def start_connection(self, dns, handler = None, randomize = False): + if handler is None: + handler = self.handler + if sys.version_info < (2,2): + s = self.start_connection_raw(dns,socket.AF_INET,handler) + else: + if self.ipv6_enable: + socktype = socket.AF_UNSPEC + else: + socktype = socket.AF_INET + try: + addrinfos = socket.getaddrinfo(dns[0], int(dns[1]), + socktype, socket.SOCK_STREAM) + except socket.error, e: + raise + except Exception, e: + raise socket.error(str(e)) + if randomize: + shuffle(addrinfos) + for addrinfo in addrinfos: + try: + s = self.start_connection_raw(addrinfo[4],addrinfo[0],handler) + break + except: + pass + else: + raise socket.error('unable to connect') + return s + + + def _sleep(self): + sleep(1) + + def handle_events(self, events): + for sock, event in events: + s = self.servers.get(sock) + if s: + if event & (POLLHUP | POLLERR) != 0: + self.poll.unregister(s) + s.close() + del self.servers[sock] + print "lost server socket" + elif len(self.single_sockets) < self.max_connects: + try: + newsock, addr = s.accept() + newsock.setblocking(0) + nss = SingleSocket(self, newsock, self.handler) + self.single_sockets[newsock.fileno()] = nss + self.poll.register(newsock, POLLIN) + self.handler.external_connection_made(nss) + except socket.error: + self._sleep() + else: + s = self.single_sockets.get(sock) + if not s: + continue + s.connected = True + if (event & (POLLHUP | POLLERR)): + self._close_socket(s) + continue + if (event & POLLIN): + try: + s.last_hit = clock() + data = s.socket.recv(100000) + if not data: + self._close_socket(s) + else: + s.handler.data_came_in(s, data) + except socket.error, e: + code, msg = e + if code != EWOULDBLOCK: + self._close_socket(s) + continue + if (event & POLLOUT) and s.socket and not s.is_flushed(): + s.try_write() + if s.is_flushed(): + s.handler.connection_flushed(s) + + def close_dead(self): + while self.dead_from_write: + old = self.dead_from_write + self.dead_from_write = [] + for s in old: + if s.socket: + self._close_socket(s) + + def _close_socket(self, s): + s.close() + s.handler.connection_lost(s) + + def do_poll(self, t): + r = self.poll.poll(t*timemult) + if r is None: + connects = len(self.single_sockets) + to_close = int(connects*0.05)+1 # close 5% of sockets + self.max_connects = connects-to_close + closelist = self.single_sockets.values() + shuffle(closelist) + closelist = closelist[:to_close] + for sock in closelist: + self._close_socket(sock) + return [] + return r + + def get_stats(self): + return { 'interfaces': self.interfaces, + 'port': self.port, + 'upnp': self.port_forwarded is not None } + + + def shutdown(self): + for ss in self.single_sockets.values(): + try: + ss.close() + except: + pass + for server in self.servers.values(): + try: + server.close() + except: + pass + if self.port_forwarded is not None: + UPnP_close_port(self.port_forwarded) + diff --git a/BitTornado/__init__.py b/BitTornado/__init__.py new file mode 100644 index 000000000..0802f02ac --- /dev/null +++ b/BitTornado/__init__.py @@ -0,0 +1,63 @@ +product_name = 'BitTornado' +version_short = 'T-0.3.17' + +version = version_short+' ('+product_name+')' +report_email = version_short+'@degreez.net' + +from types import StringType +from sha import sha +from time import time, clock +try: + from os import getpid +except ImportError: + def getpid(): + return 1 + +mapbase64 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.-' + +_idprefix = version_short[0] +for subver in version_short[2:].split('.'): + try: + subver = int(subver) + except: + subver = 0 + _idprefix += mapbase64[subver] +_idprefix += ('-' * (6-len(_idprefix))) +_idrandom = [None] + +def resetPeerIDs(): + try: + f = open('/dev/urandom','rb') + x = f.read(20) + f.close() + except: + x = '' + + l1 = 0 + t = clock() + while t == clock(): + l1 += 1 + l2 = 0 + t = long(time()*100) + while t == long(time()*100): + l2 += 1 + l3 = 0 + if l2 < 1000: + t = long(time()*10) + while t == long(clock()*10): + l3 += 1 + x += ( repr(time()) + '/' + str(time()) + '/' + + str(l1) + '/' + str(l2) + '/' + str(l3) + '/' + + str(getpid()) ) + + s = '' + for i in sha(x).digest()[-11:]: + s += mapbase64[ord(i) & 0x3F] + _idrandom[0] = s + +resetPeerIDs() + +def createPeerID(ins = '---'): + assert type(ins) is StringType + assert len(ins) == 3 + return _idprefix + ins + _idrandom[0] diff --git a/BitTornado/bencode.py b/BitTornado/bencode.py new file mode 100644 index 000000000..b4f9bb85c --- /dev/null +++ b/BitTornado/bencode.py @@ -0,0 +1,319 @@ +# Written by Petru Paler, Uoti Urpala, Ross Cohen and John Hoffman +# see LICENSE.txt for license information + +from types import IntType, LongType, StringType, ListType, TupleType, DictType +try: + from types import BooleanType +except ImportError: + BooleanType = None +try: + from types import UnicodeType +except ImportError: + UnicodeType = None +from cStringIO import StringIO + +def decode_int(x, f): + f += 1 + newf = x.index('e', f) + try: + n = int(x[f:newf]) + except: + n = long(x[f:newf]) + if x[f] == '-': + if x[f + 1] == '0': + raise ValueError + elif x[f] == '0' and newf != f+1: + raise ValueError + return (n, newf+1) + +def decode_string(x, f): + colon = x.index(':', f) + try: + n = int(x[f:colon]) + except (OverflowError, ValueError): + n = long(x[f:colon]) + if x[f] == '0' and colon != f+1: + raise ValueError + colon += 1 + return (x[colon:colon+n], colon+n) + +def decode_unicode(x, f): + s, f = decode_string(x, f+1) + return (s.decode('UTF-8'),f) + +def decode_list(x, f): + r, f = [], f+1 + while x[f] != 'e': + v, f = decode_func[x[f]](x, f) + r.append(v) + return (r, f + 1) + +def decode_dict(x, f): + r, f = {}, f+1 + lastkey = None + while x[f] != 'e': + k, f = decode_string(x, f) + if lastkey >= k: + raise ValueError + lastkey = k + r[k], f = decode_func[x[f]](x, f) + return (r, f + 1) + +decode_func = {} +decode_func['l'] = decode_list +decode_func['d'] = decode_dict +decode_func['i'] = decode_int +decode_func['0'] = decode_string +decode_func['1'] = decode_string +decode_func['2'] = decode_string +decode_func['3'] = decode_string +decode_func['4'] = decode_string +decode_func['5'] = decode_string +decode_func['6'] = decode_string +decode_func['7'] = decode_string +decode_func['8'] = decode_string +decode_func['9'] = decode_string +#decode_func['u'] = decode_unicode + +def bdecode(x, sloppy = 0): + try: + r, l = decode_func[x[0]](x, 0) +# except (IndexError, KeyError): + except (IndexError, KeyError, ValueError): + raise ValueError, "bad bencoded data" + if not sloppy and l != len(x): + raise ValueError, "bad bencoded data" + return r + +def test_bdecode(): + try: + bdecode('0:0:') + assert 0 + except ValueError: + pass + try: + bdecode('ie') + assert 0 + except ValueError: + pass + try: + bdecode('i341foo382e') + assert 0 + except ValueError: + pass + assert bdecode('i4e') == 4L + assert bdecode('i0e') == 0L + assert bdecode('i123456789e') == 123456789L + assert bdecode('i-10e') == -10L + try: + bdecode('i-0e') + assert 0 + except ValueError: + pass + try: + bdecode('i123') + assert 0 + except ValueError: + pass + try: + bdecode('') + assert 0 + except ValueError: + pass + try: + bdecode('i6easd') + assert 0 + except ValueError: + pass + try: + bdecode('35208734823ljdahflajhdf') + assert 0 + except ValueError: + pass + try: + bdecode('2:abfdjslhfld') + assert 0 + except ValueError: + pass + assert bdecode('0:') == '' + assert bdecode('3:abc') == 'abc' + assert bdecode('10:1234567890') == '1234567890' + try: + bdecode('02:xy') + assert 0 + except ValueError: + pass + try: + bdecode('l') + assert 0 + except ValueError: + pass + assert bdecode('le') == [] + try: + bdecode('leanfdldjfh') + assert 0 + except ValueError: + pass + assert bdecode('l0:0:0:e') == ['', '', ''] + try: + bdecode('relwjhrlewjh') + assert 0 + except ValueError: + pass + assert bdecode('li1ei2ei3ee') == [1, 2, 3] + assert bdecode('l3:asd2:xye') == ['asd', 'xy'] + assert bdecode('ll5:Alice3:Bobeli2ei3eee') == [['Alice', 'Bob'], [2, 3]] + try: + bdecode('d') + assert 0 + except ValueError: + pass + try: + bdecode('defoobar') + assert 0 + except ValueError: + pass + assert bdecode('de') == {} + assert bdecode('d3:agei25e4:eyes4:bluee') == {'age': 25, 'eyes': 'blue'} + assert bdecode('d8:spam.mp3d6:author5:Alice6:lengthi100000eee') == {'spam.mp3': {'author': 'Alice', 'length': 100000}} + try: + bdecode('d3:fooe') + assert 0 + except ValueError: + pass + try: + bdecode('di1e0:e') + assert 0 + except ValueError: + pass + try: + bdecode('d1:b0:1:a0:e') + assert 0 + except ValueError: + pass + try: + bdecode('d1:a0:1:a0:e') + assert 0 + except ValueError: + pass + try: + bdecode('i03e') + assert 0 + except ValueError: + pass + try: + bdecode('l01:ae') + assert 0 + except ValueError: + pass + try: + bdecode('9999:x') + assert 0 + except ValueError: + pass + try: + bdecode('l0:') + assert 0 + except ValueError: + pass + try: + bdecode('d0:0:') + assert 0 + except ValueError: + pass + try: + bdecode('d0:') + assert 0 + except ValueError: + pass + +bencached_marker = [] + +class Bencached: + def __init__(self, s): + self.marker = bencached_marker + self.bencoded = s + +BencachedType = type(Bencached('')) # insufficient, but good as a filter + +def encode_bencached(x,r): + assert x.marker == bencached_marker + r.append(x.bencoded) + +def encode_int(x,r): + r.extend(('i',str(x),'e')) + +def encode_bool(x,r): + encode_int(int(x),r) + +def encode_string(x,r): + r.extend((str(len(x)),':',x)) + +def encode_unicode(x,r): + #r.append('u') + encode_string(x.encode('UTF-8'),r) + +def encode_list(x,r): + r.append('l') + for e in x: + encode_func[type(e)](e, r) + r.append('e') + +def encode_dict(x,r): + r.append('d') + ilist = x.items() + ilist.sort() + for k,v in ilist: + r.extend((str(len(k)),':',k)) + encode_func[type(v)](v, r) + r.append('e') + +encode_func = {} +encode_func[BencachedType] = encode_bencached +encode_func[IntType] = encode_int +encode_func[LongType] = encode_int +encode_func[StringType] = encode_string +encode_func[ListType] = encode_list +encode_func[TupleType] = encode_list +encode_func[DictType] = encode_dict +if BooleanType: + encode_func[BooleanType] = encode_bool +if UnicodeType: + encode_func[UnicodeType] = encode_unicode + +def bencode(x): + r = [] + try: + encode_func[type(x)](x, r) + except: + print "*** error *** could not encode type %s (value: %s)" % (type(x), x) + assert 0 + return ''.join(r) + +def test_bencode(): + assert bencode(4) == 'i4e' + assert bencode(0) == 'i0e' + assert bencode(-10) == 'i-10e' + assert bencode(12345678901234567890L) == 'i12345678901234567890e' + assert bencode('') == '0:' + assert bencode('abc') == '3:abc' + assert bencode('1234567890') == '10:1234567890' + assert bencode([]) == 'le' + assert bencode([1, 2, 3]) == 'li1ei2ei3ee' + assert bencode([['Alice', 'Bob'], [2, 3]]) == 'll5:Alice3:Bobeli2ei3eee' + assert bencode({}) == 'de' + assert bencode({'age': 25, 'eyes': 'blue'}) == 'd3:agei25e4:eyes4:bluee' + assert bencode({'spam.mp3': {'author': 'Alice', 'length': 100000}}) == 'd8:spam.mp3d6:author5:Alice6:lengthi100000eee' + try: + bencode({1: 'foo'}) + assert 0 + except AssertionError: + pass + + +try: + import psyco + psyco.bind(bdecode) + psyco.bind(bencode) +except ImportError: + pass diff --git a/BitTornado/bitfield.py b/BitTornado/bitfield.py new file mode 100644 index 000000000..7788b08f7 --- /dev/null +++ b/BitTornado/bitfield.py @@ -0,0 +1,162 @@ +# Written by Bram Cohen, Uoti Urpala, and John Hoffman +# see LICENSE.txt for license information + +try: + True +except: + True = 1 + False = 0 + bool = lambda x: not not x + +try: + sum([1]) + negsum = lambda a: len(a)-sum(a) +except: + negsum = lambda a: reduce(lambda x,y: x+(not y), a, 0) + +def _int_to_booleans(x): + r = [] + for i in range(8): + r.append(bool(x & 0x80)) + x <<= 1 + return tuple(r) + +lookup_table = [] +reverse_lookup_table = {} +for i in xrange(256): + x = _int_to_booleans(i) + lookup_table.append(x) + reverse_lookup_table[x] = chr(i) + + +class Bitfield: + def __init__(self, length = None, bitstring = None, copyfrom = None): + if copyfrom is not None: + self.length = copyfrom.length + self.array = copyfrom.array[:] + self.numfalse = copyfrom.numfalse + return + if length is None: + raise ValueError, "length must be provided unless copying from another array" + self.length = length + if bitstring is not None: + extra = len(bitstring) * 8 - length + if extra < 0 or extra >= 8: + raise ValueError + t = lookup_table + r = [] + for c in bitstring: + r.extend(t[ord(c)]) + if extra > 0: + if r[-extra:] != [0] * extra: + raise ValueError + del r[-extra:] + self.array = r + self.numfalse = negsum(r) + else: + self.array = [False] * length + self.numfalse = length + + def __setitem__(self, index, val): + val = bool(val) + self.numfalse += self.array[index]-val + self.array[index] = val + + def __getitem__(self, index): + return self.array[index] + + def __len__(self): + return self.length + + def tostring(self): + booleans = self.array + t = reverse_lookup_table + s = len(booleans) % 8 + r = [ t[tuple(booleans[x:x+8])] for x in xrange(0, len(booleans)-s, 8) ] + if s: + r += t[tuple(booleans[-s:] + ([0] * (8-s)))] + return ''.join(r) + + def complete(self): + return not self.numfalse + + +def test_bitfield(): + try: + x = Bitfield(7, 'ab') + assert False + except ValueError: + pass + try: + x = Bitfield(7, 'ab') + assert False + except ValueError: + pass + try: + x = Bitfield(9, 'abc') + assert False + except ValueError: + pass + try: + x = Bitfield(0, 'a') + assert False + except ValueError: + pass + try: + x = Bitfield(1, '') + assert False + except ValueError: + pass + try: + x = Bitfield(7, '') + assert False + except ValueError: + pass + try: + x = Bitfield(8, '') + assert False + except ValueError: + pass + try: + x = Bitfield(9, 'a') + assert False + except ValueError: + pass + try: + x = Bitfield(7, chr(1)) + assert False + except ValueError: + pass + try: + x = Bitfield(9, chr(0) + chr(0x40)) + assert False + except ValueError: + pass + assert Bitfield(0, '').tostring() == '' + assert Bitfield(1, chr(0x80)).tostring() == chr(0x80) + assert Bitfield(7, chr(0x02)).tostring() == chr(0x02) + assert Bitfield(8, chr(0xFF)).tostring() == chr(0xFF) + assert Bitfield(9, chr(0) + chr(0x80)).tostring() == chr(0) + chr(0x80) + x = Bitfield(1) + assert x.numfalse == 1 + x[0] = 1 + assert x.numfalse == 0 + x[0] = 1 + assert x.numfalse == 0 + assert x.tostring() == chr(0x80) + x = Bitfield(7) + assert len(x) == 7 + x[6] = 1 + assert x.numfalse == 6 + assert x.tostring() == chr(0x02) + x = Bitfield(8) + x[7] = 1 + assert x.tostring() == chr(1) + x = Bitfield(9) + x[8] = 1 + assert x.numfalse == 8 + assert x.tostring() == chr(0) + chr(0x80) + x = Bitfield(8, chr(0xC4)) + assert len(x) == 8 + assert x.numfalse == 5 + assert x.tostring() == chr(0xC4) diff --git a/BitTornado/clock.py b/BitTornado/clock.py new file mode 100644 index 000000000..ad0cd6226 --- /dev/null +++ b/BitTornado/clock.py @@ -0,0 +1,27 @@ +# Written by John Hoffman +# see LICENSE.txt for license information + +from time import * +import sys + +_MAXFORWARD = 100 +_FUDGE = 1 + +class RelativeTime: + def __init__(self): + self.time = time() + self.offset = 0 + + def get_time(self): + t = time() + self.offset + if t < self.time or t > self.time + _MAXFORWARD: + self.time += _FUDGE + self.offset += self.time - t + return self.time + self.time = t + return t + +if sys.platform != 'win32': + _RTIME = RelativeTime() + def clock(): + return _RTIME.get_time() \ No newline at end of file diff --git a/BitTornado/download_bt1.py b/BitTornado/download_bt1.py new file mode 100644 index 000000000..b1a584035 --- /dev/null +++ b/BitTornado/download_bt1.py @@ -0,0 +1,880 @@ +# Written by Bram Cohen +# see LICENSE.txt for license information + +from zurllib import urlopen +from urlparse import urlparse +from BT1.btformats import check_message +from BT1.Choker import Choker +from BT1.Storage import Storage +from BT1.StorageWrapper import StorageWrapper +from BT1.FileSelector import FileSelector +from BT1.Uploader import Upload +from BT1.Downloader import Downloader +from BT1.HTTPDownloader import HTTPDownloader +from BT1.Connecter import Connecter +from RateLimiter import RateLimiter +from BT1.Encrypter import Encoder +from RawServer import RawServer, autodetect_ipv6, autodetect_socket_style +from BT1.Rerequester import Rerequester +from BT1.DownloaderFeedback import DownloaderFeedback +from RateMeasure import RateMeasure +from CurrentRateMeasure import Measure +from BT1.PiecePicker import PiecePicker +from BT1.Statistics import Statistics +from ConfigDir import ConfigDir +from bencode import bencode, bdecode +from natpunch import UPnP_test +from sha import sha +from os import path, makedirs, listdir +from parseargs import parseargs, formatDefinitions, defaultargs +from socket import error as socketerror +from random import seed +from threading import Thread, Event +from clock import clock +from __init__ import createPeerID + +try: + True +except: + True = 1 + False = 0 + +defaults = [ + ('max_uploads', 0, + "the maximum number of uploads to allow at once."), + ('keepalive_interval', 120.0, + 'number of seconds to pause between sending keepalives'), + ('download_slice_size', 2 ** 14, + "How many bytes to query for per request."), + ('upload_unit_size', 1460, + "when limiting upload rate, how many bytes to send at a time"), + ('request_backlog', 10, + "maximum number of requests to keep in a single pipe at once."), + ('max_message_length', 2 ** 23, + "maximum length prefix encoding you'll accept over the wire - larger values get the connection dropped."), + ('ip', '', + "ip to report you have to the tracker."), + ('minport', 10000, 'minimum port to listen on, counts up if unavailable'), + ('maxport', 60000, 'maximum port to listen on'), + ('random_port', 1, 'whether to choose randomly inside the port range ' + + 'instead of counting up linearly'), + ('responsefile', '', + 'file the server response was stored in, alternative to url'), + ('url', '', + 'url to get file from, alternative to responsefile'), + ('selector_enabled', 1, + 'whether to enable the file selector and fast resume function'), + ('expire_cache_data', 10, + 'the number of days after which you wish to expire old cache data ' + + '(0 = disabled)'), + ('priority', '', + 'a list of file priorities separated by commas, must be one per file, ' + + '0 = highest, 1 = normal, 2 = lowest, -1 = download disabled'), + ('saveas', '', + 'local file name to save the file as, null indicates query user'), + ('timeout', 300.0, + 'time to wait between closing sockets which nothing has been received on'), + ('timeout_check_interval', 60.0, + 'time to wait between checking if any connections have timed out'), + ('max_slice_length', 2 ** 17, + "maximum length slice to send to peers, larger requests are ignored"), + ('max_rate_period', 20.0, + "maximum amount of time to guess the current rate estimate represents"), + ('bind', '', + 'comma-separated list of ips/hostnames to bind to locally'), +# ('ipv6_enabled', autodetect_ipv6(), + ('ipv6_enabled', 0, + 'allow the client to connect to peers via IPv6'), + ('ipv6_binds_v4', autodetect_socket_style(), + "set if an IPv6 server socket won't also field IPv4 connections"), + ('upnp_nat_access', 1, + 'attempt to autoconfigure a UPnP router to forward a server port ' + + '(0 = disabled, 1 = mode 1 [fast], 2 = mode 2 [slow])'), + ('upload_rate_fudge', 5.0, + 'time equivalent of writing to kernel-level TCP buffer, for rate adjustment'), + ('tcp_ack_fudge', 0.03, + 'how much TCP ACK download overhead to add to upload rate calculations ' + + '(0 = disabled)'), + ('display_interval', .5, + 'time between updates of displayed information'), + ('rerequest_interval', 5 * 60, + 'time to wait between requesting more peers'), + ('min_peers', 20, + 'minimum number of peers to not do rerequesting'), + ('http_timeout', 60, + 'number of seconds to wait before assuming that an http connection has timed out'), + ('max_initiate', 40, + 'number of peers at which to stop initiating new connections'), + ('check_hashes', 1, + 'whether to check hashes on disk'), + ('max_upload_rate', 0, + 'maximum kB/s to upload at (0 = no limit, -1 = automatic)'), + ('max_download_rate', 0, + 'maximum kB/s to download at (0 = no limit)'), + ('alloc_type', 'normal', + 'allocation type (may be normal, background, pre-allocate or sparse)'), + ('alloc_rate', 2.0, + 'rate (in MiB/s) to allocate space at using background allocation'), + ('buffer_reads', 1, + 'whether to buffer disk reads'), + ('write_buffer_size', 4, + 'the maximum amount of space to use for buffering disk writes ' + + '(in megabytes, 0 = disabled)'), + ('breakup_seed_bitfield', 1, + 'sends an incomplete bitfield and then fills with have messages, ' + 'in order to get around stupid ISP manipulation'), + ('snub_time', 30.0, + "seconds to wait for data to come in over a connection before assuming it's semi-permanently choked"), + ('spew', 0, + "whether to display diagnostic info to stdout"), + ('rarest_first_cutoff', 2, + "number of downloads at which to switch from random to rarest first"), + ('rarest_first_priority_cutoff', 5, + 'the number of peers which need to have a piece before other partials take priority over rarest first'), + ('min_uploads', 4, + "the number of uploads to fill out to with extra optimistic unchokes"), + ('max_files_open', 20, + 'the maximum number of files to keep open at a time, 0 means no limit'), + ('round_robin_period', 30, + "the number of seconds between the client's switching upload targets"), + ('super_seeder', 0, + "whether to use special upload-efficiency-maximizing routines (only for dedicated seeds)"), + ('security', 1, + "whether to enable extra security features intended to prevent abuse"), + ('max_connections', 50, + "the absolute maximum number of peers to connect with (0 = no limit)"), + ('auto_kick', 1, + "whether to allow the client to automatically kick/ban peers that send bad data"), + ('double_check', 1, + "whether to double-check data being written to the disk for errors (may increase CPU load)"), + ('triple_check', 0, + "whether to thoroughly check data being written to the disk (may slow disk access)"), + ('lock_files', 1, + "whether to lock files the client is working with"), + ('lock_while_reading', 0, + "whether to lock access to files being read"), + ('auto_flush', 0, + "minutes between automatic flushes to disk (0 = disabled)"), + ('dedicated_seed_id', '', + "code to send to tracker identifying as a dedicated seed"), + ] + +argslistheader = 'Arguments are:\n\n' + + +def _failfunc(x): + print x + +# old-style downloader +def download(params, filefunc, statusfunc, finfunc, errorfunc, doneflag, cols, + pathFunc = None, presets = {}, exchandler = None, + failed = _failfunc, paramfunc = None): + + try: + config = parse_params(params, presets) + except ValueError, e: + failed('error: ' + str(e) + '\nrun with no args for parameter explanations') + return + if not config: + errorfunc(get_usage()) + return + + myid = createPeerID() + seed(myid) + + rawserver = RawServer(doneflag, config['timeout_check_interval'], + config['timeout'], ipv6_enable = config['ipv6_enabled'], + failfunc = failed, errorfunc = exchandler) + + upnp_type = UPnP_test(config['upnp_nat_access']) + try: + listen_port = rawserver.find_and_bind(config['minport'], config['maxport'], + config['bind'], ipv6_socket_style = config['ipv6_binds_v4'], + upnp = upnp_type, randomizer = config['random_port']) + except socketerror, e: + failed("Couldn't listen - " + str(e)) + return + + response = get_response(config['responsefile'], config['url'], failed) + if not response: + return + + infohash = sha(bencode(response['info'])).digest() + + d = BT1Download(statusfunc, finfunc, errorfunc, exchandler, doneflag, + config, response, infohash, myid, rawserver, listen_port) + + if not d.saveAs(filefunc): + return + + if pathFunc: + pathFunc(d.getFilename()) + + hashcheck = d.initFiles(old_style = True) + if not hashcheck: + return + if not hashcheck(): + return + if not d.startEngine(): + return + d.startRerequester() + d.autoStats() + + statusfunc(activity = 'connecting to peers') + + if paramfunc: + paramfunc({ 'max_upload_rate' : d.setUploadRate, # change_max_upload_rate() + 'max_uploads': d.setConns, # change_max_uploads() + 'listen_port' : listen_port, # int + 'peer_id' : myid, # string + 'info_hash' : infohash, # string + 'start_connection' : d._startConnection, # start_connection((, ), ) + }) + + rawserver.listen_forever(d.getPortHandler()) + + d.shutdown() + + +def parse_params(params, presets = {}): + config, args = parseargs(params, defaults, 0, 1, presets = presets) + if args: + if config['responsefile'] or config['url']: + raise ValueError,'must have responsefile or url as arg or parameter, not both' + if path.isfile(args[0]): + config['responsefile'] = args[0] + else: + try: + urlparse(args[0]) + except: + raise ValueError, 'bad filename or url' + config['url'] = args[0] + elif (config['responsefile'] == '') == (config['url'] == ''): + raise ValueError, 'need responsefile or url, must have one, cannot have both' + return config + + +def get_usage(defaults = defaults, cols = 100, presets = {}): + return (argslistheader + formatDefinitions(defaults, cols, presets)) + + +def get_response(file, url, errorfunc): + try: + if file: + h = open(file, 'rb') + try: + line = h.read(10) # quick test to see if responsefile contains a dict + front,garbage = line.split(':',1) + assert front[0] == 'd' + int(front[1:]) + except: + errorfunc(file+' is not a valid responsefile') + return None + try: + h.seek(0) + except: + try: + h.close() + except: + pass + h = open(file, 'rb') + else: + try: + h = urlopen(url) + except: + errorfunc(url+' bad url') + return None + response = h.read() + + except IOError, e: + errorfunc('problem getting response info - ' + str(e)) + return None + try: + h.close() + except: + pass + try: + try: + response = bdecode(response) + except: + errorfunc("warning: bad data in responsefile") + response = bdecode(response, sloppy=1) + check_message(response) + except ValueError, e: + errorfunc("got bad file info - " + str(e)) + return None + + return response + + +class BT1Download: + def __init__(self, statusfunc, finfunc, errorfunc, excfunc, doneflag, + config, response, infohash, id, rawserver, port, + appdataobj = None): + self.statusfunc = statusfunc + self.finfunc = finfunc + self.errorfunc = errorfunc + self.excfunc = excfunc + self.doneflag = doneflag + self.config = config + self.response = response + self.infohash = infohash + self.myid = id + self.rawserver = rawserver + self.port = port + + self.info = self.response['info'] + self.pieces = [self.info['pieces'][x:x+20] + for x in xrange(0, len(self.info['pieces']), 20)] + self.len_pieces = len(self.pieces) + self.argslistheader = argslistheader + self.unpauseflag = Event() + self.unpauseflag.set() + self.downloader = None + self.storagewrapper = None + self.fileselector = None + self.super_seeding_active = False + self.filedatflag = Event() + self.spewflag = Event() + self.superseedflag = Event() + self.whenpaused = None + self.finflag = Event() + self.rerequest = None + self.tcp_ack_fudge = config['tcp_ack_fudge'] + + self.selector_enabled = config['selector_enabled'] + if appdataobj: + self.appdataobj = appdataobj + elif self.selector_enabled: + self.appdataobj = ConfigDir() + self.appdataobj.deleteOldCacheData( config['expire_cache_data'], + [self.infohash] ) + + self.excflag = self.rawserver.get_exception_flag() + self.failed = False + self.checking = False + self.started = False + + self.picker = PiecePicker(self.len_pieces, config['rarest_first_cutoff'], + config['rarest_first_priority_cutoff']) + self.choker = Choker(config, rawserver.add_task, + self.picker, self.finflag.isSet) + + + def checkSaveLocation(self, loc): + if self.info.has_key('length'): + return path.exists(loc) + for x in self.info['files']: + if path.exists(path.join(loc, x['path'][0])): + return True + return False + + + def saveAs(self, filefunc, pathfunc = None): + try: + def make(f, forcedir = False): + if not forcedir: + f = path.split(f)[0] + if f != '' and not path.exists(f): + makedirs(f) + + if self.info.has_key('length'): + file_length = self.info['length'] + file = filefunc(self.info['name'], file_length, + self.config['saveas'], False) + if file is None: + return None + make(file) + files = [(file, file_length)] + else: + file_length = 0L + for x in self.info['files']: + file_length += x['length'] + file = filefunc(self.info['name'], file_length, + self.config['saveas'], True) + if file is None: + return None + + # if this path exists, and no files from the info dict exist, we assume it's a new download and + # the user wants to create a new directory with the default name + existing = 0 + if path.exists(file): + if not path.isdir(file): + self.errorfunc(file + 'is not a dir') + return None + if len(listdir(file)) > 0: # if it's not empty + for x in self.info['files']: + if path.exists(path.join(file, x['path'][0])): + existing = 1 + if not existing: + file = path.join(file, self.info['name']) + if path.exists(file) and not path.isdir(file): + if file[-8:] == '.torrent': + file = file[:-8] + if path.exists(file) and not path.isdir(file): + self.errorfunc("Can't create dir - " + self.info['name']) + return None + make(file, True) + + # alert the UI to any possible change in path + if pathfunc != None: + pathfunc(file) + + files = [] + for x in self.info['files']: + n = file + for i in x['path']: + n = path.join(n, i) + files.append((n, x['length'])) + make(n) + except OSError, e: + self.errorfunc("Couldn't allocate dir - " + str(e)) + return None + + self.filename = file + self.files = files + self.datalength = file_length + + return file + + + def getFilename(self): + return self.filename + + + def _finished(self): + self.finflag.set() + try: + self.storage.set_readonly() + except (IOError, OSError), e: + self.errorfunc('trouble setting readonly at end - ' + str(e)) + if self.superseedflag.isSet(): + self._set_super_seed() + self.choker.set_round_robin_period( + max( self.config['round_robin_period'], + self.config['round_robin_period'] * + self.info['piece length'] / 200000 ) ) + self.rerequest_complete() + self.finfunc() + + def _data_flunked(self, amount, index): + self.ratemeasure_datarejected(amount) + if not self.doneflag.isSet(): + self.errorfunc('piece %d failed hash check, re-downloading it' % index) + + def _failed(self, reason): + self.failed = True + self.doneflag.set() + if reason is not None: + self.errorfunc(reason) + + + def initFiles(self, old_style = False, statusfunc = None): + if self.doneflag.isSet(): + return None + if not statusfunc: + statusfunc = self.statusfunc + + disabled_files = None + if self.selector_enabled: + self.priority = self.config['priority'] + if self.priority: + try: + self.priority = self.priority.split(',') + assert len(self.priority) == len(self.files) + self.priority = [int(p) for p in self.priority] + for p in self.priority: + assert p >= -1 + assert p <= 2 + except: + self.errorfunc('bad priority list given, ignored') + self.priority = None + + data = self.appdataobj.getTorrentData(self.infohash) + try: + d = data['resume data']['priority'] + assert len(d) == len(self.files) + disabled_files = [x == -1 for x in d] + except: + try: + disabled_files = [x == -1 for x in self.priority] + except: + pass + + try: + try: + self.storage = Storage(self.files, self.info['piece length'], + self.doneflag, self.config, disabled_files) + except IOError, e: + self.errorfunc('trouble accessing files - ' + str(e)) + return None + if self.doneflag.isSet(): + return None + + self.storagewrapper = StorageWrapper(self.storage, self.config['download_slice_size'], + self.pieces, self.info['piece length'], self._finished, self._failed, + statusfunc, self.doneflag, self.config['check_hashes'], + self._data_flunked, self.rawserver.add_task, + self.config, self.unpauseflag) + + except ValueError, e: + self._failed('bad data - ' + str(e)) + except IOError, e: + self._failed('IOError - ' + str(e)) + if self.doneflag.isSet(): + return None + + if self.selector_enabled: + self.fileselector = FileSelector(self.files, self.info['piece length'], + self.appdataobj.getPieceDir(self.infohash), + self.storage, self.storagewrapper, + self.rawserver.add_task, + self._failed) + if data: + data = data.get('resume data') + if data: + self.fileselector.unpickle(data) + + self.checking = True + if old_style: + return self.storagewrapper.old_style_init() + return self.storagewrapper.initialize + + + def getCachedTorrentData(self): + return self.appdataobj.getTorrentData(self.infohash) + + + def _make_upload(self, connection, ratelimiter, totalup): + return Upload(connection, ratelimiter, totalup, + self.choker, self.storagewrapper, self.picker, + self.config) + + def _kick_peer(self, connection): + def k(connection = connection): + connection.close() + self.rawserver.add_task(k,0) + + def _ban_peer(self, ip): + self.encoder_ban(ip) + + def _received_raw_data(self, x): + if self.tcp_ack_fudge: + x = int(x*self.tcp_ack_fudge) + self.ratelimiter.adjust_sent(x) +# self.upmeasure.update_rate(x) + + def _received_data(self, x): + self.downmeasure.update_rate(x) + self.ratemeasure.data_came_in(x) + + def _received_http_data(self, x): + self.downmeasure.update_rate(x) + self.ratemeasure.data_came_in(x) + self.downloader.external_data_received(x) + + def _cancelfunc(self, pieces): + self.downloader.cancel_piece_download(pieces) + self.httpdownloader.cancel_piece_download(pieces) + def _reqmorefunc(self, pieces): + self.downloader.requeue_piece_download(pieces) + + def startEngine(self, ratelimiter = None, statusfunc = None): + if self.doneflag.isSet(): + return False + if not statusfunc: + statusfunc = self.statusfunc + + self.checking = False + + for i in xrange(self.len_pieces): + if self.storagewrapper.do_I_have(i): + self.picker.complete(i) + self.upmeasure = Measure(self.config['max_rate_period'], + self.config['upload_rate_fudge']) + self.downmeasure = Measure(self.config['max_rate_period']) + + if ratelimiter: + self.ratelimiter = ratelimiter + else: + self.ratelimiter = RateLimiter(self.rawserver.add_task, + self.config['upload_unit_size'], + self.setConns) + self.ratelimiter.set_upload_rate(self.config['max_upload_rate']) + + self.ratemeasure = RateMeasure() + self.ratemeasure_datarejected = self.ratemeasure.data_rejected + + self.downloader = Downloader(self.storagewrapper, self.picker, + self.config['request_backlog'], self.config['max_rate_period'], + self.len_pieces, self.config['download_slice_size'], + self._received_data, self.config['snub_time'], self.config['auto_kick'], + self._kick_peer, self._ban_peer) + self.downloader.set_download_rate(self.config['max_download_rate']) + self.connecter = Connecter(self._make_upload, self.downloader, self.choker, + self.len_pieces, self.upmeasure, self.config, + self.ratelimiter, self.rawserver.add_task) + self.encoder = Encoder(self.connecter, self.rawserver, + self.myid, self.config['max_message_length'], self.rawserver.add_task, + self.config['keepalive_interval'], self.infohash, + self._received_raw_data, self.config) + self.encoder_ban = self.encoder.ban + + self.httpdownloader = HTTPDownloader(self.storagewrapper, self.picker, + self.rawserver, self.finflag, self.errorfunc, self.downloader, + self.config['max_rate_period'], self.infohash, self._received_http_data, + self.connecter.got_piece) + if self.response.has_key('httpseeds') and not self.finflag.isSet(): + for u in self.response['httpseeds']: + self.httpdownloader.make_download(u) + + if self.selector_enabled: + self.fileselector.tie_in(self.picker, self._cancelfunc, + self._reqmorefunc, self.rerequest_ondownloadmore) + if self.priority: + self.fileselector.set_priorities_now(self.priority) + self.appdataobj.deleteTorrentData(self.infohash) + # erase old data once you've started modifying it + + if self.config['super_seeder']: + self.set_super_seed() + + self.started = True + return True + + + def rerequest_complete(self): + if self.rerequest: + self.rerequest.announce(1) + + def rerequest_stopped(self): + if self.rerequest: + self.rerequest.announce(2) + + def rerequest_lastfailed(self): + if self.rerequest: + return self.rerequest.last_failed + return False + + def rerequest_ondownloadmore(self): + if self.rerequest: + self.rerequest.hit() + + def startRerequester(self, seededfunc = None, force_rapid_update = False): + if self.response.has_key('announce-list'): + trackerlist = self.response['announce-list'] + else: + trackerlist = [[self.response['announce']]] + + self.rerequest = Rerequester(trackerlist, self.config['rerequest_interval'], + self.rawserver.add_task, self.connecter.how_many_connections, + self.config['min_peers'], self.encoder.start_connections, + self.rawserver.add_task, self.storagewrapper.get_amount_left, + self.upmeasure.get_total, self.downmeasure.get_total, self.port, self.config['ip'], + self.myid, self.infohash, self.config['http_timeout'], + self.errorfunc, self.excfunc, self.config['max_initiate'], + self.doneflag, self.upmeasure.get_rate, self.downmeasure.get_rate, + self.unpauseflag, self.config['dedicated_seed_id'], + seededfunc, force_rapid_update ) + + self.rerequest.start() + + + def _init_stats(self): + self.statistics = Statistics(self.upmeasure, self.downmeasure, + self.connecter, self.httpdownloader, self.ratelimiter, + self.rerequest_lastfailed, self.filedatflag) + if self.info.has_key('files'): + self.statistics.set_dirstats(self.files, self.info['piece length']) + if self.config['spew']: + self.spewflag.set() + + def autoStats(self, displayfunc = None): + if not displayfunc: + displayfunc = self.statusfunc + + self._init_stats() + DownloaderFeedback(self.choker, self.httpdownloader, self.rawserver.add_task, + self.upmeasure.get_rate, self.downmeasure.get_rate, + self.ratemeasure, self.storagewrapper.get_stats, + self.datalength, self.finflag, self.spewflag, self.statistics, + displayfunc, self.config['display_interval']) + + def startStats(self): + self._init_stats() + d = DownloaderFeedback(self.choker, self.httpdownloader, self.rawserver.add_task, + self.upmeasure.get_rate, self.downmeasure.get_rate, + self.ratemeasure, self.storagewrapper.get_stats, + self.datalength, self.finflag, self.spewflag, self.statistics) + return d.gather + + + def getPortHandler(self): + return self.encoder + + + def shutdown(self, torrentdata = {}): + if self.checking or self.started: + self.storagewrapper.sync() + self.storage.close() + self.rerequest_stopped() + if self.fileselector and self.started: + if not self.failed: + self.fileselector.finish() + torrentdata['resume data'] = self.fileselector.pickle() + try: + self.appdataobj.writeTorrentData(self.infohash,torrentdata) + except: + self.appdataobj.deleteTorrentData(self.infohash) # clear it + return not self.failed and not self.excflag.isSet() + # if returns false, you may wish to auto-restart the torrent + + + def setUploadRate(self, rate): + try: + def s(self = self, rate = rate): + self.config['max_upload_rate'] = rate + self.ratelimiter.set_upload_rate(rate) + self.rawserver.add_task(s) + except AttributeError: + pass + + def setConns(self, conns, conns2 = None): + if not conns2: + conns2 = conns + try: + def s(self = self, conns = conns, conns2 = conns2): + self.config['min_uploads'] = conns + self.config['max_uploads'] = conns2 + if (conns > 30): + self.config['max_initiate'] = conns + 10 + self.rawserver.add_task(s) + except AttributeError: + pass + + def setDownloadRate(self, rate): + try: + def s(self = self, rate = rate): + self.config['max_download_rate'] = rate + self.downloader.set_download_rate(rate) + self.rawserver.add_task(s) + except AttributeError: + pass + + def startConnection(self, ip, port, id): + self.encoder._start_connection((ip, port), id) + + def _startConnection(self, ipandport, id): + self.encoder._start_connection(ipandport, id) + + def setInitiate(self, initiate): + try: + def s(self = self, initiate = initiate): + self.config['max_initiate'] = initiate + self.rawserver.add_task(s) + except AttributeError: + pass + + def getConfig(self): + return self.config + + def getDefaults(self): + return defaultargs(defaults) + + def getUsageText(self): + return self.argslistheader + + def reannounce(self, special = None): + try: + def r(self = self, special = special): + if special is None: + self.rerequest.announce() + else: + self.rerequest.announce(specialurl = special) + self.rawserver.add_task(r) + except AttributeError: + pass + + def getResponse(self): + try: + return self.response + except: + return None + +# def Pause(self): +# try: +# if self.storagewrapper: +# self.rawserver.add_task(self._pausemaker, 0) +# except: +# return False +# self.unpauseflag.clear() +# return True +# +# def _pausemaker(self): +# self.whenpaused = clock() +# self.unpauseflag.wait() # sticks a monkey wrench in the main thread +# +# def Unpause(self): +# self.unpauseflag.set() +# if self.whenpaused and clock()-self.whenpaused > 60: +# def r(self = self): +# self.rerequest.announce(3) # rerequest automatically if paused for >60 seconds +# self.rawserver.add_task(r) + + def Pause(self): + if not self.storagewrapper: + return False + self.unpauseflag.clear() + self.rawserver.add_task(self.onPause) + return True + + def onPause(self): + self.whenpaused = clock() + if not self.downloader: + return + self.downloader.pause(True) + self.encoder.pause(True) + self.choker.pause(True) + + def Unpause(self): + self.unpauseflag.set() + self.rawserver.add_task(self.onUnpause) + + def onUnpause(self): + if not self.downloader: + return + self.downloader.pause(False) + self.encoder.pause(False) + self.choker.pause(False) + if self.rerequest and self.whenpaused and clock()-self.whenpaused > 60: + self.rerequest.announce(3) # rerequest automatically if paused for >60 seconds + + def set_super_seed(self): + try: + self.superseedflag.set() + def s(self = self): + if self.finflag.isSet(): + self._set_super_seed() + self.rawserver.add_task(s) + except AttributeError: + pass + + def _set_super_seed(self): + if not self.super_seeding_active: + self.super_seeding_active = True + self.errorfunc(' ** SUPER-SEED OPERATION ACTIVE **\n' + + ' please set Max uploads so each peer gets 6-8 kB/s') + def s(self = self): + self.downloader.set_super_seed() + self.choker.set_super_seed() + self.rawserver.add_task(s) + if self.finflag.isSet(): # mode started when already finished + def r(self = self): + self.rerequest.announce(3) # so after kicking everyone off, reannounce + self.rawserver.add_task(r) + + def am_I_finished(self): + return self.finflag.isSet() + + def get_transfer_stats(self): + return self.upmeasure.get_total(), self.downmeasure.get_total() diff --git a/BitTornado/inifile.py b/BitTornado/inifile.py new file mode 100644 index 000000000..091ce91b9 --- /dev/null +++ b/BitTornado/inifile.py @@ -0,0 +1,169 @@ +# Written by John Hoffman +# see LICENSE.txt for license information + +''' +reads/writes a Windows-style INI file +format: + + aa = "bb" + cc = 11 + + [eee] + ff = "gg" + +decodes to: +d = { '': {'aa':'bb','cc':'11'}, 'eee': {'ff':'gg'} } + +the encoder can also take this as input: + +d = { 'aa': 'bb, 'cc': 11, 'eee': {'ff':'gg'} } + +though it will only decode in the above format. Keywords must be strings. +Values that are strings are written surrounded by quotes, and the decoding +routine automatically strips any. +Booleans are written as integers. Anything else aside from string/int/float +may have unpredictable results. +''' + +from cStringIO import StringIO +from traceback import print_exc +from types import DictType, StringType +try: + from types import BooleanType +except ImportError: + BooleanType = None + +try: + True +except: + True = 1 + False = 0 + +DEBUG = False + +def ini_write(f, d, comment=''): + try: + a = {'':{}} + for k,v in d.items(): + assert type(k) == StringType + k = k.lower() + if type(v) == DictType: + if DEBUG: + print 'new section:' +k + if k: + assert not a.has_key(k) + a[k] = {} + aa = a[k] + for kk,vv in v: + assert type(kk) == StringType + kk = kk.lower() + assert not aa.has_key(kk) + if type(vv) == BooleanType: + vv = int(vv) + if type(vv) == StringType: + vv = '"'+vv+'"' + aa[kk] = str(vv) + if DEBUG: + print 'a['+k+']['+kk+'] = '+str(vv) + else: + aa = a[''] + assert not aa.has_key(k) + if type(v) == BooleanType: + v = int(v) + if type(v) == StringType: + v = '"'+v+'"' + aa[k] = str(v) + if DEBUG: + print 'a[\'\']['+k+'] = '+str(v) + r = open(f,'w') + if comment: + for c in comment.split('\n'): + r.write('# '+c+'\n') + r.write('\n') + l = a.keys() + l.sort() + for k in l: + if k: + r.write('\n['+k+']\n') + aa = a[k] + ll = aa.keys() + ll.sort() + for kk in ll: + r.write(kk+' = '+aa[kk]+'\n') + success = True + except: + if DEBUG: + print_exc() + success = False + try: + r.close() + except: + pass + return success + + +if DEBUG: + def errfunc(lineno, line, err): + print '('+str(lineno)+') '+err+': '+line +else: + errfunc = lambda lineno, line, err: None + +def ini_read(f, errfunc = errfunc): + try: + r = open(f,'r') + ll = r.readlines() + d = {} + dd = {'':d} + for i in xrange(len(ll)): + l = ll[i] + l = l.strip() + if not l: + continue + if l[0] == '#': + continue + if l[0] == '[': + if l[-1] != ']': + errfunc(i,l,'syntax error') + continue + l1 = l[1:-1].strip().lower() + if not l1: + errfunc(i,l,'syntax error') + continue + if dd.has_key(l1): + errfunc(i,l,'duplicate section') + d = dd[l1] + continue + d = {} + dd[l1] = d + continue + try: + k,v = l.split('=',1) + except: + try: + k,v = l.split(':',1) + except: + errfunc(i,l,'syntax error') + continue + k = k.strip().lower() + v = v.strip() + if len(v) > 1 and ( (v[0] == '"' and v[-1] == '"') or + (v[0] == "'" and v[-1] == "'") ): + v = v[1:-1] + if not k: + errfunc(i,l,'syntax error') + continue + if d.has_key(k): + errfunc(i,l,'duplicate entry') + continue + d[k] = v + if DEBUG: + print dd + except: + if DEBUG: + print_exc() + dd = None + try: + r.close() + except: + pass + return dd diff --git a/BitTornado/iprangeparse.py b/BitTornado/iprangeparse.py new file mode 100644 index 000000000..f177f041a --- /dev/null +++ b/BitTornado/iprangeparse.py @@ -0,0 +1,194 @@ +# Written by John Hoffman +# see LICENSE.txt for license information + +from bisect import bisect, insort + +try: + True +except: + True = 1 + False = 0 + bool = lambda x: not not x + + +def to_long_ipv4(ip): + ip = ip.split('.') + if len(ip) != 4: + raise ValueError, "bad address" + b = 0L + for n in ip: + b *= 256 + b += int(n) + return b + + +def to_long_ipv6(ip): + if ip == '': + raise ValueError, "bad address" + if ip == '::': # boundary handling + ip = '' + elif ip[:2] == '::': + ip = ip[1:] + elif ip[0] == ':': + raise ValueError, "bad address" + elif ip[-2:] == '::': + ip = ip[:-1] + elif ip[-1] == ':': + raise ValueError, "bad address" + + b = [] + doublecolon = False + for n in ip.split(':'): + if n == '': # double-colon + if doublecolon: + raise ValueError, "bad address" + doublecolon = True + b.append(None) + continue + if n.find('.') >= 0: # IPv4 + n = n.split('.') + if len(n) != 4: + raise ValueError, "bad address" + for i in n: + b.append(int(i)) + continue + n = ('0'*(4-len(n))) + n + b.append(int(n[:2],16)) + b.append(int(n[2:],16)) + bb = 0L + for n in b: + if n is None: + for i in xrange(17-len(b)): + bb *= 256 + continue + bb *= 256 + bb += n + return bb + +ipv4addrmask = 65535L*256*256*256*256 + +class IP_List: + def __init__(self): + self.ipv4list = [] # starts of ranges + self.ipv4dict = {} # start: end of ranges + self.ipv6list = [] # " + self.ipv6dict = {} # " + + def __nonzero__(self): + return bool(self.ipv4list or self.ipv6list) + + + def append(self, ip_beg, ip_end = None): + if ip_end is None: + ip_end = ip_beg + else: + assert ip_beg <= ip_end + if ip_beg.find(':') < 0: # IPv4 + ip_beg = to_long_ipv4(ip_beg) + ip_end = to_long_ipv4(ip_end) + l = self.ipv4list + d = self.ipv4dict + else: + ip_beg = to_long_ipv6(ip_beg) + ip_end = to_long_ipv6(ip_end) + bb = ip_beg % (256*256*256*256) + if bb == ipv4addrmask: + ip_beg -= bb + ip_end -= bb + l = self.ipv4list + d = self.ipv4dict + else: + l = self.ipv6list + d = self.ipv6dict + + pos = bisect(l,ip_beg)-1 + done = pos < 0 + while not done: + p = pos + while p < len(l): + range_beg = l[p] + if range_beg > ip_end+1: + done = True + break + range_end = d[range_beg] + if range_end < ip_beg-1: + p += 1 + if p == len(l): + done = True + break + continue + # if neither of the above conditions is true, the ranges overlap + ip_beg = min(ip_beg, range_beg) + ip_end = max(ip_end, range_end) + del l[p] + del d[range_beg] + break + + insort(l,ip_beg) + d[ip_beg] = ip_end + + + def includes(self, ip): + if not (self.ipv4list or self.ipv6list): + return False + if ip.find(':') < 0: # IPv4 + ip = to_long_ipv4(ip) + l = self.ipv4list + d = self.ipv4dict + else: + ip = to_long_ipv6(ip) + bb = ip % (256*256*256*256) + if bb == ipv4addrmask: + ip -= bb + l = self.ipv4list + d = self.ipv4dict + else: + l = self.ipv6list + d = self.ipv6dict + for ip_beg in l[bisect(l,ip)-1:]: + if ip == ip_beg: + return True + ip_end = d[ip_beg] + if ip > ip_beg and ip <= ip_end: + return True + return False + + + # reads a list from a file in the format 'whatever:whatever:ip-ip' + # (not IPv6 compatible at all) + def read_rangelist(self, file): + f = open(file, 'r') + while True: + line = f.readline() + if not line: + break + line = line.strip() + if not line or line[0] == '#': + continue + line = line.split(':')[-1] + try: + ip1,ip2 = line.split('-') + except: + ip1 = line + ip2 = line + try: + self.append(ip1.strip(),ip2.strip()) + except: + print '*** WARNING *** could not parse IP range: '+line + f.close() + +def is_ipv4(ip): + return ip.find(':') < 0 + +def is_valid_ip(ip): + try: + if is_ipv4(ip): + a = ip.split('.') + assert len(a) == 4 + for i in a: + chr(int(i)) + return True + to_long_ipv6(ip) + return True + except: + return False diff --git a/BitTornado/launchmanycore.py b/BitTornado/launchmanycore.py new file mode 100644 index 000000000..2d5513326 --- /dev/null +++ b/BitTornado/launchmanycore.py @@ -0,0 +1,381 @@ +#!/usr/bin/env python + +# Written by John Hoffman +# see LICENSE.txt for license information + +from BitTornado import PSYCO +if PSYCO.psyco: + try: + import psyco + assert psyco.__version__ >= 0x010100f0 + psyco.full() + except: + pass + +from download_bt1 import BT1Download +from RawServer import RawServer, UPnP_ERROR +from RateLimiter import RateLimiter +from ServerPortHandler import MultiHandler +from parsedir import parsedir +from natpunch import UPnP_test +from random import seed +from socket import error as socketerror +from threading import Event +from sys import argv, exit +import sys, os +from clock import clock +from __init__ import createPeerID, mapbase64, version +from cStringIO import StringIO +from traceback import print_exc + +try: + True +except: + True = 1 + False = 0 + + +def fmttime(n): + try: + n = int(n) # n may be None or too large + assert n < 5184000 # 60 days + except: + return 'downloading' + m, s = divmod(n, 60) + h, m = divmod(m, 60) + return '%d:%02d:%02d' % (h, m, s) + +class SingleDownload: + def __init__(self, controller, hash, response, config, myid): + self.controller = controller + self.hash = hash + self.response = response + self.config = config + + self.doneflag = Event() + self.waiting = True + self.checking = False + self.working = False + self.seed = False + self.closed = False + + self.status_msg = '' + self.status_err = [''] + self.status_errtime = 0 + self.status_done = 0.0 + + self.rawserver = controller.handler.newRawServer(hash, self.doneflag) + + d = BT1Download(self.display, self.finished, self.error, + controller.exchandler, self.doneflag, config, response, + hash, myid, self.rawserver, controller.listen_port) + self.d = d + + def start(self): + if not self.d.saveAs(self.saveAs): + self._shutdown() + return + self._hashcheckfunc = self.d.initFiles() + if not self._hashcheckfunc: + self._shutdown() + return + self.controller.hashchecksched(self.hash) + + + def saveAs(self, name, length, saveas, isdir): + return self.controller.saveAs(self.hash, name, saveas, isdir) + + def hashcheck_start(self, donefunc): + if self.is_dead(): + self._shutdown() + return + self.waiting = False + self.checking = True + self._hashcheckfunc(donefunc) + + def hashcheck_callback(self): + self.checking = False + if self.is_dead(): + self._shutdown() + return + if not self.d.startEngine(ratelimiter = self.controller.ratelimiter): + self._shutdown() + return + self.d.startRerequester() + self.statsfunc = self.d.startStats() + self.rawserver.start_listening(self.d.getPortHandler()) + self.working = True + + def is_dead(self): + return self.doneflag.isSet() + + def _shutdown(self): + self.shutdown(False) + + def shutdown(self, quiet=True): + if self.closed: + return + self.doneflag.set() + self.rawserver.shutdown() + if self.checking or self.working: + self.d.shutdown() + self.waiting = False + self.checking = False + self.working = False + self.closed = True + self.controller.was_stopped(self.hash) + if not quiet: + self.controller.died(self.hash) + + + def display(self, activity = None, fractionDone = None): + # really only used by StorageWrapper now + if activity: + self.status_msg = activity + if fractionDone is not None: + self.status_done = float(fractionDone) + + def finished(self): + self.seed = True + + def error(self, msg): + if self.doneflag.isSet(): + self._shutdown() + self.status_err.append(msg) + self.status_errtime = clock() + + +class LaunchMany: + def __init__(self, config, Output): + try: + self.config = config + self.Output = Output + + self.torrent_dir = config['torrent_dir'] + self.torrent_cache = {} + self.file_cache = {} + self.blocked_files = {} + self.scan_period = config['parse_dir_interval'] + self.stats_period = config['display_interval'] + + self.torrent_list = [] + self.downloads = {} + self.counter = 0 + self.doneflag = Event() + + self.hashcheck_queue = [] + self.hashcheck_current = None + + self.rawserver = RawServer(self.doneflag, config['timeout_check_interval'], + config['timeout'], ipv6_enable = config['ipv6_enabled'], + failfunc = self.failed, errorfunc = self.exchandler) + upnp_type = UPnP_test(config['upnp_nat_access']) + while True: + try: + self.listen_port = self.rawserver.find_and_bind( + config['minport'], config['maxport'], config['bind'], + ipv6_socket_style = config['ipv6_binds_v4'], + upnp = upnp_type, randomizer = config['random_port']) + break + except socketerror, e: + if upnp_type and e == UPnP_ERROR: + self.Output.message('WARNING: COULD NOT FORWARD VIA UPnP') + upnp_type = 0 + continue + self.failed("Couldn't listen - " + str(e)) + return + + self.ratelimiter = RateLimiter(self.rawserver.add_task, + config['upload_unit_size']) + self.ratelimiter.set_upload_rate(config['max_upload_rate']) + + self.handler = MultiHandler(self.rawserver, self.doneflag) + seed(createPeerID()) + self.rawserver.add_task(self.scan, 0) + self.rawserver.add_task(self.stats, 0) + + self.handler.listen_forever() + + self.Output.message('shutting down') + self.hashcheck_queue = [] + for hash in self.torrent_list: + self.Output.message('dropped "'+self.torrent_cache[hash]['path']+'"') + self.downloads[hash].shutdown() + self.rawserver.shutdown() + + except: + data = StringIO() + print_exc(file = data) + Output.exception(data.getvalue()) + + + def scan(self): + self.rawserver.add_task(self.scan, self.scan_period) + + r = parsedir(self.torrent_dir, self.torrent_cache, + self.file_cache, self.blocked_files, + return_metainfo = True, errfunc = self.Output.message) + + ( self.torrent_cache, self.file_cache, self.blocked_files, + added, removed ) = r + + for hash, data in removed.items(): + self.Output.message('dropped "'+data['path']+'"') + self.remove(hash) + for hash, data in added.items(): + self.Output.message('added "'+data['path']+'"') + self.add(hash, data) + + def stats(self): + self.rawserver.add_task(self.stats, self.stats_period) + data = [] + for hash in self.torrent_list: + cache = self.torrent_cache[hash] + if self.config['display_path']: + name = cache['path'] + else: + name = cache['name'] + size = cache['length'] + d = self.downloads[hash] + progress = '0.0%' + peers = 0 + seeds = 0 + seedsmsg = "S" + dist = 0.0 + uprate = 0.0 + dnrate = 0.0 + upamt = 0 + dnamt = 0 + t = 0 + if d.is_dead(): + status = 'stopped' + elif d.waiting: + status = 'waiting for hash check' + elif d.checking: + status = d.status_msg + progress = '%.1f%%' % (d.status_done*100) + else: + stats = d.statsfunc() + s = stats['stats'] + if d.seed: + status = 'seeding' + progress = '100.0%' + seeds = s.numOldSeeds + seedsmsg = "s" + dist = s.numCopies + else: + if s.numSeeds + s.numPeers: + t = stats['time'] + if t == 0: # unlikely + t = 0.01 + status = fmttime(t) + else: + t = -1 + status = 'connecting to peers' + progress = '%.1f%%' % (int(stats['frac']*1000)/10.0) + seeds = s.numSeeds + dist = s.numCopies2 + dnrate = stats['down'] + peers = s.numPeers + uprate = stats['up'] + upamt = s.upTotal + dnamt = s.downTotal + + if d.is_dead() or d.status_errtime+300 > clock(): + msg = d.status_err[-1] + else: + msg = '' + + data.append(( name, status, progress, peers, seeds, seedsmsg, dist, + uprate, dnrate, upamt, dnamt, size, t, msg )) + stop = self.Output.display(data) + if stop: + self.doneflag.set() + + def remove(self, hash): + self.torrent_list.remove(hash) + self.downloads[hash].shutdown() + del self.downloads[hash] + + def add(self, hash, data): + c = self.counter + self.counter += 1 + x = '' + for i in xrange(3): + x = mapbase64[c & 0x3F]+x + c >>= 6 + peer_id = createPeerID(x) + d = SingleDownload(self, hash, data['metainfo'], self.config, peer_id) + self.torrent_list.append(hash) + self.downloads[hash] = d + d.start() + + + def saveAs(self, hash, name, saveas, isdir): + x = self.torrent_cache[hash] + style = self.config['saveas_style'] + if style == 1 or style == 3: + if saveas: + saveas = os.path.join(saveas,x['file'][:-1-len(x['type'])]) + else: + saveas = x['path'][:-1-len(x['type'])] + if style == 3: + if not os.path.isdir(saveas): + try: + os.mkdir(saveas) + except: + raise OSError("couldn't create directory for "+x['path'] + +" ("+saveas+")") + if not isdir: + saveas = os.path.join(saveas, name) + else: + if saveas: + saveas = os.path.join(saveas, name) + else: + saveas = os.path.join(os.path.split(x['path'])[0], name) + + if isdir and not os.path.isdir(saveas): + try: + os.mkdir(saveas) + except: + raise OSError("couldn't create directory for "+x['path'] + +" ("+saveas+")") + return saveas + + + def hashchecksched(self, hash = None): + if hash: + self.hashcheck_queue.append(hash) + if not self.hashcheck_current: + self._hashcheck_start() + + def _hashcheck_start(self): + self.hashcheck_current = self.hashcheck_queue.pop(0) + self.downloads[self.hashcheck_current].hashcheck_start(self.hashcheck_callback) + + def hashcheck_callback(self): + self.downloads[self.hashcheck_current].hashcheck_callback() + if self.hashcheck_queue: + self._hashcheck_start() + else: + self.hashcheck_current = None + + def died(self, hash): + if self.torrent_cache.has_key(hash): + self.Output.message('DIED: "'+self.torrent_cache[hash]['path']+'"') + + def was_stopped(self, hash): + try: + self.hashcheck_queue.remove(hash) + except: + pass + if self.hashcheck_current == hash: + self.hashcheck_current = None + if self.hashcheck_queue: + self._hashcheck_start() + + def failed(self, s): + self.Output.message('FAILURE: '+s) + + def exchandler(self, s): + self.Output.exception(s) diff --git a/BitTornado/natpunch.py b/BitTornado/natpunch.py new file mode 100644 index 000000000..4ae57f6e8 --- /dev/null +++ b/BitTornado/natpunch.py @@ -0,0 +1,254 @@ +# Written by John Hoffman +# derived from NATPortMapping.py by Yejun Yang +# and from example code by Myers Carpenter +# see LICENSE.txt for license information + +import socket +from traceback import print_exc +from subnetparse import IP_List +from clock import clock +from __init__ import createPeerID +try: + True +except: + True = 1 + False = 0 + +DEBUG = False + +EXPIRE_CACHE = 30 # seconds +ID = "BT-"+createPeerID()[-4:] + +try: + import pythoncom, win32com.client + _supported = 1 +except ImportError: + _supported = 0 + + + +class _UPnP1: # derived from Myers Carpenter's code + # seems to use the machine's local UPnP + # system for its operation. Runs fairly fast + + def __init__(self): + self.map = None + self.last_got_map = -10e10 + + def _get_map(self): + if self.last_got_map + EXPIRE_CACHE < clock(): + try: + dispatcher = win32com.client.Dispatch("HNetCfg.NATUPnP") + self.map = dispatcher.StaticPortMappingCollection + self.last_got_map = clock() + except: + self.map = None + return self.map + + def test(self): + try: + assert self._get_map() # make sure a map was found + success = True + except: + success = False + return success + + + def open(self, ip, p): + map = self._get_map() + try: + map.Add(p,'TCP',p,ip,True,ID) + if DEBUG: + print 'port opened: '+ip+':'+str(p) + success = True + except: + if DEBUG: + print "COULDN'T OPEN "+str(p) + print_exc() + success = False + return success + + + def close(self, p): + map = self._get_map() + try: + map.Remove(p,'TCP') + success = True + if DEBUG: + print 'port closed: '+str(p) + except: + if DEBUG: + print 'ERROR CLOSING '+str(p) + print_exc() + success = False + return success + + + def clean(self, retry = False): + if not _supported: + return + try: + map = self._get_map() + ports_in_use = [] + for i in xrange(len(map)): + try: + mapping = map[i] + port = mapping.ExternalPort + prot = str(mapping.Protocol).lower() + desc = str(mapping.Description).lower() + except: + port = None + if port and prot == 'tcp' and desc[:3] == 'bt-': + ports_in_use.append(port) + success = True + for port in ports_in_use: + try: + map.Remove(port,'TCP') + except: + success = False + if not success and not retry: + self.clean(retry = True) + except: + pass + + +class _UPnP2: # derived from Yejun Yang's code + # apparently does a direct search for UPnP hardware + # may work in some cases where _UPnP1 won't, but is slow + # still need to implement "clean" method + + def __init__(self): + self.services = None + self.last_got_services = -10e10 + + def _get_services(self): + if not self.services or self.last_got_services + EXPIRE_CACHE < clock(): + self.services = [] + try: + f=win32com.client.Dispatch("UPnP.UPnPDeviceFinder") + for t in ( "urn:schemas-upnp-org:service:WANIPConnection:1", + "urn:schemas-upnp-org:service:WANPPPConnection:1" ): + try: + conns = f.FindByType(t,0) + for c in xrange(len(conns)): + try: + svcs = conns[c].Services + for s in xrange(len(svcs)): + try: + self.services.append(svcs[s]) + except: + pass + except: + pass + except: + pass + except: + pass + self.last_got_services = clock() + return self.services + + def test(self): + try: + assert self._get_services() # make sure some services can be found + success = True + except: + success = False + return success + + + def open(self, ip, p): + svcs = self._get_services() + success = False + for s in svcs: + try: + s.InvokeAction('AddPortMapping',['',p,'TCP',p,ip,True,ID,0],'') + success = True + except: + pass + if DEBUG and not success: + print "COULDN'T OPEN "+str(p) + print_exc() + return success + + + def close(self, p): + svcs = self._get_services() + success = False + for s in svcs: + try: + s.InvokeAction('DeletePortMapping', ['',p,'TCP'], '') + success = True + except: + pass + if DEBUG and not success: + print "COULDN'T OPEN "+str(p) + print_exc() + return success + + +class _UPnP: # master holding class + def __init__(self): + self.upnp1 = _UPnP1() + self.upnp2 = _UPnP2() + self.upnplist = (None, self.upnp1, self.upnp2) + self.upnp = None + self.local_ip = None + self.last_got_ip = -10e10 + + def get_ip(self): + if self.last_got_ip + EXPIRE_CACHE < clock(): + local_ips = IP_List() + local_ips.set_intranet_addresses() + try: + for info in socket.getaddrinfo(socket.gethostname(),0,socket.AF_INET): + # exception if socket library isn't recent + self.local_ip = info[4][0] + if local_ips.includes(self.local_ip): + self.last_got_ip = clock() + if DEBUG: + print 'Local IP found: '+self.local_ip + break + else: + raise ValueError('couldn\'t find intranet IP') + except: + self.local_ip = None + if DEBUG: + print 'Error finding local IP' + print_exc() + return self.local_ip + + def test(self, upnp_type): + if DEBUG: + print 'testing UPnP type '+str(upnp_type) + if not upnp_type or not _supported or self.get_ip() is None: + if DEBUG: + print 'not supported' + return 0 + pythoncom.CoInitialize() # leave initialized + self.upnp = self.upnplist[upnp_type] # cache this + if self.upnp.test(): + if DEBUG: + print 'ok' + return upnp_type + if DEBUG: + print 'tested bad' + return 0 + + def open(self, p): + assert self.upnp, "must run UPnP_test() with the desired UPnP access type first" + return self.upnp.open(self.get_ip(), p) + + def close(self, p): + assert self.upnp, "must run UPnP_test() with the desired UPnP access type first" + return self.upnp.close(p) + + def clean(self): + return self.upnp1.clean() + +_upnp_ = _UPnP() + +UPnP_test = _upnp_.test +UPnP_open_port = _upnp_.open +UPnP_close_port = _upnp_.close +UPnP_reset = _upnp_.clean + diff --git a/BitTornado/parseargs.py b/BitTornado/parseargs.py new file mode 100644 index 000000000..7ec9656d3 --- /dev/null +++ b/BitTornado/parseargs.py @@ -0,0 +1,145 @@ +# Written by Bill Bumgarner and Bram Cohen +# see LICENSE.txt for license information + +from types import * +from cStringIO import StringIO + + +def splitLine(line, COLS=80, indent=10): + indent = " " * indent + width = COLS - (len(indent) + 1) + if indent and width < 15: + width = COLS - 2 + indent = " " + s = StringIO() + i = 0 + for word in line.split(): + if i == 0: + s.write(indent+word) + i = len(word) + continue + if i + len(word) >= width: + s.write('\n'+indent+word) + i = len(word) + continue + s.write(' '+word) + i += len(word) + 1 + return s.getvalue() + +def formatDefinitions(options, COLS, presets = {}): + s = StringIO() + for (longname, default, doc) in options: + s.write('--' + longname + ' \n') + default = presets.get(longname, default) + if type(default) in (IntType, LongType): + try: + default = int(default) + except: + pass + if default is not None: + doc += ' (defaults to ' + repr(default) + ')' + s.write(splitLine(doc,COLS,10)) + s.write('\n\n') + return s.getvalue() + + +def usage(str): + raise ValueError(str) + + +def defaultargs(options): + l = {} + for (longname, default, doc) in options: + if default is not None: + l[longname] = default + return l + + +def parseargs(argv, options, minargs = None, maxargs = None, presets = {}): + config = {} + longkeyed = {} + for option in options: + longname, default, doc = option + longkeyed[longname] = option + config[longname] = default + for longname in presets.keys(): # presets after defaults but before arguments + config[longname] = presets[longname] + options = [] + args = [] + pos = 0 + while pos < len(argv): + if argv[pos][:2] != '--': + args.append(argv[pos]) + pos += 1 + else: + if pos == len(argv) - 1: + usage('parameter passed in at end with no value') + + key, value = argv[pos][2:], argv[pos+1] + pos += 2 + + if not longkeyed.has_key(key): + usage('unknown key --' + key) + + longname, default, doc = longkeyed[key] + + try: + t = type(config[longname]) + if t is NoneType or t is StringType: + config[longname] = value + elif t in (IntType, LongType): + config[longname] = long(value) + elif t is FloatType: + config[longname] = float(value) + else: + assert 0 + except ValueError, e: + usage('wrong format of --%s - %s' % (key, str(e))) + + for key, value in config.items(): + if value is None: + usage("Option --%s is required." % key) + + if minargs is not None and len(args) < minargs: + usage("Must supply at least %d args." % minargs) + + if maxargs is not None and len(args) > maxargs: + usage("Too many args - %d max." % maxargs) + + return (config, args) + +def test_parseargs(): + assert parseargs(('d', '--a', 'pq', 'e', '--b', '3', '--c', '4.5', 'f'), (('a', 'x', ''), ('b', 1, ''), ('c', 2.3, ''))) == ({'a': 'pq', 'b': 3, 'c': 4.5}, ['d', 'e', 'f']) + assert parseargs([], [('a', 'x', '')]) == ({'a': 'x'}, []) + assert parseargs(['--a', 'x', '--a', 'y'], [('a', '', '')]) == ({'a': 'y'}, []) + try: + parseargs([], [('a', 'x', '')]) + except ValueError: + pass + try: + parseargs(['--a', 'x'], []) + except ValueError: + pass + try: + parseargs(['--a'], [('a', 'x', '')]) + except ValueError: + pass + try: + parseargs([], [], 1, 2) + except ValueError: + pass + assert parseargs(['x'], [], 1, 2) == ({}, ['x']) + assert parseargs(['x', 'y'], [], 1, 2) == ({}, ['x', 'y']) + try: + parseargs(['x', 'y', 'z'], [], 1, 2) + except ValueError: + pass + try: + parseargs(['--a', '2.0'], [('a', 3, '')]) + except ValueError: + pass + try: + parseargs(['--a', 'z'], [('a', 2.1, '')]) + except ValueError: + pass + diff --git a/BitTornado/parsedir.py b/BitTornado/parsedir.py new file mode 100644 index 000000000..74bec1078 --- /dev/null +++ b/BitTornado/parsedir.py @@ -0,0 +1,150 @@ +# Written by John Hoffman and Uoti Urpala +# see LICENSE.txt for license information +from bencode import bencode, bdecode +from BT1.btformats import check_info +from os.path import exists, isfile +from sha import sha +import sys, os + +try: + True +except: + True = 1 + False = 0 + +NOISY = False + +def _errfunc(x): + print ":: "+x + +def parsedir(directory, parsed, files, blocked, + exts = ['.torrent'], return_metainfo = False, errfunc = _errfunc): + if NOISY: + errfunc('checking dir') + dirs_to_check = [directory] + new_files = {} + new_blocked = {} + torrent_type = {} + while dirs_to_check: # first, recurse directories and gather torrents + directory = dirs_to_check.pop() + newtorrents = False + for f in os.listdir(directory): + newtorrent = None + for ext in exts: + if f.endswith(ext): + newtorrent = ext[1:] + break + if newtorrent: + newtorrents = True + p = os.path.join(directory, f) + new_files[p] = [(os.path.getmtime(p), os.path.getsize(p)), 0] + torrent_type[p] = newtorrent + if not newtorrents: + for f in os.listdir(directory): + p = os.path.join(directory, f) + if os.path.isdir(p): + dirs_to_check.append(p) + + new_parsed = {} + to_add = [] + added = {} + removed = {} + # files[path] = [(modification_time, size), hash], hash is 0 if the file + # has not been successfully parsed + for p,v in new_files.items(): # re-add old items and check for changes + oldval = files.get(p) + if not oldval: # new file + to_add.append(p) + continue + h = oldval[1] + if oldval[0] == v[0]: # file is unchanged from last parse + if h: + if blocked.has_key(p): # parseable + blocked means duplicate + to_add.append(p) # other duplicate may have gone away + else: + new_parsed[h] = parsed[h] + new_files[p] = oldval + else: + new_blocked[p] = 1 # same broken unparseable file + continue + if parsed.has_key(h) and not blocked.has_key(p): + if NOISY: + errfunc('removing '+p+' (will re-add)') + removed[h] = parsed[h] + to_add.append(p) + + to_add.sort() + for p in to_add: # then, parse new and changed torrents + new_file = new_files[p] + v,h = new_file + if new_parsed.has_key(h): # duplicate + if not blocked.has_key(p) or files[p][0] != v: + errfunc('**warning** '+ + p +' is a duplicate torrent for '+new_parsed[h]['path']) + new_blocked[p] = 1 + continue + + if NOISY: + errfunc('adding '+p) + try: + ff = open(p, 'rb') + d = bdecode(ff.read()) + check_info(d['info']) + h = sha(bencode(d['info'])).digest() + new_file[1] = h + if new_parsed.has_key(h): + errfunc('**warning** '+ + p +' is a duplicate torrent for '+new_parsed[h]['path']) + new_blocked[p] = 1 + continue + + a = {} + a['path'] = p + f = os.path.basename(p) + a['file'] = f + a['type'] = torrent_type[p] + i = d['info'] + l = 0 + nf = 0 + if i.has_key('length'): + l = i.get('length',0) + nf = 1 + elif i.has_key('files'): + for li in i['files']: + nf += 1 + if li.has_key('length'): + l += li['length'] + a['numfiles'] = nf + a['length'] = l + a['name'] = i.get('name', f) + def setkey(k, d = d, a = a): + if d.has_key(k): + a[k] = d[k] + setkey('failure reason') + setkey('warning message') + setkey('announce-list') + if return_metainfo: + a['metainfo'] = d + except: + errfunc('**warning** '+p+' has errors') + new_blocked[p] = 1 + continue + try: + ff.close() + except: + pass + if NOISY: + errfunc('... successful') + new_parsed[h] = a + added[h] = a + + for p,v in files.items(): # and finally, mark removed torrents + if not new_files.has_key(p) and not blocked.has_key(p): + if NOISY: + errfunc('removing '+p) + removed[v[1]] = parsed[v[1]] + + if NOISY: + errfunc('done checking') + return (new_parsed, new_files, new_blocked, added, removed) + diff --git a/BitTornado/piecebuffer.py b/BitTornado/piecebuffer.py new file mode 100644 index 000000000..1c4e6683c --- /dev/null +++ b/BitTornado/piecebuffer.py @@ -0,0 +1,86 @@ +# Written by John Hoffman +# see LICENSE.txt for license information + +from array import array +from threading import Lock +# import inspect +try: + True +except: + True = 1 + False = 0 + +DEBUG = False + +class SingleBuffer: + def __init__(self, pool): + self.pool = pool + self.buf = array('c') + + def init(self): + if DEBUG: + print self.count + ''' + for x in xrange(6,1,-1): + try: + f = inspect.currentframe(x).f_code + print (f.co_filename,f.co_firstlineno,f.co_name) + del f + except: + pass + print '' + ''' + self.length = 0 + + def append(self, s): + l = self.length+len(s) + self.buf[self.length:l] = array('c',s) + self.length = l + + def __len__(self): + return self.length + + def __getslice__(self, a, b): + if b > self.length: + b = self.length + if b < 0: + b += self.length + if a == 0 and b == self.length and len(self.buf) == b: + return self.buf # optimization + return self.buf[a:b] + + def getarray(self): + return self.buf[:self.length] + + def release(self): + if DEBUG: + print -self.count + self.pool.release(self) + + +class BufferPool: + def __init__(self): + self.pool = [] + self.lock = Lock() + if DEBUG: + self.count = 0 + + def new(self): + self.lock.acquire() + if self.pool: + x = self.pool.pop() + else: + x = SingleBuffer(self) + if DEBUG: + self.count += 1 + x.count = self.count + x.init() + self.lock.release() + return x + + def release(self, x): + self.pool.append(x) + + +_pool = BufferPool() +PieceBuffer = _pool.new diff --git a/BitTornado/selectpoll.py b/BitTornado/selectpoll.py new file mode 100644 index 000000000..4703c4fc2 --- /dev/null +++ b/BitTornado/selectpoll.py @@ -0,0 +1,109 @@ +# Written by Bram Cohen +# see LICENSE.txt for license information + +from select import select, error +from time import sleep +from types import IntType +from bisect import bisect +POLLIN = 1 +POLLOUT = 2 +POLLERR = 8 +POLLHUP = 16 + +class poll: + def __init__(self): + self.rlist = [] + self.wlist = [] + + def register(self, f, t): + if type(f) != IntType: + f = f.fileno() + if (t & POLLIN): + insert(self.rlist, f) + else: + remove(self.rlist, f) + if (t & POLLOUT): + insert(self.wlist, f) + else: + remove(self.wlist, f) + + def unregister(self, f): + if type(f) != IntType: + f = f.fileno() + remove(self.rlist, f) + remove(self.wlist, f) + + def poll(self, timeout = None): + if self.rlist or self.wlist: + try: + r, w, e = select(self.rlist, self.wlist, [], timeout) + except ValueError: + return None + else: + sleep(timeout) + return [] + result = [] + for s in r: + result.append((s, POLLIN)) + for s in w: + result.append((s, POLLOUT)) + return result + +def remove(list, item): + i = bisect(list, item) + if i > 0 and list[i-1] == item: + del list[i-1] + +def insert(list, item): + i = bisect(list, item) + if i == 0 or list[i-1] != item: + list.insert(i, item) + +def test_remove(): + x = [2, 4, 6] + remove(x, 2) + assert x == [4, 6] + x = [2, 4, 6] + remove(x, 4) + assert x == [2, 6] + x = [2, 4, 6] + remove(x, 6) + assert x == [2, 4] + x = [2, 4, 6] + remove(x, 5) + assert x == [2, 4, 6] + x = [2, 4, 6] + remove(x, 1) + assert x == [2, 4, 6] + x = [2, 4, 6] + remove(x, 7) + assert x == [2, 4, 6] + x = [2, 4, 6] + remove(x, 5) + assert x == [2, 4, 6] + x = [] + remove(x, 3) + assert x == [] + +def test_insert(): + x = [2, 4] + insert(x, 1) + assert x == [1, 2, 4] + x = [2, 4] + insert(x, 3) + assert x == [2, 3, 4] + x = [2, 4] + insert(x, 5) + assert x == [2, 4, 5] + x = [2, 4] + insert(x, 2) + assert x == [2, 4] + x = [2, 4] + insert(x, 4) + assert x == [2, 4] + x = [2, 3, 4] + insert(x, 3) + assert x == [2, 3, 4] + x = [] + insert(x, 3) + assert x == [3] diff --git a/BitTornado/subnetparse.py b/BitTornado/subnetparse.py new file mode 100644 index 000000000..1b7378765 --- /dev/null +++ b/BitTornado/subnetparse.py @@ -0,0 +1,218 @@ +# Written by John Hoffman +# see LICENSE.txt for license information + +from bisect import bisect, insort + +try: + True +except: + True = 1 + False = 0 + bool = lambda x: not not x + +hexbinmap = { + '0': '0000', + '1': '0001', + '2': '0010', + '3': '0011', + '4': '0100', + '5': '0101', + '6': '0110', + '7': '0111', + '8': '1000', + '9': '1001', + 'a': '1010', + 'b': '1011', + 'c': '1100', + 'd': '1101', + 'e': '1110', + 'f': '1111', + 'x': '0000', +} + +chrbinmap = {} +for n in xrange(256): + b = [] + nn = n + for i in xrange(8): + if nn & 0x80: + b.append('1') + else: + b.append('0') + nn <<= 1 + chrbinmap[n] = ''.join(b) + + +def to_bitfield_ipv4(ip): + ip = ip.split('.') + if len(ip) != 4: + raise ValueError, "bad address" + b = [] + for i in ip: + b.append(chrbinmap[int(i)]) + return ''.join(b) + +def to_bitfield_ipv6(ip): + b = '' + doublecolon = False + + if ip == '': + raise ValueError, "bad address" + if ip == '::': # boundary handling + ip = '' + elif ip[:2] == '::': + ip = ip[1:] + elif ip[0] == ':': + raise ValueError, "bad address" + elif ip[-2:] == '::': + ip = ip[:-1] + elif ip[-1] == ':': + raise ValueError, "bad address" + for n in ip.split(':'): + if n == '': # double-colon + if doublecolon: + raise ValueError, "bad address" + doublecolon = True + b += ':' + continue + if n.find('.') >= 0: # IPv4 + n = to_bitfield_ipv4(n) + b += n + '0'*(32-len(n)) + continue + n = ('x'*(4-len(n))) + n + for i in n: + b += hexbinmap[i] + if doublecolon: + pos = b.find(':') + b = b[:pos]+('0'*(129-len(b)))+b[pos+1:] + if len(b) != 128: # always check size + raise ValueError, "bad address" + return b + +ipv4addrmask = to_bitfield_ipv6('::ffff:0:0')[:96] + +class IP_List: + def __init__(self): + self.ipv4list = [] + self.ipv6list = [] + + def __nonzero__(self): + return bool(self.ipv4list or self.ipv6list) + + + def append(self, ip, depth = 256): + if ip.find(':') < 0: # IPv4 + insort(self.ipv4list,to_bitfield_ipv4(ip)[:depth]) + else: + b = to_bitfield_ipv6(ip) + if b.startswith(ipv4addrmask): + insort(self.ipv4list,b[96:][:depth-96]) + else: + insort(self.ipv6list,b[:depth]) + + + def includes(self, ip): + if not (self.ipv4list or self.ipv6list): + return False + if ip.find(':') < 0: # IPv4 + b = to_bitfield_ipv4(ip) + else: + b = to_bitfield_ipv6(ip) + if b.startswith(ipv4addrmask): + b = b[96:] + if len(b) > 32: + l = self.ipv6list + else: + l = self.ipv4list + for map in l[bisect(l,b)-1:]: + if b.startswith(map): + return True + if map > b: + return False + return False + + + def read_fieldlist(self, file): # reads a list from a file in the format 'ip/len ' + f = open(file, 'r') + while True: + line = f.readline() + if not line: + break + line = line.strip().expandtabs() + if not line or line[0] == '#': + continue + try: + line, garbage = line.split(' ',1) + except: + pass + try: + line, garbage = line.split('#',1) + except: + pass + try: + ip, depth = line.split('/') + except: + ip = line + depth = None + try: + if depth is not None: + depth = int(depth) + self.append(ip,depth) + except: + print '*** WARNING *** could not parse IP range: '+line + f.close() + + + def set_intranet_addresses(self): + self.append('127.0.0.1',8) + self.append('10.0.0.0',8) + self.append('172.16.0.0',12) + self.append('192.168.0.0',16) + self.append('169.254.0.0',16) + self.append('::1') + self.append('fe80::',16) + self.append('fec0::',16) + + def set_ipv4_addresses(self): + self.append('::ffff:0:0',96) + +def ipv6_to_ipv4(ip): + ip = to_bitfield_ipv6(ip) + if not ip.startswith(ipv4addrmask): + raise ValueError, "not convertible to IPv4" + ip = ip[-32:] + x = '' + for i in range(4): + x += str(int(ip[:8],2)) + if i < 3: + x += '.' + ip = ip[8:] + return x + +def to_ipv4(ip): + if is_ipv4(ip): + _valid_ipv4(ip) + return ip + return ipv6_to_ipv4(ip) + +def is_ipv4(ip): + return ip.find(':') < 0 + +def _valid_ipv4(ip): + ip = ip.split('.') + if len(ip) != 4: + raise ValueError + for i in ip: + chr(int(i)) + +def is_valid_ip(ip): + try: + if not ip: + return False + if is_ipv4(ip): + _valid_ipv4(ip) + return True + to_bitfield_ipv6(ip) + return True + except: + return False diff --git a/BitTornado/torrentlistparse.py b/BitTornado/torrentlistparse.py new file mode 100644 index 000000000..068209b8a --- /dev/null +++ b/BitTornado/torrentlistparse.py @@ -0,0 +1,38 @@ +# Written by John Hoffman +# see LICENSE.txt for license information + +from binascii import unhexlify + +try: + True +except: + True = 1 + False = 0 + + +# parses a list of torrent hashes, in the format of one hash per line in hex format + +def parsetorrentlist(filename, parsed): + new_parsed = {} + added = {} + removed = parsed + f = open(filename, 'r') + while True: + l = f.readline() + if not l: + break + l = l.strip() + try: + if len(l) != 40: + raise ValueError, 'bad line' + h = unhexlify(l) + except: + print '*** WARNING *** could not parse line in torrent list: '+l + if parsed.has_key(h): + del removed[h] + else: + added[h] = True + new_parsed[h] = True + f.close() + return (new_parsed, added, removed) + diff --git a/BitTornado/zurllib.py b/BitTornado/zurllib.py new file mode 100644 index 000000000..f0d5f2821 --- /dev/null +++ b/BitTornado/zurllib.py @@ -0,0 +1,100 @@ +# Written by John Hoffman +# see LICENSE.txt for license information + +from httplib import HTTPConnection, HTTPSConnection, HTTPException +from urlparse import urlparse +from bencode import bdecode +import socket +from gzip import GzipFile +from StringIO import StringIO +from urllib import quote, unquote +from __init__ import product_name, version_short + +VERSION = product_name+'/'+version_short +MAX_REDIRECTS = 10 + + +class btHTTPcon(HTTPConnection): # attempt to add automatic connection timeout + def connect(self): + HTTPConnection.connect(self) + try: + self.sock.settimeout(30) + except: + pass + +class btHTTPScon(HTTPSConnection): # attempt to add automatic connection timeout + def connect(self): + HTTPSConnection.connect(self) + try: + self.sock.settimeout(30) + except: + pass + +class urlopen: + def __init__(self, url): + self.tries = 0 + self._open(url.strip()) + self.error_return = None + + def _open(self, url): + self.tries += 1 + if self.tries > MAX_REDIRECTS: + raise IOError, ('http error', 500, + "Internal Server Error: Redirect Recursion") + (scheme, netloc, path, pars, query, fragment) = urlparse(url) + if scheme != 'http' and scheme != 'https': + raise IOError, ('url error', 'unknown url type', scheme, url) + url = path + if pars: + url += ';'+pars + if query: + url += '?'+query +# if fragment: + try: + if scheme == 'http': + self.connection = btHTTPcon(netloc) + else: + self.connection = btHTTPScon(netloc) + self.connection.request('GET', url, None, + { 'User-Agent': VERSION, + 'Accept-Encoding': 'gzip' } ) + self.response = self.connection.getresponse() + except HTTPException, e: + raise IOError, ('http error', str(e)) + status = self.response.status + if status in (301,302): + try: + self.connection.close() + except: + pass + self._open(self.response.getheader('Location')) + return + if status != 200: + try: + data = self._read() + d = bdecode(data) + if d.has_key('failure reason'): + self.error_return = data + return + except: + pass + raise IOError, ('http error', status, self.response.reason) + + def read(self): + if self.error_return: + return self.error_return + return self._read() + + def _read(self): + data = self.response.read() + if self.response.getheader('Content-Encoding','').find('gzip') >= 0: + try: + compressed = StringIO(data) + f = GzipFile(fileobj = compressed) + data = f.read() + except: + raise IOError, ('http error', 'got corrupt response') + return data + + def close(self): + self.connection.close() diff --git a/LICENSE b/LICENSE index 9cc00e905..cf24eae57 100644 --- a/LICENSE +++ b/LICENSE @@ -2,7 +2,7 @@ * * Armory -- Bitcoin Wallet Software * -* Copyright (C) 2011-2013, Armory Technologies, Inc. +* Copyright (C) 2011-2014, Armory Technologies, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as diff --git a/LICENSE.py b/LICENSE.py index 913335050..d01296b88 100644 --- a/LICENSE.py +++ b/LICENSE.py @@ -4,7 +4,7 @@ def licenseText(): * * * Armory -- Advanced Bitcoin Wallet Software * * * -* Copyright (C) 2011-2013, Armory Technologies, Inc. * +* Copyright (C) 2011-2014, Armory Technologies, Inc. * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU Affero General Public License as * @@ -33,6 +33,7 @@ def licenseText(): qtreactor4.py qrcodenative.py jsonrpc/* + bittornado/* Everything in the cryptopp directory is considered public domain according to http://www.cryptopp.com/ and included with the source distribution diff --git a/Makefile b/Makefile index e13aba02e..6826b2c96 100644 --- a/Makefile +++ b/Makefile @@ -1,30 +1,38 @@ # All the actual Makefiles are deeper in the directory tree. # I am just calling them, here. -DESTDIR=/usr +PREFIX=/usr +DESTDIR= all : - $(MAKE) -C cppForSwig swig + $(MAKE) -C cppForSwig clean : $(MAKE) -C cppForSwig clean - rm -rf osxbuild/Armory.app - rm -rf osxbuild/env + rm -f osxbuild/build-app.log.txt + rm -rf osxbuild/workspace/ install : all - mkdir -p $(DESTDIR)/share/armory/img - mkdir -p $(DESTDIR)/lib/armory/extras - mkdir -p $(DESTDIR)/lib/armory/jsonrpc - mkdir -p $(DESTDIR)/lib/armory/dialogs - cp *.py *.so README $(DESTDIR)/lib/armory/ - cp img/* $(DESTDIR)/share/armory/img - cp extras/*.py $(DESTDIR)/lib/armory/extras - cp jsonrpc/*.py $(DESTDIR)/lib/armory/jsonrpc - cp dialogs/*.py $(DESTDIR)/lib/armory/dialogs - mkdir -p $(DESTDIR)/share/applications - sed "s:python /usr:python $(DESTDIR):g" < dpkgfiles/armory.desktop > $(DESTDIR)/share/applications/armory.desktop - sed "s:python /usr:python $(DESTDIR):g" < dpkgfiles/armoryoffline.desktop > $(DESTDIR)/share/applications/armoryoffline.desktop - sed "s:python /usr:python $(DESTDIR):g" < dpkgfiles/armorytestnet.desktop > $(DESTDIR)/share/applications/armorytestnet.desktop + mkdir -p $(DESTDIR)$(PREFIX)/share/armory/img + mkdir -p $(DESTDIR)$(PREFIX)/lib/armory/extras + mkdir -p $(DESTDIR)$(PREFIX)/lib/armory/jsonrpc + mkdir -p $(DESTDIR)$(PREFIX)/lib/armory/ui + mkdir -p $(DESTDIR)$(PREFIX)/lib/armory/BitTornado/BT1 + mkdir -p $(DESTDIR)$(PREFIX)/lib/armory/urllib3 + cp *.py *.so README $(DESTDIR)$(PREFIX)/lib/armory/ + rsync -rupE armoryengine $(DESTDIR)$(PREFIX)/lib/armory/ + rsync -rupE img $(DESTDIR)$(PREFIX)/share/armory/ + cp extras/*.py $(DESTDIR)$(PREFIX)/lib/armory/extras + cp jsonrpc/*.py $(DESTDIR)$(PREFIX)/lib/armory/jsonrpc + cp ui/*.py $(DESTDIR)$(PREFIX)/lib/armory/ui + cp -r urllib3/* $(DESTDIR)$(PREFIX)/lib/armory/urllib3 + mkdir -p $(DESTDIR)$(PREFIX)/share/applications + cp BitTornado/*.py $(DESTDIR)$(PREFIX)/lib/armory/BitTornado + cp BitTornado/BT1/*.py $(DESTDIR)$(PREFIX)/lib/armory/BitTornado/BT1 + cp default_bootstrap.torrent $(DESTDIR)$(PREFIX)/lib/armory + sed "s:python /usr:python $(PREFIX):g" < dpkgfiles/armory.desktop > $(DESTDIR)$(PREFIX)/share/applications/armory.desktop + sed "s:python /usr:python $(PREFIX):g" < dpkgfiles/armoryoffline.desktop > $(DESTDIR)$(PREFIX)/share/applications/armoryoffline.desktop + sed "s:python /usr:python $(PREFIX):g" < dpkgfiles/armorytestnet.desktop > $(DESTDIR)$(PREFIX)/share/applications/armorytestnet.desktop osx : diff --git a/README b/README index 0a6ffaba5..bf2764ee9 100644 --- a/README +++ b/README @@ -1,6 +1,6 @@ ################################################################################ # # -# Copyright (C) 2011-2013, Alan C. Reiner # +# Copyright (C) 2011-2014, Armory Technologies, Inc. # # Distributed under the GNU Affero General Public License (AGPL v3) # # See LICENSE or http://www.gnu.org/licenses/agpl.html # # # diff --git a/SDM.py b/SDM.py new file mode 100644 index 000000000..9371ce88d --- /dev/null +++ b/SDM.py @@ -0,0 +1,912 @@ +################################################################################ +# # +# Copyright (C) 2011-2014, Armory Technologies, Inc. # +# Distributed under the GNU Affero General Public License (AGPL v3) # +# See LICENSE or http://www.gnu.org/licenses/agpl.html # +# # +################################################################################ +import inspect +import os.path +import socket +import stat +import time +from threading import Event +from jsonrpc import ServiceProxy +from CppBlockUtils import SecureBinaryData, CryptoECDSA +from armoryengine.ArmoryUtils import BITCOIN_PORT, LOGERROR, hex_to_binary, \ + ARMORY_INFO_SIGN_PUBLICKEY, LOGINFO, BTC_HOME_DIR, LOGDEBUG, OS_WINDOWS, \ + SystemSpecs, subprocess_check_output, LOGEXCEPT, FileExistsError, OS_VARIANT, \ + BITCOIN_RPC_PORT, binary_to_base58, isASCII, USE_TESTNET, GIGABYTE, \ + launchProcess, killProcessTree, killProcess, LOGWARN, RightNow, HOUR, \ + PyBackgroundThread, touchFile, DISABLE_TORRENTDL, secondsToHumanTime, \ + bytesToHumanSize, MAGIC_BYTES, deleteBitcoindDBs, TheTDM +from jsonrpc import authproxy + + +############################################################################# +def satoshiIsAvailable(host='127.0.0.1', port=BITCOIN_PORT, timeout=0.01): + + if not isinstance(port, (list,tuple)): + port = [port] + + for p in port: + s = socket.socket() + s.settimeout(timeout) # Most of the time checking localhost -- FAST + try: + s.connect((host, p)) + s.close() + return p + except: + pass + + return 0 + + +################################################################################ +def extractSignedDataFromVersionsDotTxt(wholeFile, doVerify=True): + """ + This method returns a pair: a dictionary to lookup link by OS, and + a formatted string that is sorted by OS, and re-formatted list that + will hash the same regardless of original format or ordering + """ + + msgBegin = wholeFile.find('# -----BEGIN-SIGNED-DATA-') + msgBegin = wholeFile.find('\n', msgBegin+1) + 1 + msgEnd = wholeFile.find('# -----SIGNATURE---------') + sigBegin = wholeFile.find('\n', msgEnd+1) + 3 + sigEnd = wholeFile.find('# -----END-SIGNED-DATA---') + + MSGRAW = wholeFile[msgBegin:msgEnd] + SIGHEX = wholeFile[sigBegin:sigEnd].strip() + + if -1 in [msgBegin,msgEnd,sigBegin,sigEnd]: + LOGERROR('No signed data block found') + return '' + + + if doVerify: + Pub = SecureBinaryData(hex_to_binary(ARMORY_INFO_SIGN_PUBLICKEY)) + Msg = SecureBinaryData(MSGRAW) + Sig = SecureBinaryData(hex_to_binary(SIGHEX)) + isVerified = CryptoECDSA().VerifyData(Msg, Sig, Pub) + + if not isVerified: + LOGERROR('Signed data block failed verification!') + return '' + else: + LOGINFO('Signature on signed data block is GOOD!') + + return MSGRAW + + +################################################################################ +def parseLinkList(theData): + """ + Plug the verified data into here... + """ + DLDICT,VERDICT = {},{} + sectStr = None + for line in theData.split('\n'): + pcs = line[1:].split() + if line.startswith('# SECTION-') and 'INSTALLERS' in line: + sectStr = pcs[0].split('-')[-1] + if not sectStr in DLDICT: + DLDICT[sectStr] = {} + VERDICT[sectStr] = '' + if len(pcs)>1: + VERDICT[sectStr] = pcs[-1] + continue + + if len(pcs)==3 and pcs[1].startswith('http'): + DLDICT[sectStr][pcs[0]] = pcs[1:] + + return DLDICT,VERDICT + + + + + +################################################################################ +# jgarzik'sjj jsonrpc-bitcoin code -- stupid-easy to talk to bitcoind +class SatoshiDaemonManager(object): + """ + Use an existing implementation of bitcoind + """ + + class BitcoindError(Exception): pass + class BitcoindNotAvailableError(Exception): pass + class BitcoinDotConfError(Exception): pass + class SatoshiHomeDirDNE(Exception): pass + class ConfigFileUserDNE(Exception): pass + class ConfigFilePwdDNE(Exception): pass + + + ############################################################################# + def __init__(self): + self.executable = None + self.satoshiHome = None + self.bitconf = {} + self.proxy = None + self.bitcoind = None + self.isMidQuery = False + self.last20queries = [] + self.disabled = False + self.failedFindExe = False + self.failedFindHome = False + self.foundExe = [] + self.circBufferState = [] + self.circBufferTime = [] + self.btcOut = None + self.btcErr = None + self.lastTopBlockInfo = { \ + 'numblks': -1, + 'tophash': '', + 'toptime': -1, + 'error': 'Uninitialized', + 'blkspersec': -1 } + + # Added torrent DL before we *actually* start SDM (if it makes sense) + self.useTorrentFinalAnswer = False + self.useTorrentFile = '' + self.torrentDisabled = False + self.tdm = None + self.satoshiHome = None + + + ############################################################################# + def setSatoshiDir(self, newDir): + self.satoshiHome = newDir + + ############################################################################# + def setDisableTorrentDL(self, b): + self.torrentDisabled = b + + ############################################################################# + def tryToSetupTorrentDL(self, torrentPath): + if self.torrentDisabled: + LOGWARN('Tried to setup torrent download mgr but we are disabled') + return False + + if not torrentPath or not os.path.exists(torrentPath): + self.useTorrentFinalAnswer = False + return False + + bootfile = os.path.join(self.satoshiHome, 'bootstrap.dat') + TheTDM.setupTorrent(torrentPath, bootfile) + if not TheTDM.getTDMState()=='ReadyToStart': + LOGERROR('Unknown error trying to start torrent manager') + self.useTorrentFinalAnswer = False + return False + + + # We will tell the TDM to write status updates to the log file, and only + # every 90 seconds. After it finishes (or fails), simply launch bitcoind + # as we would've done without the torrent + ##### + def torrentLogToFile(dpflag=Event(), fractionDone=None, timeEst=None, + downRate=None, upRate=None, activity=None, + statistics=None, **kws): + statStr = '' + if fractionDone: + statStr += ' Done: %0.1f%% ' % (fractionDone*100) + if downRate: + statStr += ' / DLRate: %0.1f/sec' % (downRate/1024.) + if timeEst: + statStr += ' / TLeft: %s' % secondsToHumanTime(timeEst) + if statistics: + statStr += ' / Seeds: %d' % (statistics.numSeeds) + statStr += ' / Peers: %d' % (statistics.numPeers) + + if len(statStr)==0: + statStr = 'No torrent info available' + + LOGINFO('Torrent: %s' % statStr) + + ##### + def torrentFinished(): + bootsz = '' + if os.path.exists(bootfile): + bootsz = bytesToHumanSize(os.path.getsize(bootfile)) + + LOGINFO('Torrent finished; size of %s is %s', torrentPath, bootsz) + LOGINFO('Remove the core btc databases before doing bootstrap') + deleteBitcoindDBs() + self.launchBitcoindAndGuardian() + + ##### + def torrentFailed(): + # Not sure there's actually anything we need to do here... + bootsz = '' + if os.path.exists(bootfile): + bootsz = bytesToHumanSize(os.path.getsize(bootfile)) + + LOGERROR('Torrent failed; size of %s is %s', torrentPath, bootsz) + self.launchBitcoindAndGuardian() + + TheTDM.setSecondsBetweenUpdates(90) + TheTDM.setCallback('displayFunc', torrentLogToFile) + TheTDM.setCallback('finishedFunc', torrentFinished) + TheTDM.setCallback('failedFunc', torrentFailed) + + LOGINFO('Bootstrap file is %s' % bytesToHumanSize(TheTDM.torrentSize)) + + self.useTorrentFinalAnswer = True + self.useTorrentFile = torrentPath + return True + + + ############################################################################# + def shouldTryBootstrapTorrent(self): + if DISABLE_TORRENTDL or TheTDM.getTDMState()=='Disabled': + return False + + # The only torrent we have is for the primary Bitcoin network + if not MAGIC_BYTES=='\xf9\xbe\xb4\xd9': + return False + + + + if TheTDM.torrentSize: + bootfile = os.path.join(self.satoshiHome, 'bootstrap.dat') + if os.path.exists(bootfile): + if os.path.getsize(bootfile) >= TheTDM.torrentSize/2: + LOGWARN('Looks like a full bootstrap is already here') + LOGWARN('Skipping torrent download') + return False + + + # If they don't even have a BTC_HOME_DIR, corebtc never been installed + blockDir = os.path.join(self.satoshiHome, 'blocks') + if not os.path.exists(self.satoshiHome) or not os.path.exists(blockDir): + return True + + # Get the cumulative size of the blk*.dat files + blockDirSize = sum([os.path.getsize(os.path.join(blockDir, a)) \ + for a in os.listdir(blockDir) if a.startswith('blk')]) + sizeStr = bytesToHumanSize(blockDirSize) + LOGINFO('Total size of files in %s is %s' % (blockDir, sizeStr)) + + # If they have only a small portion of the blockchain, do it + szThresh = 100*MEGABYTE if USE_TESTNET else 6*GIGABYTE + if blockDirSize < szThresh: + return True + + # So far we know they have a BTC_HOME_DIR, with more than 6GB in blocks/ + # The only thing that can induce torrent now is if we have a partially- + # finished bootstrap file bigger than the blocks dir. + bootFiles = ['',''] + bootFiles[0] = os.path.join(self.satoshiHome, 'bootstrap.dat') + bootFiles[1] = os.path.join(self.satoshiHome, 'bootstrap.dat.partial') + for fn in bootFiles: + if os.path.exists(fn): + if os.path.getsize(fn) > blockDirSize: + return True + + # Okay, we give up -- just download [the rest] via P2P + return False + + + ############################################################################# + #def setSatoshiDir(self, newDir): + #self.satoshiHome = newDir + + ############################################################################# + def setupSDM(self, pathToBitcoindExe=None, satoshiHome=None, \ + extraExeSearch=[], createHomeIfDNE=True): + LOGDEBUG('Exec setupSDM') + self.failedFindExe = False + self.failedFindHome = False + # If we are supplied a path, then ignore the extra exe search paths + if pathToBitcoindExe==None: + pathToBitcoindExe = self.findBitcoind(extraExeSearch) + if len(pathToBitcoindExe)==0: + LOGDEBUG('Failed to find bitcoind') + self.failedFindExe = True + else: + LOGINFO('Found bitcoind in the following places:') + for p in pathToBitcoindExe: + LOGINFO(' %s', p) + pathToBitcoindExe = pathToBitcoindExe[0] + LOGINFO('Using: %s', pathToBitcoindExe) + + if not os.path.exists(pathToBitcoindExe): + LOGINFO('Somehow failed to find exe even after finding it...?') + self.failedFindExe = True + + self.executable = pathToBitcoindExe + + # Four possible conditions for already-set satoshi home dir, and input arg + if satoshiHome is not None: + self.satoshiHome = satoshiHome + else: + if self.satoshiHome is None: + self.satoshiHome = BTC_HOME_DIR + + # If no new dir is specified, leave satoshi home if it's already set + # Give it a default BTC_HOME_DIR if not. + if not os.path.exists(self.satoshiHome): + if createHomeIfDNE: + LOGINFO('Making satoshi home dir') + os.makedirs(self.satoshiHome) + else: + LOGINFO('No home dir, makedir not requested') + self.failedFindHome = True + + if self.failedFindExe: raise self.BitcoindError, 'bitcoind not found' + if self.failedFindHome: raise self.BitcoindError, 'homedir not found' + + self.disabled = False + self.proxy = None + self.bitcoind = None # this will be a Popen object + self.isMidQuery = False + self.last20queries = [] + + self.readBitcoinConf(makeIfDNE=True) + + + + + + ############################################################################# + def setDisabled(self, newBool=True): + s = self.getSDMState() + + if newBool==True: + if s in ('BitcoindInitializing', 'BitcoindSynchronizing', 'BitcoindReady'): + self.stopBitcoind() + + self.disabled = newBool + + + ############################################################################# + def getAllFoundExe(self): + return list(self.foundExe) + + + ############################################################################# + def findBitcoind(self, extraSearchPaths=[]): + self.foundExe = [] + + searchPaths = list(extraSearchPaths) # create a copy + + if OS_WINDOWS: + # Making sure the search path argument comes with /daemon and /Bitcoin on Windows + + searchPaths.extend([os.path.join(sp, 'Bitcoin') for sp in searchPaths]) + searchPaths.extend([os.path.join(sp, 'daemon') for sp in searchPaths]) + + possBaseDir = [] + + from platform import machine + if '64' in machine(): + possBaseDir.append(os.getenv("ProgramW6432")) + possBaseDir.append(os.getenv('PROGRAMFILES(X86)')) + else: + possBaseDir.append(os.getenv('PROGRAMFILES')) + + # check desktop for links + + home = os.path.expanduser('~') + desktop = os.path.join(home, 'Desktop') + + if os.path.exists(desktop): + dtopfiles = os.listdir(desktop) + for path in [os.path.join(desktop, fn) for fn in dtopfiles]: + if 'bitcoin' in path.lower() and path.lower().endswith('.lnk'): + import win32com.client + shell = win32com.client.Dispatch('WScript.Shell') + targ = shell.CreateShortCut(path).Targetpath + targDir = os.path.dirname(targ) + LOGINFO('Found Bitcoin-Qt link on desktop: %s', targDir) + possBaseDir.append( targDir ) + + # Also look in default place in ProgramFiles dirs + + + + + # Now look at a few subdirs of the + searchPaths.extend(possBaseDir) + searchPaths.extend([os.path.join(p, 'Bitcoin', 'daemon') for p in possBaseDir]) + searchPaths.extend([os.path.join(p, 'daemon') for p in possBaseDir]) + searchPaths.extend([os.path.join(p, 'Bitcoin') for p in possBaseDir]) + + for p in searchPaths: + testPath = os.path.join(p, 'bitcoind.exe') + if os.path.exists(testPath): + self.foundExe.append(testPath) + + else: + # In case this was a downloaded copy, make sure we traverse to bin/64 dir + if SystemSpecs.IsX64: + searchPaths.extend([os.path.join(p, 'bin/64') for p in extraSearchPaths]) + else: + searchPaths.extend([os.path.join(p, 'bin/32') for p in extraSearchPaths]) + + searchPaths.extend(['/usr/bin/', '/usr/lib/bitcoin/']) + + for p in searchPaths: + testPath = os.path.join(p, 'bitcoind') + if os.path.exists(testPath): + self.foundExe.append(testPath) + + try: + locs = subprocess_check_output(['whereis','bitcoind']).split() + if len(locs)>1: + locs = filter(lambda x: os.path.basename(x)=='bitcoind', locs) + LOGINFO('"whereis" returned: %s', str(locs)) + self.foundExe.extend(locs) + except: + LOGEXCEPT('Error executing "whereis" command') + + + # For logging purposes, check that the first answer matches one of the + # extra search paths. There should be some kind of notification that + # their supplied search path was invalid and we are using something else. + if len(self.foundExe)>0 and len(extraSearchPaths)>0: + foundIt = False + for p in extraSearchPaths: + if self.foundExe[0].startswith(p): + foundIt=True + + if not foundIt: + LOGERROR('Bitcoind could not be found in the specified installation:') + for p in extraSearchPaths: + LOGERROR(' %s', p) + LOGERROR('Bitcoind is being started from:') + LOGERROR(' %s', self.foundExe[0]) + + return self.foundExe + + ############################################################################# + def getGuardianPath(self): + if OS_WINDOWS: + armoryInstall = os.path.dirname(inspect.getsourcefile(SatoshiDaemonManager)) + # This should return a zip file because of py2exe + if armoryInstall.endswith('.zip'): + armoryInstall = os.path.dirname(armoryInstall) + gpath = os.path.join(armoryInstall, 'guardian.exe') + else: + theDir = os.path.dirname(inspect.getsourcefile(SatoshiDaemonManager)) + gpath = os.path.join(theDir, 'guardian.py') + + if not os.path.exists(gpath): + LOGERROR('Could not find guardian script: %s', gpath) + raise FileExistsError + return gpath + + ############################################################################# + def readBitcoinConf(self, makeIfDNE=False): + LOGINFO('Reading bitcoin.conf file') + bitconf = os.path.join( self.satoshiHome, 'bitcoin.conf' ) + if not os.path.exists(bitconf): + if not makeIfDNE: + raise self.BitcoinDotConfError, 'Could not find bitcoin.conf' + else: + LOGINFO('No bitcoin.conf available. Creating it...') + touchFile(bitconf) + + # Guarantee that bitcoin.conf file has very strict permissions + if OS_WINDOWS: + if OS_VARIANT[0].lower()=='xp': + LOGERROR('Cannot set permissions correctly in XP!') + LOGERROR('Please confirm permissions on the following file ') + LOGERROR('are set to exclusive access only for your user ') + LOGERROR('(it usually is, but Armory cannot guarantee it ') + LOGERROR('on XP systems):') + LOGERROR(' %s', bitconf) + else: + LOGINFO('Setting permissions on bitcoin.conf') + import win32api + username = win32api.GetUserName() + LOGINFO('Setting permissions on bitcoin.conf') + cmd_icacls = ['icacls',bitconf,'/inheritance:r','/grant:r', '%s:F' % username] + icacls_out = subprocess_check_output(cmd_icacls, shell=True) + LOGINFO('icacls returned: %s', icacls_out) + else: + LOGINFO('Setting permissions on bitcoin.conf') + os.chmod(bitconf, stat.S_IRUSR | stat.S_IWUSR) + + + with open(bitconf,'r') as f: + # Find the last character of the each line: either a newline or '#' + endchr = lambda line: line.find('#') if line.find('#')>1 else len(line) + + # Reduce each line to a list of key,value pairs separated with '=' + allconf = [l[:endchr(l)].strip().split('=') for l in f.readlines()] + + # Need to convert to (x[0],x[1:]) in case the password has '=' in it + allconfPairs = [[x[0], '='.join(x[1:])] for x in allconf if len(x)>1] + + # Convert the list of pairs to a dictionary + self.bitconf = dict(allconfPairs) + + + # Look for rpcport, use default if not there + self.bitconf['rpcport'] = int(self.bitconf.get('rpcport', BITCOIN_RPC_PORT)) + + # We must have a username and password. If not, append to file + if not self.bitconf.has_key('rpcuser'): + LOGDEBUG('No rpcuser: creating one') + with open(bitconf,'a') as f: + f.write('\n') + f.write('rpcuser=generated_by_armory\n') + self.bitconf['rpcuser'] = 'generated_by_armory' + + if not self.bitconf.has_key('rpcpassword'): + LOGDEBUG('No rpcpassword: creating one') + with open(bitconf,'a') as f: + randBase58 = SecureBinaryData().GenerateRandom(32).toBinStr() + randBase58 = binary_to_base58(randBase58) + f.write('\n') + f.write('rpcpassword=%s' % randBase58) + self.bitconf['rpcpassword'] = randBase58 + + + if not isASCII(self.bitconf['rpcuser']): + LOGERROR('Non-ASCII character in bitcoin.conf (rpcuser)!') + if not isASCII(self.bitconf['rpcpassword']): + LOGERROR('Non-ASCII character in bitcoin.conf (rpcpassword)!') + + self.bitconf['host'] = '127.0.0.1' + + + ############################################################################# + def cleanupFailedTorrent(self): + # Right now I think don't do anything + pass + + ############################################################################# + def startBitcoind(self): + self.btcOut, self.btcErr = None,None + if self.disabled: + LOGERROR('SDM was disabled, must be re-enabled before starting') + return + + LOGINFO('Called startBitcoind') + + if self.isRunningBitcoind() or TheTDM.getTDMState()=='Downloading': + raise self.BitcoindError, 'Looks like we have already started theSDM' + + if not os.path.exists(self.executable): + raise self.BitcoindError, 'Could not find bitcoind' + + + chk1 = os.path.exists(self.useTorrentFile) + chk2 = self.shouldTryBootstrapTorrent() + chk3 = TheTDM.getTDMState()=='ReadyToStart' + + if chk1 and chk2 and chk3: + TheTDM.startDownload() + else: + self.launchBitcoindAndGuardian() + + + + ############################################################################# + def launchBitcoindAndGuardian(self): + + pargs = [self.executable] + + if USE_TESTNET: + testhome = self.satoshiHome[:] + if self.satoshiHome.endswith('/testnet3/'): + pargs.append('-datadir=%s' % self.satoshiHome[:-10]) + elif self.satoshiHome.endswith('/testnet3'): + pargs.append('-datadir=%s' % self.satoshiHome[:-9]) + pargs.append('-testnet') + else: + pargs.append('-datadir=%s' % self.satoshiHome) + try: + # Don't want some strange error in this size-check to abort loading + blocksdir = os.path.join(self.satoshiHome, 'blocks') + sz = long(0) + if os.path.exists(blocksdir): + for fn in os.listdir(blocksdir): + fnpath = os.path.join(blocksdir, fn) + sz += long(os.path.getsize(fnpath)) + + if sz < 5*GIGABYTE: + if SystemSpecs.Memory>9.0: + pargs.append('-dbcache=2000') + elif SystemSpecs.Memory>5.0: + pargs.append('-dbcache=1000') + elif SystemSpecs.Memory>3.0: + pargs.append('-dbcache=500') + except: + LOGEXCEPT('Failed size check of blocks directory') + + + # Startup bitcoind and get its process ID (along with our own) + self.bitcoind = launchProcess(pargs) + + self.btcdpid = self.bitcoind.pid + self.selfpid = os.getpid() + + LOGINFO('PID of bitcoind: %d', self.btcdpid) + LOGINFO('PID of armory: %d', self.selfpid) + + # Startup guardian process -- it will watch Armory's PID + gpath = self.getGuardianPath() + pargs = [gpath, str(self.selfpid), str(self.btcdpid)] + if not OS_WINDOWS: + pargs.insert(0, 'python') + launchProcess(pargs) + + + + ############################################################################# + def stopBitcoind(self): + LOGINFO('Called stopBitcoind') + if not self.isRunningBitcoind(): + LOGINFO('...but bitcoind is not running, to be able to stop') + return + + killProcessTree(self.bitcoind.pid) + killProcess(self.bitcoind.pid) + + time.sleep(1) + self.bitcoind = None + + + ############################################################################# + def isRunningBitcoind(self): + """ + armoryengine satoshiIsAvailable() only tells us whether there's a + running bitcoind that is actively responding on its port. But it + won't be responding immediately after we've started it (still doing + startup operations). If bitcoind was started and still running, + then poll() will return None. Any othe poll() return value means + that the process terminated + """ + if self.bitcoind==None: + return False + else: + if not self.bitcoind.poll()==None: + LOGDEBUG('Bitcoind is no more') + if self.btcOut==None: + self.btcOut, self.btcErr = self.bitcoind.communicate() + LOGWARN('bitcoind exited, bitcoind STDOUT:') + for line in self.btcOut.split('\n'): + LOGWARN(line) + LOGWARN('bitcoind exited, bitcoind STDERR:') + for line in self.btcErr.split('\n'): + LOGWARN(line) + return self.bitcoind.poll()==None + + ############################################################################# + def wasRunningBitcoind(self): + return (not self.bitcoind==None) + + ############################################################################# + def bitcoindIsResponsive(self): + return satoshiIsAvailable(self.bitconf['host'], self.bitconf['rpcport']) + + ############################################################################# + def getSDMState(self): + """ + As for why I'm doing this: it turns out that between "initializing" + and "synchronizing", bitcoind temporarily stops responding entirely, + which causes "not-available" to be the state. I need to smooth that + out because it wreaks havoc on the GUI which will switch to showing + a nasty error. + """ + + state = self.getSDMStateLogic() + self.circBufferState.append(state) + self.circBufferTime.append(RightNow()) + if len(self.circBufferTime)>2 and \ + (self.circBufferTime[-1] - self.circBufferTime[1]) > 5: + # Only remove the first element if we have at least 5s history + self.circBufferState = self.circBufferState[1:] + self.circBufferTime = self.circBufferTime[1:] + + # Here's where we modify the output to smooth out the gap between + # "initializing" and "synchronizing" (which is a couple seconds + # of "not available"). "NotAvail" keeps getting added to the + # buffer, but if it was "initializing" in the last 5 seconds, + # we will keep "initializing" + if state=='BitcoindNotAvailable': + if 'BitcoindInitializing' in self.circBufferState: + LOGWARN('Overriding not-available state. This should happen 0-5 times') + return 'BitcoindInitializing' + + return state + + ############################################################################# + def getSDMStateLogic(self): + + if self.disabled: + return 'BitcoindMgmtDisabled' + + if self.failedFindExe: + return 'BitcoindExeMissing' + + if self.failedFindHome: + return 'BitcoindHomeMissing' + + if TheTDM.isRunning(): + return 'TorrentSynchronizing' + + latestInfo = self.getTopBlockInfo() + + if self.bitcoind==None and latestInfo['error']=='Uninitialized': + return 'BitcoindNeverStarted' + + if not self.isRunningBitcoind(): + # Not running at all: either never started, or process terminated + if not self.btcErr==None and len(self.btcErr)>0: + errstr = self.btcErr.replace(',',' ').replace('.',' ').replace('!',' ') + errPcs = set([a.lower() for a in errstr.split()]) + runPcs = set(['cannot','obtain','lock','already','running']) + dbePcs = set(['database', 'recover','backup','except','wallet','dat']) + if len(errPcs.intersection(runPcs))>=(len(runPcs)-1): + return 'BitcoindAlreadyRunning' + elif len(errPcs.intersection(dbePcs))>=(len(dbePcs)-1): + return 'BitcoindDatabaseEnvError' + else: + return 'BitcoindUnknownCrash' + else: + return 'BitcoindNotAvailable' + elif not self.bitcoindIsResponsive(): + # Running but not responsive... must still be initializing + return 'BitcoindInitializing' + else: + # If it's responsive, get the top block and check + # TODO: These conditionals are based on experimental results. May + # not be accurate what the specific errors mean... + if latestInfo['error']=='ValueError': + return 'BitcoindWrongPassword' + elif latestInfo['error']=='JsonRpcException': + return 'BitcoindInitializing' + elif latestInfo['error']=='SocketError': + return 'BitcoindNotAvailable' + + if 'BitcoindReady' in self.circBufferState: + # If ready, always ready + return 'BitcoindReady' + + # If we get here, bitcoind is gave us a response. + secSinceLastBlk = RightNow() - latestInfo['toptime'] + blkspersec = latestInfo['blkspersec'] + #print 'Blocks per 10 sec:', ('UNKNOWN' if blkspersec==-1 else blkspersec*10) + if secSinceLastBlk > 4*HOUR or blkspersec==-1: + return 'BitcoindSynchronizing' + else: + if blkspersec*20 > 2 and not 'BitcoindReady' in self.circBufferState: + return 'BitcoindSynchronizing' + else: + return 'BitcoindReady' + + + + + ############################################################################# + def createProxy(self, forceNew=False): + if self.proxy==None or forceNew: + LOGDEBUG('Creating proxy') + usr,pas,hst,prt = [self.bitconf[k] for k in ['rpcuser','rpcpassword',\ + 'host', 'rpcport']] + pstr = 'http://%s:%s@%s:%d' % (usr,pas,hst,prt) + LOGINFO('Creating proxy in SDM: host=%s, port=%s', hst,prt) + self.proxy = ServiceProxy(pstr) + + + ############################################################################# + def __backgroundRequestTopBlock(self): + self.createProxy() + self.isMidQuery = True + try: + numblks = self.proxy.getinfo()['blocks'] + blkhash = self.proxy.getblockhash(numblks) + toptime = self.proxy.getblock(blkhash)['time'] + #LOGDEBUG('RPC Call: numBlks=%d, toptime=%d', numblks, toptime) + # Only overwrite once all outputs are retrieved + self.lastTopBlockInfo['numblks'] = numblks + self.lastTopBlockInfo['tophash'] = blkhash + self.lastTopBlockInfo['toptime'] = toptime + self.lastTopBlockInfo['error'] = None # Holds error info + + if len(self.last20queries)==0 or \ + (RightNow()-self.last20queries[-1][0]) > 0.99: + # This conditional guarantees last 20 queries spans at least 20s + self.last20queries.append([RightNow(), numblks]) + self.last20queries = self.last20queries[-20:] + t0,b0 = self.last20queries[0] + t1,b1 = self.last20queries[-1] + + # Need at least 10s of data to give meaning answer + if (t1-t0)<10: + self.lastTopBlockInfo['blkspersec'] = -1 + else: + self.lastTopBlockInfo['blkspersec'] = float(b1-b0)/float(t1-t0) + + except ValueError: + # I believe this happens when you used the wrong password + LOGEXCEPT('ValueError in bkgd req top blk') + self.lastTopBlockInfo['error'] = 'ValueError' + except authproxy.JSONRPCException: + # This seems to happen when bitcoind is overwhelmed... not quite ready + LOGDEBUG('generic jsonrpc exception') + self.lastTopBlockInfo['error'] = 'JsonRpcException' + except socket.error: + # Connection isn't available... is bitcoind not running anymore? + LOGDEBUG('generic socket error') + self.lastTopBlockInfo['error'] = 'SocketError' + except: + LOGEXCEPT('generic error') + self.lastTopBlockInfo['error'] = 'UnknownError' + raise + finally: + self.isMidQuery = False + + + ############################################################################# + def updateTopBlockInfo(self): + """ + We want to get the top block information, but if bitcoind is rigorously + downloading and verifying the blockchain, it can sometimes take 10s to + to respond to JSON-RPC calls! We must do it in the background... + + If it's already querying, no need to kick off another background request, + just return the last value, which may be "stale" but we don't really + care for this particular use-case + """ + if not self.isRunningBitcoind(): + return + + if self.isMidQuery: + return + + self.createProxy() + self.queryThread = PyBackgroundThread(self.__backgroundRequestTopBlock) + self.queryThread.start() + + + ############################################################################# + def getTopBlockInfo(self): + if self.isRunningBitcoind(): + self.updateTopBlockInfo() + self.queryThread.join(0.001) # In most cases, result should come in 1 ms + # We return a copy so that the data is not changing as we use it + + return self.lastTopBlockInfo.copy() + + + ############################################################################# + def callJSON(self, func, *args): + state = self.getSDMState() + if not state in ('BitcoindReady', 'BitcoindSynchronizing'): + LOGERROR('Called callJSON(%s, %s)', func, str(args)) + LOGERROR('Current SDM state: %s', state) + raise self.BitcoindError, 'callJSON while %s'%state + + return self.proxy.__getattr__(func)(*args) + + + ############################################################################# + def returnSDMInfo(self): + sdminfo = {} + for key,val in self.bitconf.iteritems(): + sdminfo['bitconf_%s'%key] = val + + for key,val in self.lastTopBlockInfo.iteritems(): + sdminfo['topblk_%s'%key] = val + + sdminfo['executable'] = self.executable + sdminfo['isrunning'] = self.isRunningBitcoind() + sdminfo['homedir'] = self.satoshiHome + sdminfo['proxyinit'] = (not self.proxy==None) + sdminfo['ismidquery'] = self.isMidQuery + sdminfo['querycount'] = len(self.last20queries) + + return sdminfo + + ############################################################################# + def printSDMInfo(self): + print '\nCurrent SDM State:' + print '\t', 'SDM State Str'.ljust(20), ':', self.getSDMState() + for key,value in self.returnSDMInfo().iteritems(): + print '\t', str(key).ljust(20), ':', str(value) + + diff --git a/announcefetch.py b/announcefetch.py new file mode 100644 index 000000000..9ffbfcc91 --- /dev/null +++ b/announcefetch.py @@ -0,0 +1,386 @@ +from armoryengine.ALL import * +from threading import Event +from jasvet import verifySignature, readSigBlock +import os +import sys +import time +import urllib + + +DEFAULT_FETCH_INTERVAL = 30*MINUTE +DEFAULT_MIN_PRIORITY = 2048 + +if not CLI_OPTIONS.testAnnounceCode: + # Signed with the Bitcoin offline announce key (see top of ArmoryUtils.py) + ANNOUNCE_SIGN_PUBKEY = ARMORY_INFO_SIGN_PUBLICKEY + ANNOUNCE_URL = 'https://bitcoinarmory.com/announce.txt' + ANNOUNCE_URL_BACKUP = 'https://s3.amazonaws.com/bitcoinarmory-media/announce.txt' +else: + # This is a lower-security announce file, fake data, just for testing + ANNOUNCE_SIGN_PUBKEY = ('04' + '601c891a2cbc14a7b2bb1ecc9b6e42e166639ea4c2790703f8e2ed126fce432c' + '62fe30376497ad3efcd2964aa0be366010c11b8d7fc8209f586eac00bb763015') + ANNOUNCE_URL = 'https://s3.amazonaws.com/bitcoinarmory-testing/testannounce.txt' + ANNOUNCE_URL_BACKUP = ANNOUNCE_URL + + +class AnnounceDataFetcher(object): + """ + Armory Technologies, Inc, will post occasional SIGNED updates to be + processed by running instances of Armory that haven't disabled it. + + The files in the fetchDir will be small. At the time of this writing, + the only files we will fetch and store: + + announce.txt : announcements & alerts to be displayed to the user + changelog.txt : changelog of versions, triggers update nofications + dllinks.txt : URLs and hashes of installers for all OS and versions + notify.txt : Notifications & alerts + bootstrap.dat.torrent : torrent file for quick blockchain download + """ + + ############################################################################# + def __init__(self, announceURL=ANNOUNCE_URL, \ + backupURL=ANNOUNCE_URL_BACKUP, \ + fetchDir=None): + + self.loopIsIdle = Event() + self.forceCheckFlag = Event() + self.forceIsFinished = Event() + self.firstSuccess = Event() + self.shutdownFlag = Event() + self.lastFetch = 0 + self.lastChange = 0 + self.loopThread = None + self.disabled = False + self.setFetchInterval(DEFAULT_FETCH_INTERVAL) + self.loopIsIdle.set() + self.lastAnnounceChange = 0 + + # Where to fetch the data from + self.announceURL = announceURL + self.announceURL_backup = backupURL + + # Just disable ourselves if we have continuous exceptions + self.numConsecutiveExceptions = 0 + + # If we are on testnet, we may require matching a mainnnet addr + a160 = hash160(hex_to_binary(ANNOUNCE_SIGN_PUBKEY)) + self.validAddrStr = hash160_to_addrStr(a160) + + + + # Make sure the fetch directory exists (where we put downloaded files) + self.fetchDir = fetchDir + if fetchDir is None: + self.fetchDir = os.path.join(ARMORY_HOME_DIR, 'announcefiles') + if not os.path.exists(self.fetchDir): + os.mkdir(self.fetchDir) + + + # Read and hash existing files in that directory + self.fileHashMap = {} + LOGINFO('Reading files in fetcher directory:') + for fname in os.listdir(self.fetchDir): + fpath = os.path.join(self.fetchDir, fname) + if not fname.endswith('.file') or os.path.getsize(fpath) > 16*MEGABYTE: + continue + + fid = fname[:-5] + with open(fpath, 'rb') as f: + self.fileHashMap[fid] = binary_to_hex(sha256(f.read())) + LOGINFO(' %s : %s', fid.ljust(16), self.fileHashMap[fid]) + + + ############################################################################# + def start(self): + if not self.disabled: + self.loopThread = self.__runFetchLoop(async=True) + + ############################################################################# + def isDisabled(self): + return self.disabled + + ############################################################################# + def shutdown(self): + LOGINFO('Called AnnounceDataFetcher.shutdown()') + self.shutdownFlag.set() + + ############################################################################# + def setFetchInterval(self, newInterval): + self.fetchInterval = max(newInterval,10) + + ############################################################################# + def setDisabled(self, b=True): + self.disabled = b + + ############################################################################# + def isRunning(self): + return self.loopThread.isRunning() if self.loopThread else False + + ############################################################################# + def atLeastOneSuccess(self): + return self.firstSuccess.isSet() + + ############################################################################# + def numFiles(self): + return len(self.fileHashMap) + + ############################################################################# + def fetchRightNow(self, doWait=0): + self.forceIsFinished.clear() + self.forceCheckFlag.set() + + if doWait > 0: + self.forceIsFinished.wait(doWait) + self.forceCheckFlag.clear() + + ############################################################################# + def getAnnounceFilePath(self, fileID): + fpath = os.path.join(self.fetchDir, fileID+'.file') + return fpath if os.path.exists(fpath) else None + + ############################################################################# + def getAnnounceFile(self, fileID, forceCheck=False, forceWait=10): + if forceCheck: + LOGINFO('Forcing fetch before returning file') + if not self.isRunning(): + # This is safe because there's no one to collide with + self.__runFetchSequence() + else: + self.forceIsFinished.clear() + self.forceCheckFlag.set() + + if not self.forceIsFinished.wait(forceWait): + self.forceCheckFlag.clear() + return None + + self.forceCheckFlag.clear() + else: + # Wait up to one second for any current ops to finish + if not self.loopIsIdle.wait(1): + LOGERROR('Loop was busy for more than one second') + return None + + # If the above succeeded, it will be in the fetchedFiles dir + # We may have + fpath = self.getAnnounceFilePath(fileID) + + if not (fpath and os.path.exists(fpath)): + LOGERROR('No file with ID=%s was fetched', fileID) + return None + + with open(fpath, 'rb') as f: + returnData = f.read() + + return returnData + + + ############################################################################# + def getFileModTime(self, fileID): + fpath = self.getAnnounceFilePath(fileID) + if not fpath or not os.path.exists(fpath): + #LOGERROR('No file with ID=%s was fetched', fileID) + return 0 + + return os.path.getmtime(fpath) + + + + ############################################################################# + def getLastSuccessfulFetchTime(self): + announcePath = os.path.join(self.fetchDir, 'announce.file') + if os.path.exists(announcePath): + return os.path.getmtime(announcePath) + else: + return 0 + + + + + ############################################################################# + def getDecoratedURL(self, url, verbose=False): + """ + This always decorates the URL with at least Armory version. Use the + verbose=True option to add OS, subOS, and a few "random" bytes that help + reject duplicate queries. + """ + argsMap = {} + argsMap['ver'] = getVersionString(BTCARMORY_VERSION) + + if verbose: + if OS_WINDOWS: + argsMap['os'] = 'win' + elif OS_LINUX: + argsMap['os'] = 'lin' + elif OS_MACOSX: + argsMap['os'] = 'mac' + else: + argsMap['os'] = 'unk' + + try: + if OS_MACOSX: + argsMap['osvar'] = OS_VARIANT + else: + argsMap['osvar'] = OS_VARIANT[0].lower() + except: + LOGERR('Unrecognized OS while constructing version URL') + argsMap['osvar'] = 'unk' + + argsMap['id'] = binary_to_hex(hash256(USER_HOME_DIR)[:4]) + + return url + '?' + urllib.urlencode(argsMap) + + + + ############################################################################# + def __fetchAnnounceDigests(self, doDecorate=False): + self.lastFetch = RightNow() + digestURL = self.getDecoratedURL(self.announceURL, verbose=doDecorate) + backupURL = None + if self.announceURL_backup: + backupURL = self.getDecoratedURL(self.announceURL_backup) + return self.__fetchFile(digestURL, backupURL) + + + + ############################################################################# + def __fetchFile(self, url, backupURL=None): + LOGINFO('Fetching: %s', url) + try: + import urllib2 + import socket + LOGDEBUG('Downloading URL: %s' % url) + socket.setdefaulttimeout(CLI_OPTIONS.nettimeout) + urlobj = urllib2.urlopen(url, timeout=CLI_OPTIONS.nettimeout) + return urlobj.read() + except ImportError: + LOGERROR('No module urllib2 -- cannot download anything') + return '' + except (urllib2.URLError, urllib2.HTTPError): + LOGERROR('Specified URL was inaccessible') + LOGERROR('Tried: %s', url) + return self.__fetchFile(backupURL) if backupURL else '' + except: + LOGEXCEPT('Unspecified error downloading URL') + return self.__fetchFile(backupURL) if backupURL else '' + + + ############################################################################# + def __runFetchSequence(self): + ##### Always decorate the URL with OS, Armory version on the first run + digestData = self.__fetchAnnounceDigests(not self.firstSuccess.isSet()) + + if len(digestData)==0: + LOGWARN('Error fetching announce digest') + return + + self.firstSuccess.set() + + ##### Digests come in signature blocks. Verify sig using jasvet. + try: + sig, msg = readSigBlock(digestData) + signAddress = verifySignature(sig, msg, 'v1', ord(ADDRBYTE)) + if not signAddress == self.validAddrStr: + LOGERROR('Announce info carried invalid signature!') + LOGERROR('Signature addr: %s' % signAddress) + LOGERROR('Expected address: %s', self.validAddrStr) + return + except: + LOGEXCEPT('Could not verify data in signed message block') + return + + # Always rewrite file; it's small and will use mtime for info + with open(os.path.join(self.fetchDir, 'announce.file'), 'w') as f: + f.write(digestData) + + + ##### We have a valid digest, now parse it + justDownloadedMap = {} + for row in [line.split() for line in msg.strip().split('\n')]: + if len(row)==3: + justDownloadedMap[row[0]] = [row[1], row[2]] + else: + LOGERROR('Malformed announce matrix: %s' % str(row)) + return + + ##### Check whether any of the hashes have changed + for key,val in justDownloadedMap.iteritems(): + jdURL,jdHash = val[0],val[1] + + if not (key in self.fileHashMap and self.fileHashMap[key]==jdHash): + LOGINFO('Changed [ "%s" ] == [%s, %s]', key, jdURL, jdHash) + newData = self.__fetchFile(jdURL) + if len(newData) == 0: + LOGERROR('Failed downloading announce file : %s', key) + return + newHash = binary_to_hex(sha256(newData)) + if not newHash == jdHash: + LOGERROR('Downloaded file hash does not match!') + LOGERROR('Hash of downloaded data: %s', newHash) + return + + filename = os.path.join(self.fetchDir, key+'.file') + with open(filename, 'wb') as f: + f.write(newData) + self.lastChange = RightNow() + self.fileHashMap[key] = jdHash + + ##### Clean up as needed + if self.forceCheckFlag.isSet(): + self.forceIsFinished.set() + self.forceCheckFlag.clear() + self.numConsecutiveExceptions = 0 + + + ############################################################################# + # I'm taking a shortcut around adding all the threading code here + # Simply use @AllowAsync and only call with async=True. Done. + @AllowAsync + def __runFetchLoop(self): + """ + All this code runs in a separate thread (your app will freeze if + you don't call this with the async=True argument). It will + periodically check for new announce data, and update members that + are visible to other threads. + + By default, it will check once per hour. If you call + self.forceCheckFlag.set() + It will skip the time check and force a download right now. + Using getAnnounceFile(forceCheck=True) will do this for you, + and will wait until the operation completes before returning + the result. + """ + + while True: + + try: + if self.isDisabled() or self.shutdownFlag.isSet(): + self.shutdownFlag.clear() + break + + ##### Only check once per hour unless force flag is set + if not self.forceCheckFlag.isSet(): + if RightNow()-self.lastFetch < self.fetchInterval: + continue + else: + LOGINFO('Forcing announce data fetch') + self.forceIsFinished.clear() + + self.loopIsIdle.clear() + self.__runFetchSequence() + + except: + self.numConsecutiveExceptions += 1 + LOGEXCEPT('Failed download') + if self.numConsecutiveExceptions > 20: + self.setDisabled(True) + finally: + self.loopIsIdle.set() + time.sleep(0.5) + + + + + + diff --git a/armorycolors.py b/armorycolors.py index 64b459d53..b4ae662e6 100644 --- a/armorycolors.py +++ b/armorycolors.py @@ -1,6 +1,6 @@ ################################################################################ # # -# Copyright (C) 2011-2013, Armory Technologies, Inc. # +# Copyright (C) 2011-2014, Armory Technologies, Inc. # # Distributed under the GNU Affero General Public License (AGPL v3) # # See LICENSE or http://www.gnu.org/licenses/agpl.html # # # @@ -85,6 +85,11 @@ def luminance(qcolor): QAPP = QApplication(sys.argv) qpal = QAPP.palette() +# workaround for https://bugs.launchpad.net/ubuntu/+source/qt4-x11/+bug/877236 +qpal.setColor(QPalette.ToolTipBase, qpal.color(QPalette.Window)) +qpal.setColor(QPalette.ToolTipText, qpal.color(QPalette.WindowText)) +QAPP.setPalette(qpal) + # Some of the standard colors to be tweaked class ArbitraryStruct: pass Colors = ArbitraryStruct() diff --git a/armoryd.py b/armoryd.py index f5b251797..7dd82040d 100644 --- a/armoryd.py +++ b/armoryd.py @@ -1,6 +1,6 @@ ################################################################################ # # -# Copyright (C) 2011-2013, Armory Technologies, Inc. # +# Copyright (C) 2011-2014, Armory Technologies, Inc. # # Distributed under the GNU Affero General Public License (AGPL v3) # # See LICENSE or http://www.gnu.org/licenses/agpl.html # # # @@ -24,7 +24,7 @@ # Where possible this follows conventions established by the Satoshi client. # Does not require armory to be installed or running, this is a standalone application. # Requires bitcoind process to be running before starting armory-daemon. -# Requires an armory watch-only wallet to be in the same folder as the +# Requires an armory wallet (can be watching only) to be in the same folder as the # armory-daemon script. # Works with testnet, use --testnet flag when starting the script. # @@ -40,29 +40,35 @@ # https://bitcointalk.org/index.php?topic=92496.0 ##### -from twisted.internet import reactor -from twisted.web import server -from txjsonrpc.web import jsonrpc -from txjsonrpc.auth import wrapResource -from twisted.cred.checkers import FilePasswordDB - -from armoryengine import * - import datetime import decimal +import json import os +import random +import socket import sys import time -import socket + +from twisted.cred.checkers import FilePasswordDB +from twisted.internet import reactor +from twisted.web import server +from txjsonrpc.auth import wrapResource +from txjsonrpc.web import jsonrpc + +from CppBlockUtils import SecureBinaryData +from armoryengine.ALL import * +from jsonrpc import ServiceProxy +from armoryengine.Decorators import EmailOutput +from armoryengine.ArmoryUtils import addrStr_to_hash160 +from armoryengine.PyBtcWalletRecovery import ParseWallet + # Some non-twisted json imports from jgarzik's code and his UniversalEncoder -import json -from jsonrpc import ServiceProxy class UniversalEncoder(json.JSONEncoder): - def default(self, obj): - if isinstance(obj, decimal.Decimal): - return float(obj) - return json.JSONEncoder.default(self, obj) + def default(self, obj): + if isinstance(obj, decimal.Decimal): + return float(obj) + return json.JSONEncoder.default(self, obj) ARMORYD_CONF_FILE = os.path.join(ARMORY_HOME_DIR, 'armoryd.conf') @@ -83,19 +89,175 @@ class UnrecognizedCommand(Exception): pass ################################################################################ ################################################################################ +class NotEnoughCoinsError(Exception): pass +class CoinSelectError(Exception): pass +class WalletUnlockNeeded(Exception): pass +class InvalidBitcoinAddress(Exception): pass +class PrivateKeyNotFound(Exception): pass +class AddressNotInWallet(Exception): pass + +NOT_IMPLEMENTED = '--Not Implemented--' + class Armory_Json_Rpc_Server(jsonrpc.JSONRPC): - ############################################################################# + ###########################################################################g## def __init__(self, wallet): self.wallet = wallet + # Used with wallet notification code + self.addressMetaData = {} + + ############################################################################# + def jsonrpc_backupwallet(self, backupFilePath): + self.wallet.backupWalletFile(backupFilePath) + + ############################################################################# + def jsonrpc_listunspent(self): + utxoList = self.wallet.getTxOutList('unspent') + result = [u.getOutPoint().serialize() for u in utxoList] + return result + + ############################################################################# + def jsonrpc_importprivkey(self, privkey): + self.wallet.importExternalAddressData(privKey=privkey) + + ############################################################################# + def jsonrpc_getrawtransaction(self, txHash, verbose=0, endianness=BIGENDIAN): + rawTx = None + cppTx = TheBDM.getTxByHash(hex_to_binary(txHash, endianness)) + if cppTx.isInitialized(): + txBinary = cppTx.serialize() + pyTx = PyTx().unserialize(txBinary) + rawTx = binary_to_hex(pyTx.serialize()) + if verbose: + result = self.jsonrpc_decoderawtransaction(rawTx) + result['hex'] = rawTx + else: + result = rawTx + else: + LOGERROR('Tx hash not recognized by TheBDM: %s' % txHash) + result = None + + return result + + ############################################################################# + def jsonrpc_gettxout(self, txHash, n): + txOut = None + cppTx = TheBDM.getTxByHash(hex_to_binary(txHash, BIGENDIAN)) + if cppTx.isInitialized(): + txBinary = cppTx.serialize() + pyTx = PyTx().unserialize(txBinary) + if n < len(pyTx.outputs): + txOut = pyTx.outputs[n] + else: + LOGERROR('Tx no output #: %s' % n) + else: + LOGERROR('Tx hash not recognized by TheBDM: %s' % binary_to_hex(txHash)) + return txOut + + ############################################################################# + def jsonrpc_encryptwallet(self, passphrase): + if self.wallet.isLocked: + raise WalletUnlockNeeded + self.wallet.changeWalletEncryption( securePassphrase=SecureBinaryData(passphrase) ) + self.wallet.lock() + + ############################################################################# + def jsonrpc_unlockwallet(self, passphrase, timeout): + self.wallet.unlock( securePassphrase=SecureBinaryData(passphrase), + tempKeyLifetime=timeout) + + + ############################################################################# + def getScriptPubKey(self, txOut): + addrList = [] + scriptType = getTxOutScriptType(txOut.binScript) + if scriptType in CPP_TXOUT_STDSINGLESIG: + M = 1 + addrList = [script_to_addrStr(txOut.binScript)] + elif scriptType == CPP_TXOUT_P2SH: + M = -1 + addrList = [script_to_addrStr(txOut.binScript)] + elif scriptType==CPP_TXOUT_MULTISIG: + M, N, addr160List, pub65List = getMultisigScriptInfo(txOut.binScript) + addrList = [hash160_to_addrStr(a160) for a160 in addr160List] + elif scriptType == CPP_TXOUT_NONSTANDARD: + M = -1 + + opStringList = convertScriptToOpStrings(txOut.binScript) + return { 'asm' : ' '.join(opStringList), + 'hex' : binary_to_hex(txOut.binScript), + 'reqSigs' : M, + 'type' : CPP_TXOUT_SCRIPT_NAMES[scriptType], + 'addresses' : addrList } + + ############################################################################# + def jsonrpc_decoderawtransaction(self, hexString): + pyTx = PyTx().unserialize(hex_to_binary(hexString)) + + ##### + # Accumulate TxIn info + vinList = [] + for txin in pyTx.inputs: + prevHash = txin.outpoint.txHash + scrType = getTxInScriptType(txin) + # ACR: What is asm, and why is basically just binScript? + oplist = convertScriptToOpStrings(txin.binScript) + scriptSigDict = { 'asm' : ' '.join(oplist), + 'hex' : binary_to_hex(txin.binScript) } + + if not scrType == CPP_TXIN_COINBASE: + vinList.append( { 'txid' : binary_to_hex(prevHash, BIGENDIAN), + 'vout' : txin.outpoint.txOutIndex, + 'scriptSig' : scriptSigDict, + 'sequence' : txin.intSeq}) + else: + vinList.append( { 'coinbase' : binary_to_hex(txin.binScript), + 'sequence' : txin.intSeq }) + + ##### + # Accumulate TxOut info + voutList = [] + for n,txout in enumerate(pyTx.outputs): + voutList.append( { 'value' : AmountToJSON(txout.value), + 'n' : n, + 'scriptPubKey' : self.getScriptPubKey(txout) } ) + + + ##### + # Accumulate all the data to return + result = { 'txid' : pyTx.getHashHex(BIGENDIAN), + 'version' : pyTx.version, + 'locktime' : pyTx.lockTime, + 'vin' : vinList, + 'vout' : voutList } + + return result + ############################################################################# def jsonrpc_getnewaddress(self): addr = self.wallet.getNextUnusedAddress() return addr.getAddrStr() + ############################################################################# + def jsonrpc_dumpprivkey(self, addr58): + # Cannot dump the private key for a locked wallet + if self.wallet.isLocked: + raise WalletUnlockNeeded + # The first byte must be the correct net byte, and the + # last 4 bytes must be the correct checksum + if not checkAddrStrValid(addr58): + raise InvalidBitcoinAddress + + atype, addr160 = addrStr_to_hash160(addr58, False) + + pyBtcAddress = self.wallet.getAddrByHash160(addr160) + if pyBtcAddress == None: + raise PrivateKeyNotFound + return pyBtcAddress.serializePlainPrivateKey() + ############################################################################# def jsonrpc_getwalletinfo(self): wltInfo = { \ @@ -108,7 +270,6 @@ def jsonrpc_getwalletinfo(self): } return wltInfo - ############################################################################# def jsonrpc_getbalance(self, baltype='spendable'): if not baltype in ['spendable','spend', 'unconf', 'unconfirmed', \ @@ -123,7 +284,8 @@ def jsonrpc_getreceivedbyaddress(self, address): if CLI_OPTIONS.offline: raise ValueError('Cannot get received amount when offline') # Only gets correct amount for addresses in the wallet, otherwise 0 - addr160 = addrStr_to_hash160(address) + atype, addr160 = addrStr_to_hash160(address, False) + txs = self.wallet.getAddrTxLedger(addr160) balance = sum([x.getValue() for x in txs if x.getValue() > 0]) return AmountToJSON(balance) @@ -132,21 +294,21 @@ def jsonrpc_getreceivedbyaddress(self, address): def jsonrpc_sendtoaddress(self, bitcoinaddress, amount): if CLI_OPTIONS.offline: raise ValueError('Cannot create transactions when offline') - addr160 = addrStr_to_hash160(bitcoinaddress) + scraddr = addrStr_to_scrAddr(bitcoinaddress) amtCoin = JSONtoAmount(amount) - return self.create_unsigned_transaction([[addr160, amtCoin]]) + return self.create_unsigned_transaction([[scraddr, amtCoin]]) ############################################################################# def jsonrpc_sendmany(self, *args): if CLI_OPTIONS.offline: raise ValueError('Cannot create transactions when offline') - recipvalpairs = [] + scraddrValuePairs = [] for a in args: r,v = a.split(':') - recipvalpairs.append([addrStr_to_hash160(r), JSONtoAmount(v)]) + scraddrValuePairs.append([addrStr_to_scrAddr(r), JSONtoAmount(v)]) - return self.create_unsigned_transaction(recipvalpairs) + return self.create_unsigned_transaction(scraddrValuePairs) ############################################################################# @@ -173,8 +335,9 @@ def jsonrpc_getledger(self, tx_count=10, from_tx=0, simple=False): if not cppTx.isInitialized(): LOGERROR('Tx hash not recognized by TheBDM: %s' % txHashHex) - cppHead = cppTx.getHeaderPtr() - if not cppHead.isInitialized: + #cppHead = cppTx.getHeaderPtr() + cppHead = TheBDM.getHeaderPtrForTx(cppTx) + if not cppHead.isInitialized(): LOGERROR('Header pointer is not available!') headHashBin = '' headHashHex = '' @@ -188,7 +351,8 @@ def jsonrpc_getledger(self, tx_count=10, from_tx=0, simple=False): netCoins = le.getValue() feeCoins = getFeeForTx(txHashBin) - allRecips = [cppTx.getTxOut(i).getRecipientAddr() for i in range(cppTx.getNumTxOut())] + scrAddrs = [cppTx.getTxOutCopy(i).getScrAddressStr() for i in range(cppTx.getNumTxOut())] + allRecips = [CheckHash160(r) for r in scrAddrs] first160 = '' if cppTx.getNumTxOut()==1: first160 = allRecips[0] @@ -239,8 +403,8 @@ def jsonrpc_getledger(self, tx_count=10, from_tx=0, simple=False): myinputs, otherinputs = [],[] for iin in range(cppTx.getNumTxIn()): - sender = TheBDM.getSenderAddr20(cppTx.getTxIn(iin)) - val = TheBDM.getSentValue(cppTx.getTxIn(iin)) + sender = CheckHash160(TheBDM.getSenderScrAddr(cppTx.getTxInCopy(iin))) + val = TheBDM.getSentValue(cppTx.getTxInCopy(iin)) addTo = (myinputs if self.wallet.hasAddr(sender) else otherinputs) addTo.append( {'address': hash160_to_addrStr(sender), \ 'amount': AmountToJSON(val)} ) @@ -248,8 +412,8 @@ def jsonrpc_getledger(self, tx_count=10, from_tx=0, simple=False): myoutputs, otheroutputs = [], [] for iout in range(cppTx.getNumTxOut()): - recip = cppTx.getTxOut(iout).getRecipientAddr(); - val = cppTx.getTxOut(iout).getValue(); + recip = CheckHash160(cppTx.getTxOutCopy(iout).getScrAddressStr()) + val = cppTx.getTxOutCopy(iout).getValue(); addTo = (myoutputs if self.wallet.hasAddr(recip) else otheroutputs) addTo.append( {'address': hash160_to_addrStr(recip), \ 'amount': AmountToJSON(val)} ) @@ -299,7 +463,7 @@ def jsonrpc_listtransactions(self, tx_count=10, from_tx=0): txSet = set([]) - for i in range(sz): + for i in range(lower,upper): le = ledgerEntries[i] txHashBin = le.getTxHash() @@ -313,7 +477,8 @@ def jsonrpc_listtransactions(self, tx_count=10, from_tx=0): if not cppTx.isInitialized(): LOGERROR('Tx hash not recognized by TheBDM: %s' % txHashHex) - cppHead = cppTx.getHeaderPtr() + #cppHead = cppTx.getHeaderPtr() + cppHead = TheBDM.getHeaderPtrForTx(cppTx) if not cppHead.isInitialized: LOGERROR('Header pointer is not available!') @@ -331,15 +496,15 @@ def jsonrpc_listtransactions(self, tx_count=10, from_tx=0): # are receives recipVals = [] for iout in range(cppTx.getNumTxOut()): - recip = cppTx.getTxOut(iout).getRecipientAddr() - val = cppTx.getTxOut(iout).getValue() + recip = CheckHash160(cppTx.getTxOutCopy(iout).getScrAddressStr()) + val = cppTx.getTxOutCopy(iout).getValue() recipVals.append([recip,val]) if cppTx.getNumTxOut()==1: changeAddr160 = "" - targAddr160 = cppTx.getTxOut(0).getRecipientAddr() + targAddr160 = CheckHash160(cppTx.getTxOutCopy(0).getScrAddressStr()) elif isToSelf: selfamt,changeIdx = determineSentToSelfAmt(le, self.wallet) if changeIdx==-1: @@ -439,11 +604,7 @@ def jsonrpc_listtransactions(self, tx_count=10, from_tx=0): final_tx_list.append(tx_info) return final_tx_list - - - - - + ############################################################################# def jsonrpc_getinfo(self): isReady = TheBDM.getBDMState() == 'BlockchainReady' @@ -518,7 +679,7 @@ def jsonrpc_gettransaction(self, txHash): inputvalues = [] outputvalues = [] for i in range(tx.getNumTxIn()): - op = tx.getTxIn(i).getOutPoint() + op = tx.getTxInCopy(i).getOutPoint() prevtx = TheBDM.getTxByHash(op.getTxHash()) if not prevtx.isInitialized(): haveAllInputs = False @@ -529,9 +690,9 @@ def jsonrpc_gettransaction(self, txHash): 'fromtxindex': op.getTxOutIndex()}) else: - txout = prevtx.getTxOut(op.getTxOutIndex()) + txout = prevtx.getTxOutCopy(op.getTxOutIndex()) inputvalues.append(txout.getValue()) - recip160 = txout.getRecipientAddr() + recip160 = CheckHash160(txout.getScrAddressStr()) txindata.append( { 'address': hash160_to_addrStr(recip160), 'value': AmountToJSON(txout.getValue()), 'ismine': self.wallet.hasAddr(recip160), @@ -540,10 +701,11 @@ def jsonrpc_gettransaction(self, txHash): txoutdata = [] for i in range(tx.getNumTxOut()): - txout = tx.getTxOut(i) + txout = tx.getTxOutCopy(i) + a160 = CheckHash160(txout.getScrAddressStr()) txoutdata.append( { 'value': AmountToJSON(txout.getValue()), - 'ismine': self.wallet.hasAddr(txout.getRecipientAddr()), - 'address': hash160_to_addrStr(txout.getRecipientAddr())}) + 'ismine': self.wallet.hasAddr(a160), + 'address': hash160_to_addrStr(a160)}) outputvalues.append(txout.getValue()) fee = sum(inputvalues)-sum(outputvalues) @@ -585,11 +747,11 @@ def jsonrpc_gettransaction(self, txHash): ############################################################################# # https://bitcointalk.org/index.php?topic=92496.msg1126310#msg1126310 - def create_unsigned_transaction(self, recipValPairs): + def create_unsigned_transaction(self, scraddrValuePairs): # Get unspent TxOutList and select the coins #addr160_recipient = addrStr_to_hash160(bitcoinaddress_str) - totalSend = long( sum([rv[1] for rv in recipValPairs]) ) + totalSend = long( sum([rv[1] for rv in scraddrValuePairs]) ) fee = 0 spendBal = self.wallet.getBalance('Spendable') @@ -609,16 +771,73 @@ def create_unsigned_transaction(self, recipValPairs): totalSelected = sum([u.getValue() for u in utxoSelect]) totalChange = totalSelected - (totalSend + fee) - outputPairs = recipValPairs + outputPairs = scraddrValuePairs[:] if totalChange > 0: - outputPairs.append( [self.wallet.getNextUnusedAddress().getAddr160(), totalChange] ) + nextAddr = self.wallet.getNextUnusedAddress().getAddrStr() + outputPairs.append( [addrStr_to_scrAddr(nextAddr), totalChange] ) random.shuffle(outputPairs) txdp = PyTxDistProposal().createFromTxOutSelection(utxoSelect, outputPairs) return txdp.serializeAscii() - + ################################################################################ + # For each transaction in a block that triggers a notification: + # List the inputs, and output, indicate the one we are watching, displays balance data + # Also, display meta data associated with the address. + # + # Example usage: + # started the daemon with these arguments: --testnet armory_286jcNJRc_.wallet + # Then I called the daemon with: --testnet watchwallet + def jsonrpc_watchwallet(self, send_from=None, password=None, send_to=None, subject=None): + + @EmailOutput(send_from, password, [send_to], subject) + def reportTxFromAddrInNewBlock(pyHeader, pyTxList): + result = '' + for pyTx in pyTxList: + for pyTxIn in pyTx.inputs: + sendingAddrStr = TxInExtractAddrStrIfAvail(pyTxIn) + if len(sendingAddrStr) > 0: + sendingAddrHash160 = addrStr_to_hash160(sendingAddrStr, False)[1] + if self.wallet.addrMap.has_key(sendingAddrHash160): + sendingAddr = self.wallet.addrMap[sendingAddrHash160] + result = ''.join([result, '\n', sendingAddr.toString(), '\n']) + # print the meta data + if sendingAddrStr in self.addressMetaData: + result = ''.join([result, "\nMeta Data: ", str(self.addressMetaData[sendingAddrStr]), '\n']) + result = ''.join([result, '\n', pyTx.toString()]) + return result + + # TODO: Need stop assuming that this is the only method using newBlockFunctions + # Remove existing newBlockFunction to allow user to change the email args + rpc_server.newBlockFunctions = [] + rpc_server.newBlockFunctions.append(reportTxFromAddrInNewBlock) + + ################################################################################ + # Associate meta data to an address or addresses + # Example input: "{\"mzAtXhy3Z6SLd7rAwNJrL17e8mQkjDVDXh\": {\"chain\": 5, + # \"index\": 2}, \"mkF5L93F5HLhLmQagX26TdXcvPGHvfjoTM\": {\"CrazyField\": \"what\", + # \"1\": 1, \"2\": 2}}" + def jsonrpc_setaddressmetadata(self, newAddressMetaData): + # Loop once to check the addresses + # Don't add any meta data if one of the addresses wrong. + for addr in newAddressMetaData.keys(): + if not checkAddrStrValid(addr): + raise InvalidBitcoinAddress + if not self.wallet.addrMap.has_key(addrStr_to_hash160(addr, False)[1]): + raise AddressNotInWallet + self.addressMetaData.update(newAddressMetaData) + + ################################################################################ + # Clear the meta data + def jsonrpc_clearaddressmetadata(self): + self.addressMetaData = {} + + ################################################################################ + # get the meta data + def jsonrpc_getaddressmetadata(self): + return self.addressMetaData + ################################################################################ ################################################################################ class Armory_Daemon(object): @@ -629,6 +848,11 @@ def __init__(self): # Check if armoryd is already running, bail if it is self.checkForAlreadyRunning() + self.lock = threading.Lock() + self.lastChecked + + #check wallet consistency every hour + self.checkStep = 3600 print '' print '*'*80 @@ -690,10 +914,19 @@ def set_auth(self, resource): ############################################################################# def start(self): + #run a wallet consistency check before starting the BDM + self.checkWallet() + + #try to grab checkWallet lock to block start() until the check is over + self.lock.acquire() + self.lock.release() + + # This is not a UI so no need to worry about the main thread being blocked. + # Any UI that uses this Daemon can put the call to the Daemon on it's own thread. + TheBDM.setBlocking(True) LOGINFO('Server started...') if(not TheBDM.getBDMState()=='Offline'): TheBDM.registerWallet(self.wallet) - TheBDM.setBlocking(False) TheBDM.setOnlineMode(True) LOGINFO('Blockchain loading') @@ -715,12 +948,12 @@ def start(self): # This is CONNECT call for armoryd to talk to bitcoind LOGINFO('Set up connection to bitcoind') self.NetworkingFactory = ArmoryClientFactory( \ + TheBDM, func_loseConnect = self.showOfflineMsg, \ func_madeConnect = self.showOnlineMsg, \ func_newTx = self.execOnNewTx, \ func_newBlock = self.execOnNewBlock) reactor.connectTCP('127.0.0.1', BITCOIN_PORT, self.NetworkingFactory) - reactor.run() @@ -739,11 +972,15 @@ def checkForAlreadyRunning(self): if CLI_ARGS: proxyobj = ServiceProxy("http://%s:%s@127.0.0.1:%d" % (usr,pwd,ARMORY_RPC_PORT)) - extraArgs = [] if len(CLI_ARGS)==1 else CLI_ARGS[1:] try: #if not proxyobj.__hasattr__(CLI_ARGS[0]): #raise UnrecognizedCommand, 'No json command %s'%CLI_ARGS[0] - + extraArgs = [] + for arg in ([] if len(CLI_ARGS)==1 else CLI_ARGS[1:]): + if arg[0] == '{': + extraArgs.append(json.loads(arg)) + else: + extraArgs.append(arg) result = proxyobj.__getattr__(CLI_ARGS[0])(*extraArgs) print json.dumps(result, indent=4, \ @@ -775,7 +1012,7 @@ def execOnNewTx(self, pytxObj): TheBDM.addNewZeroConfTx(pytxObj.serialize(), long(RightNow()), True) TheBDM.rescanWalletZeroConf(self.wallet.cppWallet) - # Add anything else you'd like to do on a new block + # Add anything else you'd like to do on a new transaction # for txFunc in self.newTxFunctions: txFunc(pytxObj) @@ -826,6 +1063,22 @@ def checkMemoryPoolCorruption(self, mempoolname): PyTx().unserialize(binunpacker) except: os.remove(mempoolname); + + ############################################################################# + @AllowAsync + def checkWallet(self): + if self.lock.acquire(False) == False: return + wltStatus = ParseWallet(None, self.wallet, 5, None) + if wltStatus != 0: + print 'Wallet consistency check failed in wallet %s!!!' \ + % (self.wallet.uniqueIDB58) + print 'Aborting...' + + quit() + else: + self.lastChecked = datetime.time.now() + self.lock.release() + ############################################################################# def Heartbeat(self, nextBeatSec=1): @@ -836,7 +1089,14 @@ def Heartbeat(self, nextBeatSec=1): """ # Check for new blocks in the blk000X.dat file if TheBDM.getBDMState()=='BlockchainReady': - + + #check wallet every checkStep seconds + nextCheck = self.lastChecked + \ + datetime.timedelta(seconds=self.checkStep) + if nextCheck >= datetime.time.now(): + self.checkWallet() + + # Check for new blocks in the blk000X.dat file prevTopBlock = TheBDM.getTopBlockHeight() newBlks = TheBDM.readBlkFileUpdate() if newBlks>0: @@ -861,9 +1121,11 @@ def Heartbeat(self, nextBeatSec=1): # blocks on the main chain, not the invalid ones for blknum in range(prevTopBlock+1, self.latestBlockNum+1): cppHeader = TheBDM.getHeaderByHeight(blknum) - txHashToPy = lambda h: PyTx().unserialize(TheBDM.getTxByHash(h).serialize()) - pyHeader = PyBlockHeader().unserialize(header.serialize()) - pyTxList = [txHashToPy(hsh) for hsh in header.getTxHashList()] + pyHeader = PyBlockHeader().unserialize(cppHeader.serialize()) + + cppBlock = TheBDM.getMainBlockFromDB(blknum) + pyTxList = [PyTx().unserialize(cppBlock.getSerializedTx(i)) for + i in range(cppBlock.getNumTx())] for blockFunc in self.newBlockFunctions: blockFunc(pyHeader, pyTxList) @@ -892,9 +1154,7 @@ def default(self, obj): -#if __name__ == "__main__": -if True: - +if __name__ == "__main__": rpc_server = Armory_Daemon() rpc_server.start() diff --git a/armoryengine.py b/armoryengine.py deleted file mode 100644 index f3f5969ac..000000000 --- a/armoryengine.py +++ /dev/null @@ -1,13830 +0,0 @@ -################################################################################ -# # -# Copyright (C) 2011-2013, Armory Technologies, Inc. # -# Distributed under the GNU Affero General Public License (AGPL v3) # -# See LICENSE or http://www.gnu.org/licenses/agpl.html # -# # -################################################################################ - -# Version Numbers -BTCARMORY_VERSION = (0, 90, 0, 0) # (Major, Minor, Bugfix, AutoIncrement) -PYBTCWALLET_VERSION = (1, 35, 0, 0) # (Major, Minor, Bugfix, AutoIncrement) - -ARMORY_DONATION_ADDR = '1ArmoryXcfq7TnCSuZa9fQjRYwJ4bkRKfv' -ARMORY_DONATION_PUBKEY = ( '04' - '11d14f8498d11c33d08b0cd7b312fb2e6fc9aebd479f8e9ab62b5333b2c395c5' - 'f7437cab5633b5894c4a5c2132716bc36b7571cbe492a7222442b75df75b9a84') -ARMORY_INFO_SIGN_ADDR = '1NWvhByxfTXPYNT4zMBmEY3VL8QJQtQoei' -ARMORY_INFO_SIGN_PUBLICKEY = ('04' - 'af4abc4b24ef57547dd13a1110e331645f2ad2b99dfe1189abb40a5b24e4ebd8' - 'de0c1c372cc46bbee0ce3d1d49312e416a1fa9c7bb3e32a7eb3867d1c6d1f715') -SATOSHI_PUBLIC_KEY = ( '04' - 'fc9702847840aaf195de8442ebecedf5b095cdbb9bc716bda9110971b28a49e0' - 'ead8564ff0db22209e0374782c093bb899692d524e9d6a6956e7c5ecbcd68284') - - - - -import copy -import hashlib -import random -import time -import os -import string -import sys -import stat -import shutil -import math -import logging -import logging.handlers -import locale -import ast -import traceback -import threading -import signal -import inspect -import multiprocessing -import psutil -from struct import pack, unpack -from datetime import datetime - -# In Windows with py2exe, we have a problem unless we PIPE all streams -from subprocess import Popen, PIPE - -from sys import argv - -import optparse -parser = optparse.OptionParser(usage="%prog [options]\n") -parser.add_option("--settings", dest="settingsPath",default='DEFAULT', type="str", help="load Armory with a specific settings file") -parser.add_option("--datadir", dest="datadir", default='DEFAULT', type="str", help="Change the directory that Armory calls home") -parser.add_option("--satoshi-datadir", dest="satoshiHome", default='DEFAULT', type='str', help="The Bitcoin-Qt/bitcoind home directory") -parser.add_option("--satoshi-port", dest="satoshiPort", default='DEFAULT', type="str", help="For Bitcoin-Qt instances operating on a non-standard port") -parser.add_option("--dbdir", dest="leveldbDir", default='DEFAULT', type='str', help="Location to store blocks database (defaults to --datadir)") -parser.add_option("--rpcport", dest="rpcport", default='DEFAULT', type="str", help="RPC port for running armoryd.py") -parser.add_option("--testnet", dest="testnet", default=False, action="store_true", help="Use the testnet protocol") -parser.add_option("--offline", dest="offline", default=False, action="store_true", help="Force Armory to run in offline mode") -parser.add_option("--nettimeout", dest="nettimeout", default=2, type="int", help="Timeout for detecting internet connection at startup") -parser.add_option("--interport", dest="interport", default=-1, type="int", help="Port for inter-process communication between Armory instances") -parser.add_option("--debug", dest="doDebug", default=False, action="store_true", help="Increase amount of debugging output") -parser.add_option("--nologging", dest="logDisable", default=False, action="store_true", help="Disable all logging") -parser.add_option("--netlog", dest="netlog", default=False, action="store_true", help="Log networking messages sent and received by Armory") -parser.add_option("--logfile", dest="logFile", default='DEFAULT', type='str', help="Specify a non-default location to send logging information") -parser.add_option("--mtdebug", dest="mtdebug", default=False, action="store_true", help="Log multi-threaded call sequences") -parser.add_option("--skip-online-check", dest="forceOnline", default=False, action="store_true", help="Go into online mode, even if internet connection isn't detected") -parser.add_option("--skip-version-check", dest="skipVerCheck", default=False, action="store_true", help="Do not contact bitcoinarmory.com to check for new versions") -parser.add_option("--keypool", dest="keypool", default=100, type="int", help="Default number of addresses to lookahead in Armory wallets") -parser.add_option("--rebuild", dest="rebuild", default=False, action="store_true", help="Rebuild blockchain database and rescan") -parser.add_option("--rescan", dest="rescan", default=False, action="store_true", help="Rescan existing blockchain DB") -parser.add_option("--maxfiles", dest="maxOpenFiles",default=0, type="int", help="Set maximum allowed open files for LevelDB databases") - -# These are arguments passed by running unit-tests that need to be handled -parser.add_option("--port", dest="port", default=None, type="int", help="Unit Test Argument - Do not consume") -parser.add_option("--verbosity", dest="verbosity", default=None, type="int", help="Unit Test Argument - Do not consume") -parser.add_option("--coverage_output_dir", dest="coverageOutputDir", default=None, type="str", help="Unit Test Argument - Do not consume") -parser.add_option("--coverage_include", dest="coverageInclude", default=None, type="str", help="Unit Test Argument - Do not consume") - -################################################################################ -# We need to have some methods for casting ASCII<->Unicode<->Preferred -DEFAULT_ENCODING = 'utf-8' - -def isASCII(theStr): - try: - theStr.decode('ascii') - return True - except UnicodeEncodeError: - return False - except UnicodeDecodeError: - return False - except: - LOGEXCEPT('What was passed to this function? %s', theStr) - return False - - -def toBytes(theStr, theEncoding=DEFAULT_ENCODING): - if isinstance(theStr, unicode): - return theStr.encode(theEncoding) - elif isinstance(theStr, str): - return theStr - else: - LOGERROR('toBytes() not been defined for input: %s', str(type(theStr))) - - -def toUnicode(theStr, theEncoding=DEFAULT_ENCODING): - if isinstance(theStr, unicode): - return theStr - elif isinstance(theStr, str): - return unicode(theStr, theEncoding) - else: - LOGERROR('toUnicode() not been defined for input: %s', str(type(theStr))) - - -def toPreferred(theStr): - return toUnicode(theStr).encode(locale.getpreferredencoding()) - - -def lenBytes(theStr, theEncoding=DEFAULT_ENCODING): - return len(toBytes(theStr, theEncoding)) -################################################################################ - - - -(CLI_OPTIONS, CLI_ARGS) = parser.parse_args() - - -# Use CLI args to determine testnet or not -USE_TESTNET = CLI_OPTIONS.testnet -#USE_TESTNET = True - - -# Set default port for inter-process communication -if CLI_OPTIONS.interport < 0: - CLI_OPTIONS.interport = 8223 + (1 if USE_TESTNET else 0) - - - - -def getVersionString(vquad, numPieces=4): - vstr = '%d.%02d' % vquad[:2] - if (vquad[2] > 0 or vquad[3] > 0) and numPieces>2: - vstr += '.%d' % vquad[2] - if vquad[3] > 0 and numPieces>3: - vstr += '.%d' % vquad[3] - return vstr - -def getVersionInt(vquad, numPieces=4): - vint = int(vquad[0] * 1e7) - vint += int(vquad[1] * 1e5) - if numPieces>2: - vint += int(vquad[2] * 1e3) - if numPieces>3: - vint += int(vquad[3]) - return vint - -def readVersionString(verStr): - verList = [int(piece) for piece in verStr.split('.')] - while len(verList)<4: - verList.append(0) - return tuple(verList) - -def readVersionInt(verInt): - verStr = str(verInt).rjust(10,'0') - verList = [] - verList.append( int(verStr[ -3:]) ) - verList.append( int(verStr[ -5:-3 ]) ) - verList.append( int(verStr[ -7:-5 ]) ) - verList.append( int(verStr[:-7 ]) ) - return tuple(verList[::-1]) - -# Get the host operating system -import platform -opsys = platform.system() -OS_WINDOWS = 'win32' in opsys.lower() or 'windows' in opsys.lower() -OS_LINUX = 'nix' in opsys.lower() or 'nux' in opsys.lower() -OS_MACOSX = 'darwin' in opsys.lower() or 'osx' in opsys.lower() - -# Figure out the default directories for Satoshi client, and BicoinArmory -OS_NAME = '' -OS_VARIANT = '' -USER_HOME_DIR = '' -BTC_HOME_DIR = '' -ARMORY_HOME_DIR = '' -LEVELDB_DIR = '' -SUBDIR = 'testnet3' if USE_TESTNET else '' -if OS_WINDOWS: - OS_NAME = 'Windows' - OS_VARIANT = platform.win32_ver() - USER_HOME_DIR = os.getenv('APPDATA') - BTC_HOME_DIR = os.path.join(USER_HOME_DIR, 'Bitcoin', SUBDIR) - ARMORY_HOME_DIR = os.path.join(USER_HOME_DIR, 'Armory', SUBDIR) - BLKFILE_DIR = os.path.join(BTC_HOME_DIR, 'blocks') -elif OS_LINUX: - OS_NAME = 'Linux' - OS_VARIANT = platform.linux_distribution() - USER_HOME_DIR = os.getenv('HOME') - BTC_HOME_DIR = os.path.join(USER_HOME_DIR, '.bitcoin', SUBDIR) - ARMORY_HOME_DIR = os.path.join(USER_HOME_DIR, '.armory', SUBDIR) - BLKFILE_DIR = os.path.join(BTC_HOME_DIR, 'blocks') -elif OS_MACOSX: - platform.mac_ver() - OS_NAME = 'MacOSX' - OS_VARIANT = platform.mac_ver() - USER_HOME_DIR = os.path.expanduser('~/Library/Application Support') - BTC_HOME_DIR = os.path.join(USER_HOME_DIR, 'Bitcoin', SUBDIR) - ARMORY_HOME_DIR = os.path.join(USER_HOME_DIR, 'Armory', SUBDIR) - BLKFILE_DIR = os.path.join(BTC_HOME_DIR, 'blocks') -else: - print '***Unknown operating system!' - print '***Cannot determine default directory locations' - - -# Allow user to override default bitcoin-qt/bitcoind home directory -if not CLI_OPTIONS.satoshiHome.lower()=='default': - success = True - if USE_TESTNET: - testnetTry = os.path.join(CLI_OPTIONS.satoshiHome, 'testnet3') - if os.path.exists(testnetTry): - CLI_OPTIONS.satoshiHome = testnetTry - - if not os.path.exists(CLI_OPTIONS.satoshiHome): - print 'Directory "%s" does not exist! Using default!' % \ - CLI_OPTIONS.satoshiHome - else: - BTC_HOME_DIR = CLI_OPTIONS.satoshiHome - - - -# Allow user to override default Armory home directory -if not CLI_OPTIONS.datadir.lower()=='default': - if not os.path.exists(CLI_OPTIONS.datadir): - print 'Directory "%s" does not exist! Using default!' % \ - CLI_OPTIONS.datadir - else: - ARMORY_HOME_DIR = CLI_OPTIONS.datadir - -# Same for the directory that holds the LevelDB databases -LEVELDB_DIR = os.path.join(ARMORY_HOME_DIR, 'databases') -if not CLI_OPTIONS.leveldbDir.lower()=='default': - if not os.path.exists(CLI_OPTIONS.leveldbDir): - print 'Directory "%s" does not exist! Using default!' % \ - CLI_OPTIONS.leveldbDir - os.makedirs(CLI_OPTIONS.leveldbDir) - else: - LEVELDB_DIR = CLI_OPTIONS.leveldbDir - - - -# Change the settings file to use -#BITCOIND_PATH = None -#if not CLI_OPTIONS.bitcoindPath.lower()=='default': - #BITCOIND_PATH = CLI_OPTIONS.bitcoindPath - -# Change the settings file to use -if CLI_OPTIONS.settingsPath.lower()=='default': - CLI_OPTIONS.settingsPath = os.path.join(ARMORY_HOME_DIR, 'ArmorySettings.txt') - -# Change the log file to use -ARMORY_LOG_FILE = os.path.join(ARMORY_HOME_DIR, 'armorylog.txt') -ARMCPP_LOG_FILE = os.path.join(ARMORY_HOME_DIR, 'armorycpplog.txt') -if sys.argv[0] in ['ArmoryQt.py', 'ArmoryQt.exe', 'Armory.exe']: - ARMORY_LOG_FILElogFile = os.path.join(ARMORY_HOME_DIR, 'armorylog.txt') -else: - basename = os.path.basename(sys.argv[0]) - CLI_OPTIONS.logFile = os.path.join(ARMORY_HOME_DIR, '%s.log.txt' % basename) - -SETTINGS_PATH = CLI_OPTIONS.settingsPath - - - -# If this is the first Armory has been run, create directories -if ARMORY_HOME_DIR and not os.path.exists(ARMORY_HOME_DIR): - os.makedirs(ARMORY_HOME_DIR) - - -if not os.path.exists(LEVELDB_DIR): - os.makedirs(LEVELDB_DIR) - - -if sys.argv[0]=='ArmoryQt.py': - print '********************************************************************************' - print 'Loading Armory Engine:' - print ' Armory Version: ', getVersionString(BTCARMORY_VERSION) - print ' PyBtcWallet Version:', getVersionString(PYBTCWALLET_VERSION) - print 'Detected Operating system:', OS_NAME - print ' OS Variant :', OS_VARIANT - print ' User home-directory :', USER_HOME_DIR - print ' Satoshi BTC directory :', BTC_HOME_DIR - print ' Armory home dir :', ARMORY_HOME_DIR - print ' LevelDB directory :', LEVELDB_DIR - print ' Armory settings file :', SETTINGS_PATH - print ' Armory log file :', ARMORY_LOG_FILE - - - -class UnserializeError(Exception): pass -class BadAddressError(Exception): pass -class VerifyScriptError(Exception): pass -class FileExistsError(Exception): pass -class ECDSA_Error(Exception): pass -class PackerError(Exception): pass -class UnpackerError(Exception): pass -class UnitializedBlockDataError(Exception): pass -class WalletLockError(Exception): pass -class SignatureError(Exception): pass -class KeyDataError(Exception): pass -class ChecksumError(Exception): pass -class WalletAddressError(Exception): pass -class PassphraseError(Exception): pass -class EncryptionError(Exception): pass -class InterruptTestError(Exception): pass -class NetworkIDError(Exception): pass -class WalletExistsError(Exception): pass -class ConnectionError(Exception): pass -class BlockchainUnavailableError(Exception): pass -class InvalidHashError(Exception): pass -class BadURIError(Exception): pass -class CompressedKeyError(Exception): pass -class TooMuchPrecisionError(Exception): pass -class NegativeValueError(Exception): pass -class FiniteFieldError(Exception): pass -class BitcoindError(Exception): pass -class ShouldNotGetHereError(Exception): pass -class BadInputError(Exception): pass - - - - -##### MAIN NETWORK IS DEFAULT ##### -if not USE_TESTNET: - # TODO: The testnet genesis tx hash can't be the same...? - BITCOIN_PORT = 8333 - BITCOIN_RPC_PORT = 8332 - ARMORY_RPC_PORT = 8225 - MAGIC_BYTES = '\xf9\xbe\xb4\xd9' - GENESIS_BLOCK_HASH_HEX = '6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000' - GENESIS_BLOCK_HASH = 'o\xe2\x8c\n\xb6\xf1\xb3r\xc1\xa6\xa2F\xaec\xf7O\x93\x1e\x83e\xe1Z\x08\x9ch\xd6\x19\x00\x00\x00\x00\x00' - GENESIS_TX_HASH_HEX = '3ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a' - GENESIS_TX_HASH = ';\xa3\xed\xfdz{\x12\xb2z\xc7,>gv\x8fa\x7f\xc8\x1b\xc3\x88\x8aQ2:\x9f\xb8\xaaK\x1e^J' - ADDRBYTE = '\x00' - P2SHBYTE = '\x05' - PRIVKEYBYTE = '\x80' -else: - BITCOIN_PORT = 18333 - BITCOIN_RPC_PORT = 18332 - ARMORY_RPC_PORT = 18225 - MAGIC_BYTES = '\x0b\x11\x09\x07' - GENESIS_BLOCK_HASH_HEX = '43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000' - GENESIS_BLOCK_HASH = 'CI\x7f\xd7\xf8&\x95q\x08\xf4\xa3\x0f\xd9\xce\xc3\xae\xbay\x97 \x84\xe9\x0e\xad\x01\xea3\t\x00\x00\x00\x00' - GENESIS_TX_HASH_HEX = '3ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a' - GENESIS_TX_HASH = ';\xa3\xed\xfdz{\x12\xb2z\xc7,>gv\x8fa\x7f\xc8\x1b\xc3\x88\x8aQ2:\x9f\xb8\xaaK\x1e^J' - ADDRBYTE = '\x6f' - P2SHBYTE = '\xc4' - PRIVKEYBYTE = '\xef' - -if not CLI_OPTIONS.satoshiPort == 'DEFAULT': - try: - BITCOIN_PORT = int(CLI_OPTIONS.satoshiPort) - except: - raise TypeError, 'Invalid port for Bitcoin-Qt, using ' + str(BITCOIN_PORT) - - -if not CLI_OPTIONS.rpcport == 'DEFAULT': - try: - ARMORY_RPC_PORT = int(CLI_OPTIONS.rpcport) - except: - raise TypeError, 'Invalid RPC port for armoryd ' + str(ARMORY_RPC_PORT) - - -BLOCKCHAINS = {} -BLOCKCHAINS['\xf9\xbe\xb4\xd9'] = "Main Network" -BLOCKCHAINS['\xfa\xbf\xb5\xda'] = "Old Test Network" -BLOCKCHAINS['\x0b\x11\x09\x07'] = "Test Network (testnet3)" - -NETWORKS = {} -NETWORKS['\x00'] = "Main Network" -NETWORKS['\x6f'] = "Test Network" -NETWORKS['\x34'] = "Namecoin Network" - - - -######### INITIALIZE LOGGING UTILITIES ########## -# -# Setup logging to write INFO+ to file, and WARNING+ to console -# In debug mode, will write DEBUG+ to file and INFO+ to console -# - -# Want to get the line in which an error was triggered, but by wrapping -# the logger function (as I will below), the displayed "file:linenum" -# references the logger function, not the function that called it. -# So I use traceback to find the file and line number two up in the -# stack trace, and return that to be displayed instead of default -# [Is this a hack? Yes and no. I see no other way to do this] -def getCallerLine(): - stkTwoUp = traceback.extract_stack()[-3] - filename,method = stkTwoUp[0], stkTwoUp[1] - return '%s:%d' % (os.path.basename(filename),method) - -# When there's an error in the logging function, it's impossible to find! -# These wrappers will print the full stack so that it's possible to find -# which line triggered the error -def LOGDEBUG(msg, *a): - try: - logstr = msg if len(a)==0 else (msg%a) - callerStr = getCallerLine() + ' - ' - logging.debug(callerStr + logstr) - except TypeError: - traceback.print_stack() - raise - -def LOGINFO(msg, *a): - try: - logstr = msg if len(a)==0 else (msg%a) - callerStr = getCallerLine() + ' - ' - logging.info(callerStr + logstr) - except TypeError: - traceback.print_stack() - raise -def LOGWARN(msg, *a): - try: - logstr = msg if len(a)==0 else (msg%a) - callerStr = getCallerLine() + ' - ' - logging.warn(callerStr + logstr) - except TypeError: - traceback.print_stack() - raise -def LOGERROR(msg, *a): - try: - logstr = msg if len(a)==0 else (msg%a) - callerStr = getCallerLine() + ' - ' - logging.error(callerStr + logstr) - except TypeError: - traceback.print_stack() - raise -def LOGCRIT(msg, *a): - try: - logstr = msg if len(a)==0 else (msg%a) - callerStr = getCallerLine() + ' - ' - logging.critical(callerStr + logstr) - except TypeError: - traceback.print_stack() - raise -def LOGEXCEPT(msg, *a): - try: - logstr = msg if len(a)==0 else (msg%a) - callerStr = getCallerLine() + ' - ' - logging.exception(callerStr + logstr) - except TypeError: - traceback.print_stack() - raise - - - -DEFAULT_CONSOLE_LOGTHRESH = logging.WARNING -DEFAULT_FILE_LOGTHRESH = logging.INFO - -DEFAULT_PPRINT_LOGLEVEL = logging.DEBUG -DEFAULT_RAWDATA_LOGLEVEL = logging.DEBUG - -rootLogger = logging.getLogger('') -if CLI_OPTIONS.doDebug or CLI_OPTIONS.netlog or CLI_OPTIONS.mtdebug: - # Drop it all one level: console will see INFO, file will see DEBUG - DEFAULT_CONSOLE_LOGTHRESH -= 10 - DEFAULT_FILE_LOGTHRESH -= 10 - - -def chopLogFile(filename, size): - if not os.path.exists(filename): - print 'Log file doesn\'t exist [yet]' - return - - logfile = open(filename, 'r') - allLines = logfile.readlines() - logfile.close() - - nBytes,nLines = 0,0; - for line in allLines[::-1]: - nBytes += len(line) - nLines += 1 - if nBytes>size: - break - - logfile = open(filename, 'w') - for line in allLines[-nLines:]: - logfile.write(line) - logfile.close() - - - -# Cut down the log file to just the most recent 1 MB -chopLogFile(ARMORY_LOG_FILE, 1024*1024) - - -# Now set loglevels -DateFormat = '%Y-%m-%d %H:%M' -logging.getLogger('').setLevel(logging.DEBUG) -fileFormatter = logging.Formatter('%(asctime)s (%(levelname)s) -- %(message)s', \ - datefmt=DateFormat) -fileHandler = logging.FileHandler(ARMORY_LOG_FILE) -fileHandler.setLevel(DEFAULT_FILE_LOGTHRESH) -fileHandler.setFormatter(fileFormatter) -logging.getLogger('').addHandler(fileHandler) - -consoleFormatter = logging.Formatter('(%(levelname)s) %(message)s') -consoleHandler = logging.StreamHandler() -consoleHandler.setLevel(DEFAULT_CONSOLE_LOGTHRESH) -consoleHandler.setFormatter( consoleFormatter ) -logging.getLogger('').addHandler(consoleHandler) - - - -class stringAggregator(object): - def __init__(self): - self.theStr = '' - def getStr(self): - return self.theStr - def write(self, theStr): - self.theStr += theStr - - -# A method to redirect pprint() calls to the log file -# Need a way to take a pprint-able object, and redirect its output to file -# Do this by swapping out sys.stdout temporarily, execute theObj.pprint() -# then set sys.stdout back to the original. -def LOGPPRINT(theObj, loglevel=DEFAULT_PPRINT_LOGLEVEL): - sys.stdout = stringAggregator() - theObj.pprint() - printedStr = sys.stdout.getStr() - sys.stdout = sys.__stdout__ - stkOneUp = traceback.extract_stack()[-2] - filename,method = stkOneUp[0], stkOneUp[1] - methodStr = '(PPRINT from %s:%d)\n' % (filename,method) - logging.log(loglevel, methodStr + printedStr) - -# For super-debug mode, we'll write out raw data -def LOGRAWDATA(rawStr, loglevel=DEFAULT_RAWDATA_LOGLEVEL): - dtype = isLikelyDataType(rawStr) - stkOneUp = traceback.extract_stack()[-2] - filename,method = stkOneUp[0], stkOneUp[1] - methodStr = '(PPRINT from %s:%d)\n' % (filename,method) - pstr = rawStr[:] - if dtype==DATATYPE.Binary: - pstr = binary_to_hex(rawStr) - pstr = prettyHex(pstr, indent=' ', withAddr=False) - elif dtype==DATATYPE.Hex: - pstr = prettyHex(pstr, indent=' ', withAddr=False) - else: - pstr = ' ' + '\n '.join(pstr.split('\n')) - - logging.log(loglevel, methodStr + pstr) - - -cpplogfile = None -if CLI_OPTIONS.logDisable: - print 'Logging is disabled' - rootLogger.disabled = True - -# For now, ditch the C++-console-catching. Logging python is enough -# My attempt at C++ logging too was becoming a hardcore hack... -""" -elif CLI_OPTIONS.logcpp: - # In order to catch C++ output, we have to redirect ALL stdout - # (which means that console writes by python, too) - cpplogfile = open(ARMORY_LOG_FILE_CPP, 'r') - allLines = cpplogfile.readlines() - cpplogfile.close() - # Chop off the beginning of the file - nBytes,nLines = 0,0; - for line in allLines[::-1]: - nBytes += len(line) - nLines += 1 - if nBytes>100*1024: - break - cpplogfile = open(ARMORY_LOG_FILE_CPP, 'w') - print 'nlines:', nLines - for line in allLines[-nLines:]: - print line, - cpplogfile.write(line) - cpplogfile.close() - cpplogfile = open(ARMORY_LOG_FILE_CPP, 'a') - raw_input() - os.dup2(cpplogfile.fileno(), sys.stdout.fileno()) - raw_input() - os.dup2(cpplogfile.fileno(), sys.stderr.fileno()) -""" - - -fileRebuild = os.path.join(ARMORY_HOME_DIR, 'rebuild.txt') -fileRescan = os.path.join(ARMORY_HOME_DIR, 'rescan.txt') -if os.path.exists(fileRebuild): - LOGINFO('Found %s, will destroy and rebuild databases' % fileRebuild) - os.remove(fileRebuild) - if os.path.exists(fileRescan): - os.remove(fileRescan) - - CLI_OPTIONS.rebuild = True -elif os.path.exists(fileRescan): - LOGINFO('Found %s, will throw out saved history, rescan' % fileRescan) - os.remove(fileRescan) - if os.path.exists(fileRebuild): - os.remove(fileRebuild) - CLI_OPTIONS.rescan = True - - -def logexcept_override(type, value, tback): - import traceback - import logging - strList = traceback.format_exception(type,value,tback) - logging.error(''.join([s for s in strList])) - # then call the default handler - sys.__excepthook__(type, value, tback) - -sys.excepthook = logexcept_override - - -################################################################################ -def launchProcess(cmd, useStartInfo=True, *args, **kwargs): - LOGINFO('Executing popen: %s', str(cmd)) - if not OS_WINDOWS: - from subprocess import Popen, PIPE - return Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, *args, **kwargs) - else: - from subprocess import Popen, PIPE, STARTUPINFO, STARTF_USESHOWWINDOW - # Need lots of complicated stuff to accommodate quirks with Windows - if isinstance(cmd, basestring): - cmd2 = toPreferred(cmd) - else: - cmd2 = [toPreferred(c) for c in cmd] - - if useStartInfo: - startinfo = STARTUPINFO() - startinfo.dwFlags |= STARTF_USESHOWWINDOW - return Popen(cmd2, \ - *args, \ - stdin=PIPE, \ - stdout=PIPE, \ - stderr=PIPE, \ - startupinfo=startinfo, \ - **kwargs) - else: - return Popen(cmd2, \ - *args, \ - stdin=PIPE, \ - stdout=PIPE, \ - stderr=PIPE, \ - **kwargs) - - -################################################################################ -def killProcess(pid, sig='default'): - # I had to do this, because killing a process in Windows has issues - # when using py2exe (yes, os.kill does not work, for the same reason - # I had to pass stdin/stdout/stderr everywhere... - LOGWARN('Killing process pid=%d', pid) - if not OS_WINDOWS: - import os - sig = signal.SIGKILL if sig=='default' else sig - os.kill(pid, sig) - else: - import sys, os.path, ctypes, ctypes.wintypes - k32 = ctypes.WinDLL('kernel32.dll') - k32.OpenProcess.restype = ctypes.wintypes.HANDLE - k32.TerminateProcess.restype = ctypes.wintypes.BOOL - hProcess = k32.OpenProcess(1, False, pid) - k32.TerminateProcess(hProcess, 1) - k32.CloseHandle(hProcess) - - - -################################################################################ -def subprocess_check_output(*popenargs, **kwargs): - """ - Run command with arguments and return its output as a byte string. - Backported from Python 2.7, because it's stupid useful, short, and - won't exist on systems using Python 2.6 or earlier - """ - from subprocess import Popen, PIPE, CalledProcessError - process = launchProcess(*popenargs, **kwargs) - output, unused_err = process.communicate() - retcode = process.poll() - if retcode: - cmd = kwargs.get("args") - if cmd is None: - cmd = popenargs[0] - error = CalledProcessError(retcode, cmd) - error.output = output - raise error - return output - - -################################################################################ -def killProcessTree(pid): - # In this case, Windows is easier because we know it has the get_children - # call, because have bundled a recent version of psutil. Linux, however, - # does not have that function call in earlier versions. - if not OS_LINUX: - for child in psutil.Process(pid).get_children(): - killProcess(child.pid) - else: - proc = Popen("ps -o pid --ppid %d --noheaders" % pid, shell=True, stdout=PIPE) - out,err = proc.communicate() - for pid_str in out.split("\n")[:-1]: - killProcess(int(pid_str)) - - -################################################################################ -# Similar to subprocess_check_output, but used for long-running commands -def execAndWait(cli_str, timeout=0, useStartInfo=True): - """ - There may actually still be references to this function where check_output - would've been more appropriate. But I didn't know about check_output at - the time... - """ - - process = launchProcess(cli_str, shell=True, useStartInfo=useStartInfo) - pid = process.pid - start = RightNow() - while process.poll() == None: - time.sleep(0.1) - if timeout>0 and (RightNow() - start)>timeout: - print 'Process exceeded timeout, killing it' - killProcess(pid) - out,err = process.communicate() - return [out,err] - - - -################################################################################ -# Get system details for logging purposes -class DumbStruct(object): pass -def GetSystemDetails(): - """Checks memory of a given system""" - - out = DumbStruct() - - CPU,COR,X64,MEM = range(4) - sysParam = [None,None,None,None] - out.CpuStr = 'UNKNOWN' - if OS_LINUX: - # Get total RAM - freeStr = subprocess_check_output('free -m', shell=True) - totalMemory = freeStr.split('\n')[1].split()[1] - out.Memory = int(totalMemory) * 1024 - - # Get CPU name - out.CpuStr = 'Unknown' - cpuinfo = subprocess_check_output(['cat','/proc/cpuinfo']) - for line in cpuinfo.split('\n'): - if line.strip().lower().startswith('model name'): - out.CpuStr = line.split(':')[1].strip() - break - - - elif OS_WINDOWS: - import ctypes - class MEMORYSTATUSEX(ctypes.Structure): - _fields_ = [ - ("dwLength", ctypes.c_ulong), - ("dwMemoryLoad", ctypes.c_ulong), - ("ullTotalPhys", ctypes.c_ulonglong), - ("ullAvailPhys", ctypes.c_ulonglong), - ("ullTotalPageFile", ctypes.c_ulonglong), - ("ullAvailPageFile", ctypes.c_ulonglong), - ("ullTotalVirtual", ctypes.c_ulonglong), - ("ullAvailVirtual", ctypes.c_ulonglong), - ("sullAvailExtendedVirtual", ctypes.c_ulonglong), - ] - def __init__(self): - # have to initialize this to the size of MEMORYSTATUSEX - self.dwLength = ctypes.sizeof(self) - super(MEMORYSTATUSEX, self).__init__() - - stat = MEMORYSTATUSEX() - ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(stat)) - out.Memory = stat.ullTotalPhys/1024. - out.CpuStr = platform.processor() - elif OS_MACOSX: - memsizeStr = subprocess_check_output('sysctl hw.memsize', shell=True) - out.Memory = int(memsizeStr.split(": ")[1]) / 1024 - out.CpuStr = subprocess_check_output('sysctl -n machdep.cpu.brand_string', shell=True) - - out.NumCores = multiprocessing.cpu_count() - out.IsX64 = platform.architecture()[0].startswith('64') - out.Memory = out.Memory / (1024*1024.) - return out - -try: - SystemSpecs = GetSystemDetails() -except: - LOGEXCEPT('Error getting system details:') - LOGERROR('Skipping.') - SystemSpecs = DumbStruct() - SystemSpecs.Memory = -1 - SystemSpecs.CpuStr = 'Unknown' - SystemSpecs.NumCores = -1 - SystemSpecs.IsX64 = 'Unknown' - - -LOGINFO('') -LOGINFO('') -LOGINFO('') -LOGINFO('************************************************************') -LOGINFO('Invoked: ' + ' '.join(argv)) -LOGINFO('************************************************************') -LOGINFO('Loading Armory Engine:') -LOGINFO(' Armory Version : ' + getVersionString(BTCARMORY_VERSION)) -LOGINFO(' PyBtcWallet Version : ' + getVersionString(PYBTCWALLET_VERSION)) -LOGINFO('Detected Operating system: ' + OS_NAME) -LOGINFO(' OS Variant : ' + (str(OS_VARIANT) if OS_MACOSX else '-'.join(OS_VARIANT))) -LOGINFO(' User home-directory : ' + USER_HOME_DIR) -LOGINFO(' Satoshi BTC directory : ' + BTC_HOME_DIR) -LOGINFO(' Armory home dir : ' + ARMORY_HOME_DIR) -LOGINFO('Detected System Specs : ') -LOGINFO(' Total Available RAM : %0.2f GB', SystemSpecs.Memory) -LOGINFO(' CPU ID string : ' + SystemSpecs.CpuStr) -LOGINFO(' Number of CPU cores : %d cores', SystemSpecs.NumCores) -LOGINFO(' System is 64-bit : ' + str(SystemSpecs.IsX64)) -LOGINFO(' Preferred Encoding : ' + locale.getpreferredencoding()) -LOGINFO('') -LOGINFO('Network Name: ' + NETWORKS[ADDRBYTE]) -LOGINFO('Satoshi Port: %d', BITCOIN_PORT) -LOGINFO('Named options/arguments to armoryengine.py:') -for key,val in ast.literal_eval(str(CLI_OPTIONS)).iteritems(): - LOGINFO(' %-16s: %s', key,val) -LOGINFO('Other arguments:') -for val in CLI_ARGS: - LOGINFO(' %s', val) -LOGINFO('************************************************************') - - -def GetExecDir(): - """ - Return the path from where armoryengine was imported. Inspect method - expects a function or module name, it can actually inspect its own - name... - """ - srcfile = inspect.getsourcefile(GetExecDir) - srcpath = os.path.dirname(srcfile) - srcpath = os.path.abspath(srcpath) - return srcpath - - - -def coin2str(nSatoshi, ndec=8, rJust=True, maxZeros=8): - """ - Converts a raw value (1e-8 BTC) into a formatted string for display - - ndec, guarantees that we get get a least N decimal places in our result - - maxZeros means we will replace zeros with spaces up to M decimal places - in order to declutter the amount field - - """ - - nBtc = float(nSatoshi) / float(ONE_BTC) - s = ('%%0.%df' % ndec) % nBtc - s = s.rjust(18, ' ') - - if maxZeros < ndec: - maxChop = ndec - maxZeros - nChop = min(len(s) - len(str(s.strip('0'))), maxChop) - if nChop>0: - s = s[:-nChop] + nChop*' ' - - if nSatoshi < 10000*ONE_BTC: - s.lstrip() - - if not rJust: - s = s.strip(' ') - - s = s.replace('. ', '') - - return s - - -def coin2strNZ(nSatoshi): - """ Right-justified, minimum zeros, but with padding for alignment""" - return coin2str(nSatoshi, 8, True, 0) - -def coin2strNZS(nSatoshi): - """ Right-justified, minimum zeros, stripped """ - return coin2str(nSatoshi, 8, True, 0).strip() - -def coin2str_approx(nSatoshi, sigfig=3): - posVal = nSatoshi - isNeg = False - if nSatoshi<0: - isNeg = True - posVal *= -1 - - nDig = max(round(math.log(posVal+1, 10)-0.5), 0) - nChop = max(nDig-2, 0 ) - approxVal = round((10**nChop) * round(posVal / (10**nChop))) - return coin2str( (-1 if isNeg else 1)*approxVal, maxZeros=0) - - -def str2coin(theStr, negAllowed=True, maxDec=8, roundHighPrec=True): - coinStr = str(theStr) - if len(coinStr.strip())==0: - raise ValueError - - isNeg = ('-' in coinStr) - coinStrPos = coinStr.replace('-','') - if not '.' in coinStrPos: - if not negAllowed and isNeg: - raise NegativeValueError - return (int(coinStrPos)*ONE_BTC)*(-1 if isNeg else 1) - else: - lhs,rhs = coinStrPos.strip().split('.') - if len(lhs.strip('-'))==0: - lhs='0' - if len(rhs)>maxDec and not roundHighPrec: - raise TooMuchPrecisionError - if not negAllowed and isNeg: - raise NegativeValueError - fullInt = (int(lhs + rhs[:9].ljust(9,'0')) + 5) / 10 - return fullInt*(-1 if isNeg else 1) - - -# This is a sweet trick for create enum-like dictionaries. -# Either automatically numbers (*args), or name-val pairs (**kwargs) -#http://stackoverflow.com/questions/36932/whats-the-best-way-to-implement-an-enum-in-python -def enum(*sequential, **named): - enums = dict(zip(sequential, range(len(sequential))), **named) - return type('Enum', (), enums) - - -# Some useful constants to be used throughout everything -BASE58CHARS = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' -BASE16CHARS = '0123 4567 89ab cdef'.replace(' ','') -LITTLEENDIAN = '<'; -BIGENDIAN = '>'; -NETWORKENDIAN = '!'; -ONE_BTC = long(100000000) -CENT = long(1000000) -UNINITIALIZED = None -UNKNOWN = -2 -MIN_TX_FEE = 10000 -MIN_RELAY_TX_FEE = 10000 -MT_WAIT_TIMEOUT_SEC = 20; - -UINT8_MAX = 2**8-1 -UINT16_MAX = 2**16-1 -UINT32_MAX = 2**32-1 -UINT64_MAX = 2**64-1 - -RightNow = time.time -SECOND = 1 -MINUTE = 60 -HOUR = 3600 -DAY = 24*HOUR -WEEK = 7*DAY -MONTH = 30*DAY -YEAR = 365*DAY - -KILOBYTE = 1024.0 -MEGABYTE = 1024*KILOBYTE -GIGABYTE = 1024*MEGABYTE -TERABYTE = 1024*GIGABYTE -PETABYTE = 1024*TERABYTE - -# Set the default-default -DEFAULT_DATE_FORMAT = '%Y-%b-%d %I:%M%p' -FORMAT_SYMBOLS = [ \ - ['%y', 'year, two digit (00-99)'], \ - ['%Y', 'year, four digit'], \ - ['%b', 'month name (abbrev)'], \ - ['%B', 'month name (full)'], \ - ['%m', 'month number (01-12)'], \ - ['%d', 'day of month (01-31)'], \ - ['%H', 'hour 24h (00-23)'], \ - ['%I', 'hour 12h (01-12)'], \ - ['%M', 'minute (00-59)'], \ - ['%p', 'morning/night (am,pm)'], \ - ['%a', 'day of week (abbrev)'], \ - ['%A', 'day of week (full)'], \ - ['%%', 'percent symbol'] ] - - -# The database uses prefixes to identify type of address. Until the new -# wallet format is created that supports more than just hash160 addresses -# we have to explicitly add the prefix to any hash160 values that are being -# sent to any of the C++ utilities. For instance, the BlockDataManager (BDM) -# (C++ stuff) tracks regular hash160 addresses, P2SH, multisig, and all -# non-standard scripts. Any such "scrAddrs" (script-addresses) will eventually -# be valid entities for tracking in a wallet. Until then, all of our python -# utilities all use just hash160 values, and we manually add the prefix -# before talking to the BDM. -HASH160PREFIX = '\x00' -P2SHPREFIX = '\x05' -MSIGPREFIX = '\xfe' -NONSTDPREFIX = '\xff' -def CheckHash160(scrAddr): - if not len(scrAddr)==21: - raise BadAddressError, "Supplied scrAddr is not a Hash160 value!" - if not scrAddr[0] == HASH160PREFIX: - raise BadAddressError, "Supplied scrAddr is not a Hash160 value!" - return scrAddr[1:] - -def Hash160ToScrAddr(a160): - if not len(a160)==20: - LOGERROR('Invalid hash160 value!') - return HASH160PREFIX + a160 - -def HexHash160ToScrAddr(a160): - if not len(a160)==40: - LOGERROR('Invalid hash160 value!') - return HASH160PREFIX + hex_to_binary(a160) - - -# Some more constants that are needed to play nice with the C++ utilities -ARMORY_DB_BARE, ARMORY_DB_LITE, ARMORY_DB_PARTIAL, ARMORY_DB_FULL, ARMORY_DB_SUPER = range(5) -DB_PRUNE_ALL, DB_PRUNE_NONE = range(2) - - - - -# Some time methods (RightNow() return local unix timestamp) -RightNow = time.time -def RightNowUTC(): - return time.mktime(time.gmtime(RightNow())) - - - -################################################################################ -# Load the C++ utilites here -# -# The SWIG/C++ block utilities give us access to the blockchain, fast ECDSA -# operations, and general encryption/secure-binary containers -################################################################################ -try: - import CppBlockUtils as Cpp - from CppBlockUtils import KdfRomix, CryptoECDSA, CryptoAES, SecureBinaryData - LOGINFO('C++ block utilities loaded successfully') -except: - LOGCRIT('C++ block utilities not available.') - LOGCRIT(' Make sure that you have the SWIG-compiled modules') - LOGCRIT(' in the current directory (or added to the PATH)') - LOGCRIT(' Specifically, you need:') - LOGCRIT(' CppBlockUtils.py and') - if OS_LINUX or OS_MACOSX: - LOGCRIT(' _CppBlockUtils.so') - elif OS_WINDOWS: - LOGCRIT(' _CppBlockUtils.pyd') - else: - LOGCRIT('\n\n... UNKNOWN operating system') - raise - - - - - -DATATYPE = enum("Binary", 'Base58', 'Hex') -def isLikelyDataType(theStr, dtype=None): - """ - This really shouldn't be used on short strings. Hence - why it's called "likely" datatype... - """ - ret = None - hexCount = sum([1 if c in BASE16CHARS else 0 for c in theStr]) - b58Count = sum([1 if c in BASE58CHARS else 0 for c in theStr]) - canBeHex = hexCount==len(theStr) - canBeB58 = b58Count==len(theStr) - if canBeHex: - ret = DATATYPE.Hex - elif canBeB58 and not canBeHex: - ret = DATATYPE.Base58 - else: - ret = DATATYPE.Binary - - if dtype==None: - return ret - else: - return dtype==ret - - -def getCurrTimeAndBlock(): - time0 = long(RightNowUTC()) - if TheBDM.getBDMState()=='BlockchainReady': - return (time0, TheBDM.getTopBlockHeight()) - else: - return (time0, UINT32_MAX) - - - -# Define all the hashing functions we're going to need. We don't actually -# use any of the first three directly (sha1, sha256, ripemd160), we only -# use hash256 and hash160 which use the first three to create the ONLY hash -# operations we ever do in the bitcoin network -# UPDATE: mini-private-key format requires vanilla sha256... -def sha1(bits): - return hashlib.new('sha1', bits).digest() -def sha256(bits): - return hashlib.new('sha256', bits).digest() -def sha512(bits): - return hashlib.new('sha512', bits).digest() -def ripemd160(bits): - # It turns out that not all python has ripemd160...? - #return hashlib.new('ripemd160', bits).digest() - return Cpp.BtcUtils().ripemd160_SWIG(bits) -def hash256(s): - """ Double-SHA256 """ - return sha256(sha256(s)) -def hash160(s): - """ RIPEMD160( SHA256( binaryStr ) ) """ - return Cpp.BtcUtils().getHash160_SWIG(s) - - -def HMAC(key, msg, hashfunc=sha512, hashsz=None): - """ This is intended to be simple, not fast. For speed, use HDWalletCrypto() """ - hashsz = len(hashfunc('')) if hashsz==None else hashsz - key = (hashfunc(key) if len(key)>hashsz else key) - key = key.ljust(hashsz, '\x00') - okey = ''.join([chr(ord('\x5c')^ord(c)) for c in key]) - ikey = ''.join([chr(ord('\x36')^ord(c)) for c in key]) - return hashfunc( okey + hashfunc(ikey + msg) ) - -HMAC256 = lambda key,msg: HMAC(key,msg,sha256, 32) -HMAC512 = lambda key,msg: HMAC(key,msg,sha512, 64) - -################################################################################ -def prettyHex(theStr, indent='', withAddr=True, major=8, minor=8): - """ - This is the same as pprintHex(), but returns the string instead of - printing it to console. This is useful for redirecting output to - files, or doing further modifications to the data before display - """ - outStr = '' - sz = len(theStr) - nchunk = int((sz-1)/minor) + 1; - for i in range(nchunk): - if i%major==0: - outStr += '\n' + indent - if withAddr: - locStr = int_to_hex(i*minor/2, widthBytes=2, endOut=BIGENDIAN) - outStr += '0x' + locStr + ': ' - outStr += theStr[i*minor:(i+1)*minor] + ' ' - return outStr - - - - - -################################################################################ -def pprintHex(theStr, indent='', withAddr=True, major=8, minor=8): - """ - This method takes in a long hex string and prints it out into rows - of 64 hex chars, in chunks of 8 hex characters, and with address - markings on each row. This means that each row displays 32 bytes, - which is usually pleasant. - - The format is customizable: you can adjust the indenting of the - entire block, remove address markings, or change the major/minor - grouping size (major * minor = hexCharsPerRow) - """ - print prettyHex(theStr, indent, withAddr, major, minor) - - - -def pprintDiff(str1, str2, indent=''): - if not len(str1)==len(str2): - print 'pprintDiff: Strings are different length!' - return - - byteDiff = [] - for i in range(len(str1)): - if str1[i]==str2[i]: - byteDiff.append('-') - else: - byteDiff.append('X') - - pprintHex(''.join(byteDiff), indent=indent) - - - - -##### Switch endian-ness ##### -def hex_switchEndian(s): - """ Switches the endianness of a hex string (in pairs of hex chars) """ - pairList = [s[i]+s[i+1] for i in xrange(0,len(s),2)] - return ''.join(pairList[::-1]) -def binary_switchEndian(s): - """ Switches the endianness of a binary string """ - return s[::-1] - - -##### INT/HEXSTR ##### -def int_to_hex(i, widthBytes=0, endOut=LITTLEENDIAN): - """ - Convert an integer (int() or long()) to hexadecimal. Default behavior is - to use the smallest even number of hex characters necessary, and using - little-endian. Use the widthBytes argument to add 0-padding where needed - if you are expecting constant-length output. - """ - h = hex(i)[2:] - if isinstance(i,long): - h = h[:-1] - if len(h)%2 == 1: - h = '0'+h - if not widthBytes==0: - nZero = 2*widthBytes - len(h) - if nZero > 0: - h = '0'*nZero + h - if endOut==LITTLEENDIAN: - h = hex_switchEndian(h) - return h - -def hex_to_int(h, endIn=LITTLEENDIAN): - """ - Convert hex-string to integer (or long). Default behavior is to interpret - hex string as little-endian - """ - hstr = h.replace(' ','') # copies data, no references - if endIn==LITTLEENDIAN: - hstr = hex_switchEndian(hstr) - return( int(hstr, 16) ) - - -##### HEXSTR/BINARYSTR ##### -def hex_to_binary(h, endIn=LITTLEENDIAN, endOut=LITTLEENDIAN): - """ - Converts hexadecimal to binary (in a python string). Endianness is - only switched if (endIn != endOut) - """ - bout = h.replace(' ','') # copies data, no references - if not endIn==endOut: - bout = hex_switchEndian(bout) - return bout.decode('hex_codec') - - -def binary_to_hex(b, endOut=LITTLEENDIAN, endIn=LITTLEENDIAN): - """ - Converts binary to hexadecimal. Endianness is only switched - if (endIn != endOut) - """ - hout = b.encode('hex_codec') - if not endOut==endIn: - hout = hex_switchEndian(hout) - return hout - - -##### INT/BINARYSTR ##### -def int_to_binary(i, widthBytes=0, endOut=LITTLEENDIAN): - """ - Convert integer to binary. Default behavior is use as few bytes - as necessary, and to use little-endian. This can be changed with - the two optional input arguemnts. - """ - h = int_to_hex(i,widthBytes) - return hex_to_binary(h, endOut=endOut) - -def binary_to_int(b, endIn=LITTLEENDIAN): - """ - Converts binary to integer (or long). Interpret as LE by default - """ - h = binary_to_hex(b, endIn, LITTLEENDIAN) - return hex_to_int(h) - -##### INT/BITS ##### - -def int_to_bitset(i, widthBytes=0): - bitsOut = [] - while i>0: - i,r = divmod(i,2) - bitsOut.append(['0','1'][r]) - result = ''.join(bitsOut) - if widthBytes != 0: - result = result.ljust(widthBytes*8,'0') - return result - -def bitset_to_int(bitset): - n = 0 - for i,bit in enumerate(bitset): - n += (0 if bit=='0' else 1) * 2**i - return n - - - -EmptyHash = hex_to_binary('00'*32) - - -################################################################################ -# BINARY/BASE58 CONVERSIONS -def binary_to_base58(binstr): - """ - This method applies the Bitcoin-specific conversion from binary to Base58 - which may includes some extra "zero" bytes, such as is the case with the - main-network addresses. - - This method is labeled as outputting an "addrStr", but it's really this - special kind of Base58 converter, which makes it usable for encoding other - data, such as ECDSA keys or scripts. - """ - padding = 0; - for b in binstr: - if b=='\x00': - padding+=1 - else: - break - - n = 0 - for ch in binstr: - n *= 256 - n += ord(ch) - - b58 = '' - while n > 0: - n, r = divmod (n, 58) - b58 = BASE58CHARS[r] + b58 - return '1'*padding + b58 - - -################################################################################ -def base58_to_binary(addr): - """ - This method applies the Bitcoin-specific conversion from Base58 to binary - which may includes some extra "zero" bytes, such as is the case with the - main-network addresses. - - This method is labeled as inputting an "addrStr", but it's really this - special kind of Base58 converter, which makes it usable for encoding other - data, such as ECDSA keys or scripts. - """ - # Count the zeros ('1' characters) at the beginning - padding = 0; - for c in addr: - if c=='1': - padding+=1 - else: - break - - n = 0 - for ch in addr: - n *= 58 - n += BASE58CHARS.index(ch) - - binOut = '' - while n>0: - d,m = divmod(n,256) - binOut = chr(m) + binOut - n = d - return '\x00'*padding + binOut - - - - - - - -################################################################################ -def hash160_to_addrStr(binStr, isP2SH=False): - """ - Converts the 20-byte pubKeyHash to 25-byte binary Bitcoin address - which includes the network byte (prefix) and 4-byte checksum (suffix) - """ - addr21 = (P2SHBYTE if isP2SH else ADDRBYTE) + binStr - addr25 = addr21 + hash256(addr21)[:4] - return binary_to_base58(addr25); - -################################################################################ -def addrStr_is_p2sh(b58Str): - binStr = base58_to_binary(b58Str) - if not len(binStr)==25: - return False - return (binStr[0] == P2SHBYTE) - -################################################################################ -def addrStr_to_hash160(b58Str): - return base58_to_binary(b58Str)[1:-4] - - -###### Typing-friendly Base16 ##### -# Implements "hexadecimal" encoding but using only easy-to-type -# characters in the alphabet. Hex usually includes the digits 0-9 -# which can be slow to type, even for good typists. On the other -# hand, by changing the alphabet to common, easily distinguishable, -# lowercase characters, typing such strings will become dramatically -# faster. Additionally, some default encodings of QRCodes do not -# preserve the capitalization of the letters, meaning that Base58 -# is not a feasible options -NORMALCHARS = '0123 4567 89ab cdef'.replace(' ','') -EASY16CHARS = 'asdf ghjk wert uion'.replace(' ','') -hex_to_base16_map = {} -base16_to_hex_map = {} -for n,b in zip(NORMALCHARS,EASY16CHARS): - hex_to_base16_map[n] = b - base16_to_hex_map[b] = n - -def binary_to_easyType16(binstr): - return ''.join([hex_to_base16_map[c] for c in binary_to_hex(binstr)]) - -# Treat unrecognized characters as 0, to facilitate possibly later recovery of -# their correct values from the checksum. -def easyType16_to_binary(b16str): - return hex_to_binary(''.join([base16_to_hex_map.get(c, '0') for c in b16str])) - - -def makeSixteenBytesEasy(b16): - if not len(b16)==16: - raise ValueError, 'Must supply 16-byte input' - chk2 = computeChecksum(b16, nBytes=2) - et18 = binary_to_easyType16(b16 + chk2) - nineQuads = [et18[i*4:(i+1)*4] for i in range(9)] - first4 = ' '.join(nineQuads[:4]) - second4 = ' '.join(nineQuads[4:8]) - last1 = nineQuads[8] - return ' '.join([first4, second4, last1]) - -def readSixteenEasyBytes(et18): - b18 = easyType16_to_binary(et18.strip().replace(' ','')) - b16 = b18[:16] - chk = b18[ 16:] - if chk=='': - LOGWARN('Missing checksum when reading EasyType') - return (b16, 'No_Checksum') - b16new = verifyChecksum(b16, chk) - if len(b16new)==0: - return ('','Error_2+') - elif not b16new==b16: - return (b16new,'Fixed_1') - else: - return (b16new,None) - -##### FLOAT/BTC ##### -# https://en.bitcoin.it/wiki/Proper_Money_Handling_(JSON-RPC) -def ubtc_to_floatStr(n): - return '%d.%08d' % divmod (n, ONE_BTC) -def floatStr_to_ubtc(s): - return long(round(float(s) * ONE_BTC)) -def float_to_btc (f): - return long (round(f * ONE_BTC)) - - - -##### And a few useful utilities ##### -def unixTimeToFormatStr(unixTime, formatStr=DEFAULT_DATE_FORMAT): - """ - Converts a unix time (like those found in block headers) to a - pleasant, human-readable format - """ - dtobj = datetime.fromtimestamp(unixTime) - dtstr = u'' + dtobj.strftime(formatStr).decode('utf-8') - return dtstr[:-2] + dtstr[-2:].lower() - -def secondsToHumanTime(nSec): - strPieces = [] - floatSec = float(nSec) - if floatSec < 0.9*MINUTE: - strPieces = [floatSec, 'second'] - elif floatSec < 0.9*HOUR: - strPieces = [floatSec/MINUTE, 'minute'] - elif floatSec < 0.9*DAY: - strPieces = [floatSec/HOUR, 'hour'] - elif floatSec < 0.9*WEEK: - strPieces = [floatSec/DAY, 'day'] - elif floatSec < 0.9*MONTH: - strPieces = [floatSec/WEEK, 'week'] - else: - strPieces = [floatSec/MONTH, 'month'] - - if strPieces[0]<1.25: - return '1 '+strPieces[1] - elif strPieces[0]<=1.75: - return '1.5 '+strPieces[1]+'s' - else: - return '%d %ss' % (int(strPieces[0]+0.5), strPieces[1]) - -def bytesToHumanSize(nBytes): - if nBytes0: - if not beQuiet: LOGWARN('fixed!') - return fixStr - else: - # ONE LAST CHECK SPECIFIC TO MY SERIALIZATION SCHEME: - # If the string was originally all zeros, chksum is hash256('') - # ...which is a known value, and frequently used in my files - if chksum==hex_to_binary('5df6e0e2'): - if not beQuiet: LOGWARN('fixed!') - return '' - - - # ID a checksum byte error... - origHash = hashFunc(bin1) - for i in range(len(chksum)): - chkArray = [chksum[j] for j in range(len(chksum))] - for ch in range(256): - chkArray[i] = chr(ch) - if origHash.startswith(''.join(chkArray)): - LOGWARN('***Checksum error! Incorrect byte in checksum!') - return bin1 - - LOGWARN('Checksum fix failed') - return '' - - -# Taken directly from rpc.cpp in reference bitcoin client, 0.3.24 -def binaryBits_to_difficulty(b): - """ Converts the 4-byte binary difficulty string to a float """ - i = binary_to_int(b) - nShift = (i >> 24) & 0xff - dDiff = float(0x0000ffff) / float(i & 0x00ffffff) - while nShift < 29: - dDiff *= 256.0 - nShift += 1 - while nShift > 29: - dDiff /= 256.0 - nShift -= 1 - return dDiff - -# TODO: I don't actually know how to do this, yet... -def difficulty_to_binaryBits(i): - pass - - -################################################################################ -from qrcodenative import QRCode, QRErrorCorrectLevel -def CreateQRMatrix(dataToEncode, errLevel='L'): - sz=3 - success=False - qrmtrx = [[]] - while sz<20: - try: - errCorrectEnum = getattr(QRErrorCorrectLevel, errLevel.upper()) - qr = QRCode(sz, errCorrectEnum) - qr.addData(dataToEncode) - qr.make() - success=True - break - except TypeError: - sz += 1 - - if not success: - LOGERROR('Unsuccessful attempt to create QR code') - LOGERROR('Data to encode: (Length: %s, isAscii: %s)', \ - len(dataToEncode), isASCII(dataToEncode)) - return [[0]], 1 - - qrmtrx = [] - modCt = qr.getModuleCount() - for r in range(modCt): - tempList = [0]*modCt - for c in range(modCt): - # The matrix is transposed by default, from what we normally expect - tempList[c] = 1 if qr.isDark(c,r) else 0 - qrmtrx.append(tempList) - - return [qrmtrx, modCt] - - - -################################################################################ -################################################################################ -# Classes for reading and writing large binary objects -################################################################################ -################################################################################ -UINT8, UINT16, UINT32, UINT64, INT8, INT16, INT32, INT64, VAR_INT, VAR_STR, FLOAT, BINARY_CHUNK = range(12) - -# Seed this object with binary data, then read in its pieces sequentially -class BinaryUnpacker(object): - """ - Class for helping unpack binary streams of data. Typical usage is - >> bup = BinaryUnpacker(myBinaryData) - >> int32 = bup.get(UINT32) - >> int64 = bup.get(VAR_INT) - >> bytes10 = bup.get(BINARY_CHUNK, 10) - >> ...etc... - """ - def __init__(self, binaryStr): - self.binaryStr = binaryStr - self.pos = 0 - - def getSize(self): return len(self.binaryStr) - def getRemainingSize(self): return len(self.binaryStr) - self.pos - def getBinaryString(self): return self.binaryStr - def getRemainingString(self): return self.binaryStr[self.pos:] - def append(self, binaryStr): self.binaryStr += binaryStr - def advance(self, bytesToAdvance): self.pos += bytesToAdvance - def rewind(self, bytesToRewind): self.pos -= bytesToRewind - def resetPosition(self, toPos=0): self.pos = toPos - def getPosition(self): return self.pos - - def get(self, varType, sz=0, endianness=LITTLEENDIAN): - """ - First argument is the data-type: UINT32, VAR_INT, etc. - If BINARY_CHUNK, need to supply a number of bytes to read, as well - """ - def sizeCheck(sz): - if self.getRemainingSize()> binpack = BinaryPacker() - >> bup.put(UINT32, 12) - >> bup.put(VAR_INT, 78) - >> bup.put(BINARY_CHUNK, '\x9f'*10) - >> ...etc... - >> result = bup.getBinaryString() - """ - def __init__(self): - self.binaryConcat = [] - - def getSize(self): - return sum([len(a) for a in self.binaryConcat]) - - def getBinaryString(self): - return ''.join(self.binaryConcat) - - def __str__(self): - return self.getBinaryString() - - - def put(self, varType, theData, width=None, endianness=LITTLEENDIAN): - """ - Need to supply the argument type you are put'ing into the stream. - Values of BINARY_CHUNK will automatically detect the size as necessary - - Use width=X to include padding of BINARY_CHUNKs w/ 0x00 bytes - """ - E = endianness - if varType == UINT8: - self.binaryConcat += int_to_binary(theData, 1, endianness) - elif varType == UINT16: - self.binaryConcat += int_to_binary(theData, 2, endianness) - elif varType == UINT32: - self.binaryConcat += int_to_binary(theData, 4, endianness) - elif varType == UINT64: - self.binaryConcat += int_to_binary(theData, 8, endianness) - elif varType == INT8: - self.binaryConcat += pack(E+'b', theData) - elif varType == INT16: - self.binaryConcat += pack(E+'h', theData) - elif varType == INT32: - self.binaryConcat += pack(E+'i', theData) - elif varType == INT64: - self.binaryConcat += pack(E+'q', theData) - elif varType == VAR_INT: - self.binaryConcat += packVarInt(theData)[0] - elif varType == VAR_STR: - self.binaryConcat += packVarInt(len(theData))[0] - self.binaryConcat += theData - elif varType == FLOAT: - self.binaryConcat += pack(E+'f', theData) - elif varType == BINARY_CHUNK: - if width==None: - self.binaryConcat += theData - else: - if len(theData)>width: - raise PackerError, 'Too much data to fit into fixed width field' - self.binaryConcat += theData.ljust(width, '\x00') - else: - raise PackerError, "Var type not recognized! VarType="+str(varType) - -################################################################################ - -# The following params are for the Bitcoin elliptic curves (secp256k1) -SECP256K1_MOD = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2FL -SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141L -SECP256K1_B = 0x0000000000000000000000000000000000000000000000000000000000000007L -SECP256K1_A = 0x0000000000000000000000000000000000000000000000000000000000000000L -SECP256K1_GX = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798L -SECP256K1_GY = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8L - - - - - -################################################################################ -################################################################################ -# START FINITE FIELD OPERATIONS - - -class FiniteField(object): - """ - Create a simple, prime-order FiniteField. Because this is used only - to encode data of fixed width, I enforce prime-order by hardcoding - primes, and you just pick the data width (in bytes). If your desired - data width is not here, simply find a prime number very close to 2^N, - and add it to the PRIMES map below. - - This will be used for Shamir's Secret Sharing scheme. Encode your - data as the coeffient of finite-field polynomial, and store points - on that polynomial. The order of the polynomial determines how - many points are needed to recover the original secret. - """ - - # bytes: primeclosetomaxval - PRIMES = { 1: 2**8-5, # mainly for testing - 2: 2**16-39, - 4: 2**32-5, - 8: 2**64-59, - 16: 2**128-797, - 20: 2**160-543, - 24: 2**192-333, - 32: 2**256-357, - 48: 2**384-317, - 64: 2**512-569, - 96: 2**768-825, - 128: 2**1024-105, - 192: 2**1536-3453, - 256: 2**2048-1157 } - - def __init__(self, nbytes): - if not self.PRIMES.has_key(nbytes): - LOGERROR('No primes available for size=%d bytes', nbytes) - self.prime = None - raise FiniteFieldError - self.prime = self.PRIMES[nbytes] - - - def add(self,a,b): - return (a+b) % self.prime - - def subtract(self,a,b): - return (a-b) % self.prime - - def mult(self,a,b): - return (a*b) % self.prime - - def power(self,a,b): - result = 1 - while(b>0): - b,x = divmod(b,2) - result = (result * (a if x else 1)) % self.prime - a = a*a % self.prime - return result - - def powinv(self,a): - """ USE ONLY PRIME MODULUS """ - return self.power(a,self.prime-2) - - def divide(self,a,b): - """ USE ONLY PRIME MODULUS """ - baddinv = self.powinv(b) - return self.mult(a,baddinv) - - def mtrxrmrowcol(self,mtrx,r,c): - if not len(mtrx) == len(mtrx[0]): - LOGERROR('Must be a square matrix!') - return [] - sz = len(mtrx) - return [[mtrx[i][j] for j in range(sz) if not j==c] for i in range(sz) if not i==r] - - - ################################################################################ - def mtrxdet(self,mtrx): - if len(mtrx)==1: - return mtrx[0][0] - - if not len(mtrx) == len(mtrx[0]): - LOGERROR('Must be a square matrix!') - return -1 - - result = 0; - for i in range(len(mtrx)): - mult = mtrx[0][i] * (-1 if i%2==1 else 1) - subdet = self.mtrxdet(self.mtrxrmrowcol(mtrx,0,i)) - result = self.add(result, self.mult(mult,subdet)) - return result - - ################################################################################ - def mtrxmultvect(self,mtrx, vect): - M,N = len(mtrx), len(mtrx[0]) - if not len(mtrx[0])==len(vect): - LOGERROR('Mtrx and vect are incompatible: %dx%d, %dx1', M, N, len(vect)) - return [ sum([self.mult(mtrx[i][j],vect[j]) for j in range(N)])%self.prime for i in range(M) ] - - ################################################################################ - def mtrxmult(self,m1, m2): - M1,N1 = len(m1), len(m1[0]) - M2,N2 = len(m2), len(m2[0]) - if not N1==M2: - LOGERROR('Mtrx and vect are incompatible: %dx%d, %dx%d', M1,N1, M2,N2) - inner = lambda i,j: sum([self.mult(m1[i][k],m2[k][j]) for k in range(N1)]) - return [ [inner(i,j)%self.prime for j in range(N1)] for i in range(M1) ] - - ################################################################################ - def mtrxadjoint(self,mtrx): - sz = len(mtrx) - inner = lambda i,j: self.mtrxdet(self.mtrxrmrowcol(mtrx,i,j)) - return [[((-1 if (i+j)%2==1 else 1)*inner(j,i))%self.prime for j in range(sz)] for i in range(sz)] - - ################################################################################ - def mtrxinv(self,mtrx): - det = self.mtrxdet(mtrx) - adj = self.mtrxadjoint(mtrx) - sz = len(mtrx) - return [[self.divide(adj[i][j],det) for j in range(sz)] for i in range(sz)] - - -################################################################################ -def SplitSecret(secret, needed, pieces, nbytes=None, use_random_x=False): - if not isinstance(secret, basestring): - secret = secret.toBinStr() - - if nbytes==None: - nbytes = len(secret) - - ff = FiniteField(nbytes) - fragments = [] - - # Convert secret to an integer - a = binary_to_int(SecureBinaryData(secret).toBinStr(),BIGENDIAN) - if not a=needed: - LOGERROR('You must create more pieces than needed to reconstruct!') - raise FiniteFieldError - - if needed==1 or needed>8: - LOGERROR('Can split secrets into parts *requiring* at most 8 fragments') - LOGERROR('You can break it into as many optional fragments as you want') - raise FiniteFieldError - - - # We deterministically produce the coefficients so that we always use the - # same polynomial for a given secret - lasthmac = secret[:] - othernum = [] - for i in range(pieces+needed-1): - lasthmac = HMAC512(lasthmac, 'splitsecrets')[:nbytes] - othernum.append(binary_to_int(lasthmac)) - - def poly(x): - polyout = ff.mult(a, ff.power(x,needed-1)) - for i,e in enumerate(range(needed-2,-1,-1)): - term = ff.mult(othernum[i], ff.power(x,e)) - polyout = ff.add(polyout, term) - return polyout - - for i in range(pieces): - x = othernum[i+2] if use_random_x else i+1 - fragments.append( [x, poly(x)] ) - - secret,a = None,None - fragments = [ [int_to_binary(p, nbytes, BIGENDIAN) for p in frag] for frag in fragments] - return fragments - - -################################################################################ -def ReconstructSecret(fragments, needed, nbytes): - - ff = FiniteField(nbytes) - pairs = fragments[:needed] - m = [] - v = [] - for x,y in pairs: - x = binary_to_int(x, BIGENDIAN) - y = binary_to_int(y, BIGENDIAN) - m.append([]) - for i,e in enumerate(range(needed-1,-1,-1)): - m[-1].append( ff.power(x,e) ) - v.append(y) - - minv = ff.mtrxinv(m) - outvect = ff.mtrxmultvect(minv,v) - return int_to_binary(outvect[0], nbytes, BIGENDIAN) - - -################################################################################ -def createTestingSubsets( fragIndices, M, maxTestCount=20): - """ - Returns (IsRandomized, listOfTuplesOfSizeM) - """ - numIdx = len(fragIndices) - - if M>numIdx: - LOGERROR('Insufficent number of fragments') - raise KeyDataError - elif M==numIdx: - LOGINFO('Fragments supplied == needed. One subset to test (%s-of-N)' % M) - return ( False, [tuple(fragIndices)] ) - else: - LOGINFO('Test reconstruct %s-of-N, with %s fragments' % (M, numIdx)) - subs = [] - - # Compute the number of possible subsets. This is stable because we - # shouldn't ever have more than 12 fragments - fact = math.factorial - numCombo = fact(numIdx) / ( fact(M) * fact(numIdx-M) ) - - if numCombo <= maxTestCount: - LOGINFO('Testing all %s combinations...' % numCombo) - for x in xrange(2**numIdx): - bits = int_to_bitset(x) - if not bits.count('1') == M: - continue - - subs.append(tuple([fragIndices[i] for i,b in enumerate(bits) if b=='1'])) - - return (False, sorted(subs)) - else: - LOGINFO('#Subsets > %s, will need to randomize' % maxTestCount) - usedSubsets = set() - while len(subs) < maxTestCount: - sample = tuple(sorted(random.sample(fragIndices, M))) - if not sample in usedSubsets: - usedSubsets.add(sample) - subs.append(sample) - - return (True, sorted(subs)) - - -################################################################################ -def testReconstructSecrets(fragMap, M, maxTestCount=20): - # If fragMap has X elements, then it will test all X-choose-M subsets of - # the fragMap and return the restored secret for each one. If there's more - # subsets than maxTestCount, then just do a random sampling of the possible - # subsets - fragKeys = [k for k in fragMap.iterkeys()] - isRandom, subs = createTestingSubsets(fragKeys, M, maxTestCount) - nBytes = len(fragMap[fragKeys[0]][1]) - LOGINFO('Testing %d-byte fragments' % nBytes) - - testResults = [] - for subset in subs: - fragSubset = [fragMap[i][:] for i in subset] - - recon = ReconstructSecret(fragSubset, M, nBytes) - testResults.append((subset, recon)) - - return isRandom, testResults - - - - - -################################################################################ -def ComputeFragIDBase58(M, wltIDBin): - mBin4 = int_to_binary(M, widthBytes=4, endOut=BIGENDIAN) - fragBin = hash256(wltIDBin + mBin4)[:4] - fragB58 = str(M) + binary_to_base58(fragBin) - return fragB58 - -################################################################################ -def ComputeFragIDLineHex(M, index, wltIDBin, isSecure=False, addSpaces=False): - fragID = int_to_hex((128+M) if isSecure else M) - fragID += int_to_hex(index+1) - fragID += binary_to_hex(wltIDBin) - - if addSpaces: - fragID = ' '.join([fragID[i*4:(i+1)*4] for i in range(4)]) - - return fragID - - -################################################################################ -def ReadFragIDLineBin(binLine): - doMask = binary_to_int(binLine[0]) > 127 - M = binary_to_int(binLine[0]) & 0x7f - fnum = binary_to_int(binLine[1]) - wltID = binLine[2:] - - idBase58 = ComputeFragIDBase58(M, wltID) + '-#' + str(fnum) - return (M, fnum, wltID, doMask, idBase58) - - -################################################################################ -def ReadFragIDLineHex(hexLine): - return ReadFragIDLineBin( hex_to_binary(hexLine.strip().replace(' ',''))) - - -# END FINITE FIELD OPERATIONS -################################################################################ -################################################################################ - - -# We can identify an address string by its first byte upon conversion -# back to binary. Return -1 if checksum doesn't match -def checkAddrType(addrBin): - """ Gets the network byte of the address. Returns -1 if chksum fails """ - first21, chk4 = addrBin[:-4], addrBin[-4:] - chkBytes = hash256(first21) - if chkBytes[:4] == chk4: - return addrBin[0] - else: - return -1 - -# Check validity of a BTC address in its binary form, as would -# be found inside a pkScript. Usually about 24 bytes -def checkAddrBinValid(addrBin, netbyte=ADDRBYTE): - """ - Checks whether this address is valid for the given network - (set at the top of pybtcengine.py) - """ - return checkAddrType(addrBin) == netbyte - -# Check validity of a BTC address in Base58 form -def checkAddrStrValid(addrStr): - """ Check that a Base58 address-string is valid on this network """ - return checkAddrBinValid(base58_to_binary(addrStr)) - - -def convertKeyDataToAddress(privKey=None, pubKey=None): - if not privKey and not pubKey: - raise BadAddressError, 'No key data supplied for conversion' - elif privKey: - if isinstance(privKey, str): - privKey = SecureBinaryData(privKey) - - if not privKey.getSize()==32: - raise BadAddressError, 'Invalid private key format!' - else: - pubKey = CryptoECDSA().ComputePublicKey(privKey) - - if isinstance(pubKey,str): - pubKey = SecureBinaryData(pubKey) - return pubKey.getHash160() - - - -################################################################################ -def decodeMiniPrivateKey(keyStr): - """ - Converts a 22, 26 or 30-character Base58 mini private key into a - 32-byte binary private key. - """ - if not len(keyStr) in (22,26,30): - return '' - - keyQ = keyStr + '?' - theHash = sha256(keyQ) - - if binary_to_hex(theHash[0]) == '01': - raise KeyDataError, 'PBKDF2-based mini private keys not supported!' - elif binary_to_hex(theHash[0]) != '00': - raise KeyDataError, 'Invalid mini private key... double check the entry' - - return sha256(keyStr) - - -################################################################################ -def parsePrivateKeyData(theStr): - hexChars = '01234567890abcdef' - b58Chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' - - hexCount = sum([1 if c in hexChars else 0 for c in theStr.lower()]) - b58Count = sum([1 if c in b58Chars else 0 for c in theStr]) - canBeHex = hexCount==len(theStr) - canBeB58 = b58Count==len(theStr) - - binEntry = '' - keyType = '' - isMini = False - if canBeB58 and not canBeHex: - if len(theStr) in (22, 30): - # Mini-private key format! - try: - binEntry = decodeMiniPrivateKey(theStr) - except KeyDataError: - raise BadAddressError, 'Invalid mini-private key string' - keyType = 'Mini Private Key Format' - isMini = True - elif len(theStr) in range(48,53): - binEntry = base58_to_binary(theStr) - keyType = 'Plain Base58' - else: - raise BadAddressError, 'Unrecognized key data' - elif canBeHex: - binEntry = hex_to_binary(theStr) - keyType = 'Plain Hex' - else: - raise BadAddressError, 'Unrecognized key data' - - - if len(binEntry)==36 or (len(binEntry)==37 and binEntry[0]==PRIVKEYBYTE): - if len(binEntry)==36: - keydata = binEntry[:32 ] - chk = binEntry[ 32:] - binEntry = verifyChecksum(keydata, chk) - if not isMini: - keyType = 'Raw %s with checksum' % keyType.split(' ')[1] - else: - # Assume leading 0x80 byte, and 4 byte checksum - keydata = binEntry[ :1+32 ] - chk = binEntry[ 1+32:] - binEntry = verifyChecksum(keydata, chk) - binEntry = binEntry[1:] - if not isMini: - keyType = 'Standard %s key with checksum' % keyType.split(' ')[1] - - if binEntry=='': - raise InvalidHashError, 'Private Key checksum failed!' - elif (len(binEntry)==33 and binEntry[-1]=='\x01') or \ - (len(binEntry)==37 and binEntry[-5]=='\x01'): - raise CompressedKeyError, 'Compressed Public keys not supported!' - return binEntry, keyType - - - -################################################################################ -def encodePrivKeyBase58(privKeyBin, leadByte=PRIVKEYBYTE): - bin33 = leadByte + privKeyBin - chk = computeChecksum(bin33) - return binary_to_base58(bin33 + chk) - - - -URI_VERSION_STR = '1.0' - -################################################################################ -def parseBitcoinURI(theStr): - """ Takes a URI string, returns the pieces of it, in a dictionary """ - - # Start by splitting it into pieces on any separator - seplist = ':;?&' - for c in seplist: - theStr = theStr.replace(c,' ') - parts = theStr.split() - - # Now start walking through the parts and get the info out of it - if not parts[0] == 'bitcoin': - return {} - - uriData = {} - - try: - uriData['address'] = parts[1] - for p in parts[2:]: - if not '=' in p: - raise BadURIError, 'Unrecognized URI field: "%s"'%p - - # All fields must be "key=value" making it pretty easy to parse - key, value = p.split('=') - - # A few - if key.lower()=='amount': - uriData['amount'] = str2coin(value) - elif key.lower() in ('label','message'): - uriData[key] = uriPercentToReserved(value) - else: - uriData[key] = value - except: - return {} - - return uriData - - -################################################################################ -def uriReservedToPercent(theStr): - """ - Convert from a regular string to a percent-encoded string - """ - #Must replace '%' first, to avoid recursive (and incorrect) replacement! - reserved = "%!*'();:@&=+$,/?#[] " - - for c in reserved: - theStr = theStr.replace(c, '%%%s' % int_to_hex(ord(c))) - return theStr - - -################################################################################ -def uriPercentToReserved(theStr): - """ - This replacement direction is much easier! - Convert from a percent-encoded string to a - """ - - parts = theStr.split('%') - if len(parts)>1: - for p in parts[1:]: - parts[0] += chr( hex_to_int(p[:2]) ) + p[2:] - return parts[0][:] - - -################################################################################ -def createBitcoinURI(addr, amt=None, msg=None): - uriStr = 'bitcoin:%s' % addr - if amt or msg: - uriStr += '?' - - if amt: - uriStr += 'amount=%s' % coin2str(amt, maxZeros=0).strip() - - if amt and msg: - uriStr += '&' - - if msg: - uriStr += 'label=%s' % uriReservedToPercent(msg) - - return uriStr - -################################################################################ -def createSigScript(rBin, sBin): - # Remove all leading zero-bytes - while rBin[0]=='\x00': - rBin = rBin[1:] - while sBin[0]=='\x00': - sBin = sBin[1:] - - if binary_to_int(rBin[0])&128>0: rBin = '\x00'+rBin - if binary_to_int(sBin[0])&128>0: sBin = '\x00'+sBin - rSize = int_to_binary(len(rBin)) - sSize = int_to_binary(len(sBin)) - rsSize = int_to_binary(len(rBin) + len(sBin) + 4) - sigScript = '\x30' + rsSize + \ - '\x02' + rSize + rBin + \ - '\x02' + sSize + sBin - return sigScript - -################################################################################ -class PyBtcAddress(object): - """ - PyBtcAddress -- - - This class encapsulated EVERY kind of address object: - -- Plaintext private-key-bearing addresses - -- Encrypted private key addresses, with AES locking and unlocking - -- Watching-only public-key addresses - -- Address-only storage, representing someone else's key - -- Deterministic address generation from previous addresses - -- Serialization and unserialization of key data under all conditions - -- Checksums on all serialized fields to protect against HDD byte errors - - For deterministic wallets, new addresses will be created from a chaincode - and the previous address. What is implemented here is a special kind of - deterministic calculation that actually allows the user to securely - generate new addresses even if they don't have the private key. This - method uses Diffie-Hellman shared-secret calculations to produce the new - keys, and has the same level of security as all other ECDSA operations. - There's a lot of fantastic benefits to doing this: - - (1) If all addresses in wallet are chained, then you only need to backup - your wallet ONCE -- when you first create it. Print it out, put it - in a safety-deposit box, or tattoo the generator key to the inside - of your eyelid: it will never change. - - (2) You can keep your private keys on an offline machine, and keep a - watching-only wallet online. You will be able to generate new - keys/addresses, and verify incoming transactions, without ever - requiring your private key to touch the internet. - - (3) If your friend has the chaincode and your first public key, they - too can generate new addresses for you -- allowing them to send - you money multiple times, with different addresses, without ever - needing to specifically request the addresses. - (the downside to this is if the chaincode is compromised, all - chained addresses become de-anonymized -- but is only a loss of - privacy, not security) - - However, we do require some fairly complicated logic, due to the fact - that a user with a full, private-key-bearing wallet, may try to generate - a new key/address without supplying a passphrase. If this happens, the - wallet logic gets very complicated -- we don't want to reject the request - to generate a new address, but we can't compute the private key until the - next time the user unlocks their wallet. Thus, we have to save off the - data they will need to create the key, to be applied on next unlock. - """ - ############################################################################# - def __init__(self): - """ - We use SecureBinaryData objects to store pub, priv and IV objects, - because that is what is required by the C++ code. See EncryptionUtils.h - to see that available methods. - """ - self.addrStr20 = '' - self.binPublicKey65 = SecureBinaryData() # 0x04 X(BE) Y(BE) - self.binPrivKey32_Encr = SecureBinaryData() # BIG-ENDIAN - self.binPrivKey32_Plain = SecureBinaryData() - self.binInitVect16 = SecureBinaryData() - self.isLocked = False - self.useEncryption = False - self.isInitialized = False - self.keyChanged = False # ...since last key encryption - self.walletByteLoc = -1 - self.chaincode = SecureBinaryData() - self.chainIndex = 0 - - # Information to be used by C++ to know where to search for transactions - # in the blockchain (disabled in favor of a better search method) - self.timeRange = [2**32-1, 0] - self.blkRange = [2**32-1, 0] - - # This feels like a hack, but it's the only way I can think to handle - # the case of generating new, chained addresses, even without the - # private key currently in memory. i.e. - If we can't unlock the priv - # key when creating a new chained priv key, we will simply extend the - # public key, and store the last-known chain info, so that it can be - # generated the next time the address is unlocked - self.createPrivKeyNextUnlock = False - self.createPrivKeyNextUnlock_IVandKey = [None, None] # (IV,Key) - self.createPrivKeyNextUnlock_ChainDepth = -1 - - ############################################################################# - def isInitialized(self): - """ Keep track of whether this address has been initialized """ - return self.isInitialized - - ############################################################################# - def hasPrivKey(self): - """ - We have a private key if either the plaintext, or ciphertext private-key - fields are non-empty. We also consider ourselves to "have" the private - key if this address was chained from a key that has the private key, even - if we haven't computed it yet (due to not having unlocked the private key - before creating the new address). - """ - return (self.binPrivKey32_Encr.getSize() != 0 or \ - self.binPrivKey32_Plain.getSize() != 0 or \ - self.createPrivKeyNextUnlock) - - ############################################################################# - def hasPubKey(self): - return (self.binPublicKey65.getSize() != 0) - - ############################################################################# - def getAddrStr(self, netbyte=ADDRBYTE): - chksum = hash256(netbyte + self.addrStr20)[:4] - return binary_to_base58(netbyte + self.addrStr20 + chksum) - - ############################################################################# - def getAddr160(self): - if len(self.addrStr20)!=20: - raise KeyDataError, 'PyBtcAddress does not have an address string!' - return self.addrStr20 - - - ############################################################################# - def isCompressed(self): - # Armory wallets (v1.35) do not support compressed keys - return False - - - ############################################################################# - def touch(self, unixTime=None, blkNum=None): - """ - Just like "touching" a file, this makes sure that the firstSeen and - lastSeen fields for this address are updated to include "now" - - If we include only a block number, we will fill in the timestamp with - the unix-time for that block (if the BlockDataManager is availabled) - """ - if self.blkRange[0]==0: - self.blkRange[0]=2**32-1 - if self.timeRange[0]==0: - self.timeRange[0]=2**32-1 - - if blkNum==None: - if TheBDM.getBDMState()=='BlockchainReady': - topBlk = TheBDM.getTopBlockHeight() - self.blkRange[0] = long(min(self.blkRange[0], topBlk)) - self.blkRange[1] = long(max(self.blkRange[1], topBlk)) - else: - self.blkRange[0] = long(min(self.blkRange[0], blkNum)) - self.blkRange[1] = long(max(self.blkRange[1], blkNum)) - - if unixTime==None and TheBDM.getBDMState()=='BlockchainReady': - unixTime = TheBDM.getHeaderByHeight(blkNum).getTimestamp() - - if unixTime==None: - unixTime = RightNow() - - self.timeRange[0] = long(min(self.timeRange[0], unixTime)) - self.timeRange[1] = long(max(self.timeRange[1], unixTime)) - - - - ############################################################################# - def copy(self): - newAddr = PyBtcAddress().unserialize(self.serialize()) - newAddr.binPrivKey32_Plain = self.binPrivKey32_Plain.copy() - newAddr.binPrivKey32_Encr = self.binPrivKey32_Encr.copy() - newAddr.binPublicKey65 = self.binPublicKey65.copy() - newAddr.binInitVect16 = self.binInitVect16.copy() - newAddr.isLocked = self.isLocked - newAddr.useEncryption = self.useEncryption - newAddr.isInitialized = self.isInitialized - newAddr.keyChanged = self.keyChanged - newAddr.walletByteLoc = self.walletByteLoc - newAddr.chaincode = self.chaincode - newAddr.chainIndex = self.chainIndex - return newAddr - - - - ############################################################################# - def getTimeRange(self): - return self.timeRange - - ############################################################################# - def getBlockRange(self): - return self.blkRange - - ############################################################################# - def serializePublicKey(self): - """Converts the SecureBinaryData public key to a 65-byte python string""" - return self.binPublicKey65.toBinStr() - - ############################################################################# - def serializeEncryptedPrivateKey(self): - """Converts SecureBinaryData encrypted private key to python string""" - return self.binPrivKey32_Encr.toBinStr() - - ############################################################################# - # NOTE: This method should rarely be used, unless we are only printing it - # to the screen. Actually, it will be used for unencrypted wallets - def serializePlainPrivateKey(self): - return self.binPrivKey32_Plain.toBinStr() - - def serializeInitVector(self): - return self.binInitVect16.toBinStr() - - - ############################################################################# - def verifyEncryptionKey(self, secureKdfOutput): - """ - Determine if this data is the decryption key for this encrypted address - """ - if not self.useEncryption or not self.hasPrivKey(): - return False - - if self.useEncryption and not secureKdfOutput: - LOGERROR('No encryption key supplied to verifyEncryption!') - return False - - - decryptedKey = CryptoAES().DecryptCFB( self.binPrivKey32_Encr, \ - SecureBinaryData(secureKdfOutput), \ - self.binInitVect16) - verified = False - - if not self.isLocked: - if decryptedKey==self.binPrivKey32_Plain: - verified = True - else: - computedPubKey = CryptoECDSA().ComputePublicKey(decryptedKey) - if self.hasPubKey(): - verified = (self.binPublicKey65==computedPubKey) - else: - self.binPublicKey65 = computedPubKey - verified = (computedPubKey.getHash160()==self.addrStr20) - - decryptedKey.destroy() - return verified - - - - ############################################################################# - def setInitializationVector(self, IV16=None, random=False, force=False): - """ - Either set the IV through input arg, or explicitly call random=True - Returns the IV -- which is especially important if it is randomly gen - - This method is mainly for PREVENTING you from changing an existing IV - without meaning to. Losing the IV for encrypted data is almost as bad - as losing the encryption key. Caller must use force=True in order to - override this warning -- otherwise this method will abort. - """ - if self.binInitVect16.getSize()==16: - if self.isLocked: - LOGERROR('Address already locked with different IV.') - LOGERROR('Changing IV may cause loss of keydata.') - else: - LOGERROR('Address already contains an initialization') - LOGERROR('vector. If you change IV without updating') - LOGERROR('the encrypted storage, you may permanently') - LOGERROR('lose the encrypted data') - - if not force: - LOGERROR('If you really want to do this, re-execute this call with force=True') - return '' - - if IV16: - self.binInitVect16 = SecureBinaryData(IV16) - elif random==True: - self.binInitVect16 = SecureBinaryData().GenerateRandom(16) - else: - raise KeyDataError, 'setInitVector: set IV data, or random=True' - return self.binInitVect16 - - ############################################################################# - def enableKeyEncryption(self, IV16=None, generateIVIfNecessary=False): - """ - setIV method will raise error is we don't specify any args, but it is - acceptable HERE to not specify any args just to enable encryption - """ - self.useEncryption = True - if IV16: - self.setInitializationVector(IV16) - elif generateIVIfNecessary and self.binInitVect16.getSize()<16: - self.setInitializationVector(random=True) - - - ############################################################################# - def isKeyEncryptionEnabled(self): - return self.useEncryption - - - ############################################################################# - def createFromEncryptedKeyData(self, addr20, encrPrivKey32, IV16, \ - chkSum=None, pubKey=None): - # We expect both private key and IV to the right size - assert(encrPrivKey32.getSize()==32) - assert(IV16.getSize()==16) - self.__init__() - self.addrStr20 = addr20 - self.binPrivKey32_Encr = SecureBinaryData(encrPrivKey32) - self.setInitializationVector(IV16) - self.isLocked = True - self.useEncryption = True - self.isInitialized = True - if chkSum and not self.binPrivKey32_Encr.getHash256().startswith(chkSum): - raise ChecksumError, "Checksum doesn't match encrypted priv key data!" - if pubKey: - self.binPublicKey65 = SecureBinaryData(pubKey) - if not self.binPublicKey65.getHash160()==self.addrStr20: - raise KeyDataError, "Public key does not match supplied address" - - return self - - - ############################################################################# - def createFromPlainKeyData(self, plainPrivKey, addr160=None, willBeEncr=False, \ - generateIVIfNecessary=False, IV16=None, \ - chksum=None, publicKey65=None, \ - skipCheck=False, skipPubCompute=False): - - assert(plainPrivKey.getSize()==32) - - if not addr160: - addr160 = convertKeyDataToAddress(privKey=plainPrivKey) - - self.__init__() - self.addrStr20 = addr160 - self.isInitialized = True - self.binPrivKey32_Plain = SecureBinaryData(plainPrivKey) - self.isLocked = False - - if willBeEncr: - self.enableKeyEncryption(IV16, generateIVIfNecessary) - elif IV16: - self.binInitVect16 = IV16 - - if chksum and not verifyChecksum(self.binPrivKey32_Plain.toBinStr(), chksum): - raise ChecksumError, "Checksum doesn't match plaintext priv key!" - if publicKey65: - self.binPublicKey65 = SecureBinaryData(publicKey65) - if not self.binPublicKey65.getHash160()==self.addrStr20: - raise KeyDataError, "Public key does not match supplied address" - if not skipCheck: - if not CryptoECDSA().CheckPubPrivKeyMatch(self.binPrivKey32_Plain,\ - self.binPublicKey65): - raise KeyDataError, 'Supplied pub and priv key do not match!' - elif not skipPubCompute: - # No public key supplied, but we do want to calculate it - self.binPublicKey65 = CryptoECDSA().ComputePublicKey(plainPrivKey) - - return self - - ############################################################################# - def createFromPublicKeyData(self, publicKey65, chksum=None): - - assert(publicKey65.getSize()==65) - self.__init__() - self.addrStr20 = publicKey65.getHash160() - self.binPublicKey65 = publicKey65 - self.isInitialized = True - self.isLocked = False - self.useEncryption = False - - if chksum and not verifyChecksum(self.binPublicKey65.toBinStr(), chksum): - raise ChecksumError, "Checksum doesn't match supplied public key!" - - return self - - - ############################################################################# - def lock(self, secureKdfOutput=None, generateIVIfNecessary=False): - # We don't want to destroy the private key if it's not supposed to be - # encrypted. Similarly, if we haven't actually saved the encrypted - # version, let's not lock it - newIV = False - if not self.useEncryption or not self.hasPrivKey(): - # This isn't supposed to be encrypted, or there's no privkey to encrypt - return - else: - if self.binPrivKey32_Encr.getSize()==32 and not self.keyChanged: - # Addr should be encrypted, and we already have encrypted priv key - self.binPrivKey32_Plain.destroy() - self.isLocked = True - else: - # Addr should be encrypted, but haven't computed encrypted value yet - if secureKdfOutput!=None: - # We have an encryption key, use it - if self.binInitVect16.getSize() < 16: - if not generateIVIfNecessary: - raise KeyDataError, 'No Initialization Vector available' - else: - self.binInitVect16 = SecureBinaryData().GenerateRandom(16) - newIV = True - - # Finally execute the encryption - self.binPrivKey32_Encr = CryptoAES().EncryptCFB( \ - self.binPrivKey32_Plain, \ - SecureBinaryData(secureKdfOutput), \ - self.binInitVect16) - # Destroy the unencrypted key, reset the keyChanged flag - self.binPrivKey32_Plain.destroy() - self.isLocked = True - self.keyChanged = False - else: - # Can't encrypt the addr because we don't have encryption key - raise WalletLockError, ("\n\tTrying to destroy plaintext key, but no" - "\n\tencrypted key data is available, and no" - "\n\tencryption key provided to encrypt it.") - - - # In case we changed the IV, we should let the caller know this - return self.binInitVect16 if newIV else SecureBinaryData() - - - ############################################################################# - def unlock(self, secureKdfOutput, skipCheck=False): - """ - This method knows nothing about a key-derivation function. It simply - takes in an AES key and applies it to decrypt the data. However, it's - best if that AES key is actually derived from "heavy" key-derivation - function. - """ - if not self.useEncryption or not self.isLocked: - # Bail out if the wallet is unencrypted, or already unlocked - self.isLocked = False - return - - - if self.createPrivKeyNextUnlock: - # This is SPECIFICALLY for the case that we didn't have the encr key - # available when we tried to extend our deterministic wallet, and - # generated a new address anyway - self.binPrivKey32_Plain = CryptoAES().DecryptCFB( \ - self.createPrivKeyNextUnlock_IVandKey[1], \ - SecureBinaryData(secureKdfOutput), \ - self.createPrivKeyNextUnlock_IVandKey[0]) - - for i in range(self.createPrivKeyNextUnlock_ChainDepth): - self.binPrivKey32_Plain = CryptoECDSA().ComputeChainedPrivateKey( \ - self.binPrivKey32_Plain, \ - self.chaincode) - - - # IV should have already been randomly generated, before - self.isLocked = False - self.createPrivKeyNextUnlock = False - self.createPrivKeyNextUnlock_IVandKey = [] - self.createPrivKeyNextUnlock_ChainDepth = 0 - - # Lock/Unlock to make sure encrypted private key is filled - self.lock(secureKdfOutput,generateIVIfNecessary=True) - self.unlock(secureKdfOutput) - - else: - - if not self.binPrivKey32_Encr.getSize()==32: - raise WalletLockError, 'No encrypted private key to decrypt!' - - if not self.binInitVect16.getSize()==16: - raise WalletLockError, 'Initialization Vect (IV) is missing!' - - self.binPrivKey32_Plain = CryptoAES().DecryptCFB( \ - self.binPrivKey32_Encr, \ - secureKdfOutput, \ - self.binInitVect16) - - self.isLocked = False - - if not skipCheck: - if not self.hasPubKey(): - self.binPublicKey65 = CryptoECDSA().ComputePublicKey(\ - self.binPrivKey32_Plain) - else: - # We should usually check that keys match, but may choose to skip - # if we have a lot of keys to load - # NOTE: I run into this error if I fill the keypool without first - # unlocking the wallet. I'm not sure why it doesn't work - # when locked (it should), but this wallet format has been - # working flawless for almost a year... and will be replaced - # soon, so I won't sweat it. - if not CryptoECDSA().CheckPubPrivKeyMatch(self.binPrivKey32_Plain, \ - self.binPublicKey65): - raise KeyDataError, "Stored public key does not match priv key!" - - - - ############################################################################# - def changeEncryptionKey(self, secureOldKey, secureNewKey): - """ - We will use None to specify "no encryption", either for old or new. Of - course we throw an error is old key is "None" but the address is actually - encrypted. - """ - if not self.hasPrivKey(): - raise KeyDataError, 'No private key available to re-encrypt' - - if not secureOldKey and self.useEncryption and self.isLocked: - raise WalletLockError, 'Need old encryption key to unlock private keys' - - wasLocked = self.isLocked - - # Decrypt the original key - if self.isLocked: - self.unlock(secureOldKey, skipCheck=False) - - # Keep the old IV if we are changing the key. IV reuse is perfectly - # fine for a new key, and might save us from disaster if we otherwise - # generated a new one and then forgot to take note of it. - self.keyChanged = True - if not secureNewKey: - # If we chose not to re-encrypt, make sure we clear the encryption - self.binInitVect16 = SecureBinaryData() - self.binPrivKey32_Encr = SecureBinaryData() - self.isLocked = False - self.useEncryption = False - else: - # Re-encrypt with new key (using same IV) - self.useEncryption = True - self.lock(secureNewKey) # do this to make sure privKey_Encr filled - if wasLocked: - self.isLocked = True - else: - self.unlock(secureNewKey) - self.isLocked = False - - - - - ############################################################################# - # This is more of a static method - def checkPubPrivKeyMatch(self, securePriv, securePub): - CryptoECDSA().CheckPubPrivKeyMatch(securePriv, securePub) - - ############################################################################# - def generateDERSignature(self, binMsg, secureKdfOutput=None): - """ - This generates a DER signature for this address using the private key. - Obviously, if we don't have the private key, we throw an error. Or if - the wallet is locked and no encryption key was provided. - - If an encryption key IS provided, then we unlock the address just long - enough to sign the message and then re-lock it - """ - - TimerStart('generateDERSignature') - - if not self.hasPrivKey(): - raise KeyDataError, 'Cannot sign for address without private key!' - - if self.isLocked: - if secureKdfOutput==None: - raise WalletLockError, "Cannot sign Tx when private key is locked!" - else: - # Wallet is locked but we have a decryption key - self.unlock(secureKdfOutput, skipCheck=False) - - try: - secureMsg = SecureBinaryData(binMsg) - sig = CryptoECDSA().SignData(secureMsg, self.binPrivKey32_Plain) - sigstr = sig.toBinStr() - # We add an extra 0 byte to the beginning of each value to guarantee - # that they are interpretted as unsigned integers. Not always necessary - # but it doesn't hurt to always do it. - rBin = sigstr[:32 ] - sBin = sigstr[ 32:] - return createSigScript(rBin, sBin) - except: - LOGERROR('Failed signature generation') - finally: - # Always re-lock/cleanup after unlocking, even after an exception. - # If locking triggers an error too, we will just skip it. - TimerStop('generateDERSignature') - try: - if secureKdfOutput!=None: - self.lock(secureKdfOutput) - except: - LOGERROR('Error re-locking address') - pass - - - - - ############################################################################# - def verifyDERSignature(self, binMsgVerify, derSig): - - TimerStart('verifyDERSignature') - if not self.hasPubKey(): - raise KeyDataError, 'No public key available for this address!' - - if not isinstance(derSig, str): - # In case this is a SecureBinaryData object... - derSig = derSig.toBinStr() - - codeByte = derSig[0] - nBytes = binary_to_int(derSig[1]) - rsStr = derSig[2:2+nBytes] - assert(codeByte == '\x30') - assert(nBytes == len(rsStr)) - # Read r - codeByte = rsStr[0] - rBytes = binary_to_int(rsStr[1]) - r = rsStr[2:2+rBytes] - assert(codeByte == '\x02') - sStr = rsStr[2+rBytes:] - # Read s - codeByte = sStr[0] - sBytes = binary_to_int(sStr[1]) - s = sStr[2:2+sBytes] - assert(codeByte == '\x02') - # Now we have the (r,s) values of the - - secMsg = SecureBinaryData(binMsgVerify) - secSig = SecureBinaryData(r[-32:] + s[-32:]) - secPubKey = SecureBinaryData(self.binPublicKey65) - TimerStop('verifyDERSignature') - return CryptoECDSA().VerifyData(secMsg, secSig, secPubKey) - - - ############################################################################# - def markAsRootAddr(self, chaincode): - if not chaincode.getSize()==32: - raise KeyDataError, 'Chaincode must be 32 bytes' - else: - self.chainIndex = -1 - self.chaincode = chaincode - - - ############################################################################# - def isAddrChainRoot(self): - return (self.chainIndex==-1) - - ############################################################################# - def extendAddressChain(self, secureKdfOutput=None, newIV=None): - """ - We require some fairly complicated logic here, due to the fact that a - user with a full, private-key-bearing wallet, may try to generate a new - key/address without supplying a passphrase. If this happens, the wallet - logic gets mucked up -- we don't want to reject the request to - generate a new address, but we can't compute the private key until the - next time the user unlocks their wallet. Thus, we have to save off the - data they will need to create the key, to be applied on next unlock. - """ - LOGDEBUG('Extending address chain') - TimerStart('extendAddressChain') - if not self.chaincode.getSize() == 32: - raise KeyDataError, 'No chaincode has been defined to extend chain' - - newAddr = PyBtcAddress() - privKeyAvailButNotDecryptable = (self.hasPrivKey() and \ - self.isLocked and \ - not secureKdfOutput ) - - - if self.hasPrivKey() and not privKeyAvailButNotDecryptable: - # We are extending a chain using private key data - wasLocked = self.isLocked - if self.useEncryption and self.isLocked: - if not secureKdfOutput: - raise WalletLockError, 'Cannot create new address without passphrase' - self.unlock(secureKdfOutput) - if not newIV: - newIV = SecureBinaryData().GenerateRandom(16) - - if self.hasPubKey(): - newPriv = CryptoECDSA().ComputeChainedPrivateKey( \ - self.binPrivKey32_Plain, \ - self.chaincode, \ - self.binPublicKey65) - else: - newPriv = CryptoECDSA().ComputeChainedPrivateKey( \ - self.binPrivKey32_Plain, \ - self.chaincode) - newPub = CryptoECDSA().ComputePublicKey(newPriv) - newAddr160 = newPub.getHash160() - newAddr.createFromPlainKeyData(newPriv, newAddr160, \ - IV16=newIV, publicKey65=newPub) - - newAddr.addrStr20 = newPub.getHash160() - newAddr.useEncryption = self.useEncryption - newAddr.isInitialized = True - newAddr.chaincode = self.chaincode - newAddr.chainIndex = self.chainIndex+1 - - # We can't get here without a secureKdfOutput (I think) - if newAddr.useEncryption: - newAddr.lock(secureKdfOutput) - if not wasLocked: - newAddr.unlock(secureKdfOutput) - self.unlock(secureKdfOutput) - TimerStop('extendAddressChain') - return newAddr - else: - # We are extending the address based solely on its public key - if not self.hasPubKey(): - raise KeyDataError, 'No public key available to extend chain' - newAddr.binPublicKey65 = CryptoECDSA().ComputeChainedPublicKey( \ - self.binPublicKey65, self.chaincode) - newAddr.addrStr20 = newAddr.binPublicKey65.getHash160() - newAddr.useEncryption = self.useEncryption - newAddr.isInitialized = True - newAddr.chaincode = self.chaincode - newAddr.chainIndex = self.chainIndex+1 - - - if privKeyAvailButNotDecryptable: - # *** store what is needed to recover key on next addr unlock *** - newAddr.isLocked = True - newAddr.useEncryption = True - if not newIV: - newIV = SecureBinaryData().GenerateRandom(16) - newAddr.binInitVect16 = newIV - newAddr.createPrivKeyNextUnlock = True - newAddr.createPrivKeyNextUnlock_IVandKey = [None,None] - if self.createPrivKeyNextUnlock: - # We are chaining from address also requiring gen on next unlock - newAddr.createPrivKeyNextUnlock_IVandKey[0] = \ - self.createPrivKeyNextUnlock_IVandKey[0].copy() - newAddr.createPrivKeyNextUnlock_IVandKey[1] = \ - self.createPrivKeyNextUnlock_IVandKey[1].copy() - newAddr.createPrivKeyNextUnlock_ChainDepth = \ - self.createPrivKeyNextUnlock_ChainDepth+1 - else: - # The address from which we are extending has already been generated - newAddr.createPrivKeyNextUnlock_IVandKey[0] = self.binInitVect16.copy() - newAddr.createPrivKeyNextUnlock_IVandKey[1] = self.binPrivKey32_Encr.copy() - newAddr.createPrivKeyNextUnlock_ChainDepth = 1 - TimerStop('extendAddressChain') - return newAddr - - - def serialize(self): - """ - We define here a binary serialization scheme that will write out ALL - information needed to completely reconstruct address data from file. - This method returns a string, but presumably will be used to write addr - data to file. The following format is used. - - Address160 (20 bytes) : The 20-byte hash of the public key - This must always be the first field - AddressChk ( 4 bytes) : Checksum to make sure no error in addr160 - AddrVersion ( 4 bytes) : Early version don't specify encrypt params - Flags ( 8 bytes) : Addr-specific info, including encrypt params - - ChainCode (32 bytes) : For extending deterministic wallets - ChainChk ( 4 bytes) : Checksum for chaincode - ChainIndex ( 8 bytes) : Index in chain if deterministic addresses - ChainDepth ( 8 bytes) : How deep addr is in chain beyond last - computed private key (if base address was - locked when we tried to extend/chain it) - - InitVect (16 bytes) : Initialization vector for encryption - InitVectChk ( 4 bytes) : Checksum for IV - PrivKey (32 bytes) : Private key data (may be encrypted) - PrivKeyChk ( 4 bytes) : Checksum for private key data - - PublicKey (65 bytes) : Public key for this address - PubKeyChk ( 4 bytes) : Checksum for private key data - - - FirstTime ( 8 bytes) : The first time addr was seen in blockchain - LastTime ( 8 bytes) : The last time addr was seen in blockchain - FirstBlock ( 4 bytes) : The first block addr was seen in blockchain - LastBlock ( 4 bytes) : The last block addr was seen in blockchain - """ - - serializeWithEncryption = self.useEncryption - - if self.useEncryption and \ - self.binPrivKey32_Encr.getSize()==0 and \ - self.binPrivKey32_Plain.getSize()>0: - LOGERROR('') - LOGERROR('***WARNING: you have chosen to serialize a key you hope to be') - LOGERROR(' encrypted, but have not yet chosen a passphrase for') - LOGERROR(' it. The only way to serialize this address is with ') - LOGERROR(' the plaintext keys. Please lock this address at') - LOGERROR(' least once in order to enable encrypted output.') - serializeWithEncryption = False - - # Before starting, let's construct the flags for this address - nFlagBytes = 8 - flags = [False]*nFlagBytes*8 - flags[0] = self.hasPrivKey() - flags[1] = self.hasPubKey() - flags[2] = serializeWithEncryption - flags[3] = self.createPrivKeyNextUnlock - flags = ''.join([('1' if f else '0') for f in flags]) - - def raw(a): - if isinstance(a, str): - return a - else: - return a.toBinStr() - - def chk(a): - if isinstance(a, str): - return computeChecksum(a,4) - else: - return computeChecksum(a.toBinStr(),4) - - # Use BinaryPacker "width" fields to guaranteee BINARY_CHUNK width. - # Sure, if we have malformed data we might cut some of it off instead - # of writing it to the binary stream. But at least we'll ALWAYS be - # able to determine where each field is, and will never corrupt the - # whole wallet so badly we have to go hex-diving to figure out what - # happened. - binOut = BinaryPacker() - binOut.put(BINARY_CHUNK, self.addrStr20, width=20) - binOut.put(BINARY_CHUNK, chk(self.addrStr20), width= 4) - binOut.put(UINT32, getVersionInt(PYBTCWALLET_VERSION)) - binOut.put(UINT64, bitset_to_int(flags)) - - # Write out address-chaining parameters (for deterministic wallets) - binOut.put(BINARY_CHUNK, raw(self.chaincode), width=32) - binOut.put(BINARY_CHUNK, chk(self.chaincode), width= 4) - binOut.put(INT64, self.chainIndex) - binOut.put(INT64, self.createPrivKeyNextUnlock_ChainDepth) - - # Write out whatever is appropriate for private-key data - # Binary-unpacker will write all 0x00 bytes if empty values are given - if serializeWithEncryption: - if self.createPrivKeyNextUnlock: - binOut.put(BINARY_CHUNK, raw(self.createPrivKeyNextUnlock_IVandKey[0]), width=16) - binOut.put(BINARY_CHUNK, chk(self.createPrivKeyNextUnlock_IVandKey[0]), width= 4) - binOut.put(BINARY_CHUNK, raw(self.createPrivKeyNextUnlock_IVandKey[1]), width=32) - binOut.put(BINARY_CHUNK, chk(self.createPrivKeyNextUnlock_IVandKey[1]), width= 4) - else: - binOut.put(BINARY_CHUNK, raw(self.binInitVect16), width=16) - binOut.put(BINARY_CHUNK, chk(self.binInitVect16), width= 4) - binOut.put(BINARY_CHUNK, raw(self.binPrivKey32_Encr), width=32) - binOut.put(BINARY_CHUNK, chk(self.binPrivKey32_Encr), width= 4) - else: - binOut.put(BINARY_CHUNK, raw(self.binInitVect16), width=16) - binOut.put(BINARY_CHUNK, chk(self.binInitVect16), width= 4) - binOut.put(BINARY_CHUNK, raw(self.binPrivKey32_Plain), width=32) - binOut.put(BINARY_CHUNK, chk(self.binPrivKey32_Plain), width= 4) - - binOut.put(BINARY_CHUNK, raw(self.binPublicKey65), width=65) - binOut.put(BINARY_CHUNK, chk(self.binPublicKey65), width= 4) - - binOut.put(UINT64, self.timeRange[0]) - binOut.put(UINT64, self.timeRange[1]) - binOut.put(UINT32, self.blkRange[0]) - binOut.put(UINT32, self.blkRange[1]) - - return binOut.getBinaryString() - - ############################################################################# - def scanBlockchainForAddress(self, abortIfBDMBusy=False): - """ - This method will return null output if the BDM is currently in the - middle of a scan. You can use waitAsLongAsNecessary=True if you - want to wait for the previous scan AND the next scan. Otherwise, - you can check for bal==-1 and then try again later... - - This is particularly relevant if you know that an address has already - been scanned, and you expect this method to return immediately. Thus, - you don't want to wait for any scan at all... - - This one-stop-shop method has to be blocking. You might want to - register the address and rescan asynchronously, skipping this method - entirely: - - cppWlt = Cpp.BtcWallet() - cppWlt.addScrAddress_1_(Hash160ToScrAddr(self.getAddr160())) - TheBDM.registerScrAddr(Hash160ToScrAddr(self.getAddr160())) - TheBDM.rescanBlockchain(wait=False) - - <... do some other stuff ...> - - if TheBDM.getBDMState()=='BlockchainReady': - TheBDM.updateWalletsAfterScan(wait=True) # fast after a rescan - bal = cppWlt.getBalance('Spendable') - utxoList = cppWlt.getUnspentTxOutList() - else: - <...come back later...> - - """ - if TheBDM.getBDMState()=='BlockchainReady' or \ - (TheBDM.isScanning() and not abortIfBDMBusy): - LOGDEBUG('Scanning blockchain for address') - - # We are expecting this method to return balance - # and UTXO data, so we must make sure we're blocking. - cppWlt = Cpp.BtcWallet() - cppWlt.addScrAddress_1_(Hash160ToScrAddr(self.getAddr160())) - TheBDM.registerWallet(cppWlt, wait=True) - TheBDM.scanBlockchainForTx(cppWlt, wait=True) - - utxoList = cppWlt.getUnspentTxOutList() - bal = cppWlt.getSpendableBalance() - return (bal, utxoList) - else: - return (-1, []) - - ############################################################################# - def unserialize(self, toUnpack): - """ - We reconstruct the address from a serialized version of it. See the help - text for "serialize()" for information on what fields need to - be included and the binary mapping - - We verify all checksums, correct for one byte errors, and raise exceptions - for bigger problems that can't be fixed. - """ - if isinstance(toUnpack, BinaryUnpacker): - serializedData = toUnpack - else: - serializedData = BinaryUnpacker( toUnpack ) - - - def chkzero(a): - """ - Due to fixed-width fields, we will get lots of zero-bytes - even when the binary data container was empty - """ - if a.count('\x00')==len(a): - return '' - else: - return a - - - # Start with a fresh new address - self.__init__() - - self.addrStr20 = serializedData.get(BINARY_CHUNK, 20) - chkAddr20 = serializedData.get(BINARY_CHUNK, 4) - - addrVerInt = serializedData.get(UINT32) - flags = serializedData.get(UINT64) - self.addrStr20 = verifyChecksum(self.addrStr20, chkAddr20) - flags = int_to_bitset(flags, widthBytes=8) - - # Interpret the flags - containsPrivKey = (flags[0]=='1') - containsPubKey = (flags[1]=='1') - self.useEncryption = (flags[2]=='1') - self.createPrivKeyNextUnlock = (flags[3]=='1') - - addrChkError = False - if len(self.addrStr20)==0: - addrChkError = True - if not containsPrivKey and not containsPubKey: - raise UnserializeError, 'Checksum mismatch in addrStr' - - - - # Write out address-chaining parameters (for deterministic wallets) - self.chaincode = chkzero(serializedData.get(BINARY_CHUNK, 32)) - chkChaincode = serializedData.get(BINARY_CHUNK, 4) - self.chainIndex = serializedData.get(INT64) - depth = serializedData.get(INT64) - self.createPrivKeyNextUnlock_ChainDepth = depth - - # Correct errors, convert to secure container - self.chaincode = SecureBinaryData(verifyChecksum(self.chaincode, chkChaincode)) - - - # Write out whatever is appropriate for private-key data - # Binary-unpacker will write all 0x00 bytes if empty values are given - iv = chkzero(serializedData.get(BINARY_CHUNK, 16)) - chkIv = serializedData.get(BINARY_CHUNK, 4) - privKey = chkzero(serializedData.get(BINARY_CHUNK, 32)) - chkPriv = serializedData.get(BINARY_CHUNK, 4) - iv = SecureBinaryData(verifyChecksum(iv, chkIv)) - privKey = SecureBinaryData(verifyChecksum(privKey, chkPriv)) - - # If this is SUPPOSED to contain a private key... - if containsPrivKey: - if privKey.getSize()==0: - raise UnserializeError, 'Checksum mismatch in PrivateKey '+\ - '('+hash160_to_addrStr(self.addrStr20)+')' - - if self.useEncryption: - if iv.getSize()==0: - raise UnserializeError, 'Checksum mismatch in IV ' +\ - '('+hash160_to_addrStr(self.addrStr20)+')' - if self.createPrivKeyNextUnlock: - self.createPrivKeyNextUnlock_IVandKey[0] = iv.copy() - self.createPrivKeyNextUnlock_IVandKey[1] = privKey.copy() - else: - self.binInitVect16 = iv.copy() - self.binPrivKey32_Encr = privKey.copy() - else: - self.binInitVect16 = iv.copy() - self.binPrivKey32_Plain = privKey.copy() - - pubKey = chkzero(serializedData.get(BINARY_CHUNK, 65)) - chkPub = serializedData.get(BINARY_CHUNK, 4) - pubKey = SecureBinaryData(verifyChecksum(pubKey, chkPub)) - - if containsPubKey: - if not pubKey.getSize()==65: - if self.binPrivKey32_Plain.getSize()==32: - pubKey = CryptoAES().ComputePublicKey(self.binPrivKey32_Plain) - else: - raise UnserializeError, 'Checksum mismatch in PublicKey ' +\ - '('+hash160_to_addrStr(self.addrStr20)+')' - - self.binPublicKey65 = pubKey - - if addrChkError: - self.addrStr20 = self.binPublicKey65.getHash160() - - self.timeRange[0] = serializedData.get(UINT64) - self.timeRange[1] = serializedData.get(UINT64) - self.blkRange[0] = serializedData.get(UINT32) - self.blkRange[1] = serializedData.get(UINT32) - - self.isInitialized = True - return self - - ############################################################################# - # The following methods are the SIMPLE address operations that can be used - # to juggle address data without worrying at all about encryption details. - # The addresses created here can later be endowed with encryption. - ############################################################################# - def createFromPrivateKey(self, privKey, pubKey=None, skipCheck=False): - """ - Creates address from a user-supplied random INTEGER. - This method DOES perform elliptic-curve operations - """ - if isinstance(privKey, str) and len(privKey)==32: - self.binPrivKey32_Plain = SecureBinaryData(privKey) - elif isinstance(privKey, int) or isinstance(privKey, long): - binPriv = int_to_binary(privKey, widthBytes=32, endOut=BIGENDIAN) - self.binPrivKey32_Plain = SecureBinaryData(binPriv) - else: - raise KeyDataError, 'Unknown private key format' - - if pubKey==None: - self.binPublicKey65 = CryptoECDSA().ComputePublicKey(self.binPrivKey32_Plain) - else: - self.binPublicKey65 = SecureBinaryData(pubKey) - - if not skipCheck: - assert(CryptoECDSA().CheckPubPrivKeyMatch( \ - self.binPrivKey32_Plain, \ - self.binPublicKey65)) - - self.addrStr20 = self.binPublicKey65.getHash160() - - self.isInitialized = True - return self - - def createFromPublicKey(self, pubkey): - """ - Creates address from a user-supplied ECDSA public key. - - The key can be supplied as an (x,y) pair of integers, an EC_Point - as defined in the lisecdsa class, or as a 65-byte binary string - (the 64 public key bytes with a 0x04 prefix byte) - - This method will fail if the supplied pair of points is not - on the secp256k1 curve. - """ - if isinstance(pubkey, tuple) and len(pubkey)==2: - # We are given public-key (x,y) pair - binXBE = int_to_binary(pubkey[0], widthBytes=32, endOut=BIGENDIAN) - binYBE = int_to_binary(pubkey[1], widthBytes=32, endOut=BIGENDIAN) - self.binPublicKey65 = SecureBinaryData('\x04' + binXBE + binYBE) - if not CryptoECDSA().VerifyPublicKeyValid(self.binPublicKey65): - raise KeyDataError, 'Supplied public key is not on secp256k1 curve' - elif isinstance(pubkey, str) and len(pubkey)==65: - self.binPublicKey65 = SecureBinaryData(pubkey) - if not CryptoECDSA().VerifyPublicKeyValid(self.binPublicKey65): - raise KeyDataError, 'Supplied public key is not on secp256k1 curve' - else: - raise KeyDataError, 'Unknown public key format!' - - # TODO: I should do a test to see which is faster: - # 1) Compute the hash directly like this - # 2) Get the string, hash it in python - self.addrStr20 = self.binPublicKey65.getHash160() - self.isInitialized = True - return self - - - def createFromPublicKeyHash160(self, pubkeyHash160, netbyte=ADDRBYTE): - """ - Creates an address from just the 20-byte binary hash of a public key. - - In binary form without a chksum, there is no protection against byte - errors, since there's no way to distinguish an invalid address from - a valid one (they both look like random data). - - If you are creating an address using 20 bytes you obtained in an - unreliable manner (such as manually typing them in), you should - double-check the input before sending money using the address created - here -- the tx will appear valid and be accepted by the network, - but will be permanently tied up in the network - """ - self.__init__() - self.addrStr20 = pubkeyHash160 - self.isInitialized = True - return self - - def createFromAddrStr(self, addrStr): - """ - Creates an address from a Base58 address string. Since the address - string includes a checksum, this method will fail if there was any - errors entering/copying the address - """ - self.__init__() - self.addrStr = addrStr - if not self.checkAddressValid(): - raise BadAddressError, 'Invalid address string: '+addrStr - self.isInitialized = True - return self - - def calculateAddrStr(self, netbyte=ADDRBYTE): - """ - Forces a recalculation of the address string from the public key - """ - if not self.hasPubKey(): - raise KeyDataError, 'Cannot compute address without PublicKey' - keyHash = self.binPublicKey65.getHash160() - chksum = hash256(netbyte + keyHash)[:4] - return binary_to_base58(netbyte + keyHash + chksum) - - - - def checkAddressValid(self): - return checkAddrStrValid(self.addrStr); - - - def pprint(self, withPrivKey=True, indent=''): - def pp(x, nchar=1000): - if x.getSize()==0: - return '--'*32 - else: - return x.toHexStr()[:nchar] - print indent + 'BTC Address :', self.getAddrStr() - print indent + 'Hash160[BE] :', binary_to_hex(self.getAddr160()) - print indent + 'Wallet Location :', self.walletByteLoc - print indent + 'Chained Address :', self.chainIndex >= -1 - print indent + 'Have (priv,pub) : (%s,%s)' % \ - (str(self.hasPrivKey()), str(self.hasPubKey())) - print indent + 'First/Last Time : (%s,%s)' % \ - (str(self.timeRange[0]), str(self.timeRange[1])) - print indent + 'First/Last Block : (%s,%s)' % \ - (str(self.blkRange[0]), str(self.blkRange[1])) - if self.hasPubKey(): - print indent + 'PubKeyX(BE) :', \ - binary_to_hex(self.binPublicKey65.toBinStr()[1:33 ]) - print indent + 'PubKeyY(BE) :', \ - binary_to_hex(self.binPublicKey65.toBinStr()[ 33:]) - print indent + 'Encryption parameters:' - print indent + ' UseEncryption :', self.useEncryption - print indent + ' IsLocked :', self.isLocked - print indent + ' KeyChanged :', self.keyChanged - print indent + ' ChainIndex :', self.chainIndex - print indent + ' Chaincode :', pp(self.chaincode) - print indent + ' InitVector :', pp(self.binInitVect16) - if withPrivKey and self.hasPrivKey(): - print indent + 'PrivKeyPlain(BE) :', pp(self.binPrivKey32_Plain) - print indent + 'PrivKeyCiphr(BE) :', pp(self.binPrivKey32_Encr) - else: - print indent + 'PrivKeyPlain(BE) :', pp(SecureBinaryData()) - print indent + 'PrivKeyCiphr(BE) :', pp(SecureBinaryData()) - if self.createPrivKeyNextUnlock: - print indent + ' ***** :', 'PrivKeys available on next unlock' - - -############################################################################# -def calcWalletIDFromRoot(root, chain): - """ Helper method for computing a wallet ID """ - root = PyBtcAddress().createFromPlainKeyData(SecureBinaryData(root)) - root.chaincode = SecureBinaryData(chain) - first = root.extendAddressChain() - return binary_to_base58((ADDRBYTE + first.getAddr160()[:5])[::-1]) - - - -################################################################################ -# Identify all the codes/strings that are needed for dealing with scripts -################################################################################ - -# Start list of OP codes -OP_0 = 0 -OP_FALSE = 0 -OP_PUSHDATA1 = 76 -OP_PUSHDATA2 = 77 -OP_PUSHDATA4 = 78 -OP_1NEGATE = 79 -OP_1 = 81 -OP_TRUE = 81 -OP_2 = 82 -OP_3 = 83 -OP_4 = 84 -OP_5 = 85 -OP_6 = 86 -OP_7 = 87 -OP_8 = 88 -OP_9 = 89 -OP_10 = 90 -OP_11 = 91 -OP_12 = 92 -OP_13 = 93 -OP_14 = 94 -OP_15 = 95 -OP_16 = 96 -OP_NOP = 97 -OP_IF = 99 -OP_NOTIF = 100 -OP_ELSE = 103 -OP_ENDIF = 104 -OP_VERIFY = 105 -OP_RETURN = 106 -OP_TOALTSTACK = 107 -OP_FROMALTSTACK = 108 -OP_IFDUP = 115 -OP_DEPTH = 116 -OP_DROP = 117 -OP_DUP = 118 -OP_NIP = 119 -OP_OVER = 120 -OP_PICK = 121 -OP_ROLL = 122 -OP_ROT = 123 -OP_SWAP = 124 -OP_TUCK = 125 -OP_2DROP = 109 -OP_2DUP = 110 -OP_3DUP = 111 -OP_2OVER = 112 -OP_2ROT = 113 -OP_2SWAP = 114 -OP_CAT = 126 -OP_SUBSTR = 127 -OP_LEFT = 128 -OP_RIGHT = 129 -OP_SIZE = 130 -OP_INVERT = 131 -OP_AND = 132 -OP_OR = 133 -OP_XOR = 134 -OP_EQUAL = 135 -OP_EQUALVERIFY = 136 -OP_1ADD = 139 -OP_1SUB = 140 -OP_2MUL = 141 -OP_2DIV = 142 -OP_NEGATE = 143 -OP_ABS = 144 -OP_NOT = 145 -OP_0NOTEQUAL = 146 -OP_ADD = 147 -OP_SUB = 148 -OP_MUL = 149 -OP_DIV = 150 -OP_MOD = 151 -OP_LSHIFT = 152 -OP_RSHIFT = 153 -OP_BOOLAND = 154 -OP_BOOLOR = 155 -OP_NUMEQUAL = 156 -OP_NUMEQUALVERIFY = 157 -OP_NUMNOTEQUAL = 158 -OP_LESSTHAN = 159 -OP_GREATERTHAN = 160 -OP_LESSTHANOREQUAL = 161 -OP_GREATERTHANOREQUAL = 162 -OP_MIN = 163 -OP_MAX = 164 -OP_WITHIN = 165 -OP_RIPEMD160 = 166 -OP_SHA1 = 167 -OP_SHA256 = 168 -OP_HASH160 = 169 -OP_HASH256 = 170 -OP_CODESEPARATOR = 171 -OP_CHECKSIG = 172 -OP_CHECKSIGVERIFY = 173 -OP_CHECKMULTISIG = 174 -OP_CHECKMULTISIGVERIFY = 175 - -opnames = ['']*256 -opnames[0] = 'OP_0' -for i in range(1,76): - opnames[i] ='OP_PUSHDATA' -opnames[76] = 'OP_PUSHDATA1' -opnames[77] = 'OP_PUSHDATA2' -opnames[78] = 'OP_PUSHDATA4' -opnames[79] = 'OP_1NEGATE' -opnames[81] = 'OP_1' -opnames[81] = 'OP_TRUE' -for i in range(1,17): - opnames[80+i] = 'OP_' + str(i) -opnames[97] = 'OP_NOP' -opnames[99] = 'OP_IF' -opnames[100] = 'OP_NOTIF' -opnames[103] = 'OP_ELSE' -opnames[104] = 'OP_ENDIF' -opnames[105] = 'OP_VERIFY' -opnames[106] = 'OP_RETURN' -opnames[107] = 'OP_TOALTSTACK' -opnames[108] = 'OP_FROMALTSTACK' -opnames[115] = 'OP_IFDUP' -opnames[116] = 'OP_DEPTH' -opnames[117] = 'OP_DROP' -opnames[118] = 'OP_DUP' -opnames[119] = 'OP_NIP' -opnames[120] = 'OP_OVER' -opnames[121] = 'OP_PICK' -opnames[122] = 'OP_ROLL' -opnames[123] = 'OP_ROT' -opnames[124] = 'OP_SWAP' -opnames[125] = 'OP_TUCK' -opnames[109] = 'OP_2DROP' -opnames[110] = 'OP_2DUP' -opnames[111] = 'OP_3DUP' -opnames[112] = 'OP_2OVER' -opnames[113] = 'OP_2ROT' -opnames[114] = 'OP_2SWAP' -opnames[126] = 'OP_CAT' -opnames[127] = 'OP_SUBSTR' -opnames[128] = 'OP_LEFT' -opnames[129] = 'OP_RIGHT' -opnames[130] = 'OP_SIZE' -opnames[131] = 'OP_INVERT' -opnames[132] = 'OP_AND' -opnames[133] = 'OP_OR' -opnames[134] = 'OP_XOR' -opnames[135] = 'OP_EQUAL' -opnames[136] = 'OP_EQUALVERIFY' -opnames[139] = 'OP_1ADD' -opnames[140] = 'OP_1SUB' -opnames[141] = 'OP_2MUL' -opnames[142] = 'OP_2DIV' -opnames[143] = 'OP_NEGATE' -opnames[144] = 'OP_ABS' -opnames[145] = 'OP_NOT' -opnames[146] = 'OP_0NOTEQUAL' -opnames[147] = 'OP_ADD' -opnames[148] = 'OP_SUB' -opnames[149] = 'OP_MUL' -opnames[150] = 'OP_DIV' -opnames[151] = 'OP_MOD' -opnames[152] = 'OP_LSHIFT' -opnames[153] = 'OP_RSHIFT' -opnames[154] = 'OP_BOOLAND' -opnames[155] = 'OP_BOOLOR' -opnames[156] = 'OP_NUMEQUAL' -opnames[157] = 'OP_NUMEQUALVERIFY' -opnames[158] = 'OP_NUMNOTEQUAL' -opnames[159] = 'OP_LESSTHAN' -opnames[160] = 'OP_GREATERTHAN' -opnames[161] = 'OP_LESSTHANOREQUAL' -opnames[162] = 'OP_GREATERTHANOREQUAL' -opnames[163] = 'OP_MIN' -opnames[164] = 'OP_MAX' -opnames[165] = 'OP_WITHIN' -opnames[166] = 'OP_RIPEMD160' -opnames[167] = 'OP_SHA1' -opnames[168] = 'OP_SHA256' -opnames[169] = 'OP_HASH160' -opnames[170] = 'OP_HASH256' -opnames[171] = 'OP_CODESEPARATOR' -opnames[172] = 'OP_CHECKSIG' -opnames[173] = 'OP_CHECKSIGVERIFY' -opnames[174] = 'OP_CHECKMULTISIG' -opnames[175] = 'OP_CHECKMULTISIGVERIFY' - - -opCodeLookup = {} -opCodeLookup['OP_FALSE'] = 0 -opCodeLookup['OP_PUSHDATA1'] = 76 -opCodeLookup['OP_PUSHDATA2'] = 77 -opCodeLookup['OP_PUSHDATA4'] = 78 -opCodeLookup['OP_1NEGATE'] = 79 -opCodeLookup['OP_1'] = 81 -for i in range(1,17): - opCodeLookup['OP_'+str(i)] = 80+i -opCodeLookup['OP_TRUE'] = 81 -opCodeLookup['OP_NOP'] = 97 -opCodeLookup['OP_IF'] = 99 -opCodeLookup['OP_NOTIF'] = 100 -opCodeLookup['OP_ELSE'] = 103 -opCodeLookup['OP_ENDIF'] = 104 -opCodeLookup['OP_VERIFY'] = 105 -opCodeLookup['OP_RETURN'] = 106 -opCodeLookup['OP_TOALTSTACK'] = 107 -opCodeLookup['OP_FROMALTSTACK'] = 108 -opCodeLookup['OP_IFDUP'] = 115 -opCodeLookup['OP_DEPTH'] = 116 -opCodeLookup['OP_DROP'] = 117 -opCodeLookup['OP_DUP'] = 118 -opCodeLookup['OP_NIP'] = 119 -opCodeLookup['OP_OVER'] = 120 -opCodeLookup['OP_PICK'] = 121 -opCodeLookup['OP_ROLL'] = 122 -opCodeLookup['OP_ROT'] = 123 -opCodeLookup['OP_SWAP'] = 124 -opCodeLookup['OP_TUCK'] = 125 -opCodeLookup['OP_2DROP'] = 109 -opCodeLookup['OP_2DUP'] = 110 -opCodeLookup['OP_3DUP'] = 111 -opCodeLookup['OP_2OVER'] = 112 -opCodeLookup['OP_2ROT'] = 113 -opCodeLookup['OP_2SWAP'] = 114 -opCodeLookup['OP_CAT'] = 126 -opCodeLookup['OP_SUBSTR'] = 127 -opCodeLookup['OP_LEFT'] = 128 -opCodeLookup['OP_RIGHT'] = 129 -opCodeLookup['OP_SIZE'] = 130 -opCodeLookup['OP_INVERT'] = 131 -opCodeLookup['OP_AND'] = 132 -opCodeLookup['OP_OR'] = 133 -opCodeLookup['OP_XOR'] = 134 -opCodeLookup['OP_EQUAL'] = 135 -opCodeLookup['OP_EQUALVERIFY'] = 136 -opCodeLookup['OP_1ADD'] = 139 -opCodeLookup['OP_1SUB'] = 140 -opCodeLookup['OP_2MUL'] = 141 -opCodeLookup['OP_2DIV'] = 142 -opCodeLookup['OP_NEGATE'] = 143 -opCodeLookup['OP_ABS'] = 144 -opCodeLookup['OP_NOT'] = 145 -opCodeLookup['OP_0NOTEQUAL'] = 146 -opCodeLookup['OP_ADD'] = 147 -opCodeLookup['OP_SUB'] = 148 -opCodeLookup['OP_MUL'] = 149 -opCodeLookup['OP_DIV'] = 150 -opCodeLookup['OP_MOD'] = 151 -opCodeLookup['OP_LSHIFT'] = 152 -opCodeLookup['OP_RSHIFT'] = 153 -opCodeLookup['OP_BOOLAND'] = 154 -opCodeLookup['OP_BOOLOR'] = 155 -opCodeLookup['OP_NUMEQUAL'] = 156 -opCodeLookup['OP_NUMEQUALVERIFY'] = 157 -opCodeLookup['OP_NUMNOTEQUAL'] = 158 -opCodeLookup['OP_LESSTHAN'] = 159 -opCodeLookup['OP_GREATERTHAN'] = 160 -opCodeLookup['OP_LESSTHANOREQUAL'] = 161 -opCodeLookup['OP_GREATERTHANOREQUAL'] = 162 -opCodeLookup['OP_MIN'] = 163 -opCodeLookup['OP_MAX'] = 164 -opCodeLookup['OP_WITHIN'] = 165 -opCodeLookup['OP_RIPEMD160'] = 166 -opCodeLookup['OP_SHA1'] = 167 -opCodeLookup['OP_SHA256'] = 168 -opCodeLookup['OP_HASH160'] = 169 -opCodeLookup['OP_HASH256'] = 170 -opCodeLookup['OP_CODESEPARATOR'] = 171 -opCodeLookup['OP_CHECKSIG'] = 172 -opCodeLookup['OP_CHECKSIGVERIFY'] = 173 -opCodeLookup['OP_CHECKMULTISIG'] = 174 -opCodeLookup['OP_CHECKMULTISIGVERIFY'] = 175 -#Word Opcode Description -#OP_PUBKEYHASH = 253 Represents a public key hashed with OP_HASH160. -#OP_PUBKEY = 254 Represents a public key compatible with OP_CHECKSIG. -#OP_INVALIDOPCODE = 255 Matches any opcode that is not yet assigned. -#[edit] Reserved words -#Any opcode not assigned is also reserved. Using an unassigned opcode makes the transaction invalid. -#Word Opcode When used... -#OP_RESERVED = 80 Transaction is invalid -#OP_VER = 98 Transaction is invalid -#OP_VERIF = 101 Transaction is invalid -#OP_VERNOTIF = 102 Transaction is invalid -#OP_RESERVED1 = 137 Transaction is invalid -#OP_RESERVED2 = 138 Transaction is invalid -#OP_NOP1 = OP_NOP10 176-185 The word is ignored. - - -def getOpCode(name): - return int_to_binary(opCodeLookup[name], widthBytes=1) - - -TXIN_SCRIPT_STANDARD = 0 -TXIN_SCRIPT_COINBASE = 1 -TXIN_SCRIPT_SPENDCB = 2 -TXIN_SCRIPT_UNSIGNED = 3 -TXIN_SCRIPT_UNKNOWN = 4 - -TXOUT_SCRIPT_STANDARD = 0 -TXOUT_SCRIPT_COINBASE = 1 -TXOUT_SCRIPT_MULTISIG = 2 -TXOUT_SCRIPT_OP_EVAL = 3 -TXOUT_SCRIPT_UNKNOWN = 4 - -MULTISIG_1of1 = (1,1) -MULTISIG_1of2 = (1,2) -MULTISIG_2oF2 = (2,2) -MULTISIG_1oF3 = (1,3) -MULTISIG_2oF3 = (2,3) -MULTISIG_3oF3 = (3,3) -MULTISIG_UNKNOWN = (0,0) - -TXOUT_TYPE_NAMES = { TXOUT_SCRIPT_STANDARD: 'Standard', \ - TXOUT_SCRIPT_COINBASE: 'Coinbase', \ - TXOUT_SCRIPT_MULTISIG: 'Multi-Signature', \ - TXOUT_SCRIPT_UNKNOWN: '', \ - TXOUT_SCRIPT_OP_EVAL: 'OP-EVAL' } -TXIN_TYPE_NAMES = { TXIN_SCRIPT_STANDARD: 'Standard', \ - TXIN_SCRIPT_COINBASE: 'Coinbase', \ - TXIN_SCRIPT_SPENDCB: 'Spend-CB', \ - TXIN_SCRIPT_UNSIGNED: 'Unsigned', \ - TXIN_SCRIPT_UNKNOWN: ''} - -################################################################################ -def getTxOutMultiSigInfo(binScript): - """ - Gets the Multi-Sig tx type, as well as all the address-160 strings of - the keys that are needed to satisfy this transaction. This currently - only identifies M-of-N transaction types, returning unknown otherwise. - - However, the address list it returns should be valid regardless of - whether the type was unknown: we assume all 20-byte chunks of data - are public key hashes, and 65-byte chunks are public keys. - - NOTE: Because the address list is always valid, there is no reason - not to use this method to extract addresses from ANY scripts, - not just multi-sig... - """ - addr160List = [] - pub65List = [] - bup = BinaryUnpacker(binScript) - opcodes = [] - while bup.getRemainingSize() > 0: - nextByte = bup.get(UINT8) - binChunk = '' - if 0 < nextByte < 76: - nBytes = nextByte - binChunk = bup.get(BINARY_CHUNK, nBytes) - elif nextByte == OP_PUSHDATA1: - nBytes = scriptUnpacker.get(UINT8) - binChunk = bup.get(BINARY_CHUNK, nBytes) - elif nextByte == OP_PUSHDATA2: - nBytes = scriptUnpacker.get(UINT16) - binChunk = bup.get(BINARY_CHUNK, nBytes) - elif nextByte == OP_PUSHDATA4: - nBytes = scriptUnpacker.get(UINT32) - binChunk = bup.get(BINARY_CHUNK, nBytes) - else: - opcodes.append(nextByte) - - - if len(binChunk) == 20: - addr160List.append(binChunk) - pub65List.append('') - opcodes.append('') - elif len(binChunk) == 65: - addr160List.append(convertKeyDataToAddress(pubKey=binChunk)) - pub65List.append(binChunk) - opcodes.append('') - - - mstype = MULTISIG_UNKNOWN - #print 'Transaction:', - #for op in opcodes: - #print op, - - # First assume that this is an M-of-N script - try: - isCMS = opcodes[-1]==getOpCode('OP_CHECKMULTISIG') - M = int(opcodes[ 0]) - N = int(opcodes[-2]) - keys = opcodes[1:-2] - nPub = sum([(1 if p=='PubKey65' else 0) for p in keys]) - if 00: - print indstr + indent + 'Sender: ', hash160_to_addrStr(inAddr160) - print indstr + indent + 'Seq: ', self.intSeq - - # Before broadcasting a transaction make sure that the script is canonical - # This TX could have been signed by an older version of the software. - # Either on the offline Armory installation which may not have been upgraded - # or on a previous installation of Armory on this computer. - def minimizeDERSignaturePadding(self): - rsLen = binary_to_int(self.binScript[2:3]) - rLen = binary_to_int(self.binScript[4:5]) - rBin = self.binScript[5:5+rLen] - sLen = binary_to_int(self.binScript[6+rLen:7+rLen]) - sBin = self.binScript[7+rLen:7+rLen+sLen] - sigScript = createSigScript(rBin, sBin) - newBinScript = int_to_binary(len(sigScript)+1) + sigScript + self.binScript[3+rsLen:] - paddingRemoved = newBinScript != self.binScript - newTxIn = self.copy() - newTxIn.binScript = newBinScript - return paddingRemoved, newTxIn - -##### -class PyTxOut(object): - def __init__(self): - self.value = UNINITIALIZED - self.binScript = UNINITIALIZED - - def unserialize(self, toUnpack): - if isinstance(toUnpack, BinaryUnpacker): - txOutData = toUnpack - else: - txOutData = BinaryUnpacker( toUnpack ) - - self.value = txOutData.get(UINT64) - scriptSize = txOutData.get(VAR_INT) - if txOutData.getRemainingSize() < scriptSize: raise UnserializeError - self.binScript = txOutData.get(BINARY_CHUNK, scriptSize) - return self - - def getValue(self): - return self.value - - def getScript(self): - return self.binScript - - def serialize(self): - binOut = BinaryPacker() - binOut.put(UINT64, self.value) - binOut.put(VAR_INT, len(self.binScript)) - binOut.put(BINARY_CHUNK, self.binScript) - return binOut.getBinaryString() - - def copy(self): - return PyTxOut().unserialize(self.serialize()) - - def fromCpp(self, cppTxOut): - return self.unserialize(cppTxOut.serialize()) - - def createCpp(self): - """ Convert a raw PyTxOut with no context, to a C++ TxOut """ - cppout = Cpp.TxOut() - cppout.unserialize_swigsafe_(self.serialize()) - return cppout - - def pprint(self, nIndent=0, endian=BIGENDIAN): - indstr = indent*nIndent - print indstr + 'TxOut:' - print indstr + indent + 'Value: ', self.value, '(', float(self.value) / ONE_BTC, ')' - txoutType = getTxOutScriptType(self.binScript) - if txoutType == TXOUT_SCRIPT_COINBASE: - print indstr + indent + 'Script: PubKey(%s) OP_CHECKSIG' % \ - (TxOutScriptExtractAddrStr(self.binScript),) - elif txoutType == TXOUT_SCRIPT_STANDARD: - print indstr + indent + 'Script: OP_DUP OP_HASH (%s) OP_EQUAL OP_CHECKSIG' % \ - (TxOutScriptExtractAddrStr(self.binScript),) - else: - print indstr + indent + 'Script: ' - -##### -class PyTx(object): - def __init__(self): - self.version = UNINITIALIZED - self.inputs = UNINITIALIZED - self.outputs = UNINITIALIZED - self.lockTime = 0 - self.thisHash = UNINITIALIZED - self.isSigned = False - - def serialize(self): - binOut = BinaryPacker() - binOut.put(UINT32, self.version) - binOut.put(VAR_INT, len(self.inputs)) - for txin in self.inputs: - binOut.put(BINARY_CHUNK, txin.serialize()) - binOut.put(VAR_INT, len(self.outputs)) - for txout in self.outputs: - binOut.put(BINARY_CHUNK, txout.serialize()) - binOut.put(UINT32, self.lockTime) - return binOut.getBinaryString() - - def unserialize(self, toUnpack): - if isinstance(toUnpack, BinaryUnpacker): - txData = toUnpack - else: - txData = BinaryUnpacker( toUnpack ) - - startPos = txData.getPosition() - self.inputs = [] - self.outputs = [] - self.version = txData.get(UINT32) - numInputs = txData.get(VAR_INT) - for i in xrange(numInputs): - self.inputs.append( PyTxIn().unserialize(txData) ) - numOutputs = txData.get(VAR_INT) - for i in xrange(numOutputs): - self.outputs.append( PyTxOut().unserialize(txData) ) - self.lockTime = txData.get(UINT32) - endPos = txData.getPosition() - self.nBytes = endPos - startPos - self.thisHash = hash256(self.serialize()) - return self - - def copy(self): - return PyTx().unserialize(self.serialize()) - - # Before broadcasting a transaction make sure that the script is canonical - # This TX could have been signed by an older version of the software. - # Either on the offline Armory installation which may not have been upgraded - # or on a previous installation of Armory on this computer. - def minimizeDERSignaturePadding(self): - paddingRemoved = False - newTx = self.copy() - newTx.inputs = [] - for txIn in self.inputs: - paddingRemovedFromTxIn, newTxIn = txIn.minimizeDERSignaturePadding() - if paddingRemovedFromTxIn: - paddingRemoved = True - newTx.inputs.append(newTxIn) - else: - newTx.inputs.append(txIn) - return paddingRemoved, newTx.copy() - - def getHash(self): - return hash256(self.serialize()) - - def getHashHex(self, endianness=LITTLEENDIAN): - return binary_to_hex(self.getHash(), endOut=endianness) - - def makeRecipientsList(self): - """ - Make a list of lists, each one containing information about - an output in this tx. Usually contains - [ScriptType, Value, Addr160] - May include more information if any of the scripts are multi-sig, - such as public keys and multi-sig type (M-of-N) - """ - recipInfoList = [] - for txout in self.outputs: - recipInfoList.append([]) - - scrType = getTxOutScriptType(txout.binScript) - recipInfoList[-1].append(scrType) - recipInfoList[-1].append(txout.value) - if scrType in (TXOUT_SCRIPT_STANDARD, TXOUT_SCRIPT_COINBASE): - recipInfoList[-1].append(TxOutScriptExtractAddr160(txout.binScript)) - elif scrType in (TXOUT_SCRIPT_MULTISIG,): - mstype, addr160s, pubs = getTxOutMultiSigInfo(txout.binScript) - recipInfoList[-1].append(addr160s) - recipInfoList[-1].append(pubs) - recipInfoList[-1].append(mstype[0]) # this is M (from M-of-N) - elif scrType in (TXOUT_SCRIPT_OP_EVAL,): - LOGERROR('OP_EVAL doesn\'t exist anymore. How did we get here?') - recipInfoList[-1].append(txout.binScript) - elif scrType in (TXOUT_SCRIPT_UNKNOWN,): - LOGERROR('Unknown TxOut type') - recipInfoList[-1].append(txout.binScript) - else: - LOGERROR('Unrecognized txout script that isn\'t TXOUT_SCRIPT_UNKNOWN...?') - return recipInfoList - - - def pprint(self, nIndent=0, endian=BIGENDIAN): - indstr = indent*nIndent - print indstr + 'Transaction:' - print indstr + indent + 'TxHash: ', self.getHashHex(endian), \ - '(BE)' if endian==BIGENDIAN else '(LE)' - print indstr + indent + 'Version: ', self.version - print indstr + indent + 'nInputs: ', len(self.inputs) - print indstr + indent + 'nOutputs: ', len(self.outputs) - print indstr + indent + 'LockTime: ', self.lockTime - print indstr + indent + 'Inputs: ' - for inp in self.inputs: - inp.pprint(nIndent+2, endian=endian) - print indstr + indent + 'Outputs: ' - for out in self.outputs: - out.pprint(nIndent+2, endian=endian) - - - - #def pprintShort(self, nIndent=0, endian=BIGENDIAN): - #print '\nTransaction: %s' % self.getHashHex() - - - def fromCpp(self, cppTx): - return self.unserialize(cppTx.serialize()) - - def createCpp(self): - """ Convert a raw PyTx with no context, to a C++ Tx """ - cpptx = Cpp.Tx() - cpptx.unserialize_swigsafe_(self.serialize()) - return cpptx - - def fetchCpp(self): - """ Use the info in this PyTx to get the C++ version from TheBDM """ - return TheBDM.getTxByHash(self.getHash()) - - def pprintHex(self, nIndent=0): - bu = BinaryUnpacker(self.serialize()) - theSer = self.serialize() - print binary_to_hex(bu.get(BINARY_CHUNK, 4)) - nTxin = bu.get(VAR_INT) - print 'VAR_INT(%d)' % nTxin - for i in range(nTxin): - print binary_to_hex(bu.get(BINARY_CHUNK,32)) - print binary_to_hex(bu.get(BINARY_CHUNK,4)) - scriptSz = bu.get(VAR_INT) - print 'VAR_IN(%d)' % scriptSz - print binary_to_hex(bu.get(BINARY_CHUNK,scriptSz)) - print binary_to_hex(bu.get(BINARY_CHUNK,4)) - nTxout = bu.get(VAR_INT) - print 'VAR_INT(%d)' % nTxout - for i in range(nTxout): - print binary_to_hex(bu.get(BINARY_CHUNK,8)) - scriptSz = bu.get(VAR_INT) - print binary_to_hex(bu.get(BINARY_CHUNK,scriptSz)) - print binary_to_hex(bu.get(BINARY_CHUNK, 4)) - - - - -################################################################################ -# Block Information -################################################################################ - - -class PyBlockHeader(object): - def __init__(self): - self.version = 1 - self.prevBlkHash = '' - self.merkleRoot = UNINITIALIZED - self.timestamp = UNINITIALIZED - self.diffBits = UNINITIALIZED - self.nonce = UNINITIALIZED - # Use these fields for storage of block information, but are not otherwise - # part of the serialized data structure - self.theHash = '' - self.numTx = UNINITIALIZED - self.blkHeight = UNINITIALIZED - self.fileByteLoc = UNINITIALIZED - self.nextBlkHash = UNINITIALIZED - self.intDifficult = UNINITIALIZED - self.sumDifficult = UNINITIALIZED - self.isMainChain = False - self.isOrphan = True - - def serialize(self): - if self.version == UNINITIALIZED: - raise UnitializedBlockDataError, 'PyBlockHeader object not initialized!' - binOut = BinaryPacker() - binOut.put(UINT32, self.version) - binOut.put(BINARY_CHUNK, self.prevBlkHash) - binOut.put(BINARY_CHUNK, self.merkleRoot) - binOut.put(UINT32, self.timestamp) - binOut.put(BINARY_CHUNK, self.diffBits) - binOut.put(UINT32, self.nonce) - return binOut.getBinaryString() - - - def unserialize(self, toUnpack): - if isinstance(toUnpack, BinaryUnpacker): - blkData = toUnpack - else: - blkData = BinaryUnpacker( toUnpack ) - - self.version = blkData.get(UINT32) - self.prevBlkHash = blkData.get(BINARY_CHUNK, 32) - self.merkleRoot = blkData.get(BINARY_CHUNK, 32) - self.timestamp = blkData.get(UINT32) - self.diffBits = blkData.get(BINARY_CHUNK, 4) - self.nonce = blkData.get(UINT32) - self.theHash = hash256(self.serialize()) - return self - - def copy(self): - return PyBlockHeader().unserialize(self.serialize()) - - def getHash(self, endian=LITTLEENDIAN): - if self.version == UNINITIALIZED: - raise UnitializedBlockDataError, 'PyBlockHeader object not initialized!' - if len(self.theHash) < 32: - self.theHash = hash256(self.serialize()) - outHash = self.theHash - if endian==BIGENDIAN: - outHash = binary_switchEndian(outHash) - return outHash - - def getHashHex(self, endian=LITTLEENDIAN): - if self.version == UNINITIALIZED: - raise UnitializedBlockDataError, 'PyBlockHeader object not initialized!' - if len(self.theHash) < 32: - self.theHash = hash256(self.serialize()) - return binary_to_hex(self.theHash, endian) - - def getDifficulty(self): - if self.diffBits == UNINITIALIZED: - raise UnitializedBlockDataError, 'PyBlockHeader object not initialized!' - self.intDifficult = binaryBits_to_difficulty(self.diffBits) - return self.intDifficult - - def fromCpp(self, cppHead): - return self.unserialize(cppHead.serialize()) - - def createCpp(self): - """ Convert a raw blockheader with no context, to a C++ BlockHeader """ - cppbh = Cpp.BlockHeader() - cppbh.unserialize_swigsafe_(self.serialize()) - return cppbh - - def fetchCpp(self): - """ Convert a raw blockheader with no context, to a C++ BlockHeader """ - return TheBDM.getHeaderByHash(self.getHash()) - - - def pprint(self, nIndent=0, endian=BIGENDIAN): - indstr = indent*nIndent - print indstr + 'BlockHeader:' - print indstr + indent + 'Version: ', self.version - print indstr + indent + 'ThisHash: ', binary_to_hex( self.theHash, endOut=endian), \ - '(BE)' if endian==BIGENDIAN else '(LE)' - print indstr + indent + 'PrevBlock: ', binary_to_hex(self.prevBlkHash, endOut=endian), \ - '(BE)' if endian==BIGENDIAN else '(LE)' - print indstr + indent + 'MerkRoot: ', binary_to_hex(self.merkleRoot, endOut=endian), \ - '(BE)' if endian==BIGENDIAN else '(LE)' - print indstr + indent + 'Timestamp: ', self.timestamp - fltDiff = binaryBits_to_difficulty(self.diffBits) - print indstr + indent + 'Difficulty:', fltDiff, '('+binary_to_hex(self.diffBits)+')' - print indstr + indent + 'Nonce: ', self.nonce - if not self.blkHeight==UNINITIALIZED: - print indstr + indent + 'BlkHeight: ', self.blkHeight - if not self.blkHeight==UNINITIALIZED: - print indstr + indent + 'BlkFileLoc:', self.fileByteLoc - if not self.nextBlkHash==UNINITIALIZED: - #print indstr + indent + 'NextBlock: ', binary_to_hex(self.nextBlkHash) - print indstr + indent + 'NextBlock: ', self.nextBlkHash - if not self.numTx==UNINITIALIZED: - print indstr + indent + 'NumTx: ', self.numTx - if not self.intDifficult==UNINITIALIZED: - print indstr + indent + 'Difficulty:', self.intDifficult - if not self.sumDifficult==UNINITIALIZED: - print indstr + indent + 'DiffSum: ', self.sumDifficult - - -################################################################################ -################################################################################ -class PyBlockData(object): - def __init__(self, txList=[]): - self.txList = txList - self.numTx = len(txList) - self.merkleTree = [] - self.merkleRoot = UNINITIALIZED - - - def serialize(self): - if self.numTx == UNINITIALIZED: - raise UnitializedBlockDataError, 'PyBlockData object not initialized!' - binOut = BinaryPacker() - binOut.put(VAR_INT, self.numTx) - for tx in self.txList: - binOut.put(BINARY_CHUNK, tx.serialize()) - return binOut.getBinaryString() - - def unserialize(self, toUnpack): - if isinstance(toUnpack, BinaryUnpacker): - blkData = toUnpack - else: - blkData = BinaryUnpacker( toUnpack ) - - self.txList = [] - self.numTx = blkData.get(VAR_INT) - for i in xrange(self.numTx): - self.txList.append( PyTx().unserialize(blkData) ) - self.merkleTree = [] - self.merkleRoot = '' - return self - - - def getTxHashList(self): - if( self.numTx == UNINITIALIZED ): - self.getMerkleRoot() - return self.merkleTree[:self.numTx] - - - def getMerkleRoot(self): - assert( not self.numTx == UNINITIALIZED ) - if len(self.merkleTree)==0 and not self.numTx==0: - #Create the merkle tree - self.merkleTree = [hash256(tx.serialize()) for tx in self.txList] - sz = len(self.merkleTree) - while sz > 1: - hashes = self.merkleTree[-sz:] - mod2 = sz%2 - for i in range(sz/2): - self.merkleTree.append( hash256(hashes[2*i] + hashes[2*i+1]) ) - if mod2==1: - self.merkleTree.append( hash256(hashes[-1] + hashes[-1]) ) - sz = (sz+1) / 2 - self.merkleRoot = self.merkleTree[-1] - return self.merkleRoot - - def printMerkleTree(self, reverseHash=False, indent=''): - print indent + 'Printing Merkle Tree:' - if reverseHash: - print indent + '(hashes will be reversed, like shown on BlockExplorer.com)' - root = self.getMerkleRoot() - print indent + 'Merkle Root:', binary_to_hex(root) - for h in self.merkleTree: - phash = binary_to_hex(h) if not reverseHash else binary_to_hex(h, endOut=BIGENDIAN) - print indent + '\t' + phash - - - def pprint(self, nIndent=0, endian=BIGENDIAN): - indstr = indent*nIndent - print indstr + 'BlockData:' - print indstr + indent + 'MerkleRoot: ', binary_to_hex(self.getMerkleRoot(), endian), \ - '(BE)' if endian==BIGENDIAN else '(LE)' - print indstr + indent + 'NumTx: ', self.numTx - for tx in self.txList: - tx.pprint(nIndent+1, endian=endian) - - -################################################################################ -################################################################################ -class PyBlock(object): - def __init__(self, prevHeader=None, txlist=[]): - self.blockHeader = PyBlockHeader() - self.blockData = PyBlockData() - if prevHeader: - self.setPrevHeader(prevHeader) - if txlist: - self.setTxList(txlist) - - def serialize(self): - assert( not self.blockHeader == UNINITIALIZED ) - binOut = BinaryPacker() - binOut.put(BINARY_CHUNK, self.blockHeader.serialize()) - binOut.put(BINARY_CHUNK, self.blockData.serialize()) - return binOut.getBinaryString() - - def unserialize(self, toUnpack): - if isinstance(toUnpack, BinaryUnpacker): - blkData = toUnpack - else: - blkData = BinaryUnpacker( toUnpack ) - - self.txList = [] - self.blockHeader = PyBlockHeader().unserialize(blkData) - self.blockData = PyBlockData().unserialize(blkData) - return self - - def getNumTx(self): - return len(self.blockData.txList) - - def getSize(self): - return len(self.serialize()) - - # Not sure how useful these manual block-construction methods - # are. For now, I just need something with non-ridiculous vals - def setPrevHeader(self, prevHeader, copyAttr=True): - self.blockHeader.prevBlkHash = prevHeader.theHash - self.blockHeader.nonce = 0 - if copyAttr: - self.blockHeader.version = prevHeader.version - self.blockHeader.timestamp = prevHeader.timestamp+600 - self.blockHeader.diffBits = prevHeader.diffBits - - def setTxList(self, txlist): - self.blockData = PyBlockData(txlist) - if not self.blockHeader == UNINITIALIZED: - self.blockHeader.merkleRoot = self.blockData.getMerkleRoot() - - def tx(self, idx): - return self.blockData.txList[idx] - - def pprint(self, nIndent=0, endian=BIGENDIAN): - indstr = indent*nIndent - print indstr + 'Block:' - self.blockHeader.pprint(nIndent+1, endian=endian) - self.blockData.pprint(nIndent+1, endian=endian) - - -############################################################################# -def getFeeForTx(txHash): - if TheBDM.getBDMState()=='BlockchainReady': - if not TheBDM.hasTxWithHash(txHash): - LOGERROR('Attempted to get fee for tx we don\'t have...? %s', \ - binary_to_hex(txHash,BIGENDIAN)) - return 0 - txref = TheBDM.getTxByHash(txHash) - valIn, valOut = 0,0 - for i in range(txref.getNumTxIn()): - valIn += TheBDM.getSentValue(txref.getTxInCopy(i)) - for i in range(txref.getNumTxOut()): - valOut += txref.getTxOutCopy(i).getValue() - return valIn - valOut - - -############################################################################# -def determineSentToSelfAmt(le, wlt): - """ - NOTE: this method works ONLY because we always generate a new address - whenever creating a change-output, which means it must have a - higher chainIndex than all other addresses. If you did something - creative with this tx, this may not actually work. - """ - amt = 0 - if TheBDM.isInitialized() and le.isSentToSelf(): - txref = TheBDM.getTxByHash(le.getTxHash()) - if not txref.isInitialized(): - return (0, 0) - if txref.getNumTxOut()==1: - return (txref.getTxOutCopy(0).getValue(), -1) - maxChainIndex = -5 - txOutChangeVal = 0 - changeIndex = -1 - valSum = 0 - for i in range(txref.getNumTxOut()): - valSum += txref.getTxOutCopy(i).getValue() - addr160 = CheckHash160(txref.getTxOutCopy(i).getScrAddressStr()) - addr = wlt.getAddrByHash160(addr160) - if addr and addr.chainIndex > maxChainIndex: - maxChainIndex = addr.chainIndex - txOutChangeVal = txref.getTxOutCopy(i).getValue() - changeIndex = i - - amt = valSum - txOutChangeVal - return (amt, changeIndex) - - - - -################################################################################ -# -# SCRIPTING! -# -################################################################################ - - -def convertScriptToOpStrings(binScript): - opList = [] - - i = 0; - sz = len(binScript) - error = False; - while i < sz: - nextOp = ord(binScript[i]); - if nextOp == 0: - opList.append("0") - i+=1 - elif nextOp < 76: - opList.append("[PUSHDATA -- " + str(nextOp) + " BYTES:]") - binObj = binScript[i+1:i+1+nextOp] - opList.append(binary_to_hex(binObj)) - i += nextOp+1 - elif nextOp == 76: - nb = binary_to_int(binScript[i+1:i+2]) - if i+1+1+nb > sz: - error = True; - break - binObj = binScript[i+2:i+2+nb] - opList.append("[OP_PUSHDATA1 -- " + str(nb) + " BYTES:]"); - opList.append(binary_to_hex(binObj)) - i += nb+2 - elif nextOp == 77: - nb = binScript[i+1:i+3]; - if i+1+2+nb > sz: - error = True; - break - nbprint = min(nb,256) - binObj = binScript[i+3,i+3+nbprint] - opList.append("[OP_PUSHDATA2 -- " + str(nb) + " BYTES:]"); - opList.append(binary_to_hex(binObj) + '...') - i += nb+3 - elif nextOp == 78: - nb = binScript[i+1:i+5]; - if i+1+4+nb > sz: - error = True; - break - nbprint = min(nb,256) - binObj = binScript[i+5,i+5+nbprint] - opList.append("[OP_PUSHDATA4 -- " + str(nb) + " BYTES:]"); - opList.append(binary_to_hex(binObj) + '...') - i += nb+5 - else: - opList.append(opnames[nextOp]); - i += 1 - - if error: - opList.append("ERROR PROCESSING SCRIPT"); - - return opList; - - -def pprintScript(binScript, nIndent=0): - indstr = indent*nIndent - print indstr + 'Script:' - opList = convertScriptToOpStrings(binScript) - for op in opList: - print indstr + indent + op - - - -TX_INVALID = 0 -OP_NOT_IMPLEMENTED = 1 -OP_DISABLED = 2 -SCRIPT_STACK_SIZE_ERROR = 3 -SCRIPT_ERROR = 4 -SCRIPT_NO_ERROR = 5 - - -class PyScriptProcessor(object): - """ - Use this class to evaluate a script. This method is more complicated - than some might expect, due to the fact that any OP_CHECKSIG or - OP_CHECKMULTISIG code requires the full transaction of the TxIn script - and also needs the TxOut script being spent. Since nearly every useful - script will have one of these operations, this class/method assumes - that all that data will be supplied. - - To simply execute a script not requiring any crypto operations: - - scriptIsValid = PyScriptProcessor().executeScript(binScript) - """ - - def __init__(self, txOldData=None, txNew=None, txInIndex=None): - self.stack = [] - self.txNew = None - self.script1 = None - self.script2 = None - if txOldData and txNew and not txInIndex==None: - self.setTxObjects(txOldData, txNew, txInIndex) - - - def setTxObjects(self, txOldData, txNew, txInIndex): - """ - The minimal amount of data necessary to evaluate a script that - has an signature check is the TxOut script that is being spent - and the entire Tx of the TxIn that is spending it. Thus, we - must supply at least the txOldScript, and a txNew with its - TxIn index (so we know which TxIn is spending that TxOut). - It is acceptable to pass in the full TxOut or the tx of the - TxOut instead of just the script itself. - """ - self.txNew = PyTx().unserialize(txNew.serialize()) - self.script1 = str(txNew.inputs[txInIndex].binScript) # copy - self.txInIndex = txInIndex - self.txOutIndex = txNew.inputs[txInIndex].outpoint.txOutIndex - self.txHash = txNew.inputs[txInIndex].outpoint.txHash - - if isinstance(txOldData, PyTx): - if not self.txHash == hash256(txOldData.serialize()): - LOGERROR('*** Supplied incorrect pair of transactions!') - self.script2 = str(txOldData.outputs[self.txOutIndex].binScript) - elif isinstance(txOldData, PyTxOut): - self.script2 = str(txOldData.binScript) - elif isinstance(txOldData, str): - self.script2 = str(txOldData) - - - - def verifyTransactionValid(self, txOldData=None, txNew=None, txInIndex=-1): - TimerStart('psp.verifyTransactionValid') - if txOldData and txNew and txInIndex != -1: - self.setTxObjects(txOldData, txNew, txInIndex) - else: - txOldData = self.script2 - txNew = self.txNew - txInIndex = self.txInIndex - - if self.script1==None or self.txNew==None: - raise VerifyScriptError, 'Cannot verify transactions, without setTxObjects call first!' - - # Execute TxIn script first - self.stack = [] - exitCode1 = self.executeScript(self.script1, self.stack) - - if not exitCode1 == SCRIPT_NO_ERROR: - raise VerifyScriptError, ('First script failed! Exit Code: ' + str(exitCode1)) - - exitCode2 = self.executeScript(self.script2, self.stack) - - if not exitCode2 == SCRIPT_NO_ERROR: - raise VerifyScriptError, ('Second script failed! Exit Code: ' + str(exitCode2)) - - TimerStop('psp.verifyTransactionValid') - return self.stack[-1]==1 - - - def executeScript(self, binaryScript, stack=[]): - self.stack = stack - self.stackAlt = [] - scriptData = BinaryUnpacker(binaryScript) - self.lastOpCodeSepPos = None - - while scriptData.getRemainingSize() > 0: - opcode = scriptData.get(UINT8) - exitCode = self.executeOpCode(opcode, scriptData, self.stack, self.stackAlt) - if not exitCode == SCRIPT_NO_ERROR: - if exitCode==OP_NOT_IMPLEMENTED: - LOGERROR('***ERROR: OpCodes OP_IF, OP_NOTIF, OP_ELSE, OP_ENDIF,') - LOGERROR(' have not been implemented, yet. This script') - LOGERROR(' could not be evaluated.') - if exitCode==OP_DISABLED: - LOGERROR('***ERROR: This script included an op code that has been') - LOGERROR(' disabled for security reasons. Script eval') - LOGERROR(' failed.') - return exitCode - - return SCRIPT_NO_ERROR - - - # Implementing this method exactly as in the client because it looks like - # there could be some subtleties with how it determines "true" - def castToBool(self, binData): - if isinstance(binData, int): - binData = int_to_binary(binData) - - for i,byte in enumerate(binData): - if not ord(byte) == 0: - # This looks like it's assuming LE encoding (?) - if (i == len(binData)-1) and (byte==0x80): - return False - return True - return False - - - def checkSig(self,binSig, binPubKey, txOutScript, txInTx, txInIndex, lastOpCodeSep=None): - """ - Generic method for checking Bitcoin tx signatures. This needs to be used for both - OP_CHECKSIG and OP_CHECKMULTISIG. Step 1 is to pop signature and public key off - the stack, which must be done outside this method and passed in through the argument - list. The remaining steps do not require access to the stack. - """ - - # 2. Subscript is from latest OP_CODESEPARATOR until end... if DNE, use whole script - subscript = txOutScript - if lastOpCodeSep: - subscript = subscript[lastOpCodeSep:] - - # 3. Signature is deleted from subscript - # I'm not sure why this line is necessary - maybe for non-standard scripts? - lengthInBinary = int_to_binary(len(binSig)) - subscript = subscript.replace( lengthInBinary + binSig, "") - - # 4. Hashtype is popped and stored - hashtype = binary_to_int(binSig[-1]) - justSig = binSig[:-1] - - if not hashtype == 1: - LOGERROR('Non-unity hashtypes not implemented yet! (hashtype = %d)', hashtype) - assert(False) - - # 5. Make a copy of the transaction -- we will be hashing a modified version - txCopy = PyTx().unserialize( txInTx.serialize() ) - - # 6. Remove all OP_CODESEPARATORs - subscript.replace( int_to_binary(OP_CODESEPARATOR), '') - - # 7. All the TxIn scripts in the copy are blanked (set to empty string) - for txin in txCopy.inputs: - txin.binScript = '' - - # 8. Script for the current input in the copy is set to subscript - txCopy.inputs[txInIndex].binScript = subscript - - # 9. Prepare the signature and public key - senderAddr = PyBtcAddress().createFromPublicKey(binPubKey) - binHashCode = int_to_binary(hashtype, widthBytes=4) - toHash = txCopy.serialize() + binHashCode - - # Hashes are computed as part of CppBlockUtils::CryptoECDSA methods - ##hashToVerify = hash256(toHash) - ##hashToVerify = binary_switchEndian(hashToVerify) - - # 10. Apply ECDSA signature verification - if senderAddr.verifyDERSignature(toHash, justSig): - return True - else: - return False - - - - - def executeOpCode(self, opcode, scriptUnpacker, stack, stackAlt): - """ - Executes the next OP_CODE given the current state of the stack(s) - """ - - # TODO: Gavin clarified the effects of OP_0, and OP_1-OP_16. - # OP_0 puts an empty string onto the stack, which evaluateses to - # false and is plugged into HASH160 as '' - # OP_X puts a single byte onto the stack, 0x01 to 0x10 - # - # I haven't implemented it this way yet, because I'm still missing - # some details. Since this "works" for available scripts, I'm going - # to leave it alone for now. - - ########################################################################## - ########################################################################## - ### This block produces very nice debugging output for script eval! - #def pr(s): - #if isinstance(s,int): - #return str(s) - #elif isinstance(s,str): - #if len(s)>8: - #return binary_to_hex(s)[:8] - #else: - #return binary_to_hex(s) - - #print ' '.join([pr(i) for i in stack]) - #print opnames[opcode][:12].ljust(12,' ') + ':', - ########################################################################## - ########################################################################## - - - stackSizeAtLeast = lambda n: (len(self.stack) >= n) - - if opcode == OP_FALSE: - stack.append(0) - elif 0 < opcode < 76: - stack.append(scriptUnpacker.get(BINARY_CHUNK, opcode)) - elif opcode == OP_PUSHDATA1: - nBytes = scriptUnpacker.get(UINT8) - stack.append(scriptUnpacker.get(BINARY_CHUNK, nBytes)) - elif opcode == OP_PUSHDATA2: - nBytes = scriptUnpacker.get(UINT16) - stack.append(scriptUnpacker.get(BINARY_CHUNK, nBytes)) - elif opcode == OP_PUSHDATA4: - nBytes = scriptUnpacker.get(UINT32) - stack.append(scriptUnpacker.get(BINARY_CHUNK, nBytes)) - elif opcode == OP_1NEGATE: - stack.append(-1) - elif opcode == OP_TRUE: - stack.append(1) - elif 81 < opcode < 97: - stack.append(opcode-80) - elif opcode == OP_NOP: - pass - - # TODO: figure out the conditional op codes... - elif opcode == OP_IF: - return OP_NOT_IMPLEMENTED - elif opcode == OP_NOTIF: - return OP_NOT_IMPLEMENTED - elif opcode == OP_ELSE: - return OP_NOT_IMPLEMENTED - elif opcode == OP_ENDIF: - return OP_NOT_IMPLEMENTED - - elif opcode == OP_VERIFY: - if not self.castToBool(stack.pop()): - stack.append(0) - return TX_INVALID - elif opcode == OP_RETURN: - return TX_INVALID - elif opcode == OP_TOALTSTACK: - stackAlt.append( stack.pop() ) - elif opcode == OP_FROMALTSTACK: - stack.append( stackAlt.pop() ) - - elif opcode == OP_IFDUP: - # Looks like this method duplicates the top item if it's not zero - if not stackSizeAtLeast(1): return SCRIPT_STACK_SIZE_ERROR - if self.castToBool(stack[-1]): - stack.append(stack[-1]); - - elif opcode == OP_DEPTH: - stack.append( len(stack) ) - elif opcode == OP_DROP: - stack.pop() - elif opcode == OP_DUP: - stack.append( stack[-1] ) - elif opcode == OP_NIP: - if not stackSizeAtLeast(2): return SCRIPT_STACK_SIZE_ERROR - del stack[-2] - elif opcode == OP_OVER: - if not stackSizeAtLeast(2): return SCRIPT_STACK_SIZE_ERROR - stack.append(stack[-2]) - elif opcode == OP_PICK: - n = stack.pop() - if not stackSizeAtLeast(n): return SCRIPT_STACK_SIZE_ERROR - stack.append(stack[-n]) - elif opcode == OP_ROLL: - n = stack.pop() - if not stackSizeAtLeast(n): return SCRIPT_STACK_SIZE_ERROR - stack.append(stack[-(n+1)]) - del stack[-(n+2)] - elif opcode == OP_ROT: - if not stackSizeAtLeast(3): return SCRIPT_STACK_SIZE_ERROR - stack.append( stack[-3] ) - del stack[-4] - elif opcode == OP_SWAP: - if not stackSizeAtLeast(2): return SCRIPT_STACK_SIZE_ERROR - x2 = stack.pop() - x1 = stack.pop() - stack.extend([x2, x1]) - elif opcode == OP_TUCK: - if not stackSizeAtLeast(2): return SCRIPT_STACK_SIZE_ERROR - x2 = stack.pop() - x1 = stack.pop() - stack.extend([x2, x1, x2]) - elif opcode == OP_2DROP: - if not stackSizeAtLeast(2): return SCRIPT_STACK_SIZE_ERROR - stack.pop() - stack.pop() - elif opcode == OP_2DUP: - if not stackSizeAtLeast(2): return SCRIPT_STACK_SIZE_ERROR - stack.append( stack[-2] ) - stack.append( stack[-2] ) - elif opcode == OP_3DUP: - if not stackSizeAtLeast(3): return SCRIPT_STACK_SIZE_ERROR - stack.append( stack[-3] ) - stack.append( stack[-3] ) - stack.append( stack[-3] ) - elif opcode == OP_2OVER: - if not stackSizeAtLeast(4): return SCRIPT_STACK_SIZE_ERROR - stack.append( stack[-4] ) - stack.append( stack[-4] ) - elif opcode == OP_2ROT: - if not stackSizeAtLeast(6): return SCRIPT_STACK_SIZE_ERROR - stack.append( stack[-6] ) - stack.append( stack[-6] ) - elif opcode == OP_2SWAP: - if not stackSizeAtLeast(4): return SCRIPT_STACK_SIZE_ERROR - x4 = stack.pop() - x3 = stack.pop() - x2 = stack.pop() - x1 = stack.pop() - stack.extend( [x3, x4, x1, x2] ) - elif opcode == OP_CAT: - return OP_DISABLED - elif opcode == OP_SUBSTR: - return OP_DISABLED - elif opcode == OP_LEFT: - return OP_DISABLED - elif opcode == OP_RIGHT: - return OP_DISABLED - elif opcode == OP_SIZE: - if isinstance(stack[-1], int): - stack.append(0) - else: - stack.append( len(stack[-1]) ) - elif opcode == OP_INVERT: - return OP_DISABLED - elif opcode == OP_AND: - return OP_DISABLED - elif opcode == OP_OR: - return OP_DISABLED - elif opcode == OP_XOR: - return OP_DISABLED - elif opcode == OP_EQUAL: - x1 = stack.pop() - x2 = stack.pop() - stack.append( 1 if x1==x2 else 0 ) - elif opcode == OP_EQUALVERIFY: - x1 = stack.pop() - x2 = stack.pop() - if not x1==x2: - stack.append(0) - return TX_INVALID - - - elif opcode == OP_1ADD: - stack[-1] += 1 - elif opcode == OP_1SUB: - stack[-1] -= 1 - elif opcode == OP_2MUL: - stack[-1] *= 2 - return OP_DISABLED - elif opcode == OP_2DIV: - stack[-1] /= 2 - return OP_DISABLED - elif opcode == OP_NEGATE: - stack[-1] *= -1 - elif opcode == OP_ABS: - stack[-1] = abs(stack[-1]) - elif opcode == OP_NOT: - top = stack.pop() - if top==0: - stack.append(1) - else: - stack.append(0) - elif opcode == OP_0NOTEQUAL: - top = stack.pop() - if top==0: - stack.append(0) - else: - stack.append(1) - top = stack.pop() - if top==0: - stack.append(1) - else: - stack.append(0) - elif opcode == OP_ADD: - b = stack.pop() - a = stack.pop() - stack.append(a+b) - elif opcode == OP_SUB: - b = stack.pop() - a = stack.pop() - stack.append(a-b) - elif opcode == OP_MUL: - return OP_DISABLED - elif opcode == OP_DIV: - return OP_DISABLED - elif opcode == OP_MOD: - return OP_DISABLED - elif opcode == OP_LSHIFT: - return OP_DISABLED - elif opcode == OP_RSHIFT: - return OP_DISABLED - elif opcode == OP_BOOLAND: - b = stack.pop() - a = stack.pop() - if (not a==0) and (not b==0): - stack.append(1) - else: - stack.append(0) - elif opcode == OP_BOOLOR: - b = stack.pop() - a = stack.pop() - stack.append( 1 if (self.castToBool(a) or self.castToBool(b)) else 0 ) - elif opcode == OP_NUMEQUAL: - b = stack.pop() - a = stack.pop() - stack.append( 1 if a==b else 0 ) - elif opcode == OP_NUMEQUALVERIFY: - b = stack.pop() - a = stack.pop() - if not a==b: - stack.append(0) - return TX_INVALID - elif opcode == OP_NUMNOTEQUAL: - b = stack.pop() - a = stack.pop() - stack.append( 1 if not a==b else 0 ) - elif opcode == OP_LESSTHAN: - b = stack.pop() - a = stack.pop() - stack.append( 1 if ab else 0) - elif opcode == OP_LESSTHANOREQUAL: - b = stack.pop() - a = stack.pop() - stack.append( 1 if a<=b else 0) - elif opcode == OP_GREATERTHANOREQUAL: - b = stack.pop() - a = stack.pop() - stack.append( 1 if a>=b else 0) - elif opcode == OP_MIN: - b = stack.pop() - a = stack.pop() - stack.append( min(a,b) ) - elif opcode == OP_MAX: - b = stack.pop() - a = stack.pop() - stack.append( max(a,b) ) - elif opcode == OP_WITHIN: - xmax = stack.pop() - xmin = stack.pop() - x = stack.pop() - stack.append( 1 if (xmin <= x < xmax) else 0 ) - - elif opcode == OP_RIPEMD160: - bits = stack.pop() - stack.append( ripemd160(bits) ) - elif opcode == OP_SHA1: - bits = stack.pop() - stack.append( sha1(bits) ) - elif opcode == OP_SHA256: - bits = stack.pop() - stack.append( sha256(bits) ) - elif opcode == OP_HASH160: - bits = stack.pop() - if isinstance(bits, int): - bits = '' - stack.append( hash160(bits) ) - elif opcode == OP_HASH256: - bits = stack.pop() - if isinstance(bits, int): - bits = '' - stack.append( sha256(sha256(bits) ) ) - elif opcode == OP_CODESEPARATOR: - self.lastOpCodeSepPos = scriptUnpacker.getPosition() - elif opcode == OP_CHECKSIG or opcode == OP_CHECKSIGVERIFY: - - # 1. Pop key and sig from the stack - binPubKey = stack.pop() - binSig = stack.pop() - - # 2-10. encapsulated in sep method so CheckMultiSig can use it too - txIsValid = self.checkSig( binSig, \ - binPubKey, \ - scriptUnpacker.getBinaryString(), \ - self.txNew, \ - self.txInIndex, \ - self.lastOpCodeSepPos) - stack.append(1 if txIsValid else 0) - if opcode==OP_CHECKSIGVERIFY: - verifyCode = self.executeOpCode(OP_VERIFY) - if verifyCode == TX_INVALID: - return TX_INVALID - - elif opcode == OP_CHECKMULTISIG or opcode == OP_CHECKMULTISIGVERIFY: - # OP_CHECKMULTISIG procedure ported directly from Satoshi client code - # Location: bitcoin-0.4.0-linux/src/src/script.cpp:775 - i=1 - if len(stack) < i: - return TX_INVALID - - nKeys = int(stack[-i]) - if nKeys < 0 or nKeys > 20: - return TX_INVALID - - i += 1 - iKey = i - i += nKeys - if len(stack) < i: - return TX_INVALID - - nSigs = int(stack[-i]) - if nSigs < 0 or nSigs > nKeys: - return TX_INVALID - - iSig = i - i += 1 - i += nSigs - if len(stack) < i: - return TX_INVALID - - stack.pop() - - # Apply the ECDSA verification to each of the supplied Sig-Key-pairs - enoughSigsMatch = True - while enoughSigsMatch and nSigs > 0: - binSig = stack[-iSig] - binKey = stack[-iKey] - - if( self.checkSig(binSig, \ - binKey, \ - scriptUnpacker.getBinaryString(), \ - self.txNew, \ - self.txInIndex, \ - self.lastOpCodeSepPos) ): - iSig += 1 - nSigs -= 1 - - iKey +=1 - nKeys -=1 - - if(nSigs > nKeys): - enoughSigsMatch = False - - # Now pop the things off the stack, we only accessed in-place before - while i > 1: - i -= 1 - stack.pop() - - - stack.append(1 if enoughSigsMatch else 0) - - if opcode==OP_CHECKMULTISIGVERIFY: - verifyCode = self.executeOpCode(OP_VERIFY) - if verifyCode == TX_INVALID: - return TX_INVALID - - else: - return SCRIPT_ERROR - - return SCRIPT_NO_ERROR - - - -################################################################################ -#def getUnspentTxOutsForAddrList(addr160List, utxoType='Sweep', startBlk=-1, \ -def getUnspentTxOutsForAddr160List(addr160List, utxoType='Sweep', startBlk=-1, \ - abortIfBDMBusy=False): - """ - - You have a list of addresses (or just one) and you want to get all the - unspent TxOuts for it. This can either be for computing its balance, or - for sweeping the address(es). - - This will return a list of pairs of [addr160, utxoObj] - This isn't the most efficient method for producing the pairs - - NOTE: At the moment, this only gets STANDARD TxOuts... non-std uses - a different BDM call - - This method will return null output if the BDM is currently in the - middle of a scan. You can use waitAsLongAsNecessary=True if you - want to wait for the previous scan AND the next scan. Otherwise, - you can check for bal==-1 and then try again later... - - Multi-threading update: - - This one-stop-shop method has to be blocking. Instead, you might want - to register the address and rescan asynchronously, skipping this method - entirely: - - cppWlt = Cpp.BtcWallet() - cppWlt.addScrAddress_1_(Hash160ToScrAddr(self.getAddr160())) - TheBDM.registerScrAddr(Hash160ToScrAddr(self.getAddr160())) - TheBDM.rescanBlockchain(wait=False) - - <... do some other stuff ...> - - if TheBDM.getBDMState()=='BlockchainReady': - TheBDM.updateWalletsAfterScan(wait=True) # fast after a rescan - bal = cppWlt.getBalance('Spendable') - utxoList = cppWlt.getUnspentTxOutList() - else: - <...come back later...> - """ - if TheBDM.getBDMState()=='BlockchainReady' or \ - (TheBDM.isScanning() and not abortIfBDMBusy): - if not isinstance(addr160List, (list,tuple)): - addr160List = [addr160List] - - cppWlt = Cpp.BtcWallet() - for addr in addr160List: - if isinstance(addr, PyBtcAddress): - cppWlt.addScrAddress_1_(Hash160ToScrAddr(addr.getAddr160())) - else: - cppWlt.addScrAddress_1_(Hash160ToScrAddr(addr)) - - TheBDM.registerWallet(cppWlt) - currBlk = TheBDM.getTopBlockHeight() - TheBDM.scanBlockchainForTx(cppWlt, currBlk+1 if startBlk==-1 else startBlk) - #TheBDM.scanRegisteredTxForWallet(cppWlt, currBlk+1 if startBlk==-1 else startBlk) - - if utxoType.lower() in ('sweep','unspent','full','all','ultimate'): - return cppWlt.getFullTxOutList(currBlk) - elif utxoType.lower() in ('spend','spendable','confirmed'): - return cppWlt.getSpendableTxOutList(currBlk) - else: - raise TypeError, 'Unknown utxoType!' - else: - return [] - - - -################################################################################ -# NOTE: This method was actually used to create the Blockchain-reorg unit- -# test, and hence why coinbase transactions are supported. However, -# for normal transactions supported by PyBtcEngine, this support is -# unnecessary. -# -# Additionally, this method both creates and signs the tx: however -# PyBtcEngine employs TxDistProposals which require the construction -# and signing to be two separate steps. This method is not suited -# for most of the armoryengine CONOPS. -# -# On the other hand, this method DOES work, and there is no reason -# not to use it if you already have PyBtcAddress-w-PrivKeys avail -# and have a list of inputs and outputs as described below. -# -# This method will take an already-selected set of TxOuts, along with -# PyBtcAddress objects containing necessary the private keys -# -# Src TxOut ~ {PyBtcAddr, PrevTx, PrevTxOutIdx} --OR-- COINBASE = -1 -# Dst TxOut ~ {PyBtcAddr, value} -# -# Of course, we usually don't have the private keys of the dst addrs... -# -def PyCreateAndSignTx(srcTxOuts, dstAddrsVals): - newTx = PyTx() - newTx.version = 1 - newTx.lockTime = 0 - newTx.inputs = [] - newTx.outputs = [] - - - numInputs = len(srcTxOuts) - numOutputs = len(dstAddrsVals) - - coinbaseTx = False - if numInputs==1 and srcTxOuts[0] == -1: - coinbaseTx = True - - - ############################# - # Fill in TxOuts first - for i in range(numOutputs): - txout = PyTxOut() - txout.value = dstAddrsVals[i][1] - dstAddr = dstAddrsVals[i][0] - if(coinbaseTx): - txout.binScript = ''.join([ '\x41', \ - dstAddr.binPublicKey65.toBinStr(), \ - getOpCode('OP_CHECKSIG' )]) - else: - txout.binScript = ''.join([ getOpCode('OP_DUP' ), \ - getOpCode('OP_HASH160' ), \ - '\x14', \ - dstAddr.getAddr160(), \ - getOpCode('OP_EQUALVERIFY'), \ - getOpCode('OP_CHECKSIG' )]) - - newTx.outputs.append(txout) - - - ############################# - # Create temp TxIns with blank scripts - for i in range(numInputs): - txin = PyTxIn() - txin.outpoint = PyOutPoint() - if(coinbaseTx): - txin.outpoint.txHash = '\x00'*32 - txin.outpoint.txOutIndex = binary_to_int('\xff'*4) - else: - txin.outpoint.txHash = hash256(srcTxOuts[i][1].serialize()) - txin.outpoint.txOutIndex = srcTxOuts[i][2] - txin.binScript = '' - txin.intSeq = 2**32-1 - newTx.inputs.append(txin) - - - ############################# - # Now we apply the ultra-complicated signature procedure - # We need a copy of the Tx with all the txin scripts blanked out - txCopySerialized = newTx.serialize() - for i in range(numInputs): - if coinbaseTx: - pass - else: - txCopy = PyTx().unserialize(txCopySerialized) - srcAddr = srcTxOuts[i][0] - txoutIdx = srcTxOuts[i][2] - prevTxOut = srcTxOuts[i][1].outputs[txoutIdx] - binToSign = '' - - assert(srcAddr.hasPrivKey()) - - # Only implemented one type of hashing: SIGHASH_ALL - hashType = 1 # SIGHASH_ALL - hashCode1 = int_to_binary(1, widthBytes=1) - hashCode4 = int_to_binary(1, widthBytes=4) - - # Copy the script of the TxOut we're spending, into the txIn script - txCopy.inputs[i].binScript = prevTxOut.binScript - preHashMsg = txCopy.serialize() + hashCode4 - - # CppBlockUtils::CryptoECDSA modules do the hashing for us - ##binToSign = hash256(preHashMsg) - ##binToSign = binary_switchEndian(binToSign) - - signature = srcAddr.generateDERSignature(preHashMsg) - - - # If we are spending a Coinbase-TxOut, only need sig, no pubkey - # Don't forget to tack on the one-byte hashcode and consider it part of sig - if len(prevTxOut.binScript) > 30: - sigLenInBinary = int_to_binary(len(signature) + 1) - newTx.inputs[i].binScript = sigLenInBinary + signature + hashCode1 - else: - pubkey = srcAddr.binPublicKey65.toBinStr() - sigLenInBinary = int_to_binary(len(signature) + 1) - pubkeyLenInBinary = int_to_binary(len(pubkey) ) - newTx.inputs[i].binScript = sigLenInBinary + signature + hashCode1 + \ - pubkeyLenInBinary + pubkey - - ############################# - # Finally, our tx is complete! - return newTx - - - -################################################################################ -################################################################################ -# -# SelectCoins algorithms -# -# The following methods define multiple ways that one could select coins -# for a given transaction. However, the "best" solution is extremely -# dependent on the variety of unspent outputs, and also the preferences -# of the user. Things to take into account when selecting coins: -# -# - Number of inputs: If we have a lot of inputs in this transaction -# from different addresses, then all those addresses -# have now been linked together. We want to use -# as few outputs as possible -# -# - Tx Fess/Size: The bigger the transaction, in bytes, the more -# fee we're going to have to pay to the miners -# -# - Priority: Low-priority transactions might require higher -# fees and/or take longer to make it into the -# blockchain. Priority is the sum of TxOut -# priorities: (NumConfirm * NumBTC / SizeKB) -# We especially want to avoid 0-confirmation txs -# -# - Output values: In almost every transaction, we must return -# change to ourselves. This means there will -# be two outputs, one to the recipient, one to -# us. We prefer that both outputs be about the -# same size, so that it's not clear which is the -# recipient, which is the change. But we don't -# want to use too many inputs to do this. -# -# - Sustainability: We should pick a strategy that tends to leave our -# wallet containing a variety of TxOuts that are -# well-suited for future transactions to benefit. -# For instance, always favoring the single TxOut -# with a value close to the target, will result -# in a future wallet full of tiny TxOuts. This -# guarantees that in the future, we're going to -# have to do 10+ inputs for a single Tx. -# -# -# The strategy is to execute a half dozen different types of SelectCoins -# algorithms, each with a different goal in mind. Then we examine each -# of the results and evaluate a "select-score." Use the one with the -# best score. In the future, we could make the scoring algorithm based -# on user preferences. We expect that depending on what the availble -# list looks like, some of these algorithms could produce perfect results, -# and in other instances *terrible* results. -# -################################################################################ -################################################################################ - -################################################################################ -# These would normally be defined by C++ and fed in, but I've recreated -# the C++ class here... it's really just a container, anyway -# -# TODO: LevelDB upgrade: had to upgrade this class to use arbitrary -# ScrAddress "notation", even though everything else on the python -# side expects pure hash160 values. For now, it looks like it can -# handle arbitrary scripts, but the CheckHash160() calls will -# (correctly) throw errors if you don't. We can upgrade this in -# the future. -class PyUnspentTxOut(object): - def __init__(self, scrAddr='', val=-1, numConf=-1): - pass - #self.scrAddr = scrAddr - #self.val = long(val*ONE_BTC) - #self.conf = numConf - def createFromCppUtxo(self, cppUtxo): - self.scrAddr = cppUtxo.getRecipientScrAddr() - self.val = cppUtxo.getValue() - self.conf = cppUtxo.getNumConfirm() - # For now, this will throw errors unless we always use hash160 scraddrs - self.binScript = '\x76\xa9\x14' + CheckHash160(self.scrAddr) + '\x88\xac' - self.txHash = cppUtxo.getTxHash() - self.txOutIndex = cppUtxo.getTxOutIndex() - return self - def getTxHash(self): - return self.txHash - def getTxOutIndex(self): - return self.txOutIndex - def getValue(self): - return self.val - def getNumConfirm(self): - return self.conf - def getScript(self): - return self.binScript - def getRecipientScrAddr(self): - return self.scrAddr - def getRecipientHash160(self): - return CheckHash160(self.scrAddr) - def prettyStr(self, indent=''): - pstr = [indent] - pstr.append(binary_to_hex(self.scrAddr[:8])) - pstr.append(coin2str(self.val)) - pstr.append(str(self.conf).rjust(8,' ')) - return ' '.join(pstr) - def pprint(self, indent=''): - print self.prettyStr(indent) - - -################################################################################ -def sumTxOutList(txoutList): - return sum([u.getValue() for u in txoutList]) - -################################################################################ -# This is really just for viewing a TxOut list -- usually for debugging -def pprintUnspentTxOutList(utxoList, headerLine='Coin Selection: '): - totalSum = sum([u.getValue() for u in utxoList]) - print headerLine, '(Total = %s BTC)' % coin2str(totalSum) - print ' ','Owner Address'.ljust(34), - print ' ','TxOutValue'.rjust(18), - print ' ','NumConf'.rjust(8), - print ' ','PriorityFactor'.rjust(16) - for utxo in utxoList: - a160 = CheckHash160(utxo.getRecipientScrAddr()) - print ' ',hash160_to_addrStr(a160).ljust(34), - print ' ',(coin2str(utxo.getValue()) + ' BTC').rjust(18), - print ' ',str(utxo.getNumConfirm()).rjust(8), - print ' ', ('%0.2f' % (utxo.getValue()*utxo.getNumConfirm()/(ONE_BTC*144.))).rjust(16) - - -################################################################################ -# Sorting currently implemented in C++, but we implement a different kind, here -def PySortCoins(unspentTxOutInfo, sortMethod=1): - """ - Here we define a few different ways to sort a list of unspent TxOut objects. - Most of them are simple, some are more complex. In particular, the last - method (4) tries to be intelligent, by grouping together inputs from the - same address. - - The goal is not to do the heavy lifting for SelectCoins... we simply need - a few different ways to sort coins so that the SelectCoins algorithms has - a variety of different inputs to play with. Each sorting method is useful - for some types of unspent-TxOut lists, so as long as we have one good - sort, the PyEvalCoinSelect method will pick it out. - - As a precaution we send all the zero-confirmation UTXO's to the back - of the list, so that they will only be used if absolutely necessary. - """ - zeroConfirm = [] - - if sortMethod==0: - priorityFn = lambda a: a.getValue() * a.getNumConfirm() - return sorted(unspentTxOutInfo, key=priorityFn, reverse=True) - if sortMethod==1: - priorityFn = lambda a: (a.getValue() * a.getNumConfirm())**(1/3.) - return sorted(unspentTxOutInfo, key=priorityFn, reverse=True) - if sortMethod==2: - priorityFn = lambda a: (math.log(a.getValue()*a.getNumConfirm()+1)+4)**4 - return sorted(unspentTxOutInfo, key=priorityFn, reverse=True) - if sortMethod==3: - priorityFn = lambda a: a.getValue() if a.getNumConfirm()>0 else 0 - return sorted(unspentTxOutInfo, key=priorityFn, reverse=True) - if sortMethod==4: - addrMap = {} - zeroConfirm = [] - for utxo in unspentTxOutInfo: - if utxo.getNumConfirm() == 0: - zeroConfirm.append(utxo) - else: - addr = TxOutScriptExtractAddr160(utxo.getScript()) - if not addrMap.has_key(addr): - addrMap[addr] = [utxo] - else: - addrMap[addr].append(utxo) - - priorityUTXO = (lambda a: (a.getNumConfirm()*a.getValue()**0.333)) - for addr,txoutList in addrMap.iteritems(): - txoutList.sort(key=priorityUTXO, reverse=True) - - priorityGrp = lambda a: max([priorityUTXO(utxo) for utxo in a]) - finalSortedList = [] - for utxo in sorted(addrMap.values(), key=priorityGrp, reverse=True): - finalSortedList.extend(utxo) - - finalSortedList.extend(zeroConfirm) - return finalSortedList - if sortMethod in (5, 6, 7): - utxoSorted = PySortCoins(unspentTxOutInfo, 1) - # Rotate the top 1,2 or 3 elements to the bottom of the list - for i in range(sortMethod-4): - utxoSorted.append(utxoSorted[0]) - del utxoSorted[0] - return utxoSorted - - # TODO: Add a semi-random sort method: it will favor putting high-priority - # outputs at the front of the list, but will not be deterministic - # This should give us some high-fitness variation compared to sorting - # uniformly - if sortMethod==8: - utxosNoZC = filter(lambda a: a.getNumConfirm()!=0, unspentTxOutInfo) - random.shuffle(utxosNoZC) - utxosNoZC.extend(filter(lambda a: a.getNumConfirm()==0, unspentTxOutInfo)) - return utxosNoZC - if sortMethod==9: - utxoSorted = PySortCoins(unspentTxOutInfo, 1) - sz = len(filter(lambda a: a.getNumConfirm()!=0, utxoSorted)) - # swap 1/3 of the values at random - topsz = int(min(max(round(sz/3), 5), sz)) - for i in range(topsz): - pick1 = int(random.uniform(0,topsz)) - pick2 = int(random.uniform(0,sz-topsz)) - utxoSorted[pick1], utxoSorted[pick2] = utxoSorted[pick2], utxoSorted[pick1] - return utxoSorted - - - - -################################################################################ -# Now we try half a dozen different selection algorithms -################################################################################ - - - -################################################################################ -def PySelectCoins_SingleInput_SingleValue( \ - unspentTxOutInfo, targetOutVal, minFee=0): - """ - This method should usually be called with a small number added to target val - so that a tx can be constructed that has room for user to add some extra fee - if necessary. - - However, we must also try calling it with the exact value, in case the user - is trying to spend exactly their remaining balance. - """ - target = targetOutVal + minFee - bestMatchVal = 2**64 - bestMatchUtxo = None - for utxo in unspentTxOutInfo: - if target <= utxo.getValue() < bestMatchVal: - bestMatchVal = utxo.getValue() - bestMatchUtxo = utxo - - closeness = bestMatchVal - target - if 0 < closeness <= CENT: - # If we're going to have a change output, make sure it's above CENT - # to avoid a mandatory fee - try2Val = 2**64 - try2Utxo = None - for utxo in unspentTxOutInfo: - if target+CENT < utxo.getValue() < try2Val: - try2Val = utxo.getValue() - try2Val = utxo - if not try2Utxo==None: - bestMatchUtxo = try2Utxo - - - if bestMatchUtxo==None: - return [] - else: - return [bestMatchUtxo] - -################################################################################ -def PySelectCoins_MultiInput_SingleValue( \ - unspentTxOutInfo, targetOutVal, minFee=0): - """ - This method should usually be called with a small number added to target val - so that a tx can be constructed that has room for user to add some extra fee - if necessary. - - However, we must also try calling it with the exact value, in case the user - is trying to spend exactly their remaining balance. - """ - target = targetOutVal + minFee - outList = [] - sumVal = 0 - for utxo in unspentTxOutInfo: - sumVal += utxo.getValue() - outList.append(utxo) - if sumVal>=target: - break - - return outList - - - -################################################################################ -def PySelectCoins_SingleInput_DoubleValue( \ - unspentTxOutInfo, targetOutVal, minFee=0): - """ - We will look for a single input that is within 30% of the target - In case the tx value is tiny rel to the fee: the minTarget calc - may fail to exceed the actual tx size needed, so we add an extra - - We restrain the search to 25%. If there is no one output in this - range, then we will return nothing, and the SingleInput_SingleValue - method might return a usable result - """ - idealTarget = 2*targetOutVal + minFee - - # check to make sure we're accumulating enough - minTarget = long(0.75 * idealTarget) - minTarget = max(minTarget, targetOutVal+minFee) - maxTarget = long(1.25 * idealTarget) - - if sum([u.getValue() for u in unspentTxOutInfo]) < minTarget: - return [] - - bestMatch = 2**64-1 - bestUTXO = None - for txout in unspentTxOutInfo: - if minTarget <= txout.getValue() <= maxTarget: - if abs(txout.getValue()-idealTarget) < bestMatch: - bestMatch = abs(txout.getValue()-idealTarget) - bestUTXO = txout - - if bestUTXO==None: - return [] - else: - return [bestUTXO] - -################################################################################ -def PySelectCoins_MultiInput_DoubleValue( \ - unspentTxOutInfo, targetOutVal, minFee=0): - - idealTarget = 2.0 * targetOutVal - minTarget = long(0.80 * idealTarget) - minTarget = max(minTarget, targetOutVal+minFee) - if sum([u.getValue() for u in unspentTxOutInfo]) < minTarget: - return [] - - outList = [] - lastDiff = 2**64-1 - sumVal = 0 - for utxo in unspentTxOutInfo: - sumVal += utxo.getValue() - outList.append(utxo) - currDiff = abs(sumVal - idealTarget) - # should switch from decreasing to increasing when best match - if sumVal>=minTarget and currDiff>lastDiff: - del outList[-1] - break - lastDiff = currDiff - - return outList - - - - -################################################################################ -def getSelectCoinsScores(utxoSelectList, targetOutVal, minFee): - """ - Define a metric for scoring the output of SelectCoints. The output of - this method is a tuple of scores which identify a few different factors - of a txOut selection that users might care about in a selectCoins algorithm. - - This method only returns an absolute score, usually between 0 and 1 for - each factor. It is up to the person calling this method to decide how - much "weight" they want to give each one. You could even use the scores - as multiplicative factors if you wanted, though they were designed with - the following equation in mind: finalScore = sum(WEIGHT[i] * SCORE[i]) - - TODO: I need to recalibrate some of these factors, and modify them to - represent more directly what the user would be concerned about -- - such as PayFeeFactor, AnonymityFactor, etc. The information is - indirectly available with the current set of factors here - """ - - # Need to calculate how much the change will be returned to sender on this tx - totalIn = sum([utxo.getValue() for utxo in utxoSelectList]) - totalChange = totalIn - (targetOutVal+minFee) - - # Abort if this is an empty list (negative score) or not enough coins - if len(utxoSelectList)==0 or totalIn 0) - # - # On the other hand, if we have 1.832 and 10.00, and the 10.000 is the - # change, we don't really care that they're not close, it's still - # damned good/deceptive output anonymity (so: only execute - # the following block if outAnonFactor <= 1) - if 0 < outAnonFactor <= 1 and not totalChange==0: - outValDiff = abs(totalChange - targetOutVal) - diffPct = (outValDiff / max(totalChange, targetOutVal)) - if diffPct < 0.20: - outAnonFactor *= 1 - elif diffPct < 0.50: - outAnonFactor *= 0.7 - elif diffPct < 1.0: - outAnonFactor *= 0.3 - else: - outAnonFactor = 0 - - - ################## - # Tx size: we don't have signatures yet, but we assume that each txin is - # about 180 Bytes, TxOuts are 35, and 10 other bytes in the Tx - numBytes = 10 - numBytes += 180 * len(utxoSelectList) - numBytes += 35 * (1 if totalChange==0 else 2) - txSizeFactor = 0 - numKb = int(numBytes / 1000) - # Will compute size factor after we see this tx priority and AllowFree - # results. If the tx qualifies for free, we don't need to penalize - # a 3 kB transaction vs one that is 0.5 kB - - - ################## - # Priority: If our priority is above the 1-btc-after-1-day threshold - # then we might be allowed a free tx. But, if its priority - # isn't much above this thresh, it might take a couple blocks - # to be included - dPriority = 0 - anyZeroConfirm = False - for utxo in utxoSelectList: - if utxo.getNumConfirm() == 0: - anyZeroConfirm = True - else: - dPriority += utxo.getValue() * utxo.getNumConfirm() - - dPriority = dPriority / numBytes - priorityThresh = ONE_BTC * 144 / 250 - if dPriority < priorityThresh: - priorityFactor = 0 - elif dPriority < 10.0*priorityThresh: - priorityFactor = 0.7 - elif dPriority < 100.0*priorityThresh: - priorityFactor = 0.9 - else: - priorityFactor = 1.0 - - - ################## - # AllowFree: If three conditions are met, then the tx can be sent safely - # without a tx fee. Granted, it may not be included in the - # current block if the free space is full, but definitely in - # the next one - isFreeAllowed = 0 - haveDustOutputs = (0= priorityThresh and \ - numBytes <= 10000): - isFreeAllowed = 1 - - - ################## - # Finish size-factor calculation -- if free is allowed, kB is irrelevant - txSizeFactor = 0 - if isFreeAllowed or numKb<1: - txSizeFactor = 1 - else: - if numKb < 2: - txSizeFactor=0.2 - elif numKb<3: - txSizeFactor=0.1 - elif numKb<4: - txSizeFactor=0 - else: - txSizeFactor=-1 #if this is huge, actually subtract score - - return (isFreeAllowed, noZeroConf, priorityFactor, numAddrFactor, txSizeFactor, outAnonFactor) - - -################################################################################ -# We define default preferences for weightings. Weightings are used to -# determine the "priorities" for ranking various SelectCoins results -# By setting the weights to different orders of magnitude, you are essentially -# defining a sort-order: order by FactorA, then sub-order by FactorB... -################################################################################ -# TODO: ADJUST WEIGHTING! -IDX_ALLOWFREE = 0 -IDX_NOZEROCONF = 1 -IDX_PRIORITY = 2 -IDX_NUMADDR = 3 -IDX_TXSIZE = 4 -IDX_OUTANONYM = 5 -WEIGHTS = [None]*6 -WEIGHTS[IDX_ALLOWFREE] = 100000 -WEIGHTS[IDX_NOZEROCONF] = 1000000 # let's avoid zero-conf if possible -WEIGHTS[IDX_PRIORITY] = 50 -WEIGHTS[IDX_NUMADDR] = 100000 -WEIGHTS[IDX_TXSIZE] = 100 -WEIGHTS[IDX_OUTANONYM] = 30 - - -################################################################################ -def PyEvalCoinSelect(utxoSelectList, targetOutVal, minFee, weights=WEIGHTS): - """ - Use a specified set of weightings and sub-scores for a unspentTxOut list, - to assign an absolute "fitness" of this particular selection. The goal of - getSelectCoinsScores() is to produce weighting-agnostic subscores -- then - this method applies the weightings to these scores to get a final answer. - - If list A has a higher score than list B, then it's a better selection for - that transaction. If you the two scores don't look right to you, then you - probably just need to adjust the weightings to your liking. - - These weightings may become user-configurable in the future -- likely as an - option of coin-selection profiles -- such as "max anonymity", "min fee", - "balanced", etc). - """ - scores = getSelectCoinsScores(utxoSelectList, targetOutVal, minFee) - if scores==-1: - return -1 - - # Combine all the scores - theScore = 0 - theScore += weights[IDX_NOZEROCONF] * scores[IDX_NOZEROCONF] - theScore += weights[IDX_PRIORITY] * scores[IDX_PRIORITY] - theScore += weights[IDX_NUMADDR] * scores[IDX_NUMADDR] - theScore += weights[IDX_TXSIZE] * scores[IDX_TXSIZE] - theScore += weights[IDX_OUTANONYM] * scores[IDX_OUTANONYM] - - # If we're already paying a fee, why bother including this weight? - if minFee < 0.0005: - theScore += weights[IDX_ALLOWFREE] * scores[IDX_ALLOWFREE] - - return theScore - - -################################################################################ -def PySelectCoins(unspentTxOutInfo, targetOutVal, minFee=0, numRand=10, margin=CENT): - """ - Intense algorithm for coin selection: computes about 30 different ways to - select coins based on the desired target output and the min tx fee. Then - ranks the various solutions and picks the best one - """ - - TimerStart('PySelectCoins') - - if sum([u.getValue() for u in unspentTxOutInfo]) < targetOutVal: - return [] - - targExact = targetOutVal - targMargin = targetOutVal+margin - - selectLists = [] - - # Start with the intelligent solutions with different sortings - for sortMethod in range(8): - diffSortList = PySortCoins(unspentTxOutInfo, sortMethod) - selectLists.append(PySelectCoins_SingleInput_SingleValue( diffSortList, targExact, minFee )) - selectLists.append(PySelectCoins_MultiInput_SingleValue( diffSortList, targExact, minFee )) - selectLists.append(PySelectCoins_SingleInput_SingleValue( diffSortList, targMargin, minFee )) - selectLists.append(PySelectCoins_MultiInput_SingleValue( diffSortList, targMargin, minFee )) - selectLists.append(PySelectCoins_SingleInput_DoubleValue( diffSortList, targExact, minFee )) - selectLists.append(PySelectCoins_MultiInput_DoubleValue( diffSortList, targExact, minFee )) - selectLists.append(PySelectCoins_SingleInput_DoubleValue( diffSortList, targMargin, minFee )) - selectLists.append(PySelectCoins_MultiInput_DoubleValue( diffSortList, targMargin, minFee )) - - # Throw in a couple random solutions, maybe we get lucky - # But first, make a copy before in-place shuffling - # NOTE: using list[:] like below, really causes a swig::vector to freak out! - #utxos = unspentTxOutInfo[:] - #utxos = list(unspentTxOutInfo) - for method in range(8,10): - for i in range(numRand): - utxos = PySortCoins(unspentTxOutInfo, method) - selectLists.append(PySelectCoins_MultiInput_SingleValue(utxos, targExact, minFee)) - selectLists.append(PySelectCoins_MultiInput_DoubleValue(utxos, targExact, minFee)) - selectLists.append(PySelectCoins_MultiInput_SingleValue(utxos, targMargin, minFee)) - selectLists.append(PySelectCoins_MultiInput_DoubleValue(utxos, targMargin, minFee)) - - # Now we define PyEvalCoinSelect as our sorting metric, and find the best solution - scoreFunc = lambda ulist: PyEvalCoinSelect(ulist, targetOutVal, minFee) - finalSelection = max(selectLists, key=scoreFunc) - SCORES = getSelectCoinsScores(finalSelection, targetOutVal, minFee) - if len(finalSelection)==0: - return [] - - # If we selected a list that has only one or two inputs, and we have - # other, tiny, unspent outputs from the same addresses, we should - # throw one or two of them in to help clear them out. However, we - # only do so if a plethora of conditions exist: - # - # First, we only consider doing this if the tx has <5 inputs already. - # Also, we skip this process if the current tx doesn't have excessive - # priority already -- we don't want to risk de-prioritizing a tx for - # this purpose. - # - # Next we sort by LOWEST value, because we really benefit from this most - # by clearing out tiny outputs. Along those lines, we don't even do - # unless it has low priority -- don't want to take a high-priority utxo - # and convert it to one that will be low-priority to start. - # - # Finally, we shouldn't do this if a high score was assigned to output - # anonymity: this extra output may cause a tx with good output anonymity - # to no longer possess this property - IDEAL_NUM_INPUTS = 5 - if len(finalSelection) < IDEAL_NUM_INPUTS and \ - SCORES[IDX_OUTANONYM] == 0: - - utxoToHash160 = lambda a: CheckHash160(a.getRecipientScrAddr()) - getPriority = lambda a: a.getValue() * a.getNumConfirm() - getUtxoID = lambda a: a.getTxHash() + int_to_binary(a.getTxOutIndex()) - - alreadyUsedAddr = set( [utxoToHash160(utxo) for utxo in finalSelection] ) - utxoSmallToLarge = sorted(unspentTxOutInfo, key=getPriority) - utxoSmToLgIDs = [getUtxoID(utxo) for utxo in utxoSmallToLarge] - finalSelectIDs = [getUtxoID(utxo) for utxo in finalSelection] - - for other in utxoSmallToLarge: - - # Skip it if it is already selected - if getUtxoID(other) in finalSelectIDs: - continue - - # We only consider UTXOs that won't link any new addresses together - if not utxoToHash160(other) in alreadyUsedAddr: - continue - - # Avoid zero-conf inputs altogether - if other.getNumConfirm() == 0: - continue - - # Don't consider any inputs that are high priority already - if getPriority(other) > ONE_BTC*144: - continue - - finalSelection.append(other) - if len(finalSelection)>=IDEAL_NUM_INPUTS: - break - - TimerStop('PySelectCoins') - - return finalSelection - - -def calcMinSuggestedFees(selectCoinsResult, targetOutVal, preSelectedFee): - """ - Returns two fee options: one for relay, one for include-in-block. - In general, relay fees are required to get your block propagated - (since most nodes are Satoshi clients), but there's no guarantee - it will be included in a block -- though I'm sure there's plenty - of miners out there will include your tx for sub-standard fee. - However, it's virtually guaranteed that a miner will accept a fee - equal to the second return value from this method. - - We have to supply the fee that was used in the selection algorithm, - so that we can figure out how much change there will be. Without - this information, we might accidentally declare a tx to be freeAllow - when it actually is not. - """ - - if len(selectCoinsResult)==0: - return [-1,-1] - - paid = targetOutVal + preSelectedFee - change = sum([u.getValue() for u in selectCoinsResult]) - paid - - # Calc approx tx size - numBytes = 10 - numBytes += 180 * len(selectCoinsResult) - numBytes += 35 * (1 if change==0 else 2) - numKb = int(numBytes / 1000) - - if numKb>10: - return [(1+numKb)*MIN_RELAY_TX_FEE, (1+numKb)*MIN_TX_FEE] - - # Compute raw priority of tx - prioritySum = 0 - for utxo in selectCoinsResult: - prioritySum += utxo.getValue() * utxo.getNumConfirm() - prioritySum = prioritySum / numBytes - - # Any tiny/dust outputs? - haveDustOutputs = (0= ONE_BTC * 144 / 250. and \ - numBytes < 10000): - return [0,0] - - # This cannot be a free transaction. - minFeeMultiplier = (1 + numKb) - - # At the moment this condition never triggers - if minFeeMultiplier<1.0 and haveDustOutputs: - minFeeMultiplier = 1.0 - - - return [minFeeMultiplier * MIN_RELAY_TX_FEE, \ - minFeeMultiplier * MIN_TX_FEE] - - - - - - -################################################################################ -################################################################################ -# This class can be used for both multi-signature tx collection, as well as -# offline wallet signing (you are collecting signatures for a 1-of-1 tx only -# involving yourself). -class PyTxDistProposal(object): - """ - PyTxDistProposal is created from a PyTx object, and represents - an unsigned transaction, that may require the signatures of - multiple parties before being accepted by the network. - - This technique (https://en.bitcoin.it/wiki/BIP_0010) is that - once TxDP is created, the system signing it only needs the - ECDSA private keys and nothing else. This enables the device - providing the signatures to be extremely lightweight, since it - doesn't have to store the blockchain. - - For a given TxDP, we will be storing the following structure - in memory. Use a 3-input tx as an example, with the first - being a 2-of-3 multi-sig transaction (unsigned) - - self.scriptTypes = [TXOUT_SCRIPT_MULTISIG, - TXOUT_SCRIPT_STANDARD, - TXOUT_SCRIPT_STANDARD] - - self.inputValues = [ 2313000000, - 400000000, - 1000000000] - - self.signatures = [ ['', '', ''], - [''], - [''], ] - - self.inAddr20Lists = [ [addr1, addr2, addr3], - [addr4] - [addr5] ] - - # Usually only have public keys on multi-sig TxOuts - self.inPubKeyLists = [ [pubKey1, pubKey2, pubKey3], - [''] - [''] ] - - self.numSigsNeeded = [ 2 - 1 - 1 ] - - self.relevantTxMap = [ prevTx0Hash: prevTx0.serialize(), - prevTx1Hash: prevTx1.serialize(), - prevTx2Hash: prevTx2.serialize() ] - - UPDATE Feb 2012: Before Jan 29, 2012, BIP 0010 used a different technique - for communicating blockchain information to the offline - device. This is no longer the case - - Gregory Maxwell identified a reasonable-enough security - risk with the fact that previous BIP 0010 cannot guarantee - validity of stated input values in a TxDP. This is solved - by adding the supporting transactions to the TxDP, so that - the signing device can get the input values from those - tx and verify the hash matches the OutPoint on the tx - being signed (which *is* part of what's being signed). - The concern was that someone could manipulate your online - computer to misrepresent the inputs, and cause you to - send you entire wallet to tx-fees. Not the most useful - attack (for someone trying to steal your coins), but it is - still a risk that can be avoided by adding some "bloat" to - the TxDP - - - - """ - ############################################################################# - def __init__(self, pytx=None, txMap={}): - self.pytxObj = UNINITIALIZED - self.uniqueB58 = '' - self.scriptTypes = [] - self.signatures = [] - self.txOutScripts = [] - self.inAddr20Lists = [] - self.inPubKeyLists = [] - self.inputValues = [] - self.numSigsNeeded = [] - self.relevantTxMap = {} # needed to support input values of each TxIn - if pytx: - self.createFromPyTx(pytx, txMap) - - ############################################################################# - def createFromPyTx(self, pytx, txMap={}): - sz = len(pytx.inputs) - self.pytxObj = pytx.copy() - self.uniqueB58 = binary_to_base58(hash256(pytx.serialize()))[:8] - self.scriptTypes = [] - self.signatures = [] - self.txOutScripts = [] - self.inAddr20Lists = [] - self.inPubKeyLists = [] - self.inputValues = [] - self.numSigsNeeded = [] - self.relevantTxMap = {} # needed to support input values of each TxIn - - if len(txMap)==0 and not TheBDM.getBDMState()=='BlockchainReady': - # TxDP includes the transactions that supply the inputs to this - # transaction, so the BDM needs to be available to fetch those. - raise BlockchainUnavailableError, ('Must input supporting transactions ' - 'or access to the blockchain, to ' - 'create the TxDP') - for i in range(sz): - # First, make sure that we have the previous Tx data available - # We can't continue without it, since BIP 0010 will now require - # the full tx of outputs being spent - outpt = self.pytxObj.inputs[i].outpoint - txhash = outpt.txHash - txidx = outpt.txOutIndex - pyPrevTx = None - if len(txMap)>0: - # If supplied a txMap, we expect it to have everything we need - if not txMap.has_key(txhash): - raise InvalidHashError, ('Could not find the referenced tx ' - 'in supplied txMap') - pyPrevTx = txMap[txhash].copy() - elif TheBDM.getBDMState()=='BlockchainReady': - cppPrevTx = TheBDM.getTxByHash(txhash) - if not cppPrevTx: - raise InvalidHashError, 'Could not find the referenced tx' - pyPrevTx = PyTx().unserialize(cppPrevTx.serialize()) - else: - raise InvalidHashError, 'No previous-tx data available for TxDP' - self.relevantTxMap[txhash] = pyPrevTx.copy() - - - # Now we have the previous transaction. We need to pull the - # script out of the specific TxOut so we know how it can be - # spent. - script = pyPrevTx.outputs[txidx].binScript - value = pyPrevTx.outputs[txidx].value - scrType = getTxOutScriptType(script) - - self.inputValues.append(value) - self.txOutScripts.append(str(script)) # copy it - self.scriptTypes.append(scrType) - self.inAddr20Lists.append([]) - self.inPubKeyLists.append([]) - self.signatures.append([]) - if scrType in (TXOUT_SCRIPT_STANDARD, TXOUT_SCRIPT_COINBASE): - self.inAddr20Lists[-1].append(TxOutScriptExtractAddr160(script)) - self.inPubKeyLists[-1].append('') - self.signatures[-1].append('') - self.numSigsNeeded.append(1) - elif scrType==TXOUT_SCRIPT_MULTISIG: - mstype, addrs, pubs = getTxOutMultiSigInfo(script) - self.inAddr20Lists[-1] = addrs - self.inPubKeyLists[-1] = pubs - self.signatures[-1] = ['']*len(addrs) - self.numSigsNeeded[-1] = mstype[0] # mstype for M-of-N tx is (M,N) - elif scrType in (TXOUT_SCRIPT_OP_EVAL, TXOUT_SCRIPT_UNKNOWN): - pass - - return self - - - ############################################################################# - def createFromTxOutSelection(self, utxoSelection, recip160ValPairs, txMap={}): - """ - This creates a TxDP for a standard transaction from a list of inputs and - a list of recipient-value-pairs. - - NOTE: I have modified this so that if the "recip" is not a 20-byte binary - string, it is instead interpretted as a SCRIPT -- which could be - anything, including a multi-signature transaction - """ - - pprintUnspentTxOutList(utxoSelection) - #print sumTxOutList(utxoSelection) - #print sum([a[1] for a in recip160ValPairs]) - assert(sumTxOutList(utxoSelection) >= sum([a[1] for a in recip160ValPairs])) - thePyTx = PyTx() - thePyTx.version = 1 - thePyTx.lockTime = 0 - thePyTx.inputs = [] - thePyTx.outputs = [] - - # We can prepare the outputs, first - for recipObj,value in recip160ValPairs: - txout = PyTxOut() - txout.value = long(value) - - # Assume recipObj is either a PBA or a string - if isinstance(recipObj, PyBtcAddress): - recipObj = recipObj.getAddr160() - - # Now recipObj is def a string - if len(recipObj)!=20: - # If not an address, it's a full script - txout.binScript = recipObj - else: - # Construct a std TxOut from addr160 str - txout.binScript = ''.join([ getOpCode('OP_DUP' ), \ - getOpCode('OP_HASH160' ), \ - '\x14', \ - recipObj, - getOpCode('OP_EQUALVERIFY'), \ - getOpCode('OP_CHECKSIG' )]) - thePyTx.outputs.append(txout) - - # Prepare the inputs based on the utxo objects - for iin,utxo in enumerate(utxoSelection): - # First, make sure that we have the previous Tx data available - # We can't continue without it, since BIP 0010 will now require - # the full tx of outputs being spent - txin = PyTxIn() - txin.outpoint = PyOutPoint() - txin.binScript = '' - txin.intSeq = 2**32-1 - - txhash = utxo.getTxHash() - txidx = utxo.getTxOutIndex() - txin.outpoint.txHash = str(txhash) - txin.outpoint.txOutIndex = txidx - thePyTx.inputs.append(txin) - - return self.createFromPyTx(thePyTx, txMap) - - - - ############################################################################# - def appendSignature(self, binSig, txinIndex=None): - """ - Use this to add a signature to the TxDP object in memory. - """ - idx, pos, addr = self.processSignature(binSig, txinIndex, checkAllInputs=True) - if addr: - self.signatures[validIdx].append(binSig) - return True - - return False - - - ############################################################################# - def processSignature(self, sigStr, txinIdx, checkAllInputs=False): - """ - For standard transaction types, the signature field is actually the raw - script to be plugged into the final transaction that allows it to eval - to true -- except for multi-sig transactions. We have to mess with the - data a little bit if we want to use the script-processor to verify the - signature. Instead, we will use the crypto ops directly. - - The return value is everything we need to know about this signature: - -- TxIn-index: if checkAllInputs=True, we need to know which one worked - -- Addr-position: for multi-sig tx, we need to know which addr it matches - -- Addr160: address to which this signature corresponds - """ - - if txinIdx==None or txinIdx<0 or txinIdx>=len(self.pytxObj.inputs): - pass - else: - scriptType = self.scriptTypes[txinIdx] - txCopy = self.pytxObj.copy() - if scriptType in (TXOUT_SCRIPT_STANDARD, TXOUT_SCRIPT_COINBASE): - # For standard Tx types, sigStr is the full script itself (copy it) - txCopy.inputs[txinIdx].binScript = str(sigStr) - prevOutScript = str(self.txOutScripts[txinIdx]) - psp = PyScriptProcessor(prevOutScript, txCopy, txinIdx) - if psp.verifyTransactionValid(): - return txinIdx, 0, TxOutScriptExtractAddr160(prevOutScript) - elif scriptType == TXOUT_SCRIPT_MULTISIG: - # For multi-sig, sigStr is the raw ECDSA sig ... we will have to - # manually construct a tx that the script processor can check, - # without the other signatures - for i in range(len(txCopy.inputs)): - if not i==idx: - txCopy.inputs[i].binScript = '' - else: - txCopy.inputs[i].binScript = self.txOutScripts[i] - - hashCode = binary_to_int(sigStr[-1]) - hashCode4 = int_to_binary(hashcode, widthBytes=4) - preHashMsg = txCopy.serialize() + hashCode4 - if not hashCode==1: - raise NotImplementedError, 'Non-standard hashcodes not supported!' - - # Now check all public keys in the multi-sig TxOut script - for i,pubkey in enumerate(self.inPubKeyLists): - tempAddr = PyBtcAddress().createFromPublicKeyData(pubkey) - if tempAddr.verifyDERSignature(preHashMsg, sigStr): - return txInIdx, i, hash160(pubkey) - - - if checkAllInputs: - for i in range(len(self.pytxObj.inputs)): - idx, pos, addr160 = self.processSignature(sigStr, i) - if idx>0: - return idx, pos, addr160 - - return -1,-1,'' - - - ############################################################################# - def checkTxHasEnoughSignatures(self, alsoVerify=False): - """ - This method only counts signatures, unless verify==True - """ - for i in range(len(self.pytxObj.inputs)): - numSigsHave = sum( [(1 if sig else 0) for sig in self.signatures[i]] ) - if numSigsHave 0: - nextTx = PyTx().unserialize(binUnpacker) - self.relevantTxMap[nextTx.getHash()] = nextTx - - for txin in targetTx.inputs: - if not self.relevantTxMap.has_key(txin.outpoint.txHash): - raise TxdpError, 'Not all inputs can be verified for TxDP. Aborting!' - - self.createFromPyTx( targetTx, self.relevantTxMap ) - numIn = len(self.pytxObj.inputs) - - # Do some sanity checks - if not self.uniqueB58 == dpIdB58: - raise UnserializeError, 'TxDP: Actual DPID does not match listed ID' - if not MAGIC_BYTES==magic: - raise NetworkIDError, 'TxDP is for diff blockchain! (%s)' % \ - BLOCKCHAINS[magic] - - # At this point, we should have a TxDP constructed, now we need to - # simply scan the rest of the serialized structure looking for any - # signatures that may be included - while not 'END-TRANSACTION' in line: - [iin, val] = line.split('_')[2:] - iin = int(iin) - self.inputValues[iin] = str2coin(val) - - line = nextLine(L) - while '_SIG_' in line: - addrB58, sz, sigszHex = line.split('_')[2:] - sz = int(sz) - sigsz = hex_to_int(sigszHex, endIn=BIGENDIAN) - hexSig = '' - line = nextLine(L) - while (not '_SIG_' in line) and \ - (not 'TXINPUT' in line) and \ - (not 'END-TRANSACTION' in line): - hexSig += line - line = nextLine(L) - binSig = hex_to_binary(hexSig) - idx, sigOrder, addr160 = self.processSignature(binSig, iin) - if idx == -1: - LOGWARN('Invalid sig: Input %d, addr=%s' % (iin, addrB58)) - elif not hash160_to_addrStr(addr160)== addrB58: - LOGERROR('Listed addr does not match computed addr') - raise BadAddressError - # If we got here, the signature is valid! - self.signatures[iin][sigOrder] = binSig - - return self - - - - ############################################################################# - def pprint(self, indent=' '): - tx = self.pytxObj - propID = hash256(tx.serialize()) - print indent+'Distribution Proposal : ', binary_to_base58(propID)[:8] - print indent+'Transaction Version : ', tx.version - print indent+'Transaction Lock Time : ', tx.lockTime - print indent+'Num Inputs : ', len(tx.inputs) - for i,txin in enumerate(tx.inputs): - prevHash = txin.outpoint.txHash - prevIndex = txin.outpoint.txOutIndex - #print ' PrevOut: (%s, index=%d)' % (binary_to_hex(prevHash[:8]),prevIndex), - print indent*2 + 'Value: %s' % self.inputValues[i] - print indent*2 + 'SrcScript: %s' % binary_to_hex(self.txOutScripts[i]) - for ns, sig in enumerate(self.signatures[i]): - print indent*2 + 'Sig%d = "%s"'%(ns, binary_to_hex(sig)) - print indent+'Num Outputs : ', len(tx.outputs) - for i,txout in enumerate(tx.outputs): - print ' Recipient: %s BTC' % coin2str(txout.value), - scrType = getTxOutScriptType(txout.binScript) - if scrType in (TXOUT_SCRIPT_STANDARD, TXOUT_SCRIPT_COINBASE): - print hash160_to_addrStr(TxOutScriptExtractAddr160(txout.binScript)) - elif scrType in (TXOUT_SCRIPT_MULTISIG,): - mstype, addrs, pubs = getTxOutMultiSigInfo(txout.binScript) - print 'MULTI-SIG-SCRIPT:%d-of-%d' % mstype - for addr in addrs: - print indent*2, hash160_to_addrStr(addr) - - - -# Random method for creating -def touchFile(fname): - try: - os.utime(fname, None) - except: - f = open(fname, 'a') - f.flush() - os.fsync(f.fileno()) - f.close() - -BLOCKCHAIN_READONLY = 0 -BLOCKCHAIN_READWRITE = 1 -BLOCKCHAIN_DONOTUSE = 2 - -WLT_UPDATE_ADD = 0 -WLT_UPDATE_MODIFY = 1 - -WLT_DATATYPE_KEYDATA = 0 -WLT_DATATYPE_ADDRCOMMENT = 1 -WLT_DATATYPE_TXCOMMENT = 2 -WLT_DATATYPE_OPEVAL = 3 -WLT_DATATYPE_DELETED = 4 - -DEFAULT_COMPUTE_TIME_TARGET = 0.25 -DEFAULT_MAXMEM_LIMIT = 32*1024*1024 - - -############################################################################# -def DeriveChaincodeFromRootKey(sbdPrivKey): - return SecureBinaryData( HMAC256( sbdPrivKey.getHash256(), \ - 'Derive Chaincode from Root Key')) - - -################################################################################ -def HardcodedKeyMaskParams(): - paramMap = {} - - # Nothing up my sleeve! Need some hardcoded random numbers to use for - # encryption IV and salt. Using the first 256 digits of Pi for the - # the IV, and first 256 digits of e for the salt (hashed) - digits_pi = ( \ - 'ARMORY_ENCRYPTION_INITIALIZATION_VECTOR_' - '1415926535897932384626433832795028841971693993751058209749445923' - '0781640628620899862803482534211706798214808651328230664709384460' - '9550582231725359408128481117450284102701938521105559644622948954' - '9303819644288109756659334461284756482337867831652712019091456485') - digits_e = ( \ - 'ARMORY_KEY_DERIVATION_FUNCTION_SALT_' - '7182818284590452353602874713526624977572470936999595749669676277' - '2407663035354759457138217852516642742746639193200305992181741359' - '6629043572900334295260595630738132328627943490763233829880753195' - '2510190115738341879307021540891499348841675092447614606680822648') - - paramMap['IV'] = SecureBinaryData( hash256(digits_pi)[:16] ) - paramMap['SALT'] = SecureBinaryData( hash256(digits_e) ) - paramMap['KDFBYTES'] = long(16*MEGABYTE) - - def hardcodeCreateSecurePrintPassphrase(secret): - if isinstance(secret, basestring): - secret = SecureBinaryData(secret) - bin7 = HMAC512(secret.getHash256(), paramMap['SALT'].toBinStr())[:7] - out,bin7 = SecureBinaryData(binary_to_base58(bin7 + hash256(bin7)[0])), None - return out - - def hardcodeCheckPassphrase(passphrase): - if isinstance(passphrase, basestring): - pwd = base58_to_binary(passphrase) - else: - pwd = base58_to_binary(passphrase.toBinStr()) - - isgood,pwd = (hash256(pwd[:7])[0] == pwd[-1]), None - return isgood - - def hardcodeApplyKdf(secret): - if isinstance(secret, basestring): - secret = SecureBinaryData(secret) - kdf = KdfRomix() - kdf.usePrecomputedKdfParams(paramMap['KDFBYTES'], 1, paramMap['SALT']) - return kdf.DeriveKey(secret) - - def hardcodeMask(secret, passphrase=None, ekey=None): - if not ekey: - ekey = hardcodeApplyKdf(passphrase) - return CryptoAES().EncryptCBC(secret, ekey, paramMap['IV']) - - def hardcodeUnmask(secret, passphrase=None, ekey=None): - if not ekey: - ekey = applyKdf(passphrase) - return CryptoAES().DecryptCBC(secret, ekey, paramMap['IV']) - - paramMap['FUNC_PWD'] = hardcodeCreateSecurePrintPassphrase - paramMap['FUNC_KDF'] = hardcodeApplyKdf - paramMap['FUNC_MASK'] = hardcodeMask - paramMap['FUNC_UNMASK'] = hardcodeUnmask - paramMap['FUNC_CHKPWD'] = hardcodeCheckPassphrase - return paramMap - - -################################################################################ -################################################################################ -class PyBtcWallet(object): - """ - This class encapsulates all the concepts and variables in a "wallet", - and maintains the passphrase protection, key stretching, encryption, - etc, required to maintain the wallet. This class also includes the - file I/O methods for storing and loading wallets. - - ***NOTE: I have ONLY implemented deterministic wallets, using ECDSA - Diffie-Hellman shared-secret crypto operations. This allows - one to actually determine the next PUBLIC KEY in the address - chain without actually having access to the private keys. - This makes it possible to synchronize online-offline computers - once and never again. - - You can import random keys into your wallet, but if it is - encrypted, you will have to supply a passphrase to make sure - it can be encrypted as well. - - Presumably, wallets will be used for one of three purposes: - - (1) Spend money and receive payments - (2) Watching-only wallets - have the private keys, just not on this computer - (3) May be watching *other* people's addrs. There's a variety of reasons - we might want to watch other peoples' addresses, but most them are not - relevant to a "basic" BTC user. Nonetheless it should be supported to - watch money without considering it part of our own assets - - This class is included in the combined-python-cpp module, because we really - need to maintain a persistent Cpp.BtcWallet if this class is to be useful - (we don't want to have to rescan the entire blockchain every time we do any - wallet operations). - - The file format was designed from the outset with lots of unused space to - allow for expansion without having to redefine the file format and break - previous wallets. Luckily, wallet information is cheap, so we don't have - to stress too much about saving space (100,000 addresses should take 15 MB) - - This file is NOT for storing Tx-related information. I want this file to - be the minimal amount of information you need to secure and backup your - entire wallet. Tx information can always be recovered from examining the - blockchain... your private keys cannot be. - - We track version numbers, just in case. We start with 1.0 - - Version 1.0: - --- - fileID -- (8) '\xbaWALLET\x00' for wallet files - version -- (4) getVersionInt(PYBTCWALLET_VERSION) - magic bytes -- (4) defines the blockchain for this wallet (BTC, NMC) - wlt flags -- (8) 64 bits/flags representing info about wallet - binUniqueID -- (6) first 5 bytes of first address in wallet - (rootAddr25Bytes[:5][::-1]), reversed - This is not intended to look like the root addr str - and is reversed to avoid having all wallet IDs start - with the same characters (since the network byte is front) - create date -- (8) unix timestamp of when this wallet was created - (actually, the earliest creation date of any addr - in this wallet -- in the case of importing addr - data). This is used to improve blockchain searching - Short Name -- (32) Null-terminated user-supplied short name for wlt - Long Name -- (256) Null-terminated user-supplied description for wlt - Highest Used-- (8) The chain index of the highest used address - --- - Crypto/KDF -- (512) information identifying the types and parameters - of encryption used to secure wallet, and key - stretching used to secure your passphrase. - Includes salt. (the breakdown of this field will - be described separately) - KeyGenerator-- (237) The base address for a determinstic wallet. - Just a serialized PyBtcAddress object. - --- - UNUSED -- (1024) unused space for future expansion of wallet file - --- - Remainder of file is for key storage and various other things. Each - "entry" will start with a 4-byte code identifying the entry type, then - 20 bytes identifying what address the data is for, and finally then - the subsequent data . So far, I have three types of entries that can - be included: - - \x01 -- Address/Key data (as of PyBtcAddress version 1.0, 237 bytes) - \x02 -- Address comments (variable-width field) - \x03 -- Address comments (variable-width field) - \x04 -- OP_EVAL subscript (when this is enabled, in the future) - - Please see PyBtcAddress for information on how key data is serialized. - Comments (\x02) are var-width, and if a comment is changed to - something longer than the existing one, we'll just blank out the old - one and append a new one to the end of the file. It looks like - - 02000000 01 4f This comment is enabled (01) with 4f characters - - - For file syncing, we protect against corrupted wallets by doing atomic - operations before even telling the user that new data has been added. - We do this by copying the wallet file, and creating a walletUpdateFailed - file. We then modify the original, verify its integrity, and then delete - the walletUpdateFailed file. Then we create a backupUpdateFailed flag, - do the identical update on the backup file, and delete the failed flag. - This guaranatees that no matter which nanosecond the power goes out, - there will be an uncorrupted wallet and we know which one it is. - - We never let the user see any data until the atomic write-to-file operation - has completed - - - Additionally, we implement key locking and unlocking, with timeout. These - key locking features are only DEFINED here, not actually enforced (because - this is a library, not an application). You can set the default/temporary - time that the KDF key is maintained in memory after the passphrase is - entered, and this class will keep track of when the wallet should be next - locked. It is up to the application to check whether the current time - exceeds the lock time. This will probably be done in a kind of heartbeat - method, which checks every few seconds for all sorts of things -- including - wallet locking. - """ - - ############################################################################# - def __init__(self): - self.fileTypeStr = '\xbaWALLET\x00' - self.magicBytes = MAGIC_BYTES - self.version = PYBTCWALLET_VERSION # (Major, Minor, Minor++, even-more-minor) - self.eofByte = 0 - self.cppWallet = None # Mirror of PyBtcWallet in C++ object - self.cppInfo = {} # Extra info about each address to help sync - self.watchingOnly = False - self.wltCreateDate = 0 - - # Three dictionaries hold all data - self.addrMap = {} # maps 20-byte addresses to PyBtcAddress objects - self.commentsMap = {} # maps 20-byte addresses to user-created comments - self.commentLocs = {} # map comment keys to wallet file locations - self.opevalMap = {} # maps 20-byte addresses to OP_EVAL data (future) - self.labelName = '' - self.labelDescr = '' - self.linearAddr160List = [] - self.chainIndexMap = {} - self.txAddrMap = {} # cache for getting tx-labels based on addr search - if USE_TESTNET: - self.addrPoolSize = 10 # this makes debugging so much easier! - else: - self.addrPoolSize = CLI_OPTIONS.keypool - - # For file sync features - self.walletPath = '' - self.doBlockchainSync = BLOCKCHAIN_READONLY - self.lastSyncBlockNum = 0 - - # Private key encryption details - self.useEncryption = False - self.kdf = None - self.crypto = None - self.kdfKey = None - self.defaultKeyLifetime = 10 # seconds after unlock, that key is discarded - self.lockWalletAtTime = 0 # seconds after unlock, that key is discarded - self.isLocked = False - self.testedComputeTime=None - - # Deterministic wallet, need a root key. Though we can still import keys. - # The unique ID contains the network byte (id[-1]) but is not intended to - # resemble the address of the root key - self.uniqueIDBin = '' - self.uniqueIDB58 = '' # Base58 version of reversed-uniqueIDBin - self.lastComputedChainAddr160 = '' - self.lastComputedChainIndex = 0 - self.highestUsedChainIndex = 0 - - # All PyBtcAddress serializations are exact same size, figure it out now - self.pybtcaddrSize = len(PyBtcAddress().serialize()) - - - # All BDM calls by default go on the multi-thread-queue. But if the BDM - # is the one calling the PyBtcWallet methods, it will deadlock if it uses - # the queue. Therefore, the BDM will set this flag before making any - # calls, which will tell PyBtcWallet to use __direct methods. - self.calledFromBDM = False - - # Finally, a bunch of offsets that tell us where data is stored in the - # file: this can be generated automatically on unpacking (meaning it - # doesn't require manually updating offsets if I change the format), and - # will save us a couple lines of code later, when we need to update things - self.offsetWltFlags = -1 - self.offsetLabelName = -1 - self.offsetLabelDescr = -1 - self.offsetTopUsed = -1 - self.offsetRootAddr = -1 - self.offsetKdfParams = -1 - self.offsetCrypto = -1 - - # These flags are ONLY for unit-testing the walletFileSafeUpdate function - self.interruptTest1 = False - self.interruptTest2 = False - self.interruptTest3 = False - - ############################################################################# - def getWalletVersion(self): - return (getVersionInt(self.version), getVersionString(self.version)) - - ############################################################################# - def getWalletVersion(self): - return (getVersionInt(self.version), getVersionString(self.version)) - - - ############################################################################# - def getWalletPath(self): - return self.walletPath - - ############################################################################# - def getTimeRangeForAddress(self, addr160): - if not self.addrMap.has_key(addr160): - return None - else: - return self.addrMap[addr160].getTimeRange() - - ############################################################################# - def getBlockRangeForAddress(self, addr20): - if not self.addrMap.has_key(addr160): - return None - else: - return self.addrMap[addr160].getBlockRange() - - ############################################################################# - def setBlockchainSyncFlag(self, syncYes=True): - self.doBlockchainSync = syncYes - - ############################################################################# - def syncWithBlockchain(self, startBlk=None): - """ - Will block until getTopBlockHeader() returns, which could be a while. - If you don't want to wait, check TheBDM.getBDMState()=='BlockchainReady' - before calling this method. If you expect the blockchain will have to - be rescanned, then call TheBDM.rescanBlockchain or TheBDM.loadBlockchain - - If this method is called from the BDM itself, calledFromBDM will signal - to use the BDM methods directly, not the queue. This will deadlock - otherwise. - """ - - TimerStart('syncWithBlockchain') - - if TheBDM.getBDMState() in ('Offline', 'Uninitialized'): - LOGWARN('Called syncWithBlockchain but BDM is %s', TheBDM.getBDMState()) - return - - if not self.doBlockchainSync==BLOCKCHAIN_DONOTUSE: - if startBlk==None: - startBlk = self.lastSyncBlockNum + 1 - - # calledFromBDM means that ultimately the BDM itself called this - # method and is blocking waiting for it. So we can't use the - # BDM-thread queue, must call its methods directly - if self.calledFromBDM: - TheBDM.scanBlockchainForTx_bdm_direct(self.cppWallet, startBlk) - self.lastSyncBlockNum = TheBDM.getTopBlockHeight_bdm_direct() - else: - TheBDM.scanBlockchainForTx(self.cppWallet, startBlk, wait=True) - self.lastSyncBlockNum = TheBDM.getTopBlockHeight(wait=True) - else: - LOGERROR('Blockchain-sync requested, but current wallet') - LOGERROR('is set to BLOCKCHAIN_DONOTUSE') - - - TimerStop('syncWithBlockchain') - - - - ############################################################################# - def syncWithBlockchainLite(self, startBlk=None): - """ - This is just like a regular sync, but it won't rescan the whole blockchain - if the wallet is dirty -- if addresses were imported recently, it will - still only scan what the blockchain picked up on the last scan. Use the - non-lite version to allow a full scan. - """ - - TimerStart('syncWithBlockchain') - - if TheBDM.getBDMState() in ('Offline', 'Uninitialized'): - LOGWARN('Called syncWithBlockchainLite but BDM is %s', TheBDM.getBDMState()) - return - - if not self.doBlockchainSync==BLOCKCHAIN_DONOTUSE: - if startBlk==None: - startBlk = self.lastSyncBlockNum + 1 - - # calledFromBDM means that ultimately the BDM itself called this - # method and is blocking waiting for it. So we can't use the - # BDM-thread queue, must call its methods directly - if self.calledFromBDM: - TheBDM.scanRegisteredTxForWallet_bdm_direct(self.cppWallet, startBlk) - self.lastSyncBlockNum = TheBDM.getTopBlockHeight_bdm_direct() - else: - TheBDM.scanRegisteredTxForWallet(self.cppWallet, startBlk, wait=True) - self.lastSyncBlockNum = TheBDM.getTopBlockHeight(wait=True) - else: - LOGERROR('Blockchain-sync requested, but current wallet') - LOGERROR('is set to BLOCKCHAIN_DONOTUSE') - - - TimerStop('syncWithBlockchain') - - - ############################################################################# - def getCommentForAddrBookEntry(self, abe): - comment = self.getComment(abe.getAddr160()) - if len(comment)>0: - return comment - - # SWIG BUG! - # http://sourceforge.net/tracker/?func=detail&atid=101645&aid=3403085&group_id=1645 - # Apparently, using the -threads option when compiling the swig module - # causes the "for i in vector<...>:" mechanic to sometimes throw seg faults! - # For this reason, this method was replaced with the one below: - for regTx in abe.getTxList(): - comment = self.getComment(regTx.getTxHash()) - if len(comment)>0: - return comment - - return '' - - ############################################################################# - def getCommentForTxList(self, a160, txhashList): - comment = self.getComment(a160) - if len(comment)>0: - return comment - - for txHash in txhashList: - comment = self.getComment(txHash) - if len(comment)>0: - return comment - - return '' - - ############################################################################# - def printAddressBook(self): - addrbook = self.cppWallet.createAddressBook() - for abe in addrbook: - print hash160_to_addrStr(abe.getAddr160()), - txlist = abe.getTxList() - print len(txlist) - for rtx in txlist: - print '\t', binary_to_hex(rtx.getTxHash(), BIGENDIAN) - - ############################################################################# - def hasAnyImported(self): - for a160,addr in self.addrMap.iteritems(): - if addr.chainIndex == -2: - return True - return False - - - ############################################################################# - def getBalance(self, balType="Spendable"): - if not TheBDM.getBDMState()=='BlockchainReady' and not self.calledFromBDM: - return -1 - else: - currBlk = TheBDM.getTopBlockHeight(calledFromBDM=self.calledFromBDM) - if balType.lower() in ('spendable','spend'): - return self.cppWallet.getSpendableBalance(currBlk) - elif balType.lower() in ('unconfirmed','unconf'): - return self.cppWallet.getUnconfirmedBalance(currBlk) - elif balType.lower() in ('total','ultimate','unspent','full'): - return self.cppWallet.getFullBalance() - else: - raise TypeError, 'Unknown balance type! "' + balType + '"' - - - ############################################################################# - def getAddrBalance(self, addr160, balType="Spendable", currBlk=UINT32_MAX): - if (not TheBDM.getBDMState()=='BlockchainReady' and not self.calledFromBDM) or \ - not self.hasAddr(addr160): - return -1 - else: - addr = self.cppWallet.getScrAddrObjByKey(Hash160ToScrAddr(addr160)) - if balType.lower() in ('spendable','spend'): - return addr.getSpendableBalance(currBlk) - elif balType.lower() in ('unconfirmed','unconf'): - return addr.getUnconfirmedBalance(currBlk) - elif balType.lower() in ('ultimate','unspent','full'): - return addr.getFullBalance() - else: - raise TypeError, 'Unknown balance type!' - - ############################################################################# - def getTxLedger(self, ledgType='Full'): - """ - Gets the ledger entries for the entire wallet, from C++/SWIG data structs - """ - if not TheBDM.getBDMState()=='BlockchainReady' and not self.calledFromBDM: - return [] - else: - ledgBlkChain = self.cppWallet.getTxLedger() - ledgZeroConf = self.cppWallet.getZeroConfLedger() - if ledgType.lower() in ('full','all','ultimate'): - ledg = [] - ledg.extend(ledgBlkChain) - ledg.extend(ledgZeroConf) - return ledg - elif ledgType.lower() in ('blk', 'blkchain', 'blockchain'): - return ledgBlkChain - elif ledgType.lower() in ('zeroconf', 'zero'): - return ledgZeroConf - else: - raise TypeError, 'Unknown ledger type! "' + ledgType + '"' - - - - - ############################################################################# - def getAddrTxLedger(self, addr160, ledgType='Full'): - """ - Gets the ledger entries for the entire wallet, from C++/SWIG data structs - """ - if (not TheBDM.getBDMState()=='BlockchainReady' and not self.calledFromBDM) or \ - not self.hasAddr(addr160): - return [] - else: - scrAddr = Hash160ToScrAddr(addr160) - ledgBlkChain = self.cppWallet.getScrAddrObjByKey(scrAddr).getTxLedger() - ledgZeroConf = self.cppWallet.getScrAddrObjByKey(scrAddr).getZeroConfLedger() - if ledgType.lower() in ('full','all','ultimate'): - ledg = [] - ledg.extend(ledgBlkChain) - ledg.extend(ledgZeroConf) - return ledg - elif ledgType.lower() in ('blk', 'blkchain', 'blockchain'): - return ledgBlkChain - elif ledgType.lower() in ('zeroconf', 'zero'): - return ledgZeroConf - else: - raise TypeError, 'Unknown balance type! "' + ledgType + '"' - - - ############################################################################# - def getTxOutList(self, txType='Spendable'): - """ Returns UnspentTxOut/C++ objects """ - if TheBDM.getBDMState()=='BlockchainReady' and \ - not self.doBlockchainSync==BLOCKCHAIN_DONOTUSE: - - currBlk = TheBDM.getTopBlockHeight(calledFromBDM=self.calledFromBDM) - self.syncWithBlockchain() - if txType.lower() in ('spend', 'spendable'): - return self.cppWallet.getSpendableTxOutList(currBlk); - elif txType.lower() in ('full', 'all', 'unspent', 'ultimate'): - return self.cppWallet.getFullTxOutList(currBlk); - else: - raise TypeError, 'Unknown balance type! ' + txType - else: - LOGERROR('***Blockchain is not available for accessing wallet-tx data') - return [] - - ############################################################################# - def getAddrTxOutList(self, addr160, txType='Spendable'): - """ Returns UnspentTxOut/C++ objects """ - if TheBDM.getBDMState()=='BlockchainReady' and \ - self.hasAddr(addr160) and \ - not self.doBlockchainSync==BLOCKCHAIN_DONOTUSE: - - currBlk = TheBDM.getTopBlockHeight(calledFromBDM=self.calledFromBDM) - self.syncWithBlockchain() - scrAddrStr = Hash160ToScrAddr(addr160) - cppAddr = self.cppWallet.getScrAddrObjByKey(scrAddrStr) - if txType.lower() in ('spend', 'spendable'): - return cppAddr.getSpendableTxOutList(currBlk); - elif txType.lower() in ('full', 'all', 'unspent', 'ultimate'): - return cppAddr.getFullTxOutList(currBlk); - else: - raise TypeError, 'Unknown TxOutList type! ' + txType - else: - LOGERROR('***Blockchain is not available for accessing wallet-tx data') - return [] - - - ############################################################################# - def getAddrByHash160(self, addr160): - return (None if not self.hasAddr(addr160) else self.addrMap[addr160]) - - ############################################################################# - def hasAddr(self, addrData): - if isinstance(addrData, str): - if len(addrData) == 20: - return self.addrMap.has_key(addrData) - elif isLikelyDataType(addrData)==DATATYPE.Base58: - return self.addrMap.has_key(addrStr_to_hash160(addrData)) - else: - return False - elif isinstance(addrData, PyBtcAddress): - return self.addrMap.has_key(addrData.getAddr160()) - else: - return False - - - ############################################################################# - def setDefaultKeyLifetime(self, newlifetime): - """ Set a new default lifetime for holding the unlock key. Min 2 sec """ - self.defaultKeyLifetime = max(newlifetime, 2) - - ############################################################################# - def checkWalletLockTimeout(self): - if not self.isLocked and self.kdfKey and RightNow()>self.lockWalletAtTime: - self.lock() - if self.kdfKey: - self.kdfKey.destroy() - self.kdfKey = None - - if self.useEncryption: - self.isLocked = True - - - - ############################################################################# - def lockTxOutsOnNewTx(self, pytxObj): - for txin in pytxObj.inputs: - self.cppWallet.lockTxOutSwig(txin.outpoint.txHash, txin.outpoint.txOutIndex) - - - - ############################################################################# - def setDefaultKeyLifetime(self, lifetimeInSec): - """ - This is used to set (in memory only) the default time to keep the encrypt - key in memory after the encryption passphrase has been entered. This is - NOT enforced by PyBtcWallet, but the unlock method will use it to calc a - unix timestamp when the wallet SHOULD be locked, and the external program - can use that to decide when to call the lock method. - """ - self.defaultKeyLifetime = lifetimeInSec - - - ############################################################################# - # THIS WAS CREATED ORIGINALLY TO SUPPORT BITSAFE INTEGRATION INTO ARMORY - # But it's also a good first step into general BIP 32 support - def getChildExtPubFromRoot(self, i): - root = self.addrMap['ROOT'] - ekey = ExtendedKey().CreateFromPublic(root.binPublicKey65, root.chaincode) - newKey = HDWalletCrypto().ChildKeyDeriv(ekey, i) - newKey.setIndex(i) - return newKey - #newAddr = PyBtcAddress().createFromExtendedPublicKey(newKey) - - ############################################################################# - #def createFromExtendedPublicKey(self, ekey): - #pub65 = ekey.getPub() - #chain = ekey.getChain() - #newAddr = self.createFromPublicKeyData(pub65, chain) - #newAddr.chainIndex = newAddr.getIndex() - #return newAddr - - ############################################################################# - #def deriveChildPublicKey(self, i): - #newKey = HDWalletCrypto().ChildKeyDeriv(self.getExtendedPublicKey(), i) - #newAddr = PyBtcAddress().createFromExtendedPublicKey(newKey) - - - ############################################################################# - # THIS WAS CREATED ORIGINALLY TO SUPPORT BITSAFE INTEGRATION INTO ARMORY - # But it's also a good first step into general BIP 32 support - def createWalletFromMasterPubKey(self, masterHex, \ - isActuallyNew=True, \ - doRegisterWithBDM=True): - # This function eats hex inputs, not sure why I chose to do that... - p0 = masterHex.index('4104') + 2 - pubkey = SecureBinaryData(hex_to_binary(masterHex[p0:p0+130])) - c0 = masterHex.index('1220') + 4 - chain = SecureBinaryData(hex_to_binary(masterHex[c0:c0+64])) - - # Create the root address object - rootAddr = PyBtcAddress().createFromPublicKeyData( pubkey ) - rootAddr.markAsRootAddr(chain) - self.addrMap['ROOT'] = rootAddr - - ekey = self.getChildExtPubFromRoot(0) - firstAddr = PyBtcAddress().createFromPublicKeyData(ekey.getPub()) - firstAddr.chaincode = ekey.getChain() - firstAddr.chainIndex = 0 - first160 = firstAddr.getAddr160() - - # Update wallet object with the new data - # NEW IN WALLET VERSION 1.35: unique ID is now based on - # the first chained address: this guarantees that the unique ID - # is based not only on the private key, BUT ALSO THE CHAIN CODE - self.useEncryption = False - self.addrMap[firstAddr.getAddr160()] = firstAddr - self.uniqueIDBin = (ADDRBYTE + firstAddr.getAddr160()[:5])[::-1] - self.uniqueIDB58 = binary_to_base58(self.uniqueIDBin) - self.labelName = 'BitSafe Demo Wallet' - self.labelDescr = 'We\'ll be lucky if this works!' - self.lastComputedChainAddr160 = first160 - self.lastComputedChainIndex = firstAddr.chainIndex - self.highestUsedChainIndex = firstAddr.chainIndex-1 - self.wltCreateDate = long(RightNow()) - self.linearAddr160List = [first160] - self.chainIndexMap[firstAddr.chainIndex] = first160 - self.watchingOnly = True - - # We don't have to worry about atomic file operations when - # creating the wallet: so we just do it naively here. - newWalletFilePath = os.path.join(ARMORY_HOME_DIR, 'bitsafe_demo_%s.wallet' % self.uniqueIDB58) - self.walletPath = newWalletFilePath - if not newWalletFilePath: - shortName = self.labelName .replace(' ','_') - # This was really only needed when we were putting name in filename - #for c in ',?;:\'"?/\\=+-|[]{}<>': - #shortName = shortName.replace(c,'_') - newName = 'armory_%s_.wallet' % self.uniqueIDB58 - self.walletPath = os.path.join(ARMORY_HOME_DIR, newName) - - LOGINFO(' New wallet will be written to: %s', self.walletPath) - newfile = open(self.walletPath, 'wb') - fileData = BinaryPacker() - - # packHeader method writes KDF params and root address - headerBytes = self.packHeader(fileData) - - # We make sure we have byte locations of the two addresses, to start - self.addrMap[first160].walletByteLoc = headerBytes + 21 - - fileData.put(BINARY_CHUNK, '\x00' + first160 + firstAddr.serialize()) - - - # Store the current localtime and blocknumber. Block number is always - # accurate if available, but time may not be exactly right. Whenever - # basing anything on time, please assume that it is up to one day off! - time0,blk0 = getCurrTimeAndBlock() if isActuallyNew else (0,0) - - # Don't forget to sync the C++ wallet object - self.cppWallet = Cpp.BtcWallet() - self.cppWallet.addAddress_5_(rootAddr.getAddr160(), time0,blk0,time0,blk0) - self.cppWallet.addAddress_5_(first160, time0,blk0,time0,blk0) - - # We might be holding the wallet temporarily and not ready to register it - if doRegisterWithBDM: - TheBDM.registerWallet(self.cppWallet, isFresh=isActuallyNew) # new wallet - - newfile.write(fileData.getBinaryString()) - newfile.close() - - walletFileBackup = self.getWalletPath('backup') - shutil.copy(self.walletPath, walletFileBackup) - - - # Let's fill the address pool while we are unlocked - # It will get a lot more expensive if we do it on the next unlock - if doRegisterWithBDM: - self.fillAddressPool(self.addrPoolSize, isActuallyNew=isActuallyNew) - - return self - - - - - ############################################################################# - def createNewWallet(self, newWalletFilePath=None, \ - plainRootKey=None, chaincode=None, \ - withEncrypt=True, IV=None, securePassphrase=None, \ - kdfTargSec=DEFAULT_COMPUTE_TIME_TARGET, \ - kdfMaxMem=DEFAULT_MAXMEM_LIMIT, \ - shortLabel='', longLabel='', isActuallyNew=True, \ - doRegisterWithBDM=True, skipBackupFile=False): - """ - This method will create a new wallet, using as much customizability - as you want. You can enable encryption, and set the target params - of the key-derivation function (compute-time and max memory usage). - The KDF parameters will be experimentally determined to be as hard - as possible for your computer within the specified time target - (default, 0.25s). It will aim for maximizing memory usage and using - only 1 or 2 iterations of it, but this can be changed by scaling - down the kdfMaxMem parameter (default 32 MB). - - If you use encryption, don't forget to supply a 32-byte passphrase, - created via SecureBinaryData(pythonStr). This method will apply - the passphrase so that the wallet is "born" encrypted. - - The field plainRootKey could be used to recover a written backup - of a wallet, since all addresses are deterministically computed - from the root address. This obviously won't reocver any imported - keys, but does mean that you can recover your ENTIRE WALLET from - only those 32 plaintext bytes AND the 32-byte chaincode. - - We skip the atomic file operations since we don't even have - a wallet file yet to safely update. - - DO NOT CALL THIS FROM BDM METHOD. IT MAY DEADLOCK. - """ - - - if self.calledFromBDM: - LOGERROR('Called createNewWallet() from BDM method!') - LOGERROR('Don\'t do this!') - return None - - if securePassphrase: - securePassphrase = SecureBinaryData(securePassphrase) - if plainRootKey: - plainRootKey = SecureBinaryData(plainRootKey) - if chaincode: - chaincode = SecureBinaryData(chaincode) - - if withEncrypt and not securePassphrase: - raise EncryptionError, 'Cannot create encrypted wallet without passphrase' - - LOGINFO('***Creating new deterministic wallet') - - # Set up the KDF - if not withEncrypt: - self.kdfKey = None - else: - LOGINFO('(with encryption)') - self.kdf = KdfRomix() - LOGINFO('Target (time,RAM)=(%0.3f,%d)', kdfTargSec, kdfMaxMem) - (mem,niter,salt) = self.computeSystemSpecificKdfParams( \ - kdfTargSec, kdfMaxMem) - self.kdf.usePrecomputedKdfParams(mem, niter, salt) - self.kdfKey = self.kdf.DeriveKey(securePassphrase) - - if not plainRootKey: - # TODO: We should find a source for injecting extra entropy - # At least, Crypto++ grabs from a few different sources, itself - plainRootKey = SecureBinaryData().GenerateRandom(32) - - if not chaincode: - #chaincode = SecureBinaryData().GenerateRandom(32) - # For wallet 1.35a, derive chaincode deterministically from root key - # The root key already has 256 bits of entropy which is excessive, - # anyway. And my original reason for having the chaincode random is - # no longer valid. - chaincode = DeriveChaincodeFromRootKey(plainRootKey) - - - - # Create the root address object - rootAddr = PyBtcAddress().createFromPlainKeyData( \ - plainRootKey, \ - IV16=IV, \ - willBeEncr=withEncrypt, \ - generateIVIfNecessary=True) - rootAddr.markAsRootAddr(chaincode) - - # This does nothing if no encryption - rootAddr.lock(self.kdfKey) - rootAddr.unlock(self.kdfKey) - - firstAddr = rootAddr.extendAddressChain(self.kdfKey) - first160 = firstAddr.getAddr160() - - # Update wallet object with the new data - # NEW IN WALLET VERSION 1.35: unique ID is now based on - # the first chained address: this guarantees that the unique ID - # is based not only on the private key, BUT ALSO THE CHAIN CODE - self.useEncryption = withEncrypt - self.addrMap['ROOT'] = rootAddr - self.addrMap[firstAddr.getAddr160()] = firstAddr - self.uniqueIDBin = (ADDRBYTE + firstAddr.getAddr160()[:5])[::-1] - self.uniqueIDB58 = binary_to_base58(self.uniqueIDBin) - self.labelName = shortLabel[:32] - self.labelDescr = longLabel[:256] - self.lastComputedChainAddr160 = first160 - self.lastComputedChainIndex = firstAddr.chainIndex - self.highestUsedChainIndex = firstAddr.chainIndex-1 - self.wltCreateDate = long(RightNow()) - self.linearAddr160List = [first160] - self.chainIndexMap[firstAddr.chainIndex] = first160 - - # We don't have to worry about atomic file operations when - # creating the wallet: so we just do it naively here. - self.walletPath = newWalletFilePath - if not newWalletFilePath: - shortName = self.labelName .replace(' ','_') - # This was really only needed when we were putting name in filename - #for c in ',?;:\'"?/\\=+-|[]{}<>': - #shortName = shortName.replace(c,'_') - newName = 'armory_%s_.wallet' % self.uniqueIDB58 - self.walletPath = os.path.join(ARMORY_HOME_DIR, newName) - - LOGINFO(' New wallet will be written to: %s', self.walletPath) - newfile = open(self.walletPath, 'wb') - fileData = BinaryPacker() - - # packHeader method writes KDF params and root address - headerBytes = self.packHeader(fileData) - - # We make sure we have byte locations of the two addresses, to start - self.addrMap[first160].walletByteLoc = headerBytes + 21 - - fileData.put(BINARY_CHUNK, '\x00' + first160 + firstAddr.serialize()) - - - # Store the current localtime and blocknumber. Block number is always - # accurate if available, but time may not be exactly right. Whenever - # basing anything on time, please assume that it is up to one day off! - time0,blk0 = getCurrTimeAndBlock() if isActuallyNew else (0,0) - - # Don't forget to sync the C++ wallet object - self.cppWallet = Cpp.BtcWallet() - self.cppWallet.addScrAddress_5_(Hash160ToScrAddr(rootAddr.getAddr160()), \ - time0,blk0,time0,blk0) - self.cppWallet.addScrAddress_5_(Hash160ToScrAddr(first160), \ - time0,blk0,time0,blk0) - - # We might be holding the wallet temporarily and not ready to register it - if doRegisterWithBDM: - TheBDM.registerWallet(self.cppWallet, isFresh=isActuallyNew) # new wallet - - - newfile.write(fileData.getBinaryString()) - newfile.close() - - if not skipBackupFile: - walletFileBackup = self.getWalletPath('backup') - shutil.copy(self.walletPath, walletFileBackup) - - # Lock/unlock to make sure encrypted keys are computed and written to file - if self.useEncryption: - self.unlock(secureKdfOutput=self.kdfKey) - - # Let's fill the address pool while we are unlocked - # It will get a lot more expensive if we do it on the next unlock - if doRegisterWithBDM: - self.fillAddressPool(self.addrPoolSize, isActuallyNew=isActuallyNew) - - if self.useEncryption: - self.lock() - return self - - - - - - ############################################################################# - def advanceHighestIndex(self, ct=1): - topIndex = self.highestUsedChainIndex + ct - topIndex = min(topIndex, self.lastComputedChainIndex) - topIndex = max(topIndex, 0) - - self.highestUsedChainIndex = topIndex - self.walletFileSafeUpdate( [[WLT_UPDATE_MODIFY, self.offsetTopUsed, \ - int_to_binary(self.highestUsedChainIndex, widthBytes=8)]]) - self.fillAddressPool() - - ############################################################################# - def rewindHighestIndex(self, ct=1): - self.advanceHighestIndex(-ct) - - - ############################################################################# - def peekNextUnusedAddr160(self): - try: - return self.getAddress160ByChainIndex(self.highestUsedChainIndex+1) - except: - # Not sure why we'd fail, maybe addrPoolSize==0? - return '' - - ############################################################################# - def getNextUnusedAddress(self): - if self.lastComputedChainIndex - self.highestUsedChainIndex < \ - max(self.addrPoolSize-1,1): - self.fillAddressPool(self.addrPoolSize) - - self.advanceHighestIndex(1) - new160 = self.getAddress160ByChainIndex(self.highestUsedChainIndex) - self.addrMap[new160].touch() - self.walletFileSafeUpdate( [[WLT_UPDATE_MODIFY, \ - self.addrMap[new160].walletByteLoc, \ - self.addrMap[new160].serialize()]] ) - return self.addrMap[new160] - - - ############################################################################# - def computeNextAddress(self, addr160=None, isActuallyNew=True, doRegister=True): - """ - Use this to extend the chain beyond the last-computed address. - - We will usually be computing the next address from the tip of the - chain, but I suppose someone messing with the file format may - leave gaps in the chain requiring some to be generated in the middle - (then we can use the addr160 arg to specify which address to extend) - """ - if not addr160: - addr160 = self.lastComputedChainAddr160 - - newAddr = self.addrMap[addr160].extendAddressChain(self.kdfKey) - new160 = newAddr.getAddr160() - newDataLoc = self.walletFileSafeUpdate( \ - [[WLT_UPDATE_ADD, WLT_DATATYPE_KEYDATA, new160, newAddr]]) - self.addrMap[new160] = newAddr - self.addrMap[new160].walletByteLoc = newDataLoc[0] + 21 - - if newAddr.chainIndex > self.lastComputedChainIndex: - self.lastComputedChainAddr160 = new160 - self.lastComputedChainIndex = newAddr.chainIndex - - self.linearAddr160List.append(new160) - self.chainIndexMap[newAddr.chainIndex] = new160 - - # In the future we will enable first/last seen, but not yet - time0,blk0 = getCurrTimeAndBlock() if isActuallyNew else (0,0) - self.cppWallet.addScrAddress_5_(Hash160ToScrAddr(new160), \ - time0,blk0,time0,blk0) - - # For recovery rescans, this method will be called directly by - # the BDM, which may cause a deadlock if we go through the - # thread queue. The calledFromBDM is "permission" to access the - # BDM private methods directly - if doRegister: - if self.calledFromBDM: - TheBDM.registerScrAddr_bdm_direct(Hash160ToScrAddr(new160), timeInfo=isActuallyNew) - else: - # This uses the thread queue, which means the address will be - # registered next time the BDM is not busy - TheBDM.registerScrAddr(Hash160ToScrAddr(new160), isFresh=isActuallyNew) - - return new160 - - - - - ############################################################################# - def fillAddressPool(self, numPool=None, isActuallyNew=True, doRegister=True): - """ - Usually, when we fill the address pool, we are generating addresses - for the first time, and thus there is no chance it's ever seen the - blockchain. However, this method is also used for recovery/import - of wallets, where the address pool has addresses that probably have - transactions already in the blockchain. - """ - if not numPool: - numPool = self.addrPoolSize - - gap = self.lastComputedChainIndex - self.highestUsedChainIndex - numToCreate = max(numPool - gap, 0) - for i in range(numToCreate): - self.computeNextAddress(isActuallyNew=isActuallyNew, doRegister=doRegister) - return self.lastComputedChainIndex - - ############################################################################# - def setAddrPoolSize(self, newSize): - if newSize<5: - LOGERROR('Will not allow address pool sizes smaller than 5...') - return - - self.addrPoolSize = newSize - self.fillAddressPool(newSize) - - - ############################################################################# - def getHighestUsedIndex(self): - """ - This only retrieves the stored value, but it may not be correct if, - for instance, the wallet was just imported but has been used before. - """ - return self.highestUsedChainIndex - - - ############################################################################# - def getHighestComputedIndex(self): - """ - This only retrieves the stored value, but it may not be correct if, - for instance, the wallet was just imported but has been used before. - """ - return self.lastComputedChainIndex - - - - ############################################################################# - def detectHighestUsedIndex(self, writeResultToWallet=False, fullscan=False): - """ - This method is used to find the highestUsedChainIndex value of the - wallet WITHIN its address pool. It will NOT extend its address pool - in this search, because it is assumed that the wallet couldn't have - used any addresses it had not calculated yet. - - If you have a wallet IMPORT, though, or a wallet that has been used - before but does not have this information stored with it, then you - should be using the next method: - - self.freshImportFindHighestIndex() - - which will actually extend the address pool as necessary to find the - highest address used. - """ - if not TheBDM.getBDMState()=='BlockchainReady' and not self.calledFromBDM: - LOGERROR('Cannot detect any usage information without the blockchain') - return -1 - - oldSync = self.doBlockchainSync - self.doBlockchainSync = BLOCKCHAIN_READONLY - if fullscan: - # Will initiate rescan if wallet is dirty - self.syncWithBlockchain(0) - else: - # Will only use data already scanned, even if wallet is dirty - self.syncWithBlockchainLite(0) - self.doBlockchainSync = oldSync - - highestIndex = max(self.highestUsedChainIndex, 0) - for addr in self.getLinearAddrList(withAddrPool=True): - a160 = addr.getAddr160() - if len(self.getAddrTxLedger(a160)) > 0: - highestIndex = max(highestIndex, addr.chainIndex) - - if writeResultToWallet: - self.highestUsedChainIndex = highestIndex - self.walletFileSafeUpdate( [[WLT_UPDATE_MODIFY, self.offsetTopUsed, \ - int_to_binary(highestIndex, widthBytes=8)]]) - - - return highestIndex - - - - - ############################################################################# - def freshImportFindHighestIndex(self, stepSize=None): - """ - This is much like detectHighestUsedIndex, except this will extend the - address pool as necessary. It assumes that you have a fresh wallet - that has been used before, but was deleted and restored from its root - key and chaincode, and thus we don't know if only 10 or 10,000 addresses - were used. - - If this was an exceptionally active wallet, it's possible that we - may need to manually increase the step size to be sure we find - everything. In fact, there is no way to tell FOR SURE what is the - last addressed used: one must make an assumption that the wallet - never calculated more than X addresses without receiving a payment... - """ - if not stepSize: - stepSize = self.addrPoolSize - - topCompute = 0 - topUsed = 0 - oldPoolSize = self.addrPoolSize - self.addrPoolSize = stepSize - # When we hit the highest address, the topCompute value will extend - # out [stepsize] addresses beyond topUsed, and the topUsed will not - # change, thus escaping the while loop - nWhile = 0 - while topCompute - topUsed < 0.9*stepSize: - topCompute = self.fillAddressPool(stepSize, isActuallyNew=False) - topUsed = self.detectHighestUsedIndex(True) - nWhile += 1 - if nWhile>10000: - raise WalletAddressError, 'Escaping inf loop in freshImport...' - - - self.addrPoolSize = oldPoolSize - return topUsed - - - ############################################################################# - def writeFreshWalletFile(self, path, newName='', newDescr=''): - newFile = open(path, 'wb') - bp = BinaryPacker() - self.packHeader(bp) - newFile.write(bp.getBinaryString()) - - for addr160,addrObj in self.addrMap.iteritems(): - if not addr160=='ROOT': - newFile.write('\x00' + addr160 + addrObj.serialize()) - - for hashVal,comment in self.commentsMap.iteritems(): - twoByteLength = int_to_binary(len(comment), widthBytes=2) - if len(hashVal)==20: - typestr = int_to_binary(WLT_DATATYPE_ADDRCOMMENT) - newFile.write(typestr + hashVal + twoByteLength + comment) - elif len(hashVal)==32: - typestr = int_to_binary(WLT_DATATYPE_TXCOMMENT) - newFile.write(typestr + hashVal + twoByteLength + comment) - - newFile.close() - - - ############################################################################# - def makeUnencryptedWalletCopy(self, newPath, securePassphrase=None): - - self.writeFreshWalletFile(newPath) - if not self.useEncryption: - return True - - if self.isLocked: - if not securePassphrase: - LOGERROR('Attempted to make unencrypted copy without unlocking') - return False - else: - self.unlock(securePassphrase=SecureBinaryData(securePassphrase)) - - newWlt = PyBtcWallet().readWalletFile(newPath) - newWlt.unlock(self.kdfKey) - newWlt.changeWalletEncryption(None) - - - walletFileBackup = newWlt.getWalletPath('backup') - if os.path.exists(walletFileBackup): - LOGINFO('New wallet created, deleting backup file') - os.remove(walletFileBackup) - return True - - - ############################################################################# - def makeEncryptedWalletCopy(self, newPath, securePassphrase=None): - """ - Unlike the previous method, I can't just copy it if it's unencrypted, - because the target device probably shouldn't be exposed to the - unencrypted wallet. So for that case, we will encrypt the wallet - in place, copy, then remove the encryption. - """ - - if self.useEncryption: - # Encrypted->Encrypted: Easy! - self.writeFreshWalletFile(newPath) - return True - - if not securePassphrase: - LOGERROR("Tried to make encrypted copy, but no passphrase supplied") - return False - - # If we're starting unencrypted...encrypt it in place - (mem,nIter,salt) = self.computeSystemSpecificKdfParams(0.25) - self.changeKdfParams(mem, nIter, salt) - self.changeWalletEncryption(securePassphrase=securePassphrase) - - # Write the encrypted wallet to the target directory - self.writeFreshWalletFile(newPath) - - # Unencrypt the wallet now - self.unlock(securePassphrase=securePassphrase) - self.changeWalletEncryption(None) - return True - - - - - - ############################################################################# - def forkOnlineWallet(self, newWalletFile, shortLabel='', longLabel=''): - """ - Make a copy of this wallet that contains no private key data - """ - if not self.addrMap['ROOT'].hasPrivKey(): - LOGWARN('This wallet is already void of any private key data!') - LOGWARN('Aborting wallet fork operation.') - - onlineWallet = PyBtcWallet() - onlineWallet.fileTypeStr = self.fileTypeStr - onlineWallet.version = self.version - onlineWallet.magicBytes = self.magicBytes - onlineWallet.wltCreateDate = self.wltCreateDate - onlineWallet.useEncryption = False - onlineWallet.watchingOnly = True - - if not shortLabel: - shortLabel = self.labelName - if not longLabel: - longLabel = self.labelDescr - - onlineWallet.labelName = (shortLabel + ' (Watch)')[:32] - onlineWallet.labelDescr = (longLabel + ' (Watching-only copy)')[:256] - - newAddrMap = {} - for addr160,addrObj in self.addrMap.iteritems(): - onlineWallet.addrMap[addr160] = addrObj.copy() - onlineWallet.addrMap[addr160].binPrivKey32_Encr = SecureBinaryData() - onlineWallet.addrMap[addr160].binPrivKey32_Plain = SecureBinaryData() - onlineWallet.addrMap[addr160].useEncryption = False - onlineWallet.addrMap[addr160].createPrivKeyNextUnlock = False - - onlineWallet.commentsMap = self.commentsMap - onlineWallet.opevalMap = self.opevalMap - - onlineWallet.uniqueIDBin = self.uniqueIDBin - onlineWallet.highestUsedChainIndex = self.highestUsedChainIndex - onlineWallet.lastComputedChainAddr160 = self.lastComputedChainAddr160 - onlineWallet.lastComputedChainIndex = self.lastComputedChainIndex - - onlineWallet.writeFreshWalletFile(newWalletFile, shortLabel, longLabel) - return onlineWallet - - - ############################################################################# - def supplyRootKeyForWatchingOnlyWallet(self, securePlainRootKey32, \ - permanent=False): - """ - If you have a watching only wallet, you might want to upgrade it to a - full wallet by supplying the 32-byte root private key. Generally, this - will be used to make a 'permanent' upgrade to your wallet, and the new - keys will be written to file ( NOTE: you should setup encryption just - after doing this, to make sure that the plaintext keys get wiped from - your wallet file). - - On the other hand, if you don't want this to be a permanent upgrade, - this could potentially be used to maintain a watching only wallet on your - harddrive, and actually plug in your plaintext root key instead of an - encryption password whenever you want sign transactions. - """ - pass - - - ############################################################################# - def touchAddress(self, addr20): - """ - Use this to update your wallet file to recognize the first/last times - seen for the address. This information will improve blockchain search - speed, if it knows not to search transactions that happened before they - were created. - """ - pass - - ############################################################################# - def testKdfComputeTime(self): - """ - Experimentally determines the compute time required by this computer - to execute with the current key-derivation parameters. This may be - useful for when you transfer a wallet to a new computer that has - different speed/memory characteristic. - """ - testPassphrase = SecureBinaryData('This is a simple passphrase') - start = RightNow() - self.kdf.DeriveKey(testPassphrase) - self.testedComputeTime = (RightNow()-start) - return self.testedComputeTime - - ############################################################################# - def serializeKdfParams(self, kdfObj=None, binWidth=256): - """ - Pack key-derivation function parameters into a binary stream. - As of wallet version 1.0, there is only one KDF technique used - in these wallets, and thus we only need to store the parameters - of this KDF. In the future, we may have multiple KDFs and have - to store the selection in this serialization. - """ - if not kdfObj: - kdfObj = self.kdf - - if not kdfObj: - return '\x00'*binWidth - - binPacker = BinaryPacker() - binPacker.put(UINT64, kdfObj.getMemoryReqtBytes()) - binPacker.put(UINT32, kdfObj.getNumIterations()) - binPacker.put(BINARY_CHUNK, kdfObj.getSalt().toBinStr(), width=32) - - kdfStr = binPacker.getBinaryString() - binPacker.put(BINARY_CHUNK, computeChecksum(kdfStr,4), width=4) - padSize = binWidth - binPacker.getSize() - binPacker.put(BINARY_CHUNK, '\x00'*padSize) - - return binPacker.getBinaryString() - - - - ############################################################################# - def unserializeKdfParams(self, toUnpack, binWidth=256): - - if isinstance(toUnpack, BinaryUnpacker): - binUnpacker = toUnpack - else: - binUnpacker = BinaryUnpacker(toUnpack) - - - - allKdfData = binUnpacker.get(BINARY_CHUNK, 44) - kdfChksum = binUnpacker.get(BINARY_CHUNK, 4) - kdfBytes = len(allKdfData) + len(kdfChksum) - padding = binUnpacker.get(BINARY_CHUNK, binWidth-kdfBytes) - - if allKdfData=='\x00'*44: - return None - - fixedKdfData = verifyChecksum(allKdfData, kdfChksum) - if len(fixedKdfData)==0: - raise UnserializeError, 'Corrupted KDF params, could not fix' - elif not fixedKdfData==allKdfData: - self.walletFileSafeUpdate( \ - [[WLT_UPDATE_MODIFY, self.offsetKdfParams, fixedKdfData]]) - allKdfData = fixedKdfData - LOGWARN('KDF params in wallet were corrupted, but fixed') - - kdfUnpacker = BinaryUnpacker(allKdfData) - mem = kdfUnpacker.get(UINT64) - nIter = kdfUnpacker.get(UINT32) - salt = kdfUnpacker.get(BINARY_CHUNK, 32) - - kdf = KdfRomix(mem, nIter, SecureBinaryData(salt)) - return kdf - - - ############################################################################# - def serializeCryptoParams(self, binWidth=256): - """ - As of wallet version 1.0, all wallets use the exact same encryption types, - so there is nothing to serialize or unserialize. The 256 bytes here may - be used in the future, though. - """ - return '\x00'*binWidth - - ############################################################################# - def unserializeCryptoParams(self, toUnpack, binWidth=256): - """ - As of wallet version 1.0, all wallets use the exact same encryption types, - so there is nothing to serialize or unserialize. The 256 bytes here may - be used in the future, though. - """ - if isinstance(toUnpack, BinaryUnpacker): - binUnpacker = toUnpack - else: - binUnpacker = BinaryUnpacker(toUnpack) - - binUnpacker.get(BINARY_CHUNK, binWidth) - return CryptoAES() - - ############################################################################# - def verifyPassphrase(self, securePassphrase): - """ - Verify a user-submitted passphrase. This passphrase goes into - the key-derivation function to get actual encryption key, which - is what actually needs to be verified - - Since all addresses should have the same encryption, we only need - to verify correctness on the root key - """ - kdfOutput = self.kdf.DeriveKey(securePassphrase) - try: - isValid = self.addrMap['ROOT'].verifyEncryptionKey(kdfOutput) - return isValid - finally: - kdfOutput.destroy() - - - ############################################################################# - def verifyEncryptionKey(self, secureKdfOutput): - """ - Verify the underlying encryption key (from KDF). - Since all addresses should have the same encryption, - we only need to verify correctness on the root key. - """ - return self.addrMap['ROOT'].verifyEncryptionKey(secureKdfOutput) - - - ############################################################################# - def computeSystemSpecificKdfParams(self, targetSec=0.25, maxMem=32*1024*1024): - """ - WARNING!!! DO NOT CHANGE KDF PARAMS AFTER ALREADY ENCRYPTED THE WALLET - By changing them on an already-encrypted wallet, we are going - to lose the original AES256-encryption keys -- which are - uniquely determined by (numIter, memReqt, salt, passphrase) - - Only use this method before you have encrypted your wallet, - in order to determine good KDF parameters based on your - computer's specific speed/memory capabilities. - """ - kdf = KdfRomix() - kdf.computeKdfParams(targetSec, long(maxMem)) - - mem = kdf.getMemoryReqtBytes() - nIter = kdf.getNumIterations() - salt = SecureBinaryData(kdf.getSalt().toBinStr()) - return (mem, nIter, salt) - - ############################################################################# - def restoreKdfParams(self, mem, numIter, secureSalt): - """ - This method should only be used when we are loading an encrypted wallet - from file. DO NOT USE THIS TO CHANGE KDF PARAMETERS. Doing so may - result in data loss! - """ - self.kdf = KdfRomix(mem, numIter, secureSalt) - - - ############################################################################# - def changeKdfParams(self, mem, numIter, salt, securePassphrase=None): - """ - Changing KDF changes the wallet encryption key which means that a KDF - change is essentially the same as an encryption key change. As such, - the wallet must be unlocked if you intend to change an already- - encrypted wallet with KDF. - - TODO: this comment doesn't belong here...where does it go? : - If the KDF is NOT yet setup, this method will do it. Supply the target - compute time, and maximum memory requirements, and the underlying C++ - code will experimentally determine the "hardest" key-derivation params - that will run within the specified time and memory usage on the system - executing this method. You should set the max memory usage very low - (a few kB) for devices like smartphones, which have limited memory - availability. The KDF will then use less memory but more iterations - to achieve the same compute time. - """ - if self.useEncryption: - if not securePassphrase: - LOGERROR('') - LOGERROR('You have requested changing the key-derivation') - LOGERROR('parameters on an already-encrypted wallet, which') - LOGERROR('requires modifying the encryption on this wallet.') - LOGERROR('Please unlock your wallet before attempting to') - LOGERROR('change the KDF parameters.') - raise WalletLockError, 'Cannot change KDF without unlocking wallet' - elif not self.verifyPassphrase(securePassphrase): - LOGERROR('Incorrect passphrase to unlock wallet') - raise PassphraseError, 'Incorrect passphrase to unlock wallet' - - secureSalt = SecureBinaryData(salt) - newkdf = KdfRomix(mem, numIter, secureSalt) - bp = BinaryPacker() - bp.put(BINARY_CHUNK, self.serializeKdfParams(newkdf), width=256) - updList = [[WLT_UPDATE_MODIFY, self.offsetKdfParams, bp.getBinaryString()]] - - if not self.useEncryption: - # We may be setting the kdf params before enabling encryption - self.walletFileSafeUpdate(updList) - else: - # Must change the encryption key: and we won't get here unless - # we have a passphrase to use. This call will take the - self.changeWalletEncryption(securePassphrase=securePassphrase, \ - extraFileUpdates=updList, kdfObj=newkdf) - - self.kdf = newkdf - - - - - ############################################################################# - def changeWalletEncryption(self, secureKdfOutput=None, \ - securePassphrase=None, \ - extraFileUpdates=[], - kdfObj=None): - """ - Supply the passphrase you would like to use to encrypt this wallet - (or supply the KDF output directly, to skip the passphrase part). - This method will attempt to re-encrypt with the new passphrase. - This fails if the wallet is already locked with a different passphrase. - If encryption is already enabled, please unlock the wallet before - calling this method. - - Make sure you set up the key-derivation function (KDF) before changing - from an unencrypted to an encrypted wallet. An error will be thrown - if you don't. You can use something like the following - - # For a target of 0.05-0.1s compute time: - (mem,nIter,salt) = wlt.computeSystemSpecificKdfParams(0.1) - wlt.changeKdfParams(mem, nIter, salt) - - Use the extraFileUpdates to pass in other changes that need to be - written to the wallet file in the same atomic operation as the - encryption key modifications. - """ - - if not kdfObj: - kdfObj = self.kdf - - oldUsedEncryption = self.useEncryption - if securePassphrase or secureKdfOutput: - newUsesEncryption = True - else: - newUsesEncryption = False - - oldKdfKey = None - if oldUsedEncryption: - if self.isLocked: - raise WalletLockError, 'Must unlock wallet to change passphrase' - else: - oldKdfKey = self.kdfKey.copy() - - - if newUsesEncryption and not self.kdf: - raise EncryptionError, 'KDF must be setup before encrypting wallet' - - # Prep the file-update list with extras passed in as argument - walletUpdateInfo = list(extraFileUpdates) - - # Derive the new KDF key if a passphrase was supplied - newKdfKey = secureKdfOutput - if securePassphrase: - newKdfKey = self.kdf.DeriveKey(securePassphrase) - - if oldUsedEncryption and newUsesEncryption and self.verifyEncryptionKey(newKdfKey): - LOGWARN('Attempting to change encryption to same passphrase!') - return # Wallet is encrypted with the new passphrase already - - - # With unlocked key data, put the rest in a try/except/finally block - # To make sure we destroy the temporary kdf outputs - try: - # If keys were previously unencrypted, they will be not have - # initialization vectors and need to be generated before encrypting. - # This is why we have the enableKeyEncryption() call - - if not oldUsedEncryption==newUsesEncryption: - # If there was an encryption change, we must change the flags - # in the wallet file in the same atomic operation as changing - # the stored keys. We can't let them get out of sync. - self.useEncryption = newUsesEncryption - walletUpdateInfo.append(self.createChangeFlagsEntry()) - self.useEncryption = oldUsedEncryption - # Restore the old flag just in case the file write fails - - newAddrMap = {} - for addr160,addr in self.addrMap.iteritems(): - newAddrMap[addr160] = addr.copy() - newAddrMap[addr160].enableKeyEncryption(generateIVIfNecessary=True) - newAddrMap[addr160].changeEncryptionKey(oldKdfKey, newKdfKey) - newAddrMap[addr160].walletByteLoc = addr.walletByteLoc - walletUpdateInfo.append( \ - [WLT_UPDATE_MODIFY, addr.walletByteLoc, newAddrMap[addr160].serialize()]) - - - # Try to update the wallet file with the new encrypted key data - updateSuccess = self.walletFileSafeUpdate( walletUpdateInfo ) - - if updateSuccess: - # Finally give the new data to the user - for addr160,addr in newAddrMap.iteritems(): - self.addrMap[addr160] = addr.copy() - - self.useEncryption = newUsesEncryption - if newKdfKey: - self.unlock(newKdfKey) - finally: - # Make sure we always destroy the temporary passphrase results - if newKdfKey: newKdfKey.destroy() - if oldKdfKey: oldKdfKey.destroy() - - - - ############################################################################# - def getWalletPath(self, nameSuffix=None): - fpath = self.walletPath - - if self.walletPath=='': - fpath = os.path.join(ARMORY_HOME_DIR, 'armory_%s_.wallet' % self.uniqueIDB58) - - if not nameSuffix==None: - pieces = os.path.splitext(fpath) - if not pieces[0].endswith('_'): - fpath = pieces[0] + '_' + nameSuffix + pieces[1] - else: - fpath = pieces[0] + nameSuffix + pieces[1] - return fpath - - - - ############################################################################# - def getCommentForAddress(self, addr160): - if self.commentsMap.has_key(addr160): - return self.commentsMap[addr160] - else: - return '' - - ############################################################################# - def getComment(self, hashVal): - """ - This method is used for both address comments, as well as tx comments - In the first case, use the 20-byte binary pubkeyhash. Use 32-byte tx - hash for the tx-comment case. - """ - if self.commentsMap.has_key(hashVal): - return self.commentsMap[hashVal] - else: - return '' - - ############################################################################# - def setComment(self, hashVal, newComment): - """ - This method is used for both address comments, as well as tx comments - In the first case, use the 20-byte binary pubkeyhash. Use 32-byte tx - hash for the tx-comment case. - """ - updEntry = [] - isNewComment = False - if self.commentsMap.has_key(hashVal): - # If there is already a comment for this address, overwrite it - oldCommentLen = len(self.commentsMap[hashVal]) - oldCommentLoc = self.commentLocs[hashVal] - # The first 23 bytes are the datatype, hashVal, and 2-byte comment size - offset = 1 + len(hashVal) + 2 - updEntry.append([WLT_UPDATE_MODIFY, oldCommentLoc+offset, '\x00'*oldCommentLen]) - else: - isNewComment = True - - - dtype = WLT_DATATYPE_ADDRCOMMENT - if len(hashVal)>20: - dtype = WLT_DATATYPE_TXCOMMENT - - updEntry.append([WLT_UPDATE_ADD, dtype, hashVal, newComment]) - newCommentLoc = self.walletFileSafeUpdate(updEntry) - self.commentsMap[hashVal] = newComment - - # If there was a wallet overwrite, it's location is the first element - self.commentLocs[hashVal] = newCommentLoc[-1] - - - - ############################################################################# - def getAddrCommentIfAvail(self, txHash): - if not TheBDM.getBDMState()=='BlockchainReady': - return self.getComment(txHash) - - # If we haven't extracted relevant addresses for this tx, yet -- do it - if not self.txAddrMap.has_key(txHash): - self.txAddrMap[txHash] = [] - tx = TheBDM.getTxByHash(txHash) - if tx.isInitialized(): - for i in range(tx.getNumTxOut()): - try: - a160 = CheckHash160(tx.getScrAddrForTxOut(i)) - if self.hasAddr(a160): - self.txAddrMap[txHash].append(a160) - except: - LOGERROR("Unrecognized scraddr: " + binary_to_hex(tx.getScrAddrForTxOut(i))) - - - - - addrComments = [] - for a160 in self.txAddrMap[txHash]: - if self.commentsMap.has_key(a160): - addrComments.append(self.commentsMap[a160]) - - return '; '.join(addrComments) - - - ############################################################################# - def getCommentForLE(self, le): - # Smart comments for LedgerEntry objects: get any direct comments ... - # if none, then grab the one for any associated addresses. - txHash = le.getTxHash() - if self.commentsMap.has_key(txHash): - comment = self.commentsMap[txHash] - else: - # [[ COMMENTS ]] are not meant to be displayed on main ledger - comment = self.getAddrCommentIfAvail(txHash) - if comment.startswith('[[') and comment.endswith(']]'): - comment = '' - - return comment - - - - - - ############################################################################# - def setWalletLabels(self, lshort, llong=''): - self.labelName = lshort - self.labelDescr = llong - toWriteS = lshort.ljust( 32, '\x00') - toWriteL = llong.ljust(256, '\x00') - - updList = [] - updList.append([WLT_UPDATE_MODIFY, self.offsetLabelName, toWriteS]) - updList.append([WLT_UPDATE_MODIFY, self.offsetLabelDescr, toWriteL]) - self.walletFileSafeUpdate(updList) - - - ############################################################################# - def packWalletFlags(self, binPacker): - nFlagBytes = 8 - flags = [False]*nFlagBytes*8 - flags[0] = self.useEncryption - flags[1] = self.watchingOnly - flagsBitset = ''.join([('1' if f else '0') for f in flags]) - binPacker.put(UINT64, bitset_to_int(flagsBitset)) - - ############################################################################# - def createChangeFlagsEntry(self): - """ - Packs up the wallet flags and returns a update-entry that can be included - in a walletFileSafeUpdate call. - """ - bp = BinaryPacker() - self.packWalletFlags(bp) - toWrite = bp.getBinaryString() - return [WLT_UPDATE_MODIFY, self.offsetWltFlags, toWrite] - - ############################################################################# - def unpackWalletFlags(self, toUnpack): - if isinstance(toUnpack, BinaryUnpacker): - flagData = toUnpack - else: - flagData = BinaryUnpacker( toUnpack ) - - wltflags = flagData.get(UINT64, 8) - wltflags = int_to_bitset(wltflags, widthBytes=8) - self.useEncryption = (wltflags[0]=='1') - self.watchingOnly = (wltflags[1]=='1') - - - ############################################################################# - def packHeader(self, binPacker): - if not self.addrMap['ROOT']: - raise WalletAddressError, 'Cannot serialize uninitialzed wallet!' - - startByte = binPacker.getSize() - - binPacker.put(BINARY_CHUNK, self.fileTypeStr, width=8) - binPacker.put(UINT32, getVersionInt(self.version)) - binPacker.put(BINARY_CHUNK, self.magicBytes, width=4) - - # Wallet info flags - self.offsetWltFlags = binPacker.getSize() - startByte - self.packWalletFlags(binPacker) - - # Binary Unique ID (firstAddr25bytes[:5][::-1]) - binPacker.put(BINARY_CHUNK, self.uniqueIDBin, width=6) - - # Unix time of wallet creations - binPacker.put(UINT64, self.wltCreateDate) - - # User-supplied wallet label (short) - self.offsetLabelName = binPacker.getSize() - startByte - binPacker.put(BINARY_CHUNK, self.labelName , width=32) - - # User-supplied wallet label (long) - self.offsetLabelDescr = binPacker.getSize() - startByte - binPacker.put(BINARY_CHUNK, self.labelDescr, width=256) - - # Highest used address: - self.offsetTopUsed = binPacker.getSize() - startByte - binPacker.put(INT64, self.highestUsedChainIndex) - - # Key-derivation function parameters - self.offsetKdfParams = binPacker.getSize() - startByte - binPacker.put(BINARY_CHUNK, self.serializeKdfParams(), width=256) - - # Wallet encryption parameters (currently nothing to put here) - self.offsetCrypto = binPacker.getSize() - startByte - binPacker.put(BINARY_CHUNK, self.serializeCryptoParams(), width=256) - - # Address-chain root, (base-address for deterministic wallets) - self.offsetRootAddr = binPacker.getSize() - startByte - self.addrMap['ROOT'].walletByteLoc = self.offsetRootAddr - binPacker.put(BINARY_CHUNK, self.addrMap['ROOT'].serialize()) - - # In wallet version 1.0, this next kB is unused -- may be used in future - binPacker.put(BINARY_CHUNK, '\x00'*1024) - return binPacker.getSize() - startByte - - - - - ############################################################################# - def unpackHeader(self, binUnpacker): - """ - Unpacking the header information from a wallet file. See the help text - on the base class, PyBtcWallet, for more information on the wallet - serialization. - """ - self.fileTypeStr = binUnpacker.get(BINARY_CHUNK, 8) - self.version = readVersionInt(binUnpacker.get(UINT32)) - self.magicBytes = binUnpacker.get(BINARY_CHUNK, 4) - - # Decode the bits to get the flags - self.offsetWltFlags = binUnpacker.getPosition() - self.unpackWalletFlags(binUnpacker) - - # This is the first 4 bytes of the 25-byte address-chain-root address - # This includes the network byte (i.e. main network, testnet, namecoin) - self.uniqueIDBin = binUnpacker.get(BINARY_CHUNK, 6) - self.uniqueIDB58 = binary_to_base58(self.uniqueIDBin) - self.wltCreateDate = binUnpacker.get(UINT64) - - # We now have both the magic bytes and network byte - if not self.magicBytes == MAGIC_BYTES: - LOGERROR('Requested wallet is for a different blockchain!') - LOGERROR('Wallet is for: %s ', BLOCKCHAINS[self.magicBytes]) - LOGERROR('ArmoryEngine: %s ', BLOCKCHAINS[MAGIC_BYTES]) - return - if not self.uniqueIDBin[-1] == ADDRBYTE: - LOGERROR('Requested wallet is for a different network!') - LOGERROR('Wallet is for: %s ', NETWORKS[self.uniqueIDBin[-1]]) - LOGERROR('ArmoryEngine: %s ', NETWORKS[ADDRBYTE]) - return - - # User-supplied description/name for wallet - self.offsetLabelName = binUnpacker.getPosition() - self.labelName = binUnpacker.get(BINARY_CHUNK, 32).strip('\x00') - - - # Longer user-supplied description/name for wallet - self.offsetLabelDescr = binUnpacker.getPosition() - self.labelDescr = binUnpacker.get(BINARY_CHUNK, 256).strip('\x00') - - - self.offsetTopUsed = binUnpacker.getPosition() - self.highestUsedChainIndex = binUnpacker.get(INT64) - - - # Read the key-derivation function parameters - self.offsetKdfParams = binUnpacker.getPosition() - self.kdf = self.unserializeKdfParams(binUnpacker) - - # Read the crypto parameters - self.offsetCrypto = binUnpacker.getPosition() - self.crypto = self.unserializeCryptoParams(binUnpacker) - - # Read address-chain root address data - self.offsetRootAddr = binUnpacker.getPosition() - - - rawAddrData = binUnpacker.get(BINARY_CHUNK, self.pybtcaddrSize) - self.addrMap['ROOT'] = PyBtcAddress().unserialize(rawAddrData) - fixedAddrData = self.addrMap['ROOT'].serialize() - if not rawAddrData==fixedAddrData: - self.walletFileSafeUpdate([ \ - [WLT_UPDATE_MODIFY, self.offsetRootAddr, fixedAddrData]]) - - self.addrMap['ROOT'].walletByteLoc = self.offsetRootAddr - if self.useEncryption: - self.addrMap['ROOT'].isLocked = True - self.isLocked = True - - # In wallet version 1.0, this next kB is unused -- may be used in future - binUnpacker.advance(1024) - - # TODO: automatic conversion if the code uses a newer wallet - # version than the wallet... got a manual script, but it - # would be nice to autodetect and correct - #convertVersion - - - ############################################################################# - def unpackNextEntry(self, binUnpacker): - dtype = binUnpacker.get(UINT8) - hashVal = '' - binData = '' - if dtype==WLT_DATATYPE_KEYDATA: - hashVal = binUnpacker.get(BINARY_CHUNK, 20) - binData = binUnpacker.get(BINARY_CHUNK, self.pybtcaddrSize) - elif dtype==WLT_DATATYPE_ADDRCOMMENT: - hashVal = binUnpacker.get(BINARY_CHUNK, 20) - commentLen = binUnpacker.get(UINT16) - binData = binUnpacker.get(BINARY_CHUNK, commentLen) - elif dtype==WLT_DATATYPE_TXCOMMENT: - hashVal = binUnpacker.get(BINARY_CHUNK, 32) - commentLen = binUnpacker.get(UINT16) - binData = binUnpacker.get(BINARY_CHUNK, commentLen) - elif dtype==WLT_DATATYPE_OPEVAL: - raise NotImplementedError, 'OP_EVAL not support in wallet yet' - elif dtype==WLT_DATATYPE_DELETED: - deletedLen = binUnpacker.get(UINT16) - binUnpacker.advance(deletedLen) - - - return (dtype, hashVal, binData) - - ############################################################################# - def readWalletFile(self, wltpath, verifyIntegrity=True, doScanNow=False): - - TimerStart('readWalletFile') - - if not os.path.exists(wltpath): - raise FileExistsError, "No wallet file:"+wltpath - - self.__init__() - self.walletPath = wltpath - - if verifyIntegrity: - try: - nError = self.doWalletFileConsistencyCheck() - except KeyDataError, errmsg: - LOGEXCEPT('***ERROR: Wallet file had unfixable errors.') - raise KeyDataError, errmsg - - - wltfile = open(wltpath, 'rb') - wltdata = BinaryUnpacker(wltfile.read()) - wltfile.close() - - self.cppWallet = Cpp.BtcWallet() - self.unpackHeader(wltdata) - - self.lastComputedChainIndex = -UINT32_MAX - self.lastComputedChainAddr160 = None - while wltdata.getRemainingSize()>0: - byteLocation = wltdata.getPosition() - dtype, hashVal, rawData = self.unpackNextEntry(wltdata) - if dtype==WLT_DATATYPE_KEYDATA: - newAddr = PyBtcAddress() - newAddr.unserialize(rawData) - newAddr.walletByteLoc = byteLocation + 21 - # Fix byte errors in the address data - fixedAddrData = newAddr.serialize() - if not rawData==fixedAddrData: - self.walletFileSafeUpdate([ \ - [WLT_UPDATE_MODIFY, newAddr.walletByteLoc, fixedAddrData]]) - if newAddr.useEncryption: - newAddr.isLocked = True - self.addrMap[hashVal] = newAddr - if newAddr.chainIndex > self.lastComputedChainIndex: - self.lastComputedChainIndex = newAddr.chainIndex - self.lastComputedChainAddr160 = newAddr.getAddr160() - self.linearAddr160List.append(newAddr.getAddr160()) - self.chainIndexMap[newAddr.chainIndex] = newAddr.getAddr160() - - # Update the parallel C++ object that scans the blockchain for us - timeRng = newAddr.getTimeRange() - blkRng = newAddr.getBlockRange() - self.cppWallet.addScrAddress_5_(Hash160ToScrAddr(hashVal), \ - timeRng[0], blkRng[0], \ - timeRng[1], blkRng[1]) - if dtype in (WLT_DATATYPE_ADDRCOMMENT, WLT_DATATYPE_TXCOMMENT): - self.commentsMap[hashVal] = rawData # actually ASCII data, here - self.commentLocs[hashVal] = byteLocation - if dtype==WLT_DATATYPE_OPEVAL: - raise NotImplementedError, 'OP_EVAL not support in wallet yet' - if dtype==WLT_DATATYPE_DELETED: - pass - - - if (not doScanNow or \ - not TheBDM.getBDMState()=='BlockchainReady' or \ - self.doBlockchainSync==BLOCKCHAIN_DONOTUSE): - pass - else: - self.syncWithBlockchain() - - - ### Update the wallet version if necessary ### - if getVersionInt(self.version) < getVersionInt(PYBTCWALLET_VERSION): - LOGERROR('Wallets older than version 1.35 no loger supported!') - return - - TimerStop('readWalletFile') - - return self - - - - ############################################################################# - def walletFileSafeUpdate(self, updateList): - - """ - The input "toAddDataList" should be a list of triplets, such as: - [ - [WLT_DATA_ADD, WLT_DATATYPE_KEYDATA, addr160_1, PyBtcAddrObj1] - [WLT_DATA_ADD, WLT_DATATYPE_KEYDATA, addr160_2, PyBtcAddrObj2] - [WLT_DATA_MODIFY, modifyStartByte1, binDataForOverwrite1 ] - [WLT_DATA_ADD, WLT_DATATYPE_ADDRCOMMENT, addr160_3, 'Long-term savings'] - [WLT_DATA_MODIFY, modifyStartByte2, binDataForOverwrite2 ] - ] - - The return value is the list of new file byte offsets (from beginning of - the file), that specify the start of each modification made to the - wallet file. For MODIFY fields, this just returns the modifyStartByte - field that was provided as input. For adding data, it specifies the - starting byte of the new field (the DATATYPE byte). We keep this data - in PyBtcAddress objects so that we know where to apply modifications in - case we need to change something, like converting from unencrypted to - encrypted private keys. - - If this method fails, we simply return an empty list. We can check for - an empty list to know if the file update succeeded. - - WHY IS THIS SO COMPLICATED? -- Because it's atomic! - - When we want to add data to the wallet file, we will do so in a completely - recoverable way. We define this method to make sure a backup exists when - we start modifying the file, and keep a flag to identify when the wallet - might be corrupt. If we ever try to load the wallet file and see another - file with the _update_unsuccessful suffix, we should instead just restore - from backup. - - Similarly, we have to update the backup file after updating the main file - so we will use a similar technique with the backup_unsuccessful suffix. - We don't want to rely on a backup if somehow *the backup* got corrupted - and the original file is fine. THEREFORE -- this is implemented in such - a way that the user should know two things: - - (1) No matter when the power goes out, we ALWAYS have a uncorrupted - wallet file, and know which one it is. Either the backup is safe, - or the original is safe. Based on the flag files, we know which - one is guaranteed to be not corrupted. - (2) ALWAYS DO YOUR FILE OPERATIONS BEFORE SETTING DATA IN MEMORY - You must write it to disk FIRST using this SafeUpdate method, - THEN give the new data to the user -- never give it to them - until you are sure that it was written safely to disk. - - Number (2) is easy to screw up because you plan to write the file just - AFTER the data is created and stored in local memory. But an error - might be thrown halfway which is handled higher up, and instead the data - never made it to file. Then there is a risk that the user uses their - new address that never made it into the wallet file. - """ - - if not os.path.exists(self.walletPath): - raise FileExistsError, 'No wallet file exists to be updated!' - - if len(updateList)==0: - return [] - - # Make sure that the primary and backup files are synced before update - self.doWalletFileConsistencyCheck() - - walletFileBackup = self.getWalletPath('backup') - mainUpdateFlag = self.getWalletPath('update_unsuccessful') - backupUpdateFlag = self.getWalletPath('backup_unsuccessful') - - - # Will be passing back info about all data successfully added - oldWalletSize = os.path.getsize(self.walletPath) - updateLocations = [] - dataToChange = [] - toAppend = BinaryPacker() - - try: - for entry in updateList: - modType = entry[0] - updateInfo = entry[1:] - - if(modType==WLT_UPDATE_ADD): - dtype = updateInfo[0] - updateLocations.append(toAppend.getSize()+oldWalletSize) - if dtype==WLT_DATATYPE_KEYDATA: - if len(updateInfo[1])!=20 or not isinstance(updateInfo[2], PyBtcAddress): - raise Exception, 'Data type does not match update type' - toAppend.put(UINT8, WLT_DATATYPE_KEYDATA) - toAppend.put(BINARY_CHUNK, updateInfo[1]) - toAppend.put(BINARY_CHUNK, updateInfo[2].serialize()) - - elif dtype in (WLT_DATATYPE_ADDRCOMMENT, WLT_DATATYPE_TXCOMMENT): - if not isinstance(updateInfo[2], str): - raise Exception, 'Data type does not match update type' - toAppend.put(UINT8, dtype) - toAppend.put(BINARY_CHUNK, updateInfo[1]) - toAppend.put(UINT16, len(updateInfo[2])) - toAppend.put(BINARY_CHUNK, updateInfo[2]) - - elif dtype==WLT_DATATYPE_OPEVAL: - raise Exception, 'OP_EVAL not support in wallet yet' - - elif(modType==WLT_UPDATE_MODIFY): - updateLocations.append(updateInfo[0]) - dataToChange.append( updateInfo ) - else: - LOGERROR('Unknown wallet-update type!') - raise Exception, 'Unknown wallet-update type!' - except Exception: - LOGEXCEPT('Bad input to walletFileSafeUpdate') - return [] - - binaryToAppend = toAppend.getBinaryString() - - # We need to safely modify both the main wallet file and backup - # Start with main wallet - touchFile(mainUpdateFlag) - - try: - wltfile = open(self.walletPath, 'ab') - wltfile.write(binaryToAppend) - wltfile.close() - - # This is for unit-testing the atomic-wallet-file-update robustness - if self.interruptTest1: raise InterruptTestError - - wltfile = open(self.walletPath, 'r+b') - for loc,replStr in dataToChange: - wltfile.seek(loc) - wltfile.write(replStr) - wltfile.close() - - except IOError: - LOGEXCEPT('Could not write data to wallet. Permissions?') - shutil.copy(walletFileBackup, self.walletPath) - os.remove(mainUpdateFlag) - return [] - - # Write backup flag before removing main-update flag. If we see - # both flags, we know file IO was interrupted RIGHT HERE - touchFile(backupUpdateFlag) - - # This is for unit-testing the atomic-wallet-file-update robustness - if self.interruptTest2: raise InterruptTestError - - os.remove(mainUpdateFlag) - - # Modify backup - try: - # This is for unit-testing the atomic-wallet-file-update robustness - if self.interruptTest3: raise InterruptTestError - - backupfile = open(walletFileBackup, 'ab') - backupfile.write(binaryToAppend) - backupfile.close() - - backupfile = open(walletFileBackup, 'r+b') - for loc,replStr in dataToChange: - backupfile.seek(loc) - backupfile.write(replStr) - backupfile.close() - - except IOError: - LOGEXCEPT('Could not write backup wallet. Permissions?') - shutil.copy(self.walletPath, walletFileBackup) - os.remove(mainUpdateFlag) - return [] - - os.remove(backupUpdateFlag) - - return updateLocations - - - - ############################################################################# - def doWalletFileConsistencyCheck(self, onlySyncBackup=True): - """ - First we check the file-update flags (files we touched/removed during - file modification operations), and then restore the primary wallet file - and backup file to the exact same state -- we know that at least one of - them is guaranteed to not be corrupt, and we know based on the flags - which one that is -- so we execute the appropriate copy operation. - - ***NOTE: For now, the remaining steps are untested and unused! - - After we have guaranteed that main wallet and backup wallet are the - same, we want to do a check that the data is consistent. We do this - by simply reading in the key-data from the wallet, unserializing it - and reserializing it to see if it matches -- this works due to the - way the PyBtcAddress::unserialize() method works: it verifies the - checksums in the address data, and corrects errors automatically! - And it's part of the unit-tests that serialize/unserialize round-trip - is guaranteed to match for all address types if there's no byte errors. - - If an error is detected, we do a safe-file-modify operation to re-write - the corrected information to the wallet file, in-place. We DO NOT - check comment fields, since they do not have checksums, and are not - critical to protect against byte errors. - """ - - - - if not os.path.exists(self.walletPath): - raise FileExistsError, 'No wallet file exists to be checked!' - - walletFileBackup = self.getWalletPath('backup') - mainUpdateFlag = self.getWalletPath('update_unsuccessful') - backupUpdateFlag = self.getWalletPath('backup_unsuccessful') - - if not os.path.exists(walletFileBackup): - # We haven't even created a backup file, yet - LOGDEBUG('Creating backup file %s', walletFileBackup) - touchFile(backupUpdateFlag) - shutil.copy(self.walletPath, walletFileBackup) - os.remove(backupUpdateFlag) - - if os.path.exists(backupUpdateFlag) and os.path.exists(mainUpdateFlag): - # Here we actually have a good main file, but backup never succeeded - LOGWARN('***WARNING: error in backup file... how did that happen?') - shutil.copy(self.walletPath, walletFileBackup) - os.remove(mainUpdateFlag) - os.remove(backupUpdateFlag) - elif os.path.exists(mainUpdateFlag): - LOGWARN('***WARNING: last file operation failed! Restoring wallet from backup') - # main wallet file might be corrupt, copy from backup - shutil.copy(walletFileBackup, self.walletPath) - os.remove(mainUpdateFlag) - elif os.path.exists(backupUpdateFlag): - LOGWARN('***WARNING: creation of backup was interrupted -- fixing') - shutil.copy(self.walletPath, walletFileBackup) - os.remove(backupUpdateFlag) - - if onlySyncBackup: - return 0 - - - - - - - ############################################################################# - #def getAddrByIndex(self, i): - #return self.addrMap.values()[i] - - ############################################################################# - def deleteImportedAddress(self, addr160): - """ - We want to overwrite a particular key in the wallet. Before overwriting - the data looks like this: - [ \x00 | <20-byte addr160> | <237-byte keydata> ] - And we want it to look like: - [ \x04 | <2-byte length> | \x00\x00\x00... ] - So we need to construct a wallet-update vector to modify the data - starting at the first byte, replace it with 0x04, specifies how many - bytes are in the deleted entry, and then actually overwrite those - bytes with 0s - """ - - if not self.addrMap[addr160].chainIndex==-2: - raise WalletAddressError, 'You can only delete imported addresses!' - - overwriteLoc = self.addrMap[addr160].walletByteLoc - 21 - overwriteLen = 20 + self.pybtcaddrSize - 2 - - overwriteBin = '' - overwriteBin += int_to_binary(WLT_DATATYPE_DELETED, widthBytes=1) - overwriteBin += int_to_binary(overwriteLen, widthBytes=2) - overwriteBin += '\x00'*overwriteLen - - self.walletFileSafeUpdate([[WLT_UPDATE_MODIFY, overwriteLoc, overwriteBin]]) - - # IMPORTANT: we need to update the wallet structures to reflect the - # new state of the wallet. This will actually be easiest - # if we just "forget" the current wallet state and re-read - # the wallet from file - wltPath = self.walletPath - self.readWalletFile(wltPath, doScanNow=True) - - - ############################################################################# - def importExternalAddressData(self, privKey=None, privChk=None, \ - pubKey=None, pubChk=None, \ - addr20=None, addrChk=None, \ - firstTime=UINT32_MAX, firstBlk=UINT32_MAX, \ - lastTime=0, lastBlk=0): - """ - This wallet fully supports importing external keys, even though it is - a deterministic wallet: determinism only adds keys to the pool based - on the address-chain, but there's nothing wrong with adding new keys - not on the chain. - - We don't know when this address was created, so we have to set its - first/last-seen times to 0, to make sure we search the whole blockchain - for tx related to it. This data will be updated later after we've done - the search and know for sure when it is "relevant". - (alternatively, if you know it's first-seen time for some reason, you - can supply it as an input, but this seems rare: we don't want to get it - wrong or we could end up missing wallet-relevant transactions) - - DO NOT CALL FROM A BDM THREAD FUNCTION. IT MAY DEADLOCK. - """ - - if self.calledFromBDM: - LOGERROR('Called importExternalAddressData() from BDM method!') - LOGERROR('Don\'t do this!') - return '' - - if not privKey and not self.watchingOnly: - LOGERROR('') - LOGERROR('This wallet is strictly for addresses that you') - LOGERROR('own. You cannot import addresses without the') - LOGERROR('the associated private key. Instead, use a') - LOGERROR('watching-only wallet to import this address.') - LOGERROR('(actually, this is currently, completely disabled)') - raise WalletAddressError, 'Cannot import non-private-key addresses' - - - - # First do all the necessary type conversions and error corrections - computedPubKey = None - computedAddr20 = None - if privKey: - if isinstance(privKey, str): - privKey = SecureBinaryData(privKey) - - if privChk: - privKey = SecureBinaryData(verifyChecksum(privKey.toBinStr(), privChk)) - - computedPubkey = CryptoECDSA().ComputePublicKey(privKey) - computedAddr20 = convertKeyDataToAddress(pubKey=computedPubkey) - - # If public key is provided, we prep it so we can verify Pub/Priv match - if pubKey: - if isinstance(pubKey, str): - pubKey = SecureBinaryData(pubKey) - if pubChk: - pubKey = SecureBinaryData(verifyChecksum(pubKey.toBinStr(), pubChk)) - - if not computedAddr20: - computedAddr20 = convertKeyDataToAddress(pubKey=pubKey) - - # The 20-byte address (pubkey hash160) should always be a python string - if addr20: - if not isinstance(pubKey, str): - addr20 = addr20.toBinStr() - if addrChk: - addr20 = verifyChecksum(addr20, addrChk) - - - # Now a few sanity checks - if self.addrMap.has_key(addr20): - LOGWARN('This address is already in your wallet!') - return - - #if pubKey and not computedPubkey==pubKey: - #raise ECDSA_Error, 'Private and public keys to be imported do not match!' - #if addr20 and not computedAddr20==addr20: - #raise ECDSA_Error, 'Supplied address hash does not match key data!' - - addr20 = computedAddr20 - - if self.addrMap.has_key(addr20): - return None - - # If a private key is supplied and this wallet is encrypted&locked, then - # we have no way to secure the private key without unlocking the wallet. - if self.useEncryption and privKey and not self.kdfKey: - raise WalletLockError, 'Cannot import private key when wallet is locked!' - - - if privKey: - # For priv key, lots of extra encryption and verification options - newAddr = PyBtcAddress().createFromPlainKeyData( addr160=addr20, \ - plainPrivKey=privKey, publicKey65=computedPubkey, \ - willBeEncr=self.useEncryption, \ - generateIVIfNecessary=self.useEncryption, \ - skipCheck=True, skipPubCompute=True) - if self.useEncryption: - newAddr.lock(self.kdfKey) - newAddr.unlock(self.kdfKey) - elif pubKey: - securePubKey = SecureBinaryData(pubKey) - newAddr = PyBtcAddress().createFromPublicKeyData(securePubKey) - else: - newAddr = PyBtcAddress().createFromPublicKeyHash160(addr20) - - - newAddr.chaincode = SecureBinaryData('\xff'*32) - newAddr.chainIndex = -2 - newAddr.timeRange = [firstTime, lastTime] - newAddr.blkRange = [firstBlk, lastBlk ] - #newAddr.binInitVect16 = SecureBinaryData().GenerateRandom(16) - newAddr160 = newAddr.getAddr160() - - newDataLoc = self.walletFileSafeUpdate( \ - [[WLT_UPDATE_ADD, WLT_DATATYPE_KEYDATA, newAddr160, newAddr]]) - self.addrMap[newAddr160] = newAddr.copy() - self.addrMap[newAddr160].walletByteLoc = newDataLoc[0] + 21 - self.linearAddr160List.append(newAddr160) - if self.useEncryption and self.kdfKey: - self.addrMap[newAddr160].lock(self.kdfKey) - if not self.isLocked: - self.addrMap[newAddr160].unlock(self.kdfKey) - - self.cppWallet.addScrAddress_5_(Hash160ToScrAddr(newAddr160), \ - firstTime, firstBlk, lastTime, lastBlk) - - # The following line MAY deadlock if this method is called from the BDM - # thread. Do not write any BDM methods that calls this method! - TheBDM.registerImportedScrAddr(Hash160ToScrAddr(newAddr160), - firstTime, firstBlk, lastTime, lastBlk) - - - return newAddr160 - - - ############################################################################# - def bulkImportAddresses(self, textBlock, privKeyEndian=BIGENDIAN, \ - sepList=":;'[]()=-_*&^%$#@!,./?\n"): - """ - Attempts to import plaintext key data stored in a file. This method - expects all data to be in hex or Base58: - - 20 bytes / 40 hex chars -- public key hashes - 25 bytes / 50 hex chars -- full binary addresses - 65 bytes / 130 hex chars -- public key - 32 bytes / 64 hex chars -- private key - - 33 or 34 Base58 chars -- address strings - 50 to 52 Base58 chars -- base58-encoded private key - - Since this is python, I don't have to require any particular format: - I can pretty easily break apart the entire file into individual strings, - search for addresses and public keys, then, search for private keys that - correspond to that data. Obviously, simpler is better, but as long as - the data is encoded as in the above list and separated by whitespace or - punctuation, this method should succeed. - - We must throw an error if this is NOT a watching-only address and we - find an address without a private key. We will need to create a - separate watching-only wallet in order to import these keys. - - TODO: will finish this later - """ - - """ - STUB: (AGAIN) I just can't make this work out to be as stupid-proof - as I originally planned. I'll have to put it on hold. - self.__init__() - - newfile = open(filename,'rb') - newdata = newfile.read() - newfile.close() - - # Change all punctuation to the same char so split() works easier - for ch in sepList: - newdata.replace(ch, ' ') - - newdata = newdata.split() - hexChars = '01234567890abcdef' - b58Chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' - DATATYPES = enum( 'UNKNOWN', \ - 'Addr_Hex_20', \ - 'Addr_B58_25', \ - 'PubX_Hex_32', \ - 'PubY_Hex_32', \ - 'PubK_Hex_65', \ - 'Priv_Hex_32', \ - 'Priv_Hex_36', \ - 'Priv_Hex_37', \ - 'Priv_B58_32', \ - 'Priv_B58_37', \ - 'Priv_MiniPriv', \ - 'PubK_Hex_33_Compressed', \ - 'Priv_Hex_33_Compressed') - - DTYPES = enum('Unknown', 'Hash160', 'PubKey', 'PrivKey', 'Byte32', 'Byte33') - - - lastAddr = None - lastPubK = None - lastPriv = None - for theStr in newdata: - if len(theStr)<20: - continue - - hexCount = sum([1 if c in hexChars else 0 for c in theStr]) - b58Count = sum([1 if c in b58Chars else 0 for c in theStr]) - canBeHex = hexCount==len(theStr) - canBeB58 = b58Count==len(theStr) - isHex = canBeHex - isB58 = canBeB58 and not canBeHex - isStr = not isHex and not isB58 - - dataAndType = [DTYPES.Unknown, ''] - if isHex: - binData = hex_to_binary(theStr) - sz = len(binData) - - if sz==20: - dataAndType = [DTYPES.Hash160, binData] - elif sz==25: - dataAndType = [DTYPES.Hash160, binData[1:21]] - elif sz==32: - dataAndType = [DTYPES., binData[1:21]] - elif isB58: - binData = base58_to_binary(theStr) - sz = len(binData) - - - if isHex and sz==40: - elif isHex and sz==50: - dataAndType = [DTYPES.Hash160, hex_to_binary(theStr)[1:21]] - elif isB58 and sz>=31 and sz<=35: - dataAndType = [DTYPES.Hash160, addrStr_to_hash160(theStr)] - elif isHex is sz==130: - dataAndType = [DTYPES.PubKey, hex_to_binary(theStr)] - elif isHex is sz==128: - dataAndType = [DTYPES.PubKey, '\x04'+hex_to_binary(theStr)] - elif isHex is sz==128: - - - - potentialKey = SecureBinaryData('\x04' + piece) - isValid = CryptoECDSA().VerifyPublicKeyValid(potentialKey) - """ - pass - - - - - - ############################################################################# - def checkIfRescanRequired(self): - """ - Returns true is we have to go back to disk/mmap and rescan more than two - weeks worth of blocks - - DO NOT CALL FROM A BDM METHOD. Instead, call directly: - self.bdm.numBlocksToRescan(pywlt.cppWallet) > 2016 - """ - if self.calledFromBDM: - LOGERROR('Called checkIfRescanRequired() from BDM method!') - LOGERROR('Don\'t do this!') - - if TheBDM.getBDMState()=='BlockchainReady': - return (TheBDM.numBlocksToRescan(self.cppWallet) > 2016) - else: - return False - - - - ############################################################################# - def signTxDistProposal(self, txdp, hashcode=1): - if not hashcode==1: - LOGERROR('hashcode!=1 is not supported at this time!') - return - - # If the wallet is locked, we better bail now - if self.isLocked: - raise WalletLockError, "Cannot sign Tx when wallet is locked!" - - numInputs = len(txdp.pytxObj.inputs) - wltAddr = [] - for index,txin in enumerate(txdp.pytxObj.inputs): - scriptType = getTxOutScriptType(txdp.txOutScripts[index]) - - if scriptType in (TXOUT_SCRIPT_STANDARD, TXOUT_SCRIPT_COINBASE): - addr160 = TxOutScriptExtractAddr160(txdp.txOutScripts[index]) - if self.hasAddr(addr160) and self.addrMap[addr160].hasPrivKey(): - wltAddr.append( (self.addrMap[addr160], index, 0)) - elif scriptType==TXOUT_SCRIPT_MULTISIG: - # Basically the same check but multiple addresses to consider - addrList = getTxOutMultiSigInfo(txdp.txOutScripts[index])[1] - for addrIdx, addr in enumerate(addrList): - if self.hasAddr(addr) and self.addrMap[addr].hasPrivKey(): - wltAddr.append( (self.addrMap[addr], index, addrIdx) ) - break - - - # WltAddr now contains a list of every input we can sign for, and the - # PyBtcAddress object that can be used to sign it. Let's do it. - numMyAddr = len(wltAddr) - LOGDEBUG('Total number of inputs in transaction: %d', numInputs) - LOGDEBUG('Number of inputs that you can sign for: %d', numMyAddr) - - - # Unlock the wallet if necessary, sign inputs - maxChainIndex = -1 - for addrObj,idx, sigIdx in wltAddr: - maxChainIndex = max(maxChainIndex, addrObj.chainIndex) - if addrObj.isLocked: - if self.kdfKey: - addrObj.unlock(self.kdfKey) - else: - raise WalletLockError, 'Cannot sign tx without unlocking wallet' - - if not addrObj.hasPubKey(): - # Make sure the public key is available for this address - addrObj.binPublicKey65 = CryptoECDSA().ComputePublicKey(addrObj.binPrivKey32_Plain) - - # Copy the script, blank out out all other scripts (assume hashcode==1) - txCopy = PyTx().unserialize(txdp.pytxObj.serialize()) - for i in range(len(txCopy.inputs)): - if not i==idx: - txCopy.inputs[i].binScript = '' - else: - txCopy.inputs[i].binScript = txdp.txOutScripts[i] - - hashCode1 = int_to_binary(hashcode, widthBytes=1) - hashCode4 = int_to_binary(hashcode, widthBytes=4) - preHashMsg = txCopy.serialize() + hashCode4 - signature = addrObj.generateDERSignature(preHashMsg) + hashCode1 - - # Now we attach a binary signature or full script, depending on the type - if txdp.scriptTypes[idx]==TXOUT_SCRIPT_COINBASE: - # Only need the signature to complete coinbase TxOut - sigLenInBinary = int_to_binary(len(signature)) - txdp.signatures[idx][0] = sigLenInBinary + signature - elif txdp.scriptTypes[idx]==TXOUT_SCRIPT_STANDARD: - # Gotta include the public key, too, for standard TxOuts - pubkey = addrObj.binPublicKey65.toBinStr() - sigLenInBinary = int_to_binary(len(signature)) - pubkeyLenInBinary = int_to_binary(len(pubkey) ) - txdp.signatures[idx][0] = sigLenInBinary + signature + \ - pubkeyLenInBinary + pubkey - elif txdp.scriptTypes[idx]==TXOUT_SCRIPT_MULTISIG: - # We attach just the sig for multi-sig transactions - sigLenInBinary = int_to_binary(len(signature)) - txdp.signatures[idx][sigIdx] = (sigLenInBinary + signature) - else: - LOGERROR('Unknown txOut script type') - - - prevHighestIndex = self.highestUsedChainIndex - if prevHighestIndex=0): - # Either we want imported addresses, or this isn't one - if (withAddrPool or addr.chainIndex<=self.highestUsedChainIndex): - addrList.append(addr) - - return addrList - - - ############################################################################# - def getAddress160ByChainIndex(self, desiredIdx): - """ - It should be safe to assume that if the index is less than the highest - computed, it will be in the chainIndexMap, but I don't like making such - assumptions. Perhaps something went wrong with the wallet, or it was - manually reconstructed and has holes in the chain. We will regenerate - addresses up to that point, if necessary (but nothing past the value - self.lastComputedChainIndex. - """ - if desiredIdx>self.lastComputedChainIndex or desiredIdx<0: - # I removed the option for fillPoolIfNecessary, because of the risk - # that a bug may lead to generation of billions of addresses, which - # would saturate the system's resources and fill the HDD. - raise WalletAddressError, 'Chain index is out of range' - - - if self.chainIndexMap.has_key(desiredIdx): - return self.chainIndexMap[desiredIdx] - else: - # Somehow the address isn't here, even though it is less than the - # last computed index - closestIdx = 0 - for idx,addr160 in self.chainIndexMap.iteritems(): - if closestIdx0 else 'Sent' - - blkStr = str(le.getBlockNum()) - print indent + 'LE %s %s %s %s' % \ - (addrStr.ljust(15), leVal, txType.ljust(8), blkStr.ljust(8)) - -""" -class PyLedgerEntry(object): - def __init__(self): - self.addr20 = UNINITIALIZED - self.value = UNINITIALIZED - self.blockNum = UNINITIALIZED - self.txHash = UNINITIALIZED - self.index = UNINITIALIZED - self.isValid = UNINITIALIZED - self.isSentToSelf = UNINITIALIZED - self.isChangeBack = UNINITIALIZED - - def createForWalletFromTx(self, wlt, tx): - numIn = len(tx.inputs) - numOut = len(tx.outputs) - - - - // addr20_ - useless - originally had a purpose, but lost it - // value_ - total debit/credit on WALLET balance, in Satoshis (1e-8 BTC) - // blockNum_ - block height of the block in which this tx was included - // txHash_ - hash of this tx - // index_ - index of the tx in the block - // isValid_ - default to true -- invalidated due to reorg/double-spend - // isSentToSelf_ - if we supplied inputs and rx ALL outputs - // isChangeBack_ - if we supplied inputs and rx ANY outputs -""" - - - - -############################################################################### -############################################################################### -# -# Networking Objects -# -############################################################################### -############################################################################### - -def quad_to_str( addrQuad): - return '.'.join([str(a) for a in addrQuad]) - -def quad_to_binary( addrQuad): - return ''.join([chr(a) for a in addrQuad]) - -def binary_to_quad(addrBin): - return [ord(a) for a in addrBin] - -def str_to_quad(addrBin): - return [int(a) for a in addrBin.split('.')] - -def str_to_binary(addrBin): - """ I should come up with a better name for this -- it's net-addr only """ - return ''.join([chr(int(a)) for a in addrBin.split('.')]) - -def parseNetAddress(addrObj): - if isinstance(addrObj, str): - if len(addrObj)==4: - return binary_to_quad(addrObj) - else: - return str_to_quad(addrObj) - # Probably already in the right form - return addrObj - - - -MSG_INV_ERROR = 0 -MSG_INV_TX = 1 -MSG_INV_BLOCK = 2 - - -################################################################################ -class PyMessage(object): - """ - All payload objects have a serialize and unserialize method, making them - easy to attach to PyMessage objects - """ - def __init__(self, cmd='', payload=None): - """ - Can create a message by the command name, or the payload (or neither) - """ - self.magic = MAGIC_BYTES - self.cmd = cmd - self.payload = payload - - if payload: - self.cmd = payload.command - elif cmd: - self.payload = PayloadMap[self.cmd]() - - - - def serialize(self): - bp = BinaryPacker() - bp.put(BINARY_CHUNK, self.magic, width= 4) - bp.put(BINARY_CHUNK, self.cmd.ljust(12, '\x00'), width=12) - payloadBin = self.payload.serialize() - bp.put(UINT32, len(payloadBin)) - bp.put(BINARY_CHUNK, hash256(payloadBin)[:4], width= 4) - bp.put(BINARY_CHUNK, payloadBin) - return bp.getBinaryString() - - def unserialize(self, toUnpack): - if isinstance(toUnpack, BinaryUnpacker): - msgData = toUnpack - else: - msgData = BinaryUnpacker( toUnpack ) - - - self.magic = msgData.get(BINARY_CHUNK, 4) - self.cmd = msgData.get(BINARY_CHUNK, 12).strip('\x00') - length = msgData.get(UINT32) - chksum = msgData.get(BINARY_CHUNK, 4) - payload = msgData.get(BINARY_CHUNK, length) - payload = verifyChecksum(payload, chksum) - - self.payload = PayloadMap[self.cmd]().unserialize(payload) - - if self.magic != MAGIC_BYTES: - raise NetworkIDError, 'Message has wrong network bytes!' - return self - - - def pprint(self, nIndent=0): - indstr = indent*nIndent - print '' - print indstr + 'Bitcoin-Network-Message -- ' + self.cmd.upper() - print indstr + indent + 'Magic: ' + binary_to_hex(self.magic) - print indstr + indent + 'Command: ' + self.cmd - print indstr + indent + 'Payload: ' + str(len(self.payload.serialize())) + ' bytes' - self.payload.pprint(nIndent+1) - - -################################################################################ -class PyNetAddress(object): - - def __init__(self, time=-1, svcs='0'*16, netaddrObj=[], port=-1): - """ - For our client we will ALWAYS use svcs=0 (NODE_NETWORK=0) - - time is stored as a unix timestamp - services is stored as a bitset -- a string of 16 '0's or '1's - addrObj is stored as a list/tuple of four UINT8s - port is a regular old port number... - """ - self.time = time - self.services = svcs - self.addrQuad = parseNetAddress(netaddrObj) - self.port = port - - def unserialize(self, toUnpack, hasTimeField=True): - if isinstance(toUnpack, BinaryUnpacker): - addrData = toUnpack - else: - addrData = BinaryUnpacker( toUnpack ) - - if hasTimeField: - self.time = addrData.get(UINT32) - - self.services = addrData.get(UINT64) - self.addrQuad = addrData.get(BINARY_CHUNK,16)[-4:] - self.port = addrData.get(UINT16, endianness=NETWORKENDIAN) - - self.services = int_to_bitset(self.services) - self.addrQuad = binary_to_quad(self.addrQuad) - return self - - def serialize(self, withTimeField=True): - bp = BinaryPacker() - if withTimeField: - bp.put(UINT32, self.time) - bp.put(UINT64, bitset_to_int(self.services)) - bp.put(BINARY_CHUNK, quad_to_binary(self.addrQuad).rjust(16,'\x00')) - bp.put(UINT16, self.port, endianness=NETWORKENDIAN) - return bp.getBinaryString() - - def pprint(self, nIndent=0): - indstr = indent*nIndent - print '' - print indstr + 'Network-Address:', - print indstr + indent + 'Time: ' + unixTimeToFormatStr(self.time) - print indstr + indent + 'Svcs: ' + self.services - print indstr + indent + 'IPv4: ' + quad_to_str(self.addrQuad) - print indstr + indent + 'Port: ' + self.port - - def pprintShort(self): - print quad_to_str(self.addrQuad) + ':' + str(self.port) - -################################################################################ -################################################################################ -class PayloadAddr(object): - - command = 'addr' - - def __init__(self, addrList=[]): - self.addrList = addrList # PyNetAddress objs - - def unserialize(self, toUnpack): - if isinstance(toUnpack, BinaryUnpacker): - addrData = toUnpack - else: - addrData = BinaryUnpacker( toUnpack ) - - self.addrList = [] - naddr = addrData.get(VAR_INT) - for i in range(naddr): - self.addrList.append( PyNetAddress().unserialize(addrData) ) - return self - - def serialize(self): - bp = BinaryPacker() - bp.put(VAR_INT, len(self.addrList)) - for netaddr in self.addrList: - bp.put(BINARY_CHUNK, netaddr.serialize(), width=30) - return bp.getBinaryString() - - def pprint(self, nIndent=0): - indstr = indent*nIndent - print '' - print indstr + 'Message(addr):', - for a in self.addrList: - a.pprintShort() - - def pprintShort(self): - for a in self.addrList: - print '[' + quad_to_str(a.pprintShort()) + '], ' - -################################################################################ -################################################################################ -class PayloadPing(object): - """ - All payload objects have a serialize and unserialize method, making them - easy to attach to PyMessage objects - """ - command = 'ping' - - def __init__(self): - pass - - def unserialize(self, toUnpack): - return self - - def serialize(self): - return '' - - def pprint(self, nIndent=0): - indstr = indent*nIndent - print '' - print indstr + 'Message(ping)' - - - -################################################################################ -################################################################################ -class PayloadVersion(object): - - command = 'version' - - def __init__(self, version=0, svcs='0'*16, tstamp=-1, addrRcv=PyNetAddress(), \ - addrFrm=PyNetAddress(), nonce=-1, sub=-1, height=-1): - self.version = version - self.services = svcs - self.time = tstamp - self.addrRecv = addrRcv - self.addrFrom = addrFrm - self.nonce = nonce - self.subver = sub - self.height0 = height - - def unserialize(self, toUnpack): - if isinstance(toUnpack, BinaryUnpacker): - verData = toUnpack - else: - verData = BinaryUnpacker( toUnpack ) - - self.version = verData.get(INT32) - self.services = int_to_bitset(verData.get(UINT64), widthBytes=8) - self.time = verData.get(INT64) - self.addrRecv = PyNetAddress().unserialize(verData, hasTimeField=False) - self.addrFrom = PyNetAddress().unserialize(verData, hasTimeField=False) - self.nonce = verData.get(UINT64) - self.subver = verData.get(VAR_STR) - self.height0 = verData.get(INT32) - return self - - def serialize(self): - bp = BinaryPacker() - bp.put(INT32, self.version ) - bp.put(UINT64, bitset_to_int(self.services)) - bp.put(INT64, self.time ) # todo, should this really be int64? - bp.put(BINARY_CHUNK, self.addrRecv.serialize(withTimeField=False)) - bp.put(BINARY_CHUNK, self.addrFrom.serialize(withTimeField=False)) - bp.put(UINT64, self.nonce ) - bp.put(VAR_STR, self.subver ) - bp.put(INT32, self.height0 ) - return bp.getBinaryString() - - def pprint(self, nIndent=0): - indstr = indent*nIndent - print '' - print indstr + 'Message(version):' - print indstr + indent + 'Version: ' + str(self.version) - print indstr + indent + 'Services: ' + self.services - print indstr + indent + 'Time: ' + unixTimeToFormatStr(self.time) - print indstr + indent + 'AddrTo: ',; self.addrRecv.pprintShort() - print indstr + indent + 'AddrFrom:',; self.addrFrom.pprintShort() - print indstr + indent + 'Nonce: ' + str(self.nonce) - print indstr + indent + 'SubVer: ', self.subver - print indstr + indent + 'StartHgt: ' + str(self.height0) - -################################################################################ -class PayloadVerack(object): - """ - All payload objects have a serialize and unserialize method, making them - easy to attach to PyMessage objects - """ - - command = 'verack' - - def __init__(self): - pass - - def unserialize(self, toUnpack): - return self - - def serialize(self): - return '' - - def pprint(self, nIndent=0): - indstr = indent*nIndent - print '' - print indstr + 'Message(verack)' - - -################################################################################ -################################################################################ -class PayloadInv(object): - """ - All payload objects have a serialize and unserialize method, making them - easy to attach to PyMessage objects - """ - - command = 'inv' - - def __init__(self): - self.invList = [] # list of (type, hash) pairs - - def unserialize(self, toUnpack): - if isinstance(toUnpack, BinaryUnpacker): - invData = toUnpack - else: - invData = BinaryUnpacker( toUnpack ) - - numInv = invData.get(VAR_INT) - for i in range(numInv): - invType = invData.get(UINT32) - invHash = invData.get(BINARY_CHUNK, 32) - self.invList.append( [invType, invHash] ) - return self - - def serialize(self): - bp = BinaryPacker() - bp.put(VAR_INT, len(self.invList)) - for inv in self.invList: - bp.put(UINT32, inv[0]) - bp.put(BINARY_CHUNK, inv[1], width=32) - return bp.getBinaryString() - - - def pprint(self, nIndent=0): - indstr = indent*nIndent - print '' - print indstr + 'Message(inv):' - for inv in self.invList: - print indstr + indent + ('BLOCK: ' if inv[0]==2 else 'TX : ') + \ - binary_to_hex(inv[1]) - - - -################################################################################ -################################################################################ -class PayloadGetData(object): - """ - All payload objects have a serialize and unserialize method, making them - easy to attach to PyMessage objects - """ - - command = 'getdata' - - def __init__(self, invList=[]): - if invList: - self.invList = invList - else: - self.invList = [] - - - def unserialize(self, toUnpack): - if isinstance(toUnpack, BinaryUnpacker): - invData = toUnpack - else: - invData = BinaryUnpacker( toUnpack ) - - numInv = invData.get(VAR_INT) - for i in range(numInv): - invType = invData.get(UINT32) - invHash = invData.get(BINARY_CHUNK, 32) - self.invList.append( [invType, invHash] ) - return self - - def serialize(self): - bp = BinaryPacker() - bp.put(VAR_INT, len(self.invList)) - for inv in self.invList: - bp.put(UINT32, inv[0]) - bp.put(BINARY_CHUNK, inv[1], width=32) - return bp.getBinaryString() - - - def pprint(self, nIndent=0): - indstr = indent*nIndent - print '' - print indstr + 'Message(getdata):' - for inv in self.invList: - print indstr + indent + ('BLOCK: ' if inv[0]==2 else 'TX : ') + \ - binary_to_hex(inv[1]) - - -################################################################################ -################################################################################ -class PayloadGetHeaders(object): - command = 'getheaders' - - def __init__(self, hashStartList=[], hashStop=''): - self.version = 1 - self.hashList = hashStartList - self.hashStop = hashStop - - - def unserialize(self, toUnpack): - if isinstance(toUnpack, BinaryUnpacker): - ghData = toUnpack - else: - ghData = BinaryUnpacker( toUnpack ) - - self.version = gbData.get(UINT32) - nhash = ghData.get(VAR_INT) - for i in range(nhash): - self.hashList.append(ghData.get(BINARY_CHUNK, 32)) - self.hashStop = ghData.get(BINARY_CHUNK, 32) - return self - - def serialize(self): - nhash = len(self.hashList) - bp = BinaryPacker() - bp.put(UINT32, self.version) - bp.put(VAR_INT, nhash) - for i in range(nhash): - bp.put(BINARY_CHUNK, self.hashList[i], width=32) - bp.put(BINARY_CHUNK, self.hashStop, width=32) - return bp.getBinaryString() - - def pprint(self, nIndent=0): - indstr = indent*nIndent - print '' - print indstr + 'Message(getheaders):' - print indstr + indent + 'HashList(s) :' + binary_to_hex(self.hashList[0]) - for i in range(1,len(self.hashList)): - print indstr + indent + ' :' + binary_to_hex(self.hashList[i]) - print indstr + indent + 'HashStop :' + binary_to_hex(self.hashStop) - - - -################################################################################ -################################################################################ -class PayloadGetBlocks(object): - command = 'getblocks' - - def __init__(self, version=1, startCt=-1, hashStartList=[], hashStop=''): - self.version = 1 - self.hashList = hashStartList - self.hashStop = hashStop - - - def unserialize(self, toUnpack): - if isinstance(toUnpack, BinaryUnpacker): - gbData = toUnpack - else: - gbData = BinaryUnpacker( toUnpack ) - - self.version = gbData.get(UINT32) - nhash = gbData.get(VAR_INT) - for i in range(nhash): - self.hashList.append(gbData.get(BINARY_CHUNK, 32)) - self.hashStop = gbData.get(BINARY_CHUNK, 32) - return self - - def serialize(self): - nhash = len(self.hashList) - bp = BinaryPacker() - bp.put(UINT32, self.version) - bp.put(VAR_INT, nhash) - for i in range(nhash): - bp.put(BINARY_CHUNK, self.hashList[i], width=32) - bp.put(BINARY_CHUNK, self.hashList, width=32) - return bp.getBinaryString() - - def pprint(self, nIndent=0): - indstr = indent*nIndent - print '' - print indstr + 'Message(getheaders):' - print indstr + indent + 'Version :' + str(self.version) - print indstr + indent + 'HashList(s) :' + binary_to_hex(self.hashList[0]) - for i in range(1,len(self.hashList)): - print indstr + indent + ' :' + binary_to_hex(self.hashList[i]) - print indstr + indent + 'HashStop :' + binary_to_hex(self.hashStop) - - -################################################################################ -################################################################################ -class PayloadTx(object): - command = 'tx' - - def __init__(self, tx=PyTx()): - self.tx = tx - - def unserialize(self, toUnpack): - self.tx.unserialize(toUnpack) - return self - - def serialize(self): - return self.tx.serialize() - - def pprint(self, nIndent=0): - indstr = indent*nIndent - print '' - print indstr + 'Message(tx):' - self.tx.pprint(nIndent+1) - - -################################################################################ -################################################################################ -class PayloadHeaders(object): - command = 'headers' - - def __init__(self, header=PyBlockHeader(), headerlist=[]): - self.header = header - self.headerList = headerlist - - - def unserialize(self, toUnpack): - if isinstance(toUnpack, BinaryUnpacker): - headerData = toUnpack - else: - headerData = BinaryUnpacker( toUnpack ) - - self.headerList = [] - self.header.unserialize(headerData) - numHeader = headerData.get(VAR_INT) - for i in range(numHeader): - self.headerList.append(PyBlockHeader().unserialize(headerData)) - headerData.get(VAR_INT) # Not sure if this is even used, ever - return self - - def serialize(self): - bp = BinaryPacker() - bp.put(BINARY_CHUNK, self.header.serialize()) - bp.put(VAR_INT, len(self.headerList)) - for header in self.headerList: - bp.put(BINARY_CHUNK, header.serialize()) - bp.put(VAR_INT, 0) - return bp.getBinaryString() - - def pprint(self, nIndent=0): - indstr = indent*nIndent - print '' - print indstr + 'Message(headers):' - self.header.pprint(nIndent+1) - for header in self.headerList: - print indstr + indent + 'Header:', header.getHash() - - -################################################################################ -################################################################################ -class PayloadBlock(object): - command = 'block' - - def __init__(self, header=PyBlockHeader(), txlist=[]): - self.header = header - self.txList = txlist - - - def unserialize(self, toUnpack): - if isinstance(toUnpack, BinaryUnpacker): - blkData = toUnpack - else: - blkData = BinaryUnpacker( toUnpack ) - - self.txList = [] - self.header.unserialize(blkData) - numTx = blkData.get(VAR_INT) - for i in range(numTx): - self.txList.append(PyTx().unserialize(blkData)) - return self - - def serialize(self): - bp = BinaryPacker() - bp.put(BINARY_CHUNK, self.header.serialize()) - bp.put(VAR_INT, len(self.txList)) - for tx in self.txList: - bp.put(BINARY_CHUNK, tx.serialize()) - return bp.getBinaryString() - - def pprint(self, nIndent=0): - indstr = indent*nIndent - print '' - print indstr + 'Message(block):' - self.header.pprint(nIndent+1) - for tx in self.txList: - print indstr + indent + 'Tx:', tx.getHashHex() - - -################################################################################ -class PayloadAlert(object): - command = 'alert' - - def __init__(self): - self.version = 1 - self.relayUntil = 0 - self.expiration = 0 - self.uniqueID = 0 - self.cancelVal = 0 - self.cancelSet = [] - self.minVersion = 0 - self.maxVersion = 0 - self.subVerSet = [] - self.comment = '' - self.statusBar = '' - self.reserved = '' - self.signature = '' - - - def unserialize(self, toUnpack): - if isinstance(toUnpack, BinaryUnpacker): - blkData = toUnpack - else: - blkData = BinaryUnpacker( toUnpack ) - - return self - - def serialize(self): - bp = BinaryPacker() - return bp.getBinaryString() - - - def pprint(self, nIndent=0): - print nIndent*'\t' + 'ALERT(...)' - -################################################################################ -# Use this map to figure out which object to serialize/unserialize from a cmd -PayloadMap = { - 'ping': PayloadPing, - 'tx': PayloadTx, - 'inv': PayloadInv, - 'version': PayloadVersion, - 'verack': PayloadVerack, - 'addr': PayloadAddr, - 'getdata': PayloadGetData, - 'getheaders': PayloadGetHeaders, - 'getblocks': PayloadGetBlocks, - 'block': PayloadBlock, - 'headers': PayloadHeaders, - 'alert': PayloadAlert } - - - - - -try: - from twisted.internet.protocol import Protocol, ReconnectingClientFactory - from twisted.internet.defer import Deferred -except ImportError: - LOGERROR('***Python-Twisted is not installed -- cannot enable') - LOGERROR(' networking-related methods for ArmoryEngine' ) - - -################################################################################ -def forceDeferred(callbk): - if callbk: - if isinstance(callbk, Deferred): - return callbk - else: - d = Deferred() - d.addCallback(callbk) - - -################################################################################ -# It seems we need to do this frequently when downloading headers & blocks -# This only returns a list of numbers, but one list-comprehension to get hashes -def createBlockLocatorNumList(topblk): - blockNumList = [] - n,step,niter = topblk,1,0 - while n>0: - blockNumList.append(n) - if niter >= 10: - step *= 2 - n -= step - niter += 1 - blockNumList.append(0) - return blockNumList - -################################################################################ -# -# Armory Networking: -# -# This is where I will define all the network operations needed for -# Armory to operate, using python-twisted. There are "better" -# ways to do this with "reusable" code structures (i.e. using huge -# deferred callback chains), but this is not the central "creative" -# part of the Bitcoin protocol. I need just enough to broadcast tx -# and receive new tx that aren't in the blockchain yet. Beyond that, -# I'll just be ignoring everything else. -# -################################################################################ -class ArmoryClient(Protocol): - """ - This is where all the Bitcoin-specific networking stuff goes. - In the Twisted way, you need to inject your own chains of - callbacks through the factory in order to get this class to do - the right thing on the various events. - """ - - ############################################################ - def __init__(self): - self.recvData = '' - self.handshakeFinished = False - self.sentHeadersReq = True - self.peer = [] - - ############################################################ - def connectionMade(self): - """ - Construct the initial version message and send it right away. - Everything else will be handled by dataReceived. - """ - LOGINFO('Connection initiated. Start handshake') - addrTo = str_to_quad(self.transport.getPeer().host) - portTo = self.transport.getPeer().port - addrFrom = str_to_quad(self.transport.getHost().host) - portFrom = self.transport.getHost().port - - self.peer = [addrTo, portTo] - - services = '0'*16 - msgVersion = PayloadVersion() - msgVersion.version = 40000 # TODO: this is what my Satoshi client says - msgVersion.services = services - msgVersion.time = long(RightNow()) - msgVersion.addrRecv = PyNetAddress(0, services, addrTo, portTo ) - msgVersion.addrFrom = PyNetAddress(0, services, addrFrom, portFrom) - msgVersion.nonce = random.randint(2**60, 2**64-1) - msgVersion.subver = 'Armory:%s' % getVersionString(BTCARMORY_VERSION) - msgVersion.height0 = -1 - self.sendMessage( msgVersion ) - self.factory.func_madeConnect() - - - ############################################################ - def dataReceived(self, data): - """ - Called by the reactor when data is received over the connection. - This method will do nothing if we don't receive a full message. - """ - - - #print '\n\nData Received:', - #pprintHex(binary_to_hex(data), withAddr=False) - - # Put the current buffer into an unpacker, process until empty - self.recvData += data - buf = BinaryUnpacker(self.recvData) - - messages = [] - while True: - try: - # recvData is only modified if the unserialize succeeds - # Had a serious issue with references, so I had to convert - # messages to strings to guarantee that copies were being - # made! (yes, hacky...) - thisMsg = PyMessage().unserialize(buf) - messages.append( thisMsg.serialize() ) - self.recvData = buf.getRemainingString() - except NetworkIDError: - LOGERROR('Message for a different network!' ) - if BLOCKCHAINS.has_key(self.recvData[:4]): - LOGERROR( '(for network: %s)', BLOCKCHAINS[self.recvData[:4]]) - # Before raising the error, we should've finished reading the msg - # So pop it off the front of the buffer - self.recvData = buf.getRemainingString() - return - except UnpackerError: - # Expect this error when buffer isn't full enough for a whole msg - break - - # We might've gotten here without anything to process -- if so, bail - if len(messages)==0: - return - - - # Finally, we have some message to process, let's do it - for msgStr in messages: - msg = PyMessage().unserialize(msgStr) - cmd = msg.cmd - - # Log the message if netlog option - if CLI_OPTIONS.netlog: - LOGDEBUG( 'DataReceived: %s', msg.payload.command) - if msg.payload.command == 'tx': - LOGDEBUG('\t' + binary_to_hex(msg.payload.tx.thisHash)) - elif msg.payload.command == 'block': - LOGDEBUG('\t' + msg.payload.header.getHashHex()) - elif msg.payload.command == 'inv': - for inv in msg.payload.invList: - LOGDEBUG(('\tBLOCK: ' if inv[0]==2 else '\tTX : ') + \ - binary_to_hex(inv[1])) - - - # We process version and verackk regardless of handshakeFinished - if cmd=='version' and not self.handshakeFinished: - self.peerInfo = {} - self.peerInfo['version'] = msg.payload.version - self.peerInfo['subver'] = msg.payload.subver - self.peerInfo['time'] = msg.payload.time - self.peerInfo['height'] = msg.payload.height0 - LOGINFO('Received version message from peer:') - LOGINFO(' Version: %s', str(self.peerInfo['version'])) - LOGINFO(' SubVersion: %s', str(self.peerInfo['subver'])) - LOGINFO(' TimeStamp: %s', str(self.peerInfo['time'])) - LOGINFO(' StartHeight: %s', str(self.peerInfo['height'])) - self.sendMessage( PayloadVerack() ) - elif cmd=='verack': - self.handshakeFinished = True - self.factory.handshakeFinished(self) - #self.startHeaderDL() - - #################################################################### - # Don't process any other messages unless the handshake is finished - if self.handshakeFinished: - self.processMessage(msg) - - - ############################################################ - #def connectionLost(self, reason): - #""" - #Try to reopen connection (not impl yet) - #""" - #self.factory.connectionFailed(self, reason) - - - ############################################################ - def processMessage(self, msg): - # TODO: when I start expanding this class to be more versatile, - # I'll consider chaining/setting callbacks from the calling - # application. For now, it's pretty static. - #msg.payload.pprint(nIndent=2) - if msg.cmd=='inv': - invobj = msg.payload - getdataMsg = PyMessage('getdata') - for inv in invobj.invList: - if inv[0]==MSG_INV_BLOCK: - if TheBDM.getBDMState()=='Scanning' or \ - TheBDM.hasHeaderWithHash(inv[1]): - continue - getdataMsg.payload.invList.append(inv) - if inv[0]==MSG_INV_TX: - if TheBDM.getBDMState()=='Scanning' or \ - TheBDM.hasTxWithHash(inv[1]): - continue - getdataMsg.payload.invList.append(inv) - - # Now send the full request - if not TheBDM.getBDMState()=='Scanning': - self.sendMessage(getdataMsg) - - if msg.cmd=='tx': - pytx = msg.payload.tx - self.factory.func_newTx(pytx) - if msg.cmd=='block': - pyHeader = msg.payload.header - pyTxList = msg.payload.txList - LOGINFO('Received new block. %s', binary_to_hex(pyHeader.getHash(), BIGENDIAN)) - self.factory.func_newBlock(pyHeader, pyTxList) - - - - ############################################################ - def startHeaderDL(self): - numList = self.createBlockLocatorNumList(self.topBlk) - msg = PyMessage('getheaders') - msg.payload.version = 1 - msg.payload.hashList = [getHeaderByHeight(i).getHash() for i in numList] - msg.payload.hashStop = '\x00'*32 - - self.sentHeadersReq = True - - - - ############################################################ - def startBlockDL(self): - numList = self.createBlockLocatorNumList(self.topBlk) - msg = PyMessage('getblocks') - msg.payload.version = 1 - msg.payload.hashList = [getHeaderByHeight(i).getHash() for i in numList] - msg.payload.hashStop = '\x00'*32 - - - ############################################################ - def sendMessage(self, msg): - """ - Must pass in a PyMessage, or one of the Payload types, which - will be converted to a PyMessage -- and then sent to the peer. - If you have a fully-serialized message (with header) already, - easy enough to user PyMessage().unserialize(binMsg) - """ - - if isinstance(msg, PyMessage): - #print '\n\nSending Message:', msg.payload.command.upper() - #pprintHex(binary_to_hex(msg.serialize()), indent=' ') - if CLI_OPTIONS.netlog: - LOGDEBUG( 'SendMessage: %s', msg.payload.command) - LOGRAWDATA( msg.serialize() ) - self.transport.write(msg.serialize()) - else: - msg = PyMessage(payload=msg) - #print '\n\nSending Message:', msg.payload.command.upper() - #pprintHex(binary_to_hex(msg.serialize()), indent=' ') - if CLI_OPTIONS.netlog: - LOGDEBUG( 'SendMessage: %s', msg.payload.command) - LOGRAWDATA( msg.serialize() ) - self.transport.write(msg.serialize()) - - - ############################################################ - def sendTx(self, txObj): - """ - This is a convenience method for the special case of sending - a locally-constructed transaction. Pass in either a PyTx - object, or a binary serialized tx. It will be converted to - a PyMessage and forwarded to our peer(s) - """ - LOGINFO('sendTx called...') - if isinstance(txObj, PyMessage): - self.sendMessage( txObj ) - elif isinstance(txObj, PyTx): - self.sendMessage( PayloadTx(txObj)) - elif isinstance(txObj, str): - self.sendMessage( PayloadTx(PyTx().unserialize(txObj)) ) - - - - - - - - -################################################################################ -################################################################################ -class ArmoryClientFactory(ReconnectingClientFactory): - """ - Spawns Protocol objects used for communicating over the socket. All such - objects (ArmoryClients) can share information through this factory. - However, at the moment, this class is designed to only create a single - connection -- to localhost. - """ - protocol = ArmoryClient - lastAlert = 0 - - ############################################################################# - def __init__(self, \ - def_handshake=None, \ - func_loseConnect=(lambda: None), \ - func_madeConnect=(lambda: None), \ - func_newTx=(lambda x: None), \ - func_newBlock=(lambda x,y: None)): - """ - Initialize the ReconnectingClientFactory with a deferred for when the handshake - finishes: there should be only one handshake, and thus one firing - of the handshake-finished callback - """ - self.lastAlert = 0 - self.deferred_handshake = forceDeferred(def_handshake) - self.fileMemPool = os.path.join(ARMORY_HOME_DIR, 'mempool.bin') - - # All other methods will be regular callbacks: we plan to have a very - # static set of behaviors for each message type - # (NOTE: The logic for what I need right now is so simple, that - # I finished implementing it in a few lines of code. When I - # need to expand the versatility of this class, I'll start - # doing more OOP/deferreds/etc - self.func_loseConnect = func_loseConnect - self.func_madeConnect = func_madeConnect - self.func_newTx = func_newTx - self.func_newBlock = func_newBlock - self.proto = None - - - - ############################################################################# - def addTxToMemoryPool(self, pytx): - if not TheBDM.getBDMState()=='Offline': - TheBDM.addNewZeroConfTx(pytx.serialize(), long(RightNow()), True) - - - - ############################################################################# - def handshakeFinished(self, protoObj): - LOGINFO('Handshake finished, connection open!') - self.proto = protoObj - if self.deferred_handshake: - d, self.deferred_handshake = self.deferred_handshake, None - d.callback(protoObj) - - - - ############################################################################# - def clientConnectionLost(self, connector, reason): - LOGERROR('***Connection to Satoshi client LOST! Attempting to reconnect...') - self.func_loseConnect() - ReconnectingClientFactory.clientConnectionLost(self,connector,reason) - - - - ############################################################################# - def connectionFailed(self, protoObj, reason): - """ - This method needs some serious work... I don't quite know yet how - to reopen the connection... and I'll need to copy the Deferred so - that it is ready for the next connection failure - """ - LOGERROR('***Initial connection to Satoshi client failed! Retrying...') - ReconnectingClientFactory.connectionFailed(self, protoObj, reason) - - - - - ############################################################################# - def sendTx(self, pytxObj): - if self.proto: - self.proto.sendTx(pytxObj) - else: - raise ConnectionError, 'Connection to localhost DNE.' - - - ############################################################################# - def sendMessage(self, msgObj): - if self.proto: - self.proto.sendMessage(msgObj) - else: - raise ConnectionError, 'Connection to localhost DNE.' - - - - -class FakeClientFactory(ReconnectingClientFactory): - """ - A fake class that has the same methods as an ArmoryClientFactory, - but doesn't do anything. If there is no internet, then we want - to be able to use the same calls - """ - ############################################################################# - def __init__(self, \ - def_handshake=None, \ - func_loseConnect=(lambda: None), \ - func_madeConnect=(lambda: None), \ - func_newTx=(lambda x: None), \ - func_newBlock=(lambda x,y: None)): pass - def addTxToMemoryPool(self, pytx): pass - def handshakeFinished(self, protoObj): pass - def clientConnectionLost(self, connector, reason): pass - def connectionFailed(self, protoObj, reason): pass - def sendTx(self, pytxObj): pass - - - - - -############################################################################# -import socket -def satoshiIsAvailable(host='127.0.0.1', port=BITCOIN_PORT, timeout=0.01): - - if not isinstance(port, (list,tuple)): - port = [port] - - for p in port: - s = socket.socket() - s.settimeout(timeout) # Most of the time checking localhost -- FAST - try: - s.connect((host, p)) - s.close() - return p - except: - pass - - return 0 - - -################################################################################ -def extractSignedDataFromVersionsDotTxt(wholeFile, doVerify=True): - """ - This method returns a pair: a dictionary to lookup link by OS, and - a formatted string that is sorted by OS, and re-formatted list that - will hash the same regardless of original format or ordering - """ - - msgBegin = wholeFile.find('# -----BEGIN-SIGNED-DATA-') - msgBegin = wholeFile.find('\n', msgBegin+1) + 1 - msgEnd = wholeFile.find('# -----SIGNATURE---------') - sigBegin = wholeFile.find('\n', msgEnd+1) + 3 - sigEnd = wholeFile.find('# -----END-SIGNED-DATA---') - - MSGRAW = wholeFile[msgBegin:msgEnd] - SIGHEX = wholeFile[sigBegin:sigEnd].strip() - - if -1 in [msgBegin,msgEnd,sigBegin,sigEnd]: - LOGERROR('No signed data block found') - return '' - - - if doVerify: - Pub = SecureBinaryData(hex_to_binary(ARMORY_INFO_SIGN_PUBLICKEY)) - Msg = SecureBinaryData(MSGRAW) - Sig = SecureBinaryData(hex_to_binary(SIGHEX)) - isVerified = CryptoECDSA().VerifyData(Msg, Sig, Pub) - - if not isVerified: - LOGERROR('Signed data block failed verification!') - return '' - else: - LOGINFO('Signature on signed data block is GOOD!') - - return MSGRAW - - -################################################################################ -def parseLinkList(theData): - """ - Plug the verified data into here... - """ - DLDICT,VERDICT = {},{} - sectStr = None - for line in theData.split('\n'): - pcs = line[1:].split() - if line.startswith('# SECTION-') and 'INSTALLERS' in line: - sectStr = pcs[0].split('-')[-1] - if not sectStr in DLDICT: - DLDICT[sectStr] = {} - VERDICT[sectStr] = '' - if len(pcs)>1: - VERDICT[sectStr] = pcs[-1] - continue - - if len(pcs)==3 and pcs[1].startswith('http'): - DLDICT[sectStr][pcs[0]] = pcs[1:] - - return DLDICT,VERDICT - - - - - -################################################################################ -# jgarzik'sjj jsonrpc-bitcoin code -- stupid-easy to talk to bitcoind -from jsonrpc import ServiceProxy, authproxy -class SatoshiDaemonManager(object): - """ - Use an existing implementation of bitcoind - """ - - class BitcoindError(Exception): pass - class BitcoindNotAvailableError(Exception): pass - class BitcoinDotConfError(Exception): pass - class SatoshiHomeDirDNE(Exception): pass - class ConfigFileUserDNE(Exception): pass - class ConfigFilePwdDNE(Exception): pass - - - ############################################################################# - def __init__(self): - self.executable = None - self.satoshiHome = None - self.bitconf = {} - self.proxy = None - self.bitcoind = None - self.isMidQuery = False - self.last20queries = [] - self.disabled = False - self.failedFindExe = False - self.failedFindHome = False - self.foundExe = [] - self.circBufferState = [] - self.circBufferTime = [] - self.btcOut = None - self.btcErr = None - self.lastTopBlockInfo = { \ - 'numblks': -1, - 'tophash': '', - 'toptime': -1, - 'error': 'Uninitialized', - 'blkspersec': -1 } - - - - ############################################################################# - def setupSDM(self, pathToBitcoindExe=None, satoshiHome=BTC_HOME_DIR, \ - extraExeSearch=[], createHomeIfDNE=True): - LOGDEBUG('Exec setupSDM') - self.failedFindExe = False - self.failedFindHome = False - # If we are supplied a path, then ignore the extra exe search paths - if pathToBitcoindExe==None: - pathToBitcoindExe = self.findBitcoind(extraExeSearch) - if len(pathToBitcoindExe)==0: - LOGDEBUG('Failed to find bitcoind') - self.failedFindExe = True - else: - LOGINFO('Found bitcoind in the following places:') - for p in pathToBitcoindExe: - LOGINFO(' %s', p) - pathToBitcoindExe = pathToBitcoindExe[0] - LOGINFO('Using: %s', pathToBitcoindExe) - - if not os.path.exists(pathToBitcoindExe): - LOGINFO('Somehow failed to find exe even after finding it...?') - self.failedFindExe = True - - self.executable = pathToBitcoindExe - - if not os.path.exists(satoshiHome): - if createHomeIfDNE: - LOGINFO('Making satoshi home dir') - os.makedirs(satoshiHome) - else: - LOGINFO('No home dir, makedir not requested') - self.failedFindHome = True - - if self.failedFindExe: raise self.BitcoindError, 'bitcoind not found' - if self.failedFindHome: raise self.BitcoindError, 'homedir not found' - - self.satoshiHome = satoshiHome - self.disabled = False - self.proxy = None - self.bitcoind = None # this will be a Popen object - self.isMidQuery = False - self.last20queries = [] - - self.readBitcoinConf(makeIfDNE=True) - - - - - - ############################################################################# - def setDisabled(self, newBool=True): - s = self.getSDMState() - - if newBool==True: - if s in ('BitcoindInitializing', 'BitcoindSynchronizing', 'BitcoindReady'): - self.stopBitcoind() - - self.disabled = newBool - - - ############################################################################# - def getAllFoundExe(self): - return list(self.foundExe) - - - ############################################################################# - def findBitcoind(self, extraSearchPaths=[]): - self.foundExe = [] - - searchPaths = list(extraSearchPaths) # create a copy - - if OS_WINDOWS: - # First check desktop for links - possBaseDir = [] - home = os.path.expanduser('~') - desktop = os.path.join(home, 'Desktop') - - if os.path.exists(desktop): - dtopfiles = os.listdir(desktop) - for path in [os.path.join(desktop, fn) for fn in dtopfiles]: - if 'bitcoin' in path.lower() and path.lower().endswith('.lnk'): - import win32com.client - shell = win32com.client.Dispatch('WScript.Shell') - targ = shell.CreateShortCut(path).Targetpath - targDir = os.path.dirname(targ) - LOGINFO('Found Bitcoin-Qt link on desktop: %s', targDir) - possBaseDir.append( targDir ) - - # Also look in default place in ProgramFiles dirs - possBaseDir.append(os.getenv('PROGRAMFILES')) - if SystemSpecs.IsX64: - possBaseDir.append(os.getenv('PROGRAMFILES(X86)')) - - - # Now look at a few subdirs of the - searchPaths.extend(possBaseDir) - searchPaths.extend([os.path.join(p, 'Bitcoin', 'daemon') for p in possBaseDir]) - searchPaths.extend([os.path.join(p, 'daemon') for p in possBaseDir]) - searchPaths.extend([os.path.join(p, 'Bitcoin') for p in possBaseDir]) - - for p in searchPaths: - testPath = os.path.join(p, 'bitcoind.exe') - if os.path.exists(testPath): - self.foundExe.append(testPath) - - else: - # In case this was a downloaded copy, make sure we traverse to bin/64 dir - if SystemSpecs.IsX64: - searchPaths.extend([os.path.join(p, 'bin/64') for p in extraSearchPaths]) - else: - searchPaths.extend([os.path.join(p, 'bin/32') for p in extraSearchPaths]) - - searchPaths.extend(['/usr/bin/', '/usr/lib/bitcoin/']) - - for p in searchPaths: - testPath = os.path.join(p, 'bitcoind') - if os.path.exists(testPath): - self.foundExe.append(testPath) - - try: - locs = subprocess_check_output(['whereis','bitcoind']).split() - if len(locs)>1: - locs = filter(lambda x: os.path.basename(x)=='bitcoind', locs) - LOGINFO('"whereis" returned: %s', str(locs)) - self.foundExe.extend(locs) - except: - LOGEXCEPT('Error executing "whereis" command') - - - # For logging purposes, check that the first answer matches one of the - # extra search paths. There should be some kind of notification that - # their supplied search path was invalid and we are using something else. - if len(self.foundExe)>0 and len(extraSearchPaths)>0: - foundIt = False - for p in extraSearchPaths: - if self.foundExe[0].startswith(p): - foundIt=True - - if not foundIt: - LOGERROR('Bitcoind could not be found in the specified installation:') - for p in extraSearchPaths: - LOGERROR(' %s', p) - LOGERROR('Bitcoind is being started from:') - LOGERROR(' %s', self.foundExe[0]) - - return self.foundExe - - ############################################################################# - def getGuardianPath(self): - if OS_WINDOWS: - armoryInstall = os.path.dirname(inspect.getsourcefile(SatoshiDaemonManager)) - # This should return a zip file because of py2exe - if armoryInstall.endswith('.zip'): - armoryInstall = os.path.dirname(armoryInstall) - gpath = os.path.join(armoryInstall, 'guardian.exe') - else: - theDir = os.path.dirname(inspect.getsourcefile(SatoshiDaemonManager)) - gpath = os.path.join(theDir, 'guardian.py') - - if not os.path.exists(gpath): - LOGERROR('Could not find guardian script: %s', gpath) - raise FileExistsError - return gpath - - - - - ############################################################################# - def readBitcoinConf(self, makeIfDNE=False): - LOGINFO('Reading bitcoin.conf file') - bitconf = os.path.join( self.satoshiHome, 'bitcoin.conf' ) - if not os.path.exists(bitconf): - if not makeIfDNE: - raise self.BitcoinDotConfError, 'Could not find bitcoin.conf' - else: - LOGINFO('No bitcoin.conf available. Creating it...') - touchFile(bitconf) - - # Guarantee that bitcoin.conf file has very strict permissions - if OS_WINDOWS: - if OS_VARIANT[0].lower()=='xp': - LOGERROR('Cannot set permissions correctly in XP!') - LOGERROR('Please confirm permissions on the following file ') - LOGERROR('are set to exclusive access only for your user ') - LOGERROR('(it usually is, but Armory cannot guarantee it ') - LOGERROR('on XP systems):') - LOGERROR(' %s', bitconf) - else: - LOGINFO('Setting permissions on bitcoin.conf') - import win32api - username = win32api.GetUserName() - cmd_icacls = ['icacls',bitconf,'/inheritance:r','/grant:r', '%s:F' % username] - icacls_out = subprocess_check_output(cmd_icacls, shell=True) - LOGINFO('icacls returned: %s', icacls_out) - else: - LOGINFO('Setting permissions on bitcoin.conf') - os.chmod(bitconf, stat.S_IRUSR | stat.S_IWUSR) - - - with open(bitconf,'r') as f: - # Find the last character of the each line: either a newline or '#' - endchr = lambda line: line.find('#') if line.find('#')>1 else len(line) - - # Reduce each line to a list of key,value pairs separated with '=' - allconf = [l[:endchr(l)].strip().split('=') for l in f.readlines()] - - # Need to convert to (x[0],x[1:]) in case the password has '=' in it - allconfPairs = [[x[0], '='.join(x[1:])] for x in allconf if len(x)>1] - - # Convert the list of pairs to a dictionary - self.bitconf = dict(allconfPairs) - - - # Look for rpcport, use default if not there - self.bitconf['rpcport'] = int(self.bitconf.get('rpcport', BITCOIN_RPC_PORT)) - - # We must have a username and password. If not, append to file - if not self.bitconf.has_key('rpcuser'): - LOGDEBUG('No rpcuser: creating one') - with open(bitconf,'a') as f: - f.write('\n') - f.write('rpcuser=generated_by_armory\n') - self.bitconf['rpcuser'] = 'generated_by_armory' - - if not self.bitconf.has_key('rpcpassword'): - LOGDEBUG('No rpcpassword: creating one') - with open(bitconf,'a') as f: - randBase58 = SecureBinaryData().GenerateRandom(32).toBinStr() - randBase58 = binary_to_base58(randBase58) - f.write('\n') - f.write('rpcpassword=%s' % randBase58) - self.bitconf['rpcpassword'] = randBase58 - - - if not isASCII(self.bitconf['rpcuser']): - LOGERROR('Non-ASCII character in bitcoin.conf (rpcuser)!') - if not isASCII(self.bitconf['rpcpassword']): - LOGERROR('Non-ASCII character in bitcoin.conf (rpcpassword)!') - - self.bitconf['host'] = '127.0.0.1' - - - - ############################################################################# - def startBitcoind(self): - self.btcOut, self.btcErr = None,None - if self.disabled: - LOGERROR('SDM was disabled, must be re-enabled before starting') - return - - LOGINFO('Called startBitcoind') - import subprocess - - if self.isRunningBitcoind(): - raise self.BitcoindError, 'Looks like we have already started bitcoind' - - if not os.path.exists(self.executable): - raise self.BitcoindError, 'Could not find bitcoind' - - - pargs = [self.executable] - if USE_TESTNET: - pargs.append('-datadir=%s' % self.satoshiHome.rstrip('/testnet3/') ) - pargs.append('-testnet') - else: - pargs.append('-datadir=%s' % self.satoshiHome) - - try: - # Don't want some strange error in this size-check to abort loading - blocksdir = os.path.join(self.satoshiHome, 'blocks') - sz = long(0) - if os.path.exists(blocksdir): - for fn in os.listdir(blocksdir): - fnpath = os.path.join(blocksdir, fn) - sz += long(os.path.getsize(fnpath)) - - if sz < 5*GIGABYTE: - if SystemSpecs.Memory>9.0: - pargs.append('-dbcache=2000') - elif SystemSpecs.Memory>5.0: - pargs.append('-dbcache=1000') - elif SystemSpecs.Memory>3.0: - pargs.append('-dbcache=500') - except: - LOGEXCEPT('Failed size check of blocks directory') - - - # Startup bitcoind and get its process ID (along with our own) - self.bitcoind = launchProcess(pargs) - - self.btcdpid = self.bitcoind.pid - self.selfpid = os.getpid() - - LOGINFO('PID of bitcoind: %d', self.btcdpid) - LOGINFO('PID of armory: %d', self.selfpid) - - # Startup guardian process -- it will watch Armory's PID - gpath = self.getGuardianPath() - pargs = [gpath, str(self.selfpid), str(self.btcdpid)] - if not OS_WINDOWS: - pargs.insert(0, 'python') - launchProcess(pargs) - - - - ############################################################################# - def stopBitcoind(self): - LOGINFO('Called stopBitcoind') - if not self.isRunningBitcoind(): - LOGINFO('...but bitcoind is not running, to be able to stop') - return - - killProcessTree(self.bitcoind.pid) - killProcess(self.bitcoind.pid) - - time.sleep(1) - self.bitcoind = None - - - ############################################################################# - def isRunningBitcoind(self): - """ - armoryengine satoshiIsAvailable() only tells us whether there's a - running bitcoind that is actively responding on its port. But it - won't be responding immediately after we've started it (still doing - startup operations). If bitcoind was started and still running, - then poll() will return None. Any othe poll() return value means - that the process terminated - """ - if self.bitcoind==None: - return False - else: - if not self.bitcoind.poll()==None: - LOGDEBUG('Bitcoind is no more') - if self.btcOut==None: - self.btcOut, self.btcErr = self.bitcoind.communicate() - LOGWARN('bitcoind exited, bitcoind STDOUT:') - for line in self.btcOut.split('\n'): - LOGWARN(line) - LOGWARN('bitcoind exited, bitcoind STDERR:') - for line in self.btcErr.split('\n'): - LOGWARN(line) - return self.bitcoind.poll()==None - - ############################################################################# - def wasRunningBitcoind(self): - return (not self.bitcoind==None) - - ############################################################################# - def bitcoindIsResponsive(self): - return satoshiIsAvailable(self.bitconf['host'], self.bitconf['rpcport']) - - ############################################################################# - def getSDMState(self): - """ - As for why I'm doing this: it turns out that between "initializing" - and "synchronizing", bitcoind temporarily stops responding entirely, - which causes "not-available" to be the state. I need to smooth that - out because it wreaks havoc on the GUI which will switch to showing - a nasty error. - """ - - state = self.getSDMStateLogic() - self.circBufferState.append(state) - self.circBufferTime.append(RightNow()) - if len(self.circBufferTime)>2 and \ - (self.circBufferTime[-1] - self.circBufferTime[1]) > 5: - # Only remove the first element if we have at least 5s history - self.circBufferState = self.circBufferState[1:] - self.circBufferTime = self.circBufferTime[1:] - - # Here's where we modify the output to smooth out the gap between - # "initializing" and "synchronizing" (which is a couple seconds - # of "not available"). "NotAvail" keeps getting added to the - # buffer, but if it was "initializing" in the last 5 seconds, - # we will keep "initializing" - if state=='BitcoindNotAvailable': - if 'BitcoindInitializing' in self.circBufferState: - LOGWARN('Overriding not-available message. This should happen 0-5 times') - return 'BitcoindInitializing' - - return state - - ############################################################################# - def getSDMStateLogic(self): - - if self.disabled: - return 'BitcoindMgmtDisabled' - - if self.failedFindExe: - return 'BitcoindExeMissing' - - if self.failedFindHome: - return 'BitcoindHomeMissing' - - latestInfo = self.getTopBlockInfo() - - if self.bitcoind==None and latestInfo['error']=='Uninitialized': - return 'BitcoindNeverStarted' - - if not self.isRunningBitcoind(): - # Not running at all: either never started, or process terminated - if not self.btcErr==None and len(self.btcErr)>0: - errstr = self.btcErr.replace(',',' ').replace('.',' ').replace('!',' ') - errPcs = set([a.lower() for a in errstr.split()]) - runPcs = set(['cannot','obtain','lock','already','running']) - dbePcs = set(['database', 'recover','backup','except','wallet','dat']) - if len(errPcs.intersection(runPcs))>=(len(runPcs)-1): - return 'BitcoindAlreadyRunning' - elif len(errPcs.intersection(dbePcs))>=(len(dbePcs)-1): - return 'BitcoindDatabaseEnvError' - else: - return 'BitcoindUnknownCrash' - else: - return 'BitcoindNotAvailable' - elif not self.bitcoindIsResponsive(): - # Running but not responsive... must still be initializing - return 'BitcoindInitializing' - else: - # If it's responsive, get the top block and check - # TODO: These conditionals are based on experimental results. May - # not be accurate what the specific errors mean... - if latestInfo['error']=='ValueError': - return 'BitcoindWrongPassword' - elif latestInfo['error']=='JsonRpcException': - return 'BitcoindInitializing' - elif latestInfo['error']=='SocketError': - return 'BitcoindNotAvailable' - - if 'BitcoindReady' in self.circBufferState: - # If ready, always ready - return 'BitcoindReady' - - # If we get here, bitcoind is gave us a response. - secSinceLastBlk = RightNow() - latestInfo['toptime'] - blkspersec = latestInfo['blkspersec'] - #print 'Blocks per 10 sec:', ('UNKNOWN' if blkspersec==-1 else blkspersec*10) - if secSinceLastBlk > 4*HOUR or blkspersec==-1: - return 'BitcoindSynchronizing' - else: - if blkspersec*20 > 2 and not 'BitcoindReady' in self.circBufferState: - return 'BitcoindSynchronizing' - else: - return 'BitcoindReady' - - - - - ############################################################################# - def createProxy(self, forceNew=False): - if self.proxy==None or forceNew: - LOGDEBUG('Creating proxy') - usr,pas,hst,prt = [self.bitconf[k] for k in ['rpcuser','rpcpassword',\ - 'host', 'rpcport']] - pstr = 'http://%s:%s@%s:%d' % (usr,pas,hst,prt) - LOGINFO('Creating proxy in SDM: host=%s, port=%s', hst,prt) - self.proxy = ServiceProxy(pstr) - - - ############################################################################# - def __backgroundRequestTopBlock(self): - self.createProxy() - self.isMidQuery = True - tstart = RightNow() - try: - numblks = self.proxy.getinfo()['blocks'] - blkhash = self.proxy.getblockhash(numblks) - toptime = self.proxy.getblock(blkhash)['time'] - #LOGDEBUG('RPC Call: numBlks=%d, toptime=%d', numblks, toptime) - # Only overwrite once all outputs are retrieved - self.lastTopBlockInfo['numblks'] = numblks - self.lastTopBlockInfo['tophash'] = blkhash - self.lastTopBlockInfo['toptime'] = toptime - self.lastTopBlockInfo['error'] = None # Holds error info - - if len(self.last20queries)==0 or \ - (RightNow()-self.last20queries[-1][0]) > 0.99: - # This conditional guarantees last 20 queries spans at least 20s - self.last20queries.append([RightNow(), numblks]) - self.last20queries = self.last20queries[-20:] - t0,b0 = self.last20queries[0] - t1,b1 = self.last20queries[-1] - - # Need at least 10s of data to give meaning answer - if (t1-t0)<10: - self.lastTopBlockInfo['blkspersec'] = -1 - else: - self.lastTopBlockInfo['blkspersec'] = float(b1-b0)/float(t1-t0) - - except ValueError: - # I believe this happens when you used the wrong password - LOGEXCEPT('ValueError in bkgd req top blk') - self.lastTopBlockInfo['error'] = 'ValueError' - except authproxy.JSONRPCException: - # This seems to happen when bitcoind is overwhelmed... not quite ready - LOGDEBUG('generic jsonrpc exception') - self.lastTopBlockInfo['error'] = 'JsonRpcException' - except socket.error: - # Connection isn't available... is bitcoind not running anymore? - LOGDEBUG('generic socket error') - self.lastTopBlockInfo['error'] = 'SocketError' - except: - LOGEXCEPT('generic error') - self.lastTopBlockInfo['error'] = 'UnknownError' - raise - finally: - self.isMidQuery = False - - - ############################################################################# - def updateTopBlockInfo(self): - """ - We want to get the top block information, but if bitcoind is rigorously - downloading and verifying the blockchain, it can sometimes take 10s to - to respond to JSON-RPC calls! We must do it in the background... - - If it's already querying, no need to kick off another background request, - just return the last value, which may be "stale" but we don't really - care for this particular use-case - """ - if not self.isRunningBitcoind(): - return - - if self.isMidQuery: - return - - self.createProxy() - self.queryThread = PyBackgroundThread(self.__backgroundRequestTopBlock) - self.queryThread.start() - - - ############################################################################# - def getTopBlockInfo(self): - if self.isRunningBitcoind(): - self.updateTopBlockInfo() - self.queryThread.join(0.001) # In most cases, result should come in 1 ms - # We return a copy so that the data is not changing as we use it - - return self.lastTopBlockInfo.copy() - - - ############################################################################# - def callJSON(self, func, *args): - state = self.getSDMState() - if not state in ('BitcoindReady', 'BitcoindSynchronizing'): - LOGERROR('Called callJSON(%s, %s)', func, str(args)) - LOGERROR('Current SDM state: %s', state) - raise self.BitcoindError, 'callJSON while %s'%state - - return self.proxy.__getattr__(func)(*args) - - - ############################################################################# - def returnSDMInfo(self): - sdminfo = {} - for key,val in self.bitconf.iteritems(): - sdminfo['bitconf_%s'%key] = val - - for key,val in self.lastTopBlockInfo.iteritems(): - sdminfo['topblk_%s'%key] = val - - sdminfo['executable'] = self.executable - sdminfo['isrunning'] = self.isRunningBitcoind() - sdminfo['homedir'] = self.satoshiHome - sdminfo['proxyinit'] = (not self.proxy==None) - sdminfo['ismidquery'] = self.isMidQuery - sdminfo['querycount'] = len(self.last20queries) - - return sdminfo - - ############################################################################# - def printSDMInfo(self): - print '\nCurrent SDM State:' - print '\t', 'SDM State Str'.ljust(20), ':', self.getSDMState() - for key,value in self.returnSDMInfo().iteritems(): - print '\t', str(key).ljust(20), ':', str(value) - - - -################################################################################ -################################################################################ -class SettingsFile(object): - """ - This class could be replaced by the built-in QSettings in PyQt, except - that older versions of PyQt do not support the QSettings (or at least - I never figured it out). Easy enough to do it here - - All settings must populated with a simple datatype -- non-simple - datatypes should be broken down into pieces that are simple: numbers - and strings, or lists/tuples of them. - - Will write all the settings to file. Each line will look like: - SingleValueSetting1 | 3824.8 - SingleValueSetting2 | this is a string - Tuple Or List Obj 1 | 12 $ 43 $ 13 $ 33 - Tuple Or List Obj 2 | str1 $ another str - """ - - ############################################################################# - def __init__(self, path=None): - self.settingsPath = path - self.settingsMap = {} - if not path: - self.settingsPath = os.path.join(ARMORY_HOME_DIR, 'ArmorySettings.txt') - - LOGINFO('Using settings file: %s', self.settingsPath) - if os.path.exists(self.settingsPath): - self.loadSettingsFile(path) - - - - ############################################################################# - def pprint(self, nIndent=0): - indstr = indent*nIndent - print indstr + 'Settings:' - for k,v in self.settingsMap.iteritems(): - print indstr + indent + k.ljust(15), v - - - ############################################################################# - def hasSetting(self, name): - return self.settingsMap.has_key(name) - - ############################################################################# - def set(self, name, value): - if isinstance(value, tuple): - self.settingsMap[name] = list(value) - else: - self.settingsMap[name] = value - self.writeSettingsFile() - - ############################################################################# - def extend(self, name, value): - """ Adds/converts setting to list, appends value to the end of it """ - if not self.settingsMap.has_key(name): - if isinstance(value, list): - self.set(name, value) - else: - self.set(name, [value]) - else: - origVal = self.get(name, expectList=True) - if isinstance(value, list): - origVal.extend(value) - else: - origVal.append(value) - self.settingsMap[name] = origVal - self.writeSettingsFile() - - ############################################################################# - def get(self, name, expectList=False): - if not self.hasSetting(name) or self.settingsMap[name]=='': - return ([] if expectList else '') - else: - val = self.settingsMap[name] - if expectList: - if isinstance(val, list): - return val - else: - return [val] - else: - return val - - ############################################################################# - def getAllSettings(self): - return self.settingsMap - - ############################################################################# - def getSettingOrSetDefault(self, name, defaultVal, expectList=False): - output = defaultVal - if self.hasSetting(name): - output = self.get(name) - else: - self.set(name, defaultVal) - - return output - - - - ############################################################################# - def delete(self, name): - if self.hasSetting(name): - del self.settingsMap[name] - self.writeSettingsFile() - - ############################################################################# - def writeSettingsFile(self, path=None): - if not path: - path = self.settingsPath - f = open(path, 'w') - for key,val in self.settingsMap.iteritems(): - try: - # Skip anything that throws an exception - valStr = '' - if isinstance(val, basestring): - valStr = val - elif isinstance(val, int) or \ - isinstance(val, float) or \ - isinstance(val, long): - valStr = str(val) - elif isinstance(val, list) or \ - isinstance(val, tuple): - valStr = ' $ '.join([str(v) for v in val]) - f.write(key.ljust(36)) - f.write(' | ') - f.write(toBytes(valStr)) - f.write('\n') - except: - LOGEXCEPT('Invalid entry in SettingsFile... skipping') - f.close() - - - ############################################################################# - def loadSettingsFile(self, path=None): - if not path: - path = self.settingsPath - - if not os.path.exists(path): - raise FileExistsError, 'Settings file DNE:', path - - f = open(path, 'rb') - sdata = f.read() - f.close() - - # Automatically convert settings to numeric if possible - def castVal(v): - v = v.strip() - a,b = v.isdigit(), v.replace('.','').isdigit() - if a: - return int(v) - elif b: - return float(v) - else: - if v.lower()=='true': - return True - elif v.lower()=='false': - return False - else: - return toUnicode(v) - - - sdata = [line.strip() for line in sdata.split('\n')] - for line in sdata: - if len(line.strip())==0: - continue - - try: - key,vals = line.split('|') - valList = [castVal(v) for v in vals.split('$')] - if len(valList)==1: - self.settingsMap[key.strip()] = valList[0] - else: - self.settingsMap[key.strip()] = valList - except: - LOGEXCEPT('Invalid setting in %s (skipping...)', path) - - - - - -################################################################################ -################################################################################ -# Read Satoshi Wallets (wallet.dat) to import into Armory wallet -# BSDDB wallet-reading code taken from Joric's pywallet: he declared it -# public domain. -#try: -# from bsddb.db import * -#except ImportError: -# # Apparently bsddb3 is needed on OSX -# from bsddb3.db import * -# -#import json -#import struct -# -#class BCDataStream(object): -# def __init__(self): -# self.input = None -# self.read_cursor = 0 -# -# def clear(self): -# self.input = None -# self.read_cursor = 0 -# -# def write(self, bytes): # Initialize with string of bytes -# if self.input is None: -# self.input = bytes -# else: -# self.input += bytes -# -# def map_file(self, file, start): # Initialize with bytes from file -# self.input = mmap.mmap(file.fileno(), 0, access=mmap.ACCESS_READ) -# self.read_cursor = start -# def seek_file(self, position): -# self.read_cursor = position -# def close_file(self): -# self.input.close() -# -# def read_string(self): -# # Strings are encoded depending on length: -# # 0 to 252 : 1-byte-length followed by bytes (if any) -# # 253 to 65,535 : byte'253' 2-byte-length followed by bytes -# # 65,536 to 4,294,967,295 : byte '254' 4-byte-length followed by bytes -# # ... and the Bitcoin client is coded to understand: -# # greater than 4,294,967,295 : byte '255' 8-byte-length followed by bytes of string -# # ... but I don't think it actually handles any strings that big. -# if self.input is None: -# raise SerializationError("call write(bytes) before trying to deserialize") -# -# try: -# length = self.read_compact_size() -# except IndexError: -# raise SerializationError("attempt to read past end of buffer") -# -# return self.read_bytes(length) -# -# def write_string(self, string): -# # Length-encoded as with read-string -# self.write_compact_size(len(string)) -# self.write(string) -# -# def read_bytes(self, length): -# try: -# result = self.input[self.read_cursor:self.read_cursor+length] -# self.read_cursor += length -# return result -# except IndexError: -# raise SerializationError("attempt to read past end of buffer") -# -# return '' -# -# def read_boolean(self): return self.read_bytes(1)[0] != chr(0) -# def read_int16(self): return self._read_num('0: -# # Satoshi Wallet is encrypted! -# plainkeys = [] -# if not passphrase: -# raise EncryptionError, 'Satoshi wallet is encrypted but no passphrase supplied' -# -# pKey,IV = GetKeyFromPassphraseSatoshi( passphrase, \ -# mkey['salt'], \ -# mkey['iter'], \ -# mkey['mthd']) -# -# masterKey = CryptoAES().DecryptCBC( SecureBinaryData(mkey['mkey']), \ -# SecureBinaryData(pKey), \ -# SecureBinaryData(IV) ) -# 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) -# -# 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 - - - - - -class PyBackgroundThread(threading.Thread): - """ - Wraps a function in a threading.Thread object which will run - that function in a separate thread. Calling self.start() will - return immediately, but will start running that function in - separate thread. You can check its progress later by using - self.isRunning() or self.isFinished(). If the function returns - a value, use self.getOutput(). Use self.getElapsedSeconds() - to find out how long it took. - """ - - def __init__(self, *args, **kwargs): - threading.Thread.__init__(self) - - self.output = None - self.startedAt = UNINITIALIZED - self.finishedAt = UNINITIALIZED - - if len(args)==0: - self.func = lambda: () - else: - if not hasattr(args[0], '__call__'): - raise TypeError, ('PyBkgdThread constructor first arg ' - '(if any) must be a function') - else: - self.setThreadFunction(args[0], *args[1:], **kwargs) - - def setThreadFunction(self, thefunc, *args, **kwargs): - def funcPartial(): - return thefunc(*args, **kwargs) - self.func = funcPartial - - def isFinished(self): - return not (self.finishedAt==UNINITIALIZED) - - def isStarted(self): - return not (self.startedAt==UNINITIALIZED) - - def isRunning(self): - return (self.isStarted() and not self.isFinished()) - - def getElapsedSeconds(self): - if not self.isFinished(): - LOGERROR('Thread is not finished yet!') - return None - else: - return self.finishedAt - self.startedAt - - def getOutput(self): - if not self.isFinished(): - if self.isRunning(): - LOGERROR('Cannot get output while thread is running') - else: - LOGERROR('Thread was never .start()ed') - return None - - return self.output - - - def start(self): - # The prefunc is blocking. Probably preparing something - # that needs to be in place before we start the thread - self.startedAt = RightNow() - super(PyBackgroundThread, self).start() - - def run(self): - # This should not be called manually. Only call start() - self.output = self.func() - self.finishedAt = RightNow() - - def reset(self): - self.output = None - self.startedAt = UNINITIALIZED - self.finishedAt = UNINITIALIZED - - def restart(self): - self.reset() - self.start() - - -# Define a decorator that allows the function to be called asynchronously -def AllowAsync(func): - def wrappedFunc(*args, **kwargs): - - if not 'async' in kwargs or not kwargs['async']==True: - # Run the function normally - if 'async' in kwargs: - del kwargs['async'] - return func(*args, **kwargs) - else: - # Run the function as a background thread - del kwargs['async'] - thr = PyBackgroundThread(func, *args, **kwargs) - thr.start() - return thr - - return wrappedFunc - - - - -################################################################################ -# Let's create a thread-wrapper for the blockchain utilities. Enable the -# ability for multi-threaded blockchain scanning -- have a main thread and -# a blockchain thread: blockchain can scan, and main thread will check back -# every now and then to see if it's done - -import Queue -BLOCKCHAINMODE = enum('Offline', \ - 'Uninitialized', \ - 'Full', \ - 'Rescanning', \ - 'LiteScanning', \ - 'FullPrune', \ - 'Lite') - -BDMINPUTTYPE = enum('RegisterAddr', \ - 'ZeroConfTxToInsert', \ - 'HeaderRequested', \ - 'TxRequested', \ - 'BlockRequested', \ - 'AddrBookRequested', \ - 'BlockAtHeightRequested', \ - 'HeaderAtHeightRequested', \ - 'ForceRebuild', \ - 'RescanRequested', \ - 'WalletRecoveryScan', \ - 'UpdateWallets', \ - 'ReadBlkUpdate', \ - 'GoOnlineRequested', \ - 'GoOfflineRequested', \ - 'Passthrough', \ - 'Reset', \ - 'Shutdown') - -################################################################################ -class BlockDataManagerThread(threading.Thread): - """ - A note about this class: - - It was mainly created to allow for asynchronous blockchain scanning, - but the act of splitting the BDM into it's own thread meant that ALL - communication with the BDM requires thread-safe access. So basically, - I had to wrap EVERYTHING. And then make it flexible. - - For this reason, any calls not explicitly related to rescanning will - block by default, which could be a long time if the BDM is in the - middle of rescanning. For this reason, you are expected to either - pass wait=False if you just want to queue the function call and move - on in the main thread, or check the BDM state first, to make sure - it's not currently scanning and can expect immediate response. - - This makes using the BDM much more complicated. But comes with the - benefit of all rescanning being able to happen in the background. - If you want to run it like single-threaded, you can use - TheBDM.setBlocking(True) and all calls will block. Always (unless - you pass wait=False explicitly to one of those calls). - - Any calls that retrieve data from the BDM should block, even if you - technically can specify wait=False. This is because the class was - not designed to maintain organization of output data asynchronously. - So a call like TheBDM.getTopBlockHeader() will always block, and you - should check the BDM state if you want to make sure it returns - immediately. Since there is only one main thread, There is really no - way for a rescan to be started between the time you check the state - and the time you call the method (so if you want to access the BDM - from multiple threads, this class will need some redesign). - - - This serves as a layer between the GUI and the Blockchain utilities. - If a request is made to mess with the BDM while it is in the - middle of scanning, it will queue it for when it's done - - All private methods (those starting with two underscores, like __method), - are executed only by the BDM thread. These should never be called - externally, and are only safe to run when the BDM is ready to execute - them. - - You can use any non-private methods at any time, and if you set wait=True, - the main thread will block until that operation is complete. If the BDM - is in the middle of a scan, the main thread could block for minutes until - the scanning is complete and then it processes your request. - - Therefore, use setBlocking(True) to make sure you always wait/block after - every call, if you are interested in simplicity and don't mind waiting. - - Use setBlocking(False) along with wait=False for the appropriate calls - to queue up your request and continue the main thread immediately. You - can finish up something else, and then come back and check whether the - job is finished (usually using TheBDM.getBDMState()=='BlockchainReady') - - Any methods not defined explicitly in this class will "passthrough" the - __getattr__() method, which will then call that exact method name on - the BDM. All calls block by default. All such calls can also include - wait=False if you want to queue it and then continue asynchronously. - - - Implementation notes: - - Before the multi-threaded BDM, there was wallets, and there was the BDM. - We always gave the wallets to the BDM and said "please search the block- - chain for relevant transactions". Now that this is asynchronous, the - calling thread is going to queue the blockchain scan, and then run off - and do other things: which may include address/wallet operations that - would collide with the BDM updating it. - - THEREFORE, the BDM now has a single, master wallet. Any address you add - to any of your wallets, should be added to the master wallet, too. The - PyBtcWallet class does this for you, but if you are using raw BtcWallets - (the C++ equivalent), you need to do: - - cppWallet.addScrAddress_1_(Hash160ToScrAddr(newAddr)) - TheBDM.registerScrAddr(newAddr, isFresh=?) - - This will add the address to the TheBDM.masterCppWallet. Then when you - queue up the TheBDM to do a rescan (if necessary), it will update only - its own wallet. Luckily, I designed the BDM so that data for addresses - in one wallet (the master), can be applied immediately to other/new - wallets that have the same addresses. - - If you say isFresh=False, then the BDM will set isDirty=True. This means - that a full rescan will have to be performed, and wallet information may - not be accurate until it is performed. isFresh=True should be used for - addresses/wallets you just created, and thus there's no reason to rescan, - because there's no chance they could have any history in the blockchain. - - Tying this all together: if you add an address to a PYTHON wallet, you - just add it through an existing call. If you add it with a C++ wallet, - you need to explicitly register it with TheBDM, too. Then you need to - tell the BDM to do a rescan (if isDirty==True), and then call the method - updateWalletsAfterScan( - are ready, you can chec - - """ - ############################################################################# - def __init__(self, isOffline=False, blocking=False): - super(BlockDataManagerThread, self).__init__() - - if isOffline: - self.blkMode = BLOCKCHAINMODE.Offline - self.prefMode = BLOCKCHAINMODE.Offline - else: - self.blkMode = BLOCKCHAINMODE.Uninitialized - self.prefMode = BLOCKCHAINMODE.Full - - self.bdm = Cpp.BlockDataManager().getBDM() - - # These are for communicating with the master (GUI) thread - self.inputQueue = Queue.Queue() - self.outputQueue = Queue.Queue() - - # Flags - self.startBDM = False - self.doShutdown = False - self.aboutToRescan = False - self.errorOut = 0 - - self.setBlocking(blocking) - - self.currentActivity = 'None' - - # Lists of wallets that should be checked after blockchain updates - self.pyWltList = [] # these will be python refs - self.cppWltList = [] # these will be python refs - - # The BlockDataManager is easier to use if you put all your addresses - # into a C++ BtcWallet object, and let it - self.masterCppWallet = Cpp.BtcWallet() - self.bdm.registerWallet(self.masterCppWallet) - - self.btcdir = BTC_HOME_DIR - self.ldbdir = LEVELDB_DIR - self.lastPctLoad = 0 - - - - - ############################################################################# - def __getattr__(self, name): - ''' - Anything that is not explicitly defined in this class should - passthrough to the C++ BlockDataManager class - - This remaps such calls into "passthrough" requests via the input - queue. This makes sure that the requests are processed only when - the BDM is ready. Hopefully, this will prevent multi-threaded - disasters, such as seg faults due to trying to read memory that is - in the process of being updated. - - Specifically, any passthrough call is expected to return output - unless you add 'waitForReturn=False' to the arg list. i.e. all - calls that "passthrough" will always block unless you explicitly - tell it not to. - ''' - - - rndID = int(random.uniform(0,100000000)) - if not hasattr(self.bdm, name): - LOGERROR('No BDM method: %s', name) - raise AttributeError - else: - def passthruFunc(*args, **kwargs): - #LOGDEBUG('External thread requesting: %s (%d)', name, rndID) - waitForReturn = True - if len(kwargs)>0 and \ - kwargs.has_key('wait') and \ - not kwargs['wait']: - waitForReturn = False - - - # If this was ultimately called from the BDM thread, don't go - # through the queue, just do it! - if len(kwargs)>0 and \ - kwargs.has_key('calledFromBDM') and \ - kwargs['calledFromBDM']: - return getattr(self.bdm, name)(*args) - - self.inputQueue.put([BDMINPUTTYPE.Passthrough, rndID, waitForReturn, name] + list(args)) - - - if waitForReturn: - try: - out = self.outputQueue.get(True, self.mtWaitSec) - return out - except Queue.Empty: - LOGERROR('BDM was not ready for your request! Waited %d sec.' % self.mtWaitSec) - LOGERROR(' getattr name: %s', name) - LOGERROR('BDM currently doing: %s (%d)', self.currentActivity,self.currentID ) - LOGERROR('Waiting for completion: ID= %d', rndID) - LOGERROR('Direct traceback') - traceback.print_stack() - self.errorOut += 1 - LOGEXCEPT('Traceback:') - return passthruFunc - - - - ############################################################################# - def waitForOutputIfNecessary(self, expectOutput, rndID=0): - # The get() command will block until the thread puts something there. - # We don't always expect output, but we use this method to - # replace inputQueue.join(). The reason for doing it is so - # that we can guarantee that BDM thread knows whether we are waiting - # for output or not, and any additional requests put on the inputQueue - # won't extend our wait time for this request - if expectOutput: - try: - return self.outputQueue.get(True, self.mtWaitSec) - except Queue.Empty: - stkOneUp = traceback.extract_stack()[-2] - filename,method = stkOneUp[0], stkOneUp[1] - LOGERROR('Waiting for BDM output that didn\'t come after %ds.' % self.mtWaitSec) - LOGERROR('BDM state is currently: %s', self.getBDMState()) - LOGERROR('Called from: %s:%d (%d)', os.path.basename(filename), method, rndID) - LOGERROR('BDM currently doing: %s (%d)', self.currentActivity, self.currentID) - LOGERROR('Direct traceback') - traceback.print_stack() - LOGEXCEPT('Traceback:') - self.errorOut += 1 - else: - return None - - - ############################################################################# - def setBlocking(self, doblock=True, newTimeout=MT_WAIT_TIMEOUT_SEC): - """ - If we want TheBDM to behave as a single-threaded app, we need to disable - the timeouts so that long operations (such as reading the blockchain) do - not crash the process. - - So setting wait=True is NOT identical to setBlocking(True), since using - wait=True with blocking=False will break when the timeout has been reached - """ - if doblock: - self.alwaysBlock = True - self.mtWaitSec = None - else: - self.alwaysBlock = False - self.mtWaitSec = newTimeout - - - ############################################################################# - def Reset(self, wait=None): - expectOutput = False - if not wait==False and (self.alwaysBlock or wait==True): - expectOutput = True - - rndID = int(random.uniform(0,100000000)) - self.inputQueue.put([BDMINPUTTYPE.Reset, rndID, expectOutput] ) - return self.waitForOutputIfNecessary(expectOutput, rndID) - - ############################################################################# - def getBlkMode(self): - return self.blkMode - - ############################################################################# - def getBDMState(self): - if self.blkMode == BLOCKCHAINMODE.Offline: - # BDM will not be able to provide any blockchain data, or scan - return 'Offline' - elif self.blkMode == BLOCKCHAINMODE.Full and not self.aboutToRescan: - # The BDM is idle, waiting for things to do - return 'BlockchainReady' - elif self.blkMode == BLOCKCHAINMODE.LiteScanning and not self.aboutToRescan: - # The BDM is doing some processing but it is expected to be done within - # 0.1s. For instance, readBlkFileUpdate requires processing, but can be - # performed 100/sec. For the outside calling thread, this is not any - # different than BlockchainReady. - return 'BlockchainReady' - elif self.blkMode == BLOCKCHAINMODE.Rescanning or self.aboutToRescan: - # BDM is doing a FULL scan of the blockchain, and expected to take - - return 'Scanning' - elif self.blkMode == BLOCKCHAINMODE.Uninitialized and not self.aboutToRescan: - # BDM wants to be online, but the calling thread never initiated the - # loadBlockchain() call. Usually setOnlineMode, registerWallets, then - # load the blockchain. - return 'Uninitialized' - elif self.blkMode == BLOCKCHAINMODE.FullPrune: - # NOT IMPLEMENTED - return 'FullPrune' - elif self.blkMode == BLOCKCHAINMODE.Lite: - # NOT IMPLEMENTED - return 'Lite' - else: - return '' % self.blkMode - - - ############################################################################# - def predictLoadTime(self): - # Apparently we can't read the C++ state while it's scanning, - # specifically getLoadProgress* methods. Thus we have to resort - # to communicating via files... bleh - bfile = os.path.join(ARMORY_HOME_DIR,'blkfiles.txt') - if not os.path.exists(bfile): - return [-1,-1,-1,-1] - - try: - with open(bfile,'r') as f: - tmtrx = [line.split() for line in f.readlines() if len(line.strip())>0] - phases = [float(row[0]) for row in tmtrx] - currPhase = phases[-1] - startat = [float(row[1]) for row in tmtrx if float(row[0])==currPhase] - sofar = [float(row[2]) for row in tmtrx if float(row[0])==currPhase] - total = [float(row[3]) for row in tmtrx if float(row[0])==currPhase] - times = [float(row[4]) for row in tmtrx if float(row[0])==currPhase] - - startRow = 0 if len(startat)<=10 else -10 - todo = total[0] - startat[0] - pct0 = sofar[0] / todo - pct1 = sofar[-1] / todo - t0,t1 = times[0], times[-1] - if (not t1>t0) or todo<0: - return [-1,-1,-1,-1] - rate = (pct1-pct0) / (t1-t0) - tleft = (1-pct1)/rate - totalPct = (startat[-1] + sofar[-1]) / total[-1] - if not self.lastPctLoad == pct1: - LOGINFO('Reading blockchain, pct complete: %0.1f', 100*totalPct) - self.lastPctLoad = totalPct - return [currPhase,totalPct,rate,tleft] - except: - raise - return [-1,-1,-1,-1] - - - - - ############################################################################# - def execCleanShutdown(self, wait=True): - expectOutput = False - if not wait==False and (self.alwaysBlock or wait==True): - expectOutput = True - - rndID = int(random.uniform(0,100000000)) - self.inputQueue.put([BDMINPUTTYPE.Shutdown, rndID, expectOutput]) - return self.waitForOutputIfNecessary(expectOutput, rndID) - - ############################################################################# - def setSatoshiDir(self, newBtcDir): - if not os.path.exists(newBtcDir): - LOGERROR('setSatoshiDir: directory does not exist: %s', newBtcDir) - return - - if not self.blkMode in (BLOCKCHAINMODE.Offline, BLOCKCHAINMODE.Uninitialized): - LOGERROR('Cannot set blockchain/satoshi path after BDM is started') - return - - self.btcdir = newBtcDir - - ############################################################################# - def setLevelDBDir(self, ldbdir): - - if not self.blkMode in (BLOCKCHAINMODE.Offline, BLOCKCHAINMODE.Uninitialized): - LOGERROR('Cannot set blockchain/satoshi path after BDM is started') - return - - if not os.path.exists(ldbdir): - os.makedirs(ldbdir) - - self.ldbdir = ldbdir - - - ############################################################################# - def setOnlineMode(self, goOnline=True, wait=None): - LOGINFO('Setting online mode: %s (wait=%s)' % (str(goOnline), str(wait))) - expectOutput = False - if not wait==False and (self.alwaysBlock or wait==True): - expectOutput = True - - rndID = int(random.uniform(0,100000000)) - - if goOnline: - if TheBDM.getBDMState() in ('Offline','Uninitialized'): - self.inputQueue.put([BDMINPUTTYPE.GoOnlineRequested, rndID, expectOutput]) - else: - if TheBDM.getBDMState() in ('Scanning','BlockchainReady'): - self.inputQueue.put([BDMINPUTTYPE.GoOfflineRequested, rndID, expectOutput]) - - return self.waitForOutputIfNecessary(expectOutput, rndID) - - ############################################################################# - def isScanning(self): - return (self.aboutToRescan or self.blkMode==BLOCKCHAINMODE.Rescanning) - - - ############################################################################# - def readBlkFileUpdate(self, wait=True): - """ - This method can be blocking... it always has been without a problem, - because the block file updates are always fast. But I have to assume - that it theoretically *could* take a while. Consider using wait=False - if you want it to do its thing and not wait for it (this matters, because - you'll want to call TheBDM.updateWalletsAfterScan() when this is - finished to make sure that - """ - expectOutput = False - if not wait==False and (self.alwaysBlock or wait==True): - expectOutput = True - - rndID = int(random.uniform(0,100000000)) - self.inputQueue.put([BDMINPUTTYPE.ReadBlkUpdate, rndID, expectOutput]) - return self.waitForOutputIfNecessary(expectOutput, rndID) - - - ############################################################################# - def isInitialized(self): - return self.blkMode==BLOCKCHAINMODE.Full and self.bdm.isInitialized() - - - ############################################################################# - def isDirty(self): - return self.bdm.isDirty() - - - - - ############################################################################# - def rescanBlockchain(self, scanType='AsNeeded', wait=None): - expectOutput = False - if not wait==False and (self.alwaysBlock or wait==True): - expectOutput = True - - self.aboutToRescan = True - - rndID = int(random.uniform(0,100000000)) - self.inputQueue.put([BDMINPUTTYPE.RescanRequested, rndID, expectOutput, scanType]) - LOGINFO('Blockchain rescan requested') - return self.waitForOutputIfNecessary(expectOutput, rndID) - - - ############################################################################# - def updateWalletsAfterScan(self, wait=True): - """ - Be careful with this method: it is asking the BDM thread to update - the wallets in the main thread. If you do this with wait=False, you - need to avoid any wallet operations in the main thread until it's done. - However, this is usually very fast as long as you know the BDM is not - in the middle of a rescan, so you might as well set wait=True. - - In fact, I highly recommend you always use wait=True, in order to - guarantee thread-safety. - - NOTE: If there are multiple wallet-threads, this might not work. It - might require specifying which wallets to update after a scan, - so that other threads don't collide with the BDM updating its - wallet when called from this thread. - """ - expectOutput = False - if not wait==False and (self.alwaysBlock or wait==True): - expectOutput = True - - rndID = int(random.uniform(0,100000000)) - self.inputQueue.put([BDMINPUTTYPE.UpdateWallets, rndID, expectOutput]) - return self.waitForOutputIfNecessary(expectOutput, rndID) - - - ############################################################################# - def startWalletRecoveryScan(self, pywlt, wait=None): - """ - A wallet recovery scan may require multiple, independent rescans. This - is because we don't know how many addresses to pre-calculate for the - initial scan. So, we will calculate the first X addresses in the wallet, - do a scan, and then if any addresses have tx history beyond X/2, calculate - another X and rescan. This will usually only have to be done once, but - may need to be repeated for super-active wallets. - (In the future, I may add functionality to sample the gap between address - usage, so I can more-intelligently determine when we're at the end...) - """ - - - expectOutput = False - if not wait==False and (self.alwaysBlock or wait==True): - expectOutput = True - - self.aboutToRescan = True - - rndID = int(random.uniform(0,100000000)) - self.inputQueue.put([BDMINPUTTYPE.WalletRecoveryScan, rndID, expectOutput, pywlt]) - LOGINFO('Wallet recovery scan requested') - return self.waitForOutputIfNecessary(expectOutput, rndID) - - - - ############################################################################# - def __checkBDMReadyToServeData(self): - if self.blkMode==BLOCKCHAINMODE.Rescanning: - LOGERROR('Requested blockchain data while scanning. Don\'t do this!') - LOGERROR('Check self.getBlkModeStr()==BLOCKCHAINMODE.Full before') - LOGERROR('making requests! Skipping request') - return False - if self.blkMode==BLOCKCHAINMODE.Offline: - LOGERROR('Requested blockchain data while BDM is in offline mode.') - LOGERROR('Please start the BDM using TheBDM.setOnlineMode() before,') - LOGERROR('and then wait for it to complete, before requesting data.') - return False - if not self.bdm.isInitialized(): - LOGERROR('The BDM thread declares the BDM is ready, but the BDM ') - LOGERROR('itself reports that it is not initialized! What is ') - LOGERROR('going on...?') - return False - - - return True - - ############################################################################# - def getTxByHash(self, txHash): - """ - All calls that retrieve blockchain data are blocking calls. You have - no choice in the matter! - """ - #if not self.__checkBDMReadyToServeData(): - #return None - - rndID = int(random.uniform(0,100000000)) - self.inputQueue.put([BDMINPUTTYPE.TxRequested, rndID, True, txHash]) - - try: - result = self.outputQueue.get(True, 10) - if result==None: - LOGERROR('Requested tx does not exist:\n%s', binary_to_hex(txHash)) - return result - except Queue.Empty: - LOGERROR('Waited 10s for tx to be returned. Abort') - LOGERROR('ID: getTxByHash (%d)', rndID) - return None - #LOGERROR('Going to block until we get something...') - #return self.outputQueue.get(True) - - return None - - - ############################################################################ - def getHeaderByHash(self, headHash): - """ - All calls that retrieve blockchain data are blocking calls. You have - no choice in the matter! - """ - #if not self.__checkBDMReadyToServeData(): - #return None - - rndID = int(random.uniform(0,100000000)) - self.inputQueue.put([BDMINPUTTYPE.HeaderRequested, rndID, True, headHash]) - - try: - result = self.outputQueue.get(True, 10) - if result==None: - LOGERROR('Requested header does not exist:\n%s', \ - binary_to_hex(headHash)) - return result - except Queue.Empty: - LOGERROR('Waited 10s for header to be returned. Abort') - LOGERROR('ID: getTxByHash (%d)', rndID) - #LOGERROR('Going to block until we get something...') - #return self.outputQueue.get(True) - - return None - - - ############################################################################# - def getBlockByHash(self,headHash): - """ - All calls that retrieve blockchain data are blocking calls. You have - no choice in the matter! - - This retrives the full block, not just the header, encoded the same - way as it is in the blkXXXX.dat files (including magic bytes and - block 4-byte block size) - """ - #if not self.__checkBDMReadyToServeData(): - #return None - - rndID = int(random.uniform(0,100000000)) - self.inputQueue.put([BDMINPUTTYPE.BlockRequested, rndID, True, headHash]) - - try: - result = self.outputQueue.get(True, 10) - if result==None: - LOGERROR('Requested block does not exist:\n%s', \ - binary_to_hex(headHash)) - return result - except Queue.Empty: - LOGERROR('Waited 10s for block to be returned. Abort') - LOGERROR('ID: getTxByHash (%d)', rndID) - #LOGERROR('Going to block until we get something...') - #return self.outputQueue.get(True) - - return None - - - ############################################################################# - def getAddressBook(self, wlt): - """ - Address books are constructed from Blockchain data, which means this - must be a blocking method. - """ - rndID = int(random.uniform(0,100000000)) - if isinstance(wlt, PyBtcWallet): - self.inputQueue.put([BDMINPUTTYPE.AddrBookRequested, rndID, True, wlt.cppWallet]) - elif isinstance(wlt, Cpp.BtcWallet): - self.inputQueue.put([BDMINPUTTYPE.AddrBookRequested, rndID, True, wlt]) - - try: - result = self.outputQueue.get(True, self.mtWaitSec) - return result - except Queue.Empty: - LOGERROR('Waited %ds for addrbook to be returned. Abort' % self.mtWaitSec) - LOGERROR('ID: getTxByHash (%d)', rndID) - #LOGERROR('Going to block until we get something...') - #return self.outputQueue.get(True) - - return None - - ############################################################################# - def addNewZeroConfTx(self, rawTx, timeRecv, writeToFile, wait=None): - expectOutput = False - if not wait==False and (self.alwaysBlock or wait==True): - expectOutput = True - - rndID = int(random.uniform(0,100000000)) - self.inputQueue.put([BDMINPUTTYPE.ZeroConfTxToInsert, rndID, expectOutput, rawTx, timeRecv]) - return self.waitForOutputIfNecessary(expectOutput, rndID) - - ############################################################################# - def registerScrAddr(self, scrAddr, isFresh=False, wait=None): - """ - This is for a generic address: treat it as imported (requires rescan) - unless specifically specified otherwise - """ - if isFresh: - self.registerNewScrAddr(scrAddr, wait=wait) - else: - self.registerImportedScrAddr(scrAddr, wait=wait) - - - ############################################################################# - def registerNewScrAddr(self, scrAddr, wait=None): - """ - Variable isFresh==True means the address was just [freshly] created, - and we need to watch for transactions with it, but we don't need - to rescan any blocks - """ - expectOutput = False - if not wait==False and (self.alwaysBlock or wait==True): - expectOutput = True - - rndID = int(random.uniform(0,100000000)) - self.inputQueue.put([BDMINPUTTYPE.RegisterAddr, rndID, expectOutput, scrAddr, True]) - - return self.waitForOutputIfNecessary(expectOutput, rndID) - - - - ############################################################################# - def registerImportedScrAddr(self, scrAddr, \ - firstTime=UINT32_MAX, \ - firstBlk=UINT32_MAX, \ - lastTime=0, \ - lastBlk=0, wait=None): - """ - TODO: Need to clean up the first/last blk/time variables. Rather, - I need to make sure they are maintained and applied intelligently - and consistently - """ - expectOutput = False - if not wait==False and (self.alwaysBlock or wait==True): - expectOutput = True - - rndID = int(random.uniform(0,100000000)) - self.inputQueue.put([BDMINPUTTYPE.RegisterAddr, rndID, expectOutput, scrAddr, \ - [firstTime, firstBlk, lastTime, lastBlk]]) - - return self.waitForOutputIfNecessary(expectOutput, rndID) - - - ############################################################################# - def registerWallet(self, wlt, isFresh=False, wait=None): - """ - Will register a C++ wallet or Python wallet - """ - - expectOutput = False - if not wait==False and (self.alwaysBlock or wait==True): - expectOutput = True - - if isinstance(wlt, PyBtcWallet): - scrAddrs = [Hash160ToScrAddr(a.getAddr160()) for a in wlt.getAddrList()] - - if isFresh: - for scrad in scrAddrs: - self.registerNewScrAddr(scrad, wait=wait) - else: - for scrad in scrAddrs: - self.registerImportedScrAddr(scrad, wait=wait) - - if not wlt in self.pyWltList: - self.pyWltList.append(wlt) - - elif isinstance(wlt, Cpp.BtcWallet): - naddr = wlt.getNumScrAddr() - - for a in range(naddr): - self.registerScrAddr(wlt.getScrAddrObjByIndex(a).getScrAddr(), isFresh, wait=wait) - - if not wlt in self.cppWltList: - self.cppWltList.append(wlt) - else: - LOGERROR('Unrecognized object passed to registerWallet function') - - - - - - ############################################################################# - # These bdm_direct methods feel like a hack. They probably are. I need - # find an elegant way to get the code normally run outside the BDM thread, - # to be able to run inside the BDM thread without using the BDM queue (since - # the queue is specifically FOR non-BDM-thread calls). For now, the best - # I can do is create non-private versions of these methods that access BDM - # methods directly, but should not be used under any circumstances, unless - # we know for sure that the BDM ultimately called this method. - def registerScrAddr_bdm_direct(self, scrAddr, timeInfo): - """ - Something went awry calling __registerScrAddrNow from the PyBtcWallet - code (apparently I don't understand __methods). Use this method to - externally bypass the BDM thread queue and register the address - immediately. - - THIS METHOD IS UNSAFE UNLESS CALLED FROM A METHOD RUNNING IN THE BDM THREAD - This method can be called from a non BDM class, but should only do so if - that class method was called by the BDM (thus, no conflicts) - """ - self.__registerScrAddrNow(scrAddr, timeInfo) - - - ############################################################################# - def scanBlockchainForTx_bdm_direct(self, cppWlt, startBlk=0, endBlk=UINT32_MAX): - """ - THIS METHOD IS UNSAFE UNLESS CALLED FROM A METHOD RUNNING IN THE BDM THREAD - This method can be called from a non BDM class, but should only do so if - that class method was called by the BDM (thus, no conflicts) - """ - self.bdm.scanRegisteredTxForWallet(cppWlt, startBlk, endBlk) - - ############################################################################# - def scanRegisteredTxForWallet_bdm_direct(self, cppWlt, startBlk=0, endBlk=UINT32_MAX): - """ - THIS METHOD IS UNSAFE UNLESS CALLED FROM A METHOD RUNNING IN THE BDM THREAD - This method can be called from a non BDM class, but should only do so if - that class method was called by the BDM (thus, no conflicts) - """ - self.bdm.scanRegisteredTxForWallet(cppWlt, startBlk, endBlk) - - ############################################################################# - def getTopBlockHeight_bdm_direct(self): - """ - THIS METHOD IS UNSAFE UNLESS CALLED FROM A METHOD RUNNING IN THE BDM THREAD - This method can be called from a non BDM class, but should only do so if - that class method was called by the BDM (thus, no conflicts) - """ - return self.bdm.getTopBlockHeight() - - - - ############################################################################# - def getLoadProgress(self): - """ - This method does not actually work! The load progress in bytes is not - updated properly while the BDM thread is scanning. It might have to - emit this information explicitly in order to be useful. - """ - return (self.bdm.getLoadProgressBytes(), self.bdm.getTotalBlockchainBytes()) - - - ############################################################################# - def __registerScrAddrNow(self, scrAddr, timeInfo): - """ - Do the registration right now. This should not be called directly - outside of this class. This is only called by the BDM thread when - any previous scans have been completed - """ - - if isinstance(timeInfo, bool): - isFresh = timeInfo - if isFresh: - # We claimed to have just created this ScrAddr...(so no rescan needed) - self.masterCppWallet.addNewScrAddress_1_(scrAddr) - else: - self.masterCppWallet.addScrAddress_1_(scrAddr) - else: - if isinstance(timeInfo, (list,tuple)) and len(timeInfo)==4: - self.masterCppWallet.addScrAddress_5_(scrAddr, *timeInfo) - else: - LOGWARN('Unrecognized time information in register method.') - LOGWARN(' Data: %s', str(timeInfo)) - LOGWARN('Assuming imported key requires full rescan...') - self.masterCppWallet.addScrAddress_1_(scrAddr) - - - - ############################################################################# - def __startLoadBlockchain(self): - """ - This should only be called by the threaded BDM, and thus there should - never be a conflict. - """ - - LOGINFO('Called __startLoadBlockchain()') - - TimerStart('__startLoadBlockchain') - - if self.blkMode == BLOCKCHAINMODE.Rescanning: - LOGERROR('Blockchain is already scanning. Was this called already?') - return - elif self.blkMode == BLOCKCHAINMODE.Full: - LOGERROR('Blockchain has already been loaded -- maybe we meant') - LOGERROR('to call startRescanBlockchain()...?') - return - elif not self.blkMode == BLOCKCHAINMODE.Uninitialized: - LOGERROR('BDM should be in "Uninitialized" mode before starting ') - LOGERROR('the initial scan. If BDM is in offline mode, you should ') - LOGERROR('switch it to online-mode, first, then request the scan.') - LOGERROR('Continuing with the scan, anyway.') - - - # Remove "blkfiles.txt" to make sure we get accurate TGO - bfile = os.path.join(ARMORY_HOME_DIR,'blkfiles.txt') - if os.path.exists(bfile): - os.remove(bfile) - - # Check for the existence of the Bitcoin-Qt directory - if not os.path.exists(self.btcdir): - raise FileExistsError, ('Directory does not exist: %s' % self.btcdir) - - blkdir = os.path.join(self.btcdir, 'blocks') - blk1st = os.path.join(blkdir, 'blk00000.dat') - - # ... and its blk000X.dat files - if not os.path.exists(blk1st): - LOGERROR('Blockchain data not available: %s', blk1st) - self.prefMode = BLOCKCHAINMODE.Offline - raise FileExistsError, ('Blockchain data not available: %s' % self.blk1st) - - # We have the data, we're ready to go - self.blkMode = BLOCKCHAINMODE.Rescanning - self.aboutToRescan = False - - self.bdm.SetDatabaseModes(ARMORY_DB_BARE, DB_PRUNE_NONE); - self.bdm.SetHomeDirLocation(ARMORY_HOME_DIR) - self.bdm.SetBlkFileLocation(str(blkdir)) - self.bdm.SetLevelDBLocation(self.ldbdir) - self.bdm.SetBtcNetworkParams( GENESIS_BLOCK_HASH, \ - GENESIS_TX_HASH, \ - MAGIC_BYTES) - - # The master wallet contains all addresses of all wallets registered - self.bdm.registerWallet(self.masterCppWallet) - - # Now we actually startup the BDM and run with it - if CLI_OPTIONS.rebuild: - self.bdm.doInitialSyncOnLoad_Rebuild() - elif CLI_OPTIONS.rescan: - self.bdm.doInitialSyncOnLoad_Rescan() - else: - self.bdm.doInitialSyncOnLoad() - - # The above op populates the BDM with all relevent tx, but those tx - # still need to be scanned to collect the wallet ledger and UTXO sets - self.bdm.scanBlockchainForTx(self.masterCppWallet) - - TimerStop('__startLoadBlockchain') - - - ############################################################################# - def __startRescanBlockchain(self, scanType='AsNeeded'): - """ - This should only be called by the threaded BDM, and thus there should - never be a conflict. - - If we don't force a full scan, we let TheBDM figure out how much of the - chain needs to be rescanned. Which may not be very much. We may - force a full scan if we think there's an issue with balances. - """ - if self.blkMode==BLOCKCHAINMODE.Offline: - LOGERROR('Blockchain is in offline mode. How can we rescan?') - elif self.blkMode==BLOCKCHAINMODE.Uninitialized: - LOGERROR('Blockchain was never loaded. Why did we request rescan?') - - # Remove "blkfiles.txt" to make sure we get accurate TGO - bfile = os.path.join(ARMORY_HOME_DIR,'blkfiles.txt') - if os.path.exists(bfile): - os.remove(bfile) - - if not self.isDirty(): - LOGWARN('It does not look like we need a rescan... doing it anyway') - - if scanType=='AsNeeded': - if self.bdm.numBlocksToRescan(self.masterCppWallet) < 144: - LOGINFO('Rescan requested, but <1 day\'s worth of block to rescan') - self.blkMode = BLOCKCHAINMODE.LiteScanning - else: - LOGINFO('Rescan requested, and very large scan is necessary') - self.blkMode = BLOCKCHAINMODE.Rescanning - - - self.aboutToRescan = False - - if scanType=='AsNeeded': - self.bdm.doSyncIfNeeded() - elif scanType=='ForceRescan': - LOGINFO('Forcing full rescan of blockchain') - self.bdm.doFullRescanRegardlessOfSync() - self.blkMode = BLOCKCHAINMODE.Rescanning - elif scanType=='ForceRebuild': - LOGINFO('Forcing full rebuild of blockchain database') - self.bdm.doRebuildDatabases() - self.blkMode = BLOCKCHAINMODE.Rescanning - - self.bdm.scanBlockchainForTx(self.masterCppWallet) - - - ############################################################################# - def __startRecoveryRescan(self, pywlt): - """ - This should only be called by the threaded BDM, and thus there should - never be a conflict. - - In order to work cleanly with the threaded BDM, the search code - needed to be integrated directly here, instead of being called - from the PyBtcWallet method. Because that method is normally called - from outside the BDM thread, but this method is only called from - _inside_ the BDM thread. Those calls use the BDM stack which will - deadlock waiting for the itself before it can move on... - - Unfortunately, because of this, we have to break a python-class - privacy rules: we are accessing the PyBtcWallet object as if this - were PyBtcWallet code (accessing properties directly). - """ - if not isinstance(pywlt, PyBtcWallet): - LOGERROR('Only python wallets can be passed for recovery scans') - return - - if self.blkMode==BLOCKCHAINMODE.Offline: - LOGERROR('Blockchain is in offline mode. How can we rescan?') - elif self.blkMode==BLOCKCHAINMODE.Uninitialized: - LOGERROR('Blockchain was never loaded. Why did we request rescan?') - - - self.blkMode = BLOCKCHAINMODE.Rescanning - self.aboutToRescan = False - - ##### - - # Whenever calling PyBtcWallet methods from BDM, set flag - prevCalledFromBDM = pywlt.calledFromBDM - pywlt.calledFromBDM = True - - # Do the scan... - TimerStart('WalletRecoveryScan') - pywlt.freshImportFindHighestIndex() - TimerStop('WalletRecoveryScan') - - # Unset flag when done - pywlt.calledFromBDM = prevCalledFromBDM - - ##### - self.bdm.scanRegisteredTxForWallet(self.masterCppWallet) - - - - ############################################################################# - def __readBlockfileUpdates(self): - ''' - This method can be blocking... it always has been without a problem, - because the block file updates are always fast. But I have to assume - that it theoretically *could* take a while, and the caller might care. - ''' - if self.blkMode == BLOCKCHAINMODE.Offline: - LOGERROR('Can\'t update blockchain in %s mode!', self.getBDMState()) - return - - self.blkMode = BLOCKCHAINMODE.LiteScanning - nblk = self.bdm.readBlkFileUpdate() - return nblk - - - ############################################################################# - def __updateWalletsAfterScan(self): - """ - This will actually do a scan regardless of whether it is currently - "after scan", but it will usually only be requested right after a - full rescan - """ - - numToRescan = 0 - for pyWlt in self.pyWltList: - thisNum = self.bdm.numBlocksToRescan(pyWlt.cppWallet) - numToRescan = max(numToRescan, thisNum) - - for cppWlt in self.cppWltList: - thisNum = self.bdm.numBlocksToRescan(cppWlt) - numToRescan = max(numToRescan, thisNum) - - if numToRescan<144: - self.blkMode = BLOCKCHAINMODE.LiteScanning - else: - self.blkMode = BLOCKCHAINMODE.Rescanning - - - for pyWlt in self.pyWltList: - pyWlt.syncWithBlockchain() - - for cppWlt in self.cppWltList: - # The pre-leveldb version of Armory specifically required to call - # - # scanRegisteredTxForWallet (scan already-collected reg tx) - # - # instead of - # - # scanBlockchainForTx (search for reg tx then scan) - # - # Because the second one will induce a full rescan to find all new - # registeredTx, if we recently imported an addr or wallet. If we - # imported but decided not to rescan yet, we wan tthe first one, - # which only scans the registered tx that are already collected - # (including new blocks, but not previous blocks). - # - # However, with the leveldb stuff only supporting super-node, there - # is no rescanning, thus it's safe to always call scanBlockchainForTx, - # which grabs everything from the database almost instantaneously. - # However we may want to re-examine this after we implement new - # database modes of operation - #self.bdm.scanRegisteredTxForWallet(cppWlt) - self.bdm.scanBlockchainForTx(cppWlt) - - - - - ############################################################################# - def __shutdown(self): - if not self.blkMode == BLOCKCHAINMODE.Rescanning: - self.bdm.shutdownSaveScrAddrHistories() - - self.__reset() - self.blkMode = BLOCKCHAINMODE.Offline - self.doShutdown = True - - ############################################################################# - def __fullRebuild(self): - self.bdm.destroyAndResetDatabases() - self.__reset() - self.__startLoadBlockchain() - - ############################################################################# - def __reset(self): - LOGERROR('Resetting BDM and all wallets') - self.bdm.Reset() - - if self.blkMode in (BLOCKCHAINMODE.Full, BLOCKCHAINMODE.Rescanning): - # Uninitialized means we want to be online, but haven't loaded yet - self.blkMode = BLOCKCHAINMODE.Uninitialized - elif not self.blkMode==BLOCKCHAINMODE.Offline: - return - - self.bdm.resetRegisteredWallets() - - # Flags - self.startBDM = False - #self.btcdir = BTC_HOME_DIR - - # Lists of wallets that should be checked after blockchain updates - self.pyWltList = [] # these will be python refs - self.cppWltList = [] # these will be C++ refs - - - # The BlockDataManager is easier to use if you put all your addresses - # into a C++ BtcWallet object, and let it - self.masterCppWallet = Cpp.BtcWallet() - self.bdm.registerWallet(self.masterCppWallet) - - - ############################################################################# - def __getFullBlock(self, headerHash): - headerObj = self.bdm.getHeaderByHash(headerHash) - if not headerObj: - return None - - rawTxList = [] - txList = headerObj.getTxRefPtrList() - for txref in txList: - tx = txref.getTxCopy() - rawTxList.append(tx.serialize()) - - numTxVarInt = len(rawTxList) - blockBytes = 80 + len(numTxVarInt) + sum([len(tx) for tx in rawTxList]) - - rawBlock = MAGIC_BYTES - rawBlock += int_to_hex(blockBytes, endOut=LITTLEENDIAN, widthBytes=4) - rawBlock += headerObj.serialize() - rawBlock += packVarInt(numTx) - rawBlock += ''.join(rawTxList) - return rawBlock - - - ############################################################################# - def getBDMInputName(self, i): - for name in dir(BDMINPUTTYPE): - if getattr(BDMINPUTTYPE, name)==i: - return name - - ############################################################################# - def run(self): - """ - This thread runs in an infinite loop, waiting for things to show up - on the self.inputQueue, and then processing those entries. If there - are no requests to the BDM from the main thread, this thread will just - sit idle (in a CPU-friendly fashion) until something does. - """ - - while not self.doShutdown: - # If there were any errors, we will have that many extra output - # entries on the outputQueue. We clear them off so that this - # thread can be re-sync'd with the main thread - try: - while self.errorOut>0: - self.outputQueue.get_nowait() - self.errorOut -= 1 - except Queue.Empty: - LOGERROR('ErrorOut var over-represented number of errors!') - self.errorOut = 0 - - - # Now start the main - try: - try: - inputTuple = self.inputQueue.get_nowait() - # If we don't error out, we have stuff to process right now - except Queue.Empty: - # We only switch to offline/full/uninitialzed when the queue - # is empty. After that, then we block in a CPU-friendly way - # until data shows up on the Queue - if self.prefMode==BLOCKCHAINMODE.Full: - if self.bdm.isInitialized(): - self.blkMode = BLOCKCHAINMODE.Full - else: - self.blkMode = BLOCKCHAINMODE.Uninitialized - else: - self.blkMode = BLOCKCHAINMODE.Offline - - self.currentActivity = 'None' - - # Block until something shows up. - inputTuple = self.inputQueue.get() - except: - LOGERROR('Unknown error in BDM thread') - - - - # The first list element is always the BDMINPUTTYPE (command) - # The second argument is whether the caller will be waiting - # for the output: which means even if it's None, we need to - # put something on the output queue. - cmd = inputTuple[0] - rndID = inputTuple[1] - expectOutput = inputTuple[2] - output = None - - # Some variables that can be queried externally to figure out - # what the BDM is currently doing - self.currentActivity = self.getBDMInputName(inputTuple[0]) - self.currentID = rndID - - if CLI_OPTIONS.mtdebug: - #LOGDEBUG('BDM Start Exec: %s (%d): %s', self.getBDMInputName(inputTuple[0]), rndID, str(inputTuple)) - tstart = RightNow() - - - if cmd == BDMINPUTTYPE.RegisterAddr: - scrAddr,timeInfo = inputTuple[3:] - self.__registerScrAddrNow(scrAddr, timeInfo) - - elif cmd == BDMINPUTTYPE.ZeroConfTxToInsert: - rawTx = inputTuple[3] - timeIn = inputTuple[4] - if isinstance(rawTx, PyTx): - rawTx = rawTx.serialize() - self.bdm.addNewZeroConfTx(rawTx, timeIn, True) - - elif cmd == BDMINPUTTYPE.HeaderRequested: - headHash = inputTuple[3] - rawHeader = self.bdm.getHeaderByHash(headHash) - if rawHeader: - output = rawHeader - else: - output = None - - elif cmd == BDMINPUTTYPE.TxRequested: - txHash = inputTuple[3] - rawTx = self.bdm.getTxByHash(txHash) - if rawTx: - output = rawTx - else: - output = None - - elif cmd == BDMINPUTTYPE.BlockRequested: - headHash = inputTuple[3] - rawBlock = self.__getFullBlock(headHash) - if rawBlock: - output = rawBlock - else: - output = None - LOGERROR('Requested header does not exist:\n%s', \ - binary_to_hex(headHash)) - - elif cmd == BDMINPUTTYPE.HeaderAtHeightRequested: - height = inputTuple[3] - rawHeader = self.bdm.getHeaderByHeight(height) - if rawHeader: - output = rawHeader - else: - output = None - LOGERROR('Requested header does not exist:\nHeight=%s', height) - - elif cmd == BDMINPUTTYPE.BlockAtHeightRequested: - height = inputTuple[3] - rawBlock = self.__getFullBlock(height) - if rawBlock: - output = rawBlock - else: - output = None - LOGERROR('Requested header does not exist:\nHeight=%s', height) - - elif cmd == BDMINPUTTYPE.AddrBookRequested: - cppWlt = inputTuple[3] - TimerStart('createAddressBook') - output = cppWlt.createAddressBook() - TimerStop('createAddressBook') - - elif cmd == BDMINPUTTYPE.UpdateWallets: - TimerStart('updateWltsAfterScan') - self.__updateWalletsAfterScan() - TimerStop('updateWltsAfterScan') - - elif cmd == BDMINPUTTYPE.RescanRequested: - TimerStart('rescanBlockchain') - scanType = inputTuple[3] - if not scanType in ('AsNeeded', 'ForceRescan', 'ForceRebuild'): - LOGERROR('Invalid scan type for rescanning: ' + scanType) - scanType = 'AsNeeded' - self.__startRescanBlockchain(scanType) - TimerStop('rescanBlockchain') - - elif cmd == BDMINPUTTYPE.WalletRecoveryScan: - LOGINFO('Wallet Recovery Scan Requested') - pywlt = inputTuple[3] - TimerStart('recoveryRescan') - self.__startRecoveryRescan(pywlt) - TimerStop('recoveryRescan') - - - elif cmd == BDMINPUTTYPE.ReadBlkUpdate: - - TimerStart('readBlkFileUpdate') - output = self.__readBlockfileUpdates() - TimerStop('readBlkFileUpdate') - - - elif cmd == BDMINPUTTYPE.Passthrough: - # If the caller is waiting, then it is notified by output - funcName = inputTuple[3] - funcArgs = inputTuple[4:] - output = getattr(self.bdm, funcName)(*funcArgs) - - elif cmd == BDMINPUTTYPE.Shutdown: - LOGINFO('Shutdown Requested') - self.__shutdown() - - elif cmd == BDMINPUTTYPE.ForceRebuild: - LOGINFO('Rebuild databases requested') - self.__fullRebuild() - - elif cmd == BDMINPUTTYPE.Reset: - LOGINFO('Reset Requested') - self.__reset() - - elif cmd == BDMINPUTTYPE.GoOnlineRequested: - LOGINFO('Go online requested') - # This only sets the blkMode to what will later be - # recognized as online-requested, or offline - self.prefMode = BLOCKCHAINMODE.Full - if self.bdm.isInitialized(): - # The BDM was started and stopped at one point, without - # being reset. It can safely pick up from where it - # left off - self.__readBlockfileUpdates() - else: - self.blkMode = BLOCKCHAINMODE.Uninitialized - self.__startLoadBlockchain() - - elif cmd == BDMINPUTTYPE.GoOfflineRequested: - LOGINFO('Go offline requested') - self.prefMode = BLOCKCHAINMODE.Offline - - self.inputQueue.task_done() - if expectOutput: - self.outputQueue.put(output) - - except Queue.Empty: - continue - except: - inputName = self.getBDMInputName(inputTuple[0]) - LOGERROR('Error processing BDM input') - LOGERROR('Received inputTuple: ' + inputName + ' ' + str(inputTuple)) - LOGERROR('Error processing ID (%d)', rndID) - LOGEXCEPT('ERROR:') - if expectOutput: - self.outputQueue.put('BDM_REQUEST_ERROR') - self.inputQueue.task_done() - continue - - LOGINFO('BDM is shutdown.') - - - - - -################################################################################ -# Make TheBDM reference the asyncrhonous BlockDataManager wrapper if we are -# running - -if CLI_OPTIONS.offline: - LOGINFO('Armory loaded in offline-mode. Will not attempt to load ') - LOGINFO('blockchain without explicit command to do so.') - TheBDM = BlockDataManagerThread(isOffline=True, blocking=False) - TheBDM.start() - - # Also create the might-be-needed SatoshiDaemonManager - TheSDM = SatoshiDaemonManager() - -else: - # NOTE: "TheBDM" is sometimes used in the C++ code to reference the - # singleton BlockDataManager_LevelDB class object. Here, - # "TheBDM" refers to a python BlockDataManagerThead class - # object that wraps the C++ version. It implements some of - # it's own methods, and then passes through anything it - # doesn't recognize to the C++ object. - LOGINFO('Using the asynchronous/multi-threaded BlockDataManager.') - LOGINFO('Blockchain operations will happen in the background. ') - LOGINFO('Devs: check TheBDM.getBDMState() before asking for data.') - LOGINFO('Registering addresses during rescans will queue them for ') - LOGINFO('inclusion after the current scan is completed.') - TheBDM = BlockDataManagerThread(isOffline=False, blocking=False) - TheBDM.setDaemon(True) - TheBDM.start() - - #if CLI_OPTIONS.doDebug or CLI_OPTIONS.netlog or CLI_OPTIONS.mtdebug: - cppLogFile = os.path.join(ARMORY_HOME_DIR, 'armorycpplog.txt') - TheBDM.StartCppLogging(cppLogFile, 3) - TheBDM.EnableCppLogStdOut() - - # 32-bit linux has an issue with max open files. Rather than modifying - # the system, we can tell LevelDB to take it easy with max files to open - if OS_LINUX and not SystemSpecs.IsX64: - LOGINFO('Lowering max-open-files parameter in LevelDB for 32-bit linux') - TheBDM.setMaxOpenFiles(75) - - # Override the above if they explicitly specify it as CLI arg - if CLI_OPTIONS.maxOpenFiles > 0: - LOGINFO('Overriding max files via command-line arg') - TheBDM.setMaxOpenFiles( CLI_OPTIONS.maxOpenFiles ) - - #LOGINFO('LevelDB max-open-files is %d', TheBDM.getMaxOpenFiles()) - - # Also load the might-be-needed SatoshiDaemonManager - TheSDM = SatoshiDaemonManager() - - - - - - - - -################################################################################ -# -# Keep track of lots of different timers: -# -# Key: timerName -# Value: [cumulTime, numStart, lastStart, isRunning] -# -TimerMap = {} - -def TimerStart(timerName): - if not TimerMap.has_key(timerName): - TimerMap[timerName] = [0, 0, 0, False] - - timerEntry = TimerMap[timerName] - timerEntry[1] += 1 - timerEntry[2] = RightNow() - timerEntry[3] = True - -def TimerStop(timerName): - if not TimerMap.has_key(timerName): - LOGWARN('Requested stop timer that does not exist! (%s)' % timerName) - return - - if not TimerMap[timerName][3]: - LOGWARN('Requested stop timer that is not running! (%s)' % timerName) - return - - timerEntry = TimerMap[timerName] - timerEntry[0] += RightNow() - timerEntry[2] - timerEntry[2] = 0 - timerEntry[3] = False - - - -def TimerReset(timerName): - if not TimerMap.has_key(timerName): - LOGERROR('Requested reset timer that does not exist! (%s)' % timerName) - - # Even if it didn't exist, it will be created now - TimerMap[timerName] = [0, 0, 0, False] - - -def ReadTimer(timerName): - if not TimerMap.has_key(timerName): - LOGERROR('Requested read timer that does not exist! (%s)' % timerName) - return - - timerEntry = TimerMap[timerName] - return timerEntry[0] + (RightNow() - timerEntry[2]) - - -def PrintTimings(): - print 'Timings: '.ljust(30), - print 'nCall'.rjust(13), - print 'cumulTime'.rjust(13), - print 'avgTime'.rjust(13) - print '-'*70 - for tname,quad in TimerMap.iteritems(): - print ('%s' % tname).ljust(30), - print ('%d' % quad[1]).rjust(13), - print ('%0.6f' % quad[0]).rjust(13), - avg = quad[0]/quad[1] - print ('%0.6f' % avg).rjust(13) - print '-'*70 - - -def SaveTimingsCSV(fname): - f = open(fname, 'w') - f.write( 'TimerName,') - f.write( 'nCall,') - f.write( 'cumulTime,') - f.write( 'avgTime\n\n') - for tname,quad in TimerMap.iteritems(): - f.write('%s,' % tname) - f.write('%d,' % quad[1]) - f.write('%0.6f,' % quad[0]) - avg = quad[0]/quad[1] - f.write('%0.6f\n' % avg) - f.write('\n\nNote: timings may be incorrect if errors ' - 'were triggered in the timed functions') - print 'Saved timings to file: %s' % fname - - - - - - -################################################################################ -# I ORIGINALLY UPDATED THE TIMER TO USE collections MODULE, but it turns out -# that module is not available on many python versions. So I'm reverting to -# the older version of the timers ... will update to the version below... -# at some point... -# -# Keep track of lots of different timers: -# -# Key: timerName -# Value: [cumulTime, numStart, lastStart, isRunning] -# - -""" -import collections -TimerMap = collections.OrderedDict() -# Wanted to used namedtuple, but that would be immutable -class TimerObj(object): - def __init__(self): - self.cumulTime = 0 - self.callCount = 0 - self.lastStart = 0 - self.isRunning = False - - -################################################################################ -def TimerStart(timerName, nCall=1): - if not TimerMap.has_key(timerName): - TimerMap[timerName] = TimerObj() - - timerEntry = TimerMap[timerName] - timerEntry.callCount += nCall - timerEntry.lastStart = RightNow() - timerEntry.isRunning = True - -################################################################################ -def TimerStop(timerName): - if not TimerMap.has_key(timerName): - LOGWARN('Requested stop timer that does not exist! (%s)' % timerName) - return - - if not TimerMap[timerName].isRunning: - LOGWARN('Requested stop timer that is not running! (%s)' % timerName) - return - - timerEntry = TimerMap[timerName] - timerEntry.cumulTime += RightNow() - timerEntry.lastStart - timerEntry.lastStart = 0 - timerEntry.isRunning = False - - -################################################################################ -def TimerReset(timerName): - if not TimerMap.has_key(timerName): - LOGERROR('Requested reset timer that does not exist! (%s)' % timerName) - - # Even if it didn't exist, it will be created now - TimerMap[timerName] = TimerObj(0,0,0,False) - - -################################################################################ -def ReadTimer(timerName): - if not TimerMap.has_key(timerName): - LOGERROR('Requested read timer that does not exist! (%s)' % timerName) - return - - timerEntry = TimerMap[timerName] - return timerEntry.cumulTime + (RightNow() - timerEntry.lastStart) - - -def PrintTimings(): - print 'Timings: '.ljust(22), - print 'nCall'.rjust(8), - print 'cumulTime'.rjust(12), - print 'avgTime'.rjust(12), - print 'ops/sec'.rjust(12) - print '-'*80 - for tname,tobj in TimerMap.iteritems(): - print ('%s' % tname).ljust(22), - print ('%d' % tobj.callCount).rjust(8), - print ('%0.6f' % tobj.cumulTime).rjust(12), - avg = tobj.cumulTime/tobj.callCount - ops = tobj.callCount/tobj.cumulTime - print ('%0.6f' % avg).rjust(12), - print ('%0.2f' % ops).rjust(12) - print '-'*80 - - -## -def SaveTimingsCSV(fname): - f = open(fname, 'w') - f.write( 'TimerName,') - f.write( 'nCall,') - f.write( 'cumulTime,') - f.write( 'avgTime\n\n') - for tname,quad in TimerMap.iteritems(): - f.write('"%s",' % tname) - f.write('%d,' % quad.callCount) - f.write('%0.6f,' % quad.cumulTime) - avg = quad.cumulTime/quad.callCount - f.write('%0.6f\n' % avg) - f.write('\n\nNote: timings may be incorrect if errors ' - 'were triggered in the timed functions') - print 'Saved timings to file: %s' % fname -""" -def EstimateCumulativeBlockchainSize(blkNum): - # I tried to make a "static" variable here so that - # the string wouldn't be parsed on every call, but - # I botched that, somehow. - # - # It doesn't *have to* be fast, but why not? - # Oh well.. - blksizefile = """ - 0 285 - 20160 4496226 - 40320 9329049 - 60480 16637208 - 80640 31572990 - 82656 33260320 - 84672 35330575 - 86688 36815335 - 88704 38386205 - 100800 60605119 - 102816 64795352 - 104832 68697265 - 108864 79339447 - 112896 92608525 - 116928 116560952 - 120960 140607929 - 124992 170059586 - 129024 217718109 - 133056 303977266 - 137088 405836779 - 141120 500934468 - 145152 593217668 - 149184 673064617 - 153216 745173386 - 157248 816675650 - 161280 886105443 - 165312 970660768 - 169344 1058290613 - 173376 1140721593 - 177408 1240616018 - 179424 1306862029 - 181440 1463634913 - 183456 1639027360 - 185472 1868851317 - 187488 2019397056 - 189504 2173291204 - 191520 2352873908 - 193536 2530862533 - 195552 2744361593 - 197568 2936684028 - 199584 3115432617 - 201600 3282437367 - 203616 3490737816 - 205632 3669806064 - 207648 3848901149 - 209664 4064972247 - 211680 4278148686 - 213696 4557787597 - 215712 4786120879 - 217728 5111707340 - 219744 5419128115 - 221760 5733907456 - 223776 6053668460 - 225792 6407870776 - 227808 6652067986 - 228534 6778529822 - 257568 10838081536 - 259542 11106516992 - """ - strList = [line.strip().split() for line in blksizefile.strip().split('\n')] - BLK_SIZE_LIST = [[int(x[0]), int(x[1])] for x in strList] - - if blkNum < BLK_SIZE_LIST[-1][0]: - # Interpolate - bprev,bcurr = None, None - for i,blkpair in enumerate(BLK_SIZE_LIST): - if blkNum < blkpair[0]: - b0,d0 = BLK_SIZE_LIST[i-1] - b1,d1 = blkpair - ratio = float(blkNum-b0)/float(b1-b0) - return int(ratio*d1 + (1-ratio)*d0) - raise ValueError, 'Interpolation failed for %d' % blkNum - - else: - bend, dend = BLK_SIZE_LIST[-1] - bend2, dend2 = BLK_SIZE_LIST[-3] - rate = float(dend - dend2) / float(bend - bend2) # bytes per block - extraOnTop = (blkNum - bend) * rate - return dend+extraOnTop - - - - - - - - - diff --git a/armoryengine/ALL.py b/armoryengine/ALL.py new file mode 100644 index 000000000..07523697f --- /dev/null +++ b/armoryengine/ALL.py @@ -0,0 +1,11 @@ +from armoryengine.ArmoryUtils import * +from armoryengine.BinaryUnpacker import * +from armoryengine.BDM import * +from armoryengine.CoinSelection import * +from armoryengine.Networking import * +from armoryengine.PyBtcWallet import * +from armoryengine.Script import * +from SDM import * +from armoryengine.Timer import * +from armoryengine.Transaction import * + diff --git a/armoryengine/ArmoryUtils.py b/armoryengine/ArmoryUtils.py new file mode 100644 index 000000000..08eb169db --- /dev/null +++ b/armoryengine/ArmoryUtils.py @@ -0,0 +1,3231 @@ +################################################################################ +# +# Copyright (C) 2011-2014, Armory Technologies, Inc. +# Distributed under the GNU Affero General Public License (AGPL v3) +# See LICENSE or http://www.gnu.org/licenses/agpl.html +# +################################################################################ +# +# Project: Armory +# Author: Alan Reiner +# Website: www.bitcoinarmory.com +# Orig Date: 20 November, 2011 +# +################################################################################ +import ast +from datetime import datetime +import hashlib +import inspect +import locale +import logging +import math +import multiprocessing +import optparse +import os +import platform +import random +import signal +from struct import pack, unpack +#from subprocess import PIPE +import sys +import threading +import time +import traceback +import shutil + +#from psutil import Popen +import psutil + +from CppBlockUtils import KdfRomix, CryptoAES +from qrcodenative import QRCode, QRErrorCorrectLevel + + +# Version Numbers +BTCARMORY_VERSION = (0, 91, 1, 0) # (Major, Minor, Bugfix, AutoIncrement) +PYBTCWALLET_VERSION = (1, 35, 0, 0) # (Major, Minor, Bugfix, AutoIncrement) + +ARMORY_DONATION_ADDR = '1ArmoryXcfq7TnCSuZa9fQjRYwJ4bkRKfv' +ARMORY_DONATION_PUBKEY = ( '04' + '11d14f8498d11c33d08b0cd7b312fb2e6fc9aebd479f8e9ab62b5333b2c395c5' + 'f7437cab5633b5894c4a5c2132716bc36b7571cbe492a7222442b75df75b9a84') +ARMORY_INFO_SIGN_ADDR = '1NWvhByxfTXPYNT4zMBmEY3VL8QJQtQoei' +ARMORY_INFO_SIGN_PUBLICKEY = ('04' + 'af4abc4b24ef57547dd13a1110e331645f2ad2b99dfe1189abb40a5b24e4ebd8' + 'de0c1c372cc46bbee0ce3d1d49312e416a1fa9c7bb3e32a7eb3867d1c6d1f715') +SATOSHI_PUBLIC_KEY = ( '04' + 'fc9702847840aaf195de8442ebecedf5b095cdbb9bc716bda9110971b28a49e0' + 'ead8564ff0db22209e0374782c093bb899692d524e9d6a6956e7c5ecbcd68284') + + +indent = ' '*3 +haveGUI = [False, None] + +parser = optparse.OptionParser(usage="%prog [options]\n") +parser.add_option("--settings", dest="settingsPath",default='DEFAULT', type="str", help="load Armory with a specific settings file") +parser.add_option("--datadir", dest="datadir", default='DEFAULT', type="str", help="Change the directory that Armory calls home") +parser.add_option("--satoshi-datadir", dest="satoshiHome", default='DEFAULT', type='str', help="The Bitcoin-Qt/bitcoind home directory") +parser.add_option("--satoshi-port", dest="satoshiPort", default='DEFAULT', type="str", help="For Bitcoin-Qt instances operating on a non-standard port") +parser.add_option("--satoshi-rpcport", dest="satoshiRpcport",default='DEFAULT',type="str", help="RPC port Bitcoin-Qt instances operating on a non-standard port") +#parser.add_option("--bitcoind-path", dest="bitcoindPath",default='DEFAULT', type="str", help="Path to the location of bitcoind on your system") +parser.add_option("--dbdir", dest="leveldbDir", default='DEFAULT', type='str', help="Location to store blocks database (defaults to --datadir)") +parser.add_option("--rpcport", dest="rpcport", default='DEFAULT', type="str", help="RPC port for running armoryd.py") +parser.add_option("--testnet", dest="testnet", default=False, action="store_true", help="Use the testnet protocol") +parser.add_option("--offline", dest="offline", default=False, action="store_true", help="Force Armory to run in offline mode") +parser.add_option("--nettimeout", dest="nettimeout", default=2, type="int", help="Timeout for detecting internet connection at startup") +parser.add_option("--interport", dest="interport", default=-1, type="int", help="Port for inter-process communication between Armory instances") +parser.add_option("--debug", dest="doDebug", default=False, action="store_true", help="Increase amount of debugging output") +parser.add_option("--nologging", dest="logDisable", default=False, action="store_true", help="Disable all logging") +parser.add_option("--netlog", dest="netlog", default=False, action="store_true", help="Log networking messages sent and received by Armory") +parser.add_option("--logfile", dest="logFile", default='DEFAULT', type='str', help="Specify a non-default location to send logging information") +parser.add_option("--mtdebug", dest="mtdebug", default=False, action="store_true", help="Log multi-threaded call sequences") +parser.add_option("--skip-online-check", dest="forceOnline", default=False, action="store_true", help="Go into online mode, even if internet connection isn't detected") +parser.add_option("--skip-version-check", dest="skipVerCheck", default=False, action="store_true", help="Do not contact bitcoinarmory.com to check for new versions") +parser.add_option("--skip-announce-check", dest="skipAnnounceCheck", default=False, action="store_true", help="Do not query for Armory announcements") +parser.add_option("--keypool", dest="keypool", default=100, type="int", help="Default number of addresses to lookahead in Armory wallets") +parser.add_option("--redownload", dest="redownload", default=False, action="store_true", help="Delete Bitcoin-Qt/bitcoind databases; redownload") +parser.add_option("--rebuild", dest="rebuild", default=False, action="store_true", help="Rebuild blockchain database and rescan") +parser.add_option("--rescan", dest="rescan", default=False, action="store_true", help="Rescan existing blockchain DB") +parser.add_option("--maxfiles", dest="maxOpenFiles",default=0, type="int", help="Set maximum allowed open files for LevelDB databases") +parser.add_option("--disable-torrent", dest="disableTorrent", default=False, action="store_true", help="Only download blockchain data via P2P network (slow)") +parser.add_option("--test-announce", dest="testAnnounceCode", default=False, action="store_true", help="Only used for developers needing to test announcement code with non-offline keys") +#parser.add_option("--rebuildwithblocksize", dest="newBlockSize",default='32kB', type="str", help="Rebuild databases with new blocksize") +parser.add_option("--nospendzeroconfchange",dest="ignoreAllZC",default=False, action="store_true", help="All zero-conf funds will be unspendable, including sent-to-self coins") +parser.add_option("--force-wallet-check", dest="forceWalletCheck", default=False, action="store_true", help="Force the wallet sanity check on startup") + +# Pre-10.9 OS X sometimes passes a process serial number as -psn_0_xxxxxx. Nuke! +if sys.platform == 'darwin': + parser.add_option('-p', '--psn') + +# These are arguments passed by running unit-tests that need to be handled +parser.add_option("--port", dest="port", default=None, type="int", help="Unit Test Argument - Do not consume") +parser.add_option("--verbosity", dest="verbosity", default=None, type="int", help="Unit Test Argument - Do not consume") +parser.add_option("--coverage_output_dir", dest="coverageOutputDir", default=None, type="str", help="Unit Test Argument - Do not consume") +parser.add_option("--coverage_include", dest="coverageInclude", default=None, type="str", help="Unit Test Argument - Do not consume") + + + +# Some useful constants to be used throughout everything +BASE58CHARS = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' +BASE16CHARS = '0123 4567 89ab cdef'.replace(' ','') +LITTLEENDIAN = '<'; +BIGENDIAN = '>'; +NETWORKENDIAN = '!'; +ONE_BTC = long(100000000) +DONATION = long(5000000) +CENT = long(1000000) +UNINITIALIZED = None +UNKNOWN = -2 +MIN_TX_FEE = 10000 +MIN_RELAY_TX_FEE = 10000 +MT_WAIT_TIMEOUT_SEC = 20; + +UINT8_MAX = 2**8-1 +UINT16_MAX = 2**16-1 +UINT32_MAX = 2**32-1 +UINT64_MAX = 2**64-1 + +RightNow = time.time +SECOND = 1 +MINUTE = 60 +HOUR = 3600 +DAY = 24*HOUR +WEEK = 7*DAY +MONTH = 30*DAY +YEAR = 365*DAY + +KILOBYTE = 1024.0 +MEGABYTE = 1024*KILOBYTE +GIGABYTE = 1024*MEGABYTE +TERABYTE = 1024*GIGABYTE +PETABYTE = 1024*TERABYTE + +# Set the default-default +DEFAULT_DATE_FORMAT = '%Y-%b-%d %I:%M%p' +FORMAT_SYMBOLS = [ \ + ['%y', 'year, two digit (00-99)'], \ + ['%Y', 'year, four digit'], \ + ['%b', 'month name (abbrev)'], \ + ['%B', 'month name (full)'], \ + ['%m', 'month number (01-12)'], \ + ['%d', 'day of month (01-31)'], \ + ['%H', 'hour 24h (00-23)'], \ + ['%I', 'hour 12h (01-12)'], \ + ['%M', 'minute (00-59)'], \ + ['%p', 'morning/night (am,pm)'], \ + ['%a', 'day of week (abbrev)'], \ + ['%A', 'day of week (full)'], \ + ['%%', 'percent symbol'] ] + + +class UnserializeError(Exception): pass +class BadAddressError(Exception): pass +class VerifyScriptError(Exception): pass +class FileExistsError(Exception): pass +class ECDSA_Error(Exception): pass +class UnitializedBlockDataError(Exception): pass +class WalletLockError(Exception): pass +class SignatureError(Exception): pass +class KeyDataError(Exception): pass +class ChecksumError(Exception): pass +class WalletAddressError(Exception): pass +class PassphraseError(Exception): pass +class EncryptionError(Exception): pass +class InterruptTestError(Exception): pass +class NetworkIDError(Exception): pass +class WalletExistsError(Exception): pass +class ConnectionError(Exception): pass +class BlockchainUnavailableError(Exception): pass +class InvalidHashError(Exception): pass +class InvalidScriptError(Exception): pass +class BadURIError(Exception): pass +class CompressedKeyError(Exception): pass +class TooMuchPrecisionError(Exception): pass +class NegativeValueError(Exception): pass +class FiniteFieldError(Exception): pass +class BitcoindError(Exception): pass +class ShouldNotGetHereError(Exception): pass +class BadInputError(Exception): pass +class TxdpError(Exception): pass +class P2SHNotSupportedError(Exception): pass + +# Get the host operating system +opsys = platform.system() +OS_WINDOWS = 'win32' in opsys.lower() or 'windows' in opsys.lower() +OS_LINUX = 'nix' in opsys.lower() or 'nux' in opsys.lower() +OS_MACOSX = 'darwin' in opsys.lower() or 'osx' in opsys.lower() + + +if getattr(sys, 'frozen', False): + sys.argv = [arg.decode('utf8') for arg in sys.argv] + +CLI_OPTIONS = None +CLI_ARGS = None +(CLI_OPTIONS, CLI_ARGS) = parser.parse_args() + + +# This is probably an abuse of the CLI_OPTIONS structure, but not +# automatically expanding "~" symbols is killing me +for opt,val in CLI_OPTIONS.__dict__.iteritems(): + if not isinstance(val, basestring) or not val.startswith('~'): + continue + + if os.path.exists(os.path.expanduser(val)): + CLI_OPTIONS.__dict__[opt] = os.path.expanduser(val) + else: + # If the path doesn't exist, it still won't exist when we don't + # modify it, and I'd like to modify as few vars as possible + pass + + +# Use CLI args to determine testnet or not +USE_TESTNET = CLI_OPTIONS.testnet + +# Set default port for inter-process communication +if CLI_OPTIONS.interport < 0: + CLI_OPTIONS.interport = 8223 + (1 if USE_TESTNET else 0) + + +# Pass this bool to all getSpendable* methods, and it will consider +# all zero-conf UTXOs as unspendable, including sent-to-self (change) +IGNOREZC = CLI_OPTIONS.ignoreAllZC + + +# Figure out the default directories for Satoshi client, and BicoinArmory +OS_NAME = '' +OS_VARIANT = '' +USER_HOME_DIR = '' +BTC_HOME_DIR = '' +ARMORY_HOME_DIR = '' +LEVELDB_DIR = '' +SUBDIR = 'testnet3' if USE_TESTNET else '' +if OS_WINDOWS: + OS_NAME = 'Windows' + OS_VARIANT = platform.win32_ver() + USER_HOME_DIR = os.getenv('APPDATA') + BTC_HOME_DIR = os.path.join(USER_HOME_DIR, 'Bitcoin', SUBDIR) + ARMORY_HOME_DIR = os.path.join(USER_HOME_DIR, 'Armory', SUBDIR) + BLKFILE_DIR = os.path.join(BTC_HOME_DIR, 'blocks') + BLKFILE_1stFILE = os.path.join(BLKFILE_DIR, 'blk00000.dat') +elif OS_LINUX: + OS_NAME = 'Linux' + OS_VARIANT = platform.linux_distribution() + USER_HOME_DIR = os.getenv('HOME') + BTC_HOME_DIR = os.path.join(USER_HOME_DIR, '.bitcoin', SUBDIR) + ARMORY_HOME_DIR = os.path.join(USER_HOME_DIR, '.armory', SUBDIR) + BLKFILE_DIR = os.path.join(BTC_HOME_DIR, 'blocks') + BLKFILE_1stFILE = os.path.join(BLKFILE_DIR, 'blk00000.dat') +elif OS_MACOSX: + platform.mac_ver() + OS_NAME = 'MacOSX' + OS_VARIANT = platform.mac_ver() + USER_HOME_DIR = os.path.expanduser('~/Library/Application Support') + BTC_HOME_DIR = os.path.join(USER_HOME_DIR, 'Bitcoin', SUBDIR) + ARMORY_HOME_DIR = os.path.join(USER_HOME_DIR, 'Armory', SUBDIR) + BLKFILE_DIR = os.path.join(BTC_HOME_DIR, 'blocks') + BLKFILE_1stFILE = os.path.join(BLKFILE_DIR, 'blk00000.dat') +else: + print '***Unknown operating system!' + print '***Cannot determine default directory locations' + + + + +# Get the host operating system +opsys = platform.system() +OS_WINDOWS = 'win32' in opsys.lower() or 'windows' in opsys.lower() +OS_LINUX = 'nix' in opsys.lower() or 'nux' in opsys.lower() +OS_MACOSX = 'darwin' in opsys.lower() or 'osx' in opsys.lower() + +BLOCKCHAINS = {} +BLOCKCHAINS['\xf9\xbe\xb4\xd9'] = "Main Network" +BLOCKCHAINS['\xfa\xbf\xb5\xda'] = "Old Test Network" +BLOCKCHAINS['\x0b\x11\x09\x07'] = "Test Network (testnet3)" + +NETWORKS = {} +NETWORKS['\x00'] = "Main Network" +NETWORKS['\x05'] = "Main Network" +NETWORKS['\x6f'] = "Test Network" +NETWORKS['\xc4'] = "Test Network" +NETWORKS['\x34'] = "Namecoin Network" + + +# We disable wallet checks on ARM for the sake of resources (unless forced) +DO_WALLET_CHECK = CLI_OPTIONS.forceWalletCheck or \ + not platform.machine().lower().startswith('arm') + +# Version Handling Code +def getVersionString(vquad, numPieces=4): + vstr = '%d.%02d' % vquad[:2] + if (vquad[2] > 0 or vquad[3] > 0) and numPieces>2: + vstr += '.%d' % vquad[2] + if vquad[3] > 0 and numPieces>3: + vstr += '.%d' % vquad[3] + return vstr + +def getVersionInt(vquad, numPieces=4): + vint = int(vquad[0] * 1e7) + vint += int(vquad[1] * 1e5) + if numPieces>2: + vint += int(vquad[2] * 1e3) + if numPieces>3: + vint += int(vquad[3]) + return vint + +def readVersionString(verStr): + verList = [int(piece) for piece in verStr.split('.')] + while len(verList)<4: + verList.append(0) + return tuple(verList) + +def readVersionInt(verInt): + verStr = str(verInt).rjust(10,'0') + verList = [] + verList.append( int(verStr[ -3:]) ) + verList.append( int(verStr[ -5:-3 ]) ) + verList.append( int(verStr[ -7:-5 ]) ) + verList.append( int(verStr[:-7 ]) ) + return tuple(verList[::-1]) +# Allow user to override default bitcoin-qt/bitcoind home directory +if not CLI_OPTIONS.satoshiHome.lower()=='default': + success = True + if USE_TESTNET: + testnetTry = os.path.join(CLI_OPTIONS.satoshiHome, 'testnet3') + if os.path.exists(testnetTry): + CLI_OPTIONS.satoshiHome = testnetTry + + if not os.path.exists(CLI_OPTIONS.satoshiHome): + print 'Directory "%s" does not exist! Using default!' % \ + CLI_OPTIONS.satoshiHome + else: + BTC_HOME_DIR = CLI_OPTIONS.satoshiHome + + + + + +# Allow user to override default Armory home directory +if not CLI_OPTIONS.datadir.lower()=='default': + if not os.path.exists(CLI_OPTIONS.datadir): + print 'Directory "%s" does not exist! Using default!' % \ + CLI_OPTIONS.datadir + else: + ARMORY_HOME_DIR = CLI_OPTIONS.datadir + +# Same for the directory that holds the LevelDB databases +LEVELDB_DIR = os.path.join(ARMORY_HOME_DIR, 'databases') + +if not CLI_OPTIONS.leveldbDir.lower()=='default': + if not os.path.exists(CLI_OPTIONS.leveldbDir): + print 'Directory "%s" does not exist! Using default!' % \ + CLI_OPTIONS.leveldbDir + os.makedirs(CLI_OPTIONS.leveldbDir) + else: + LEVELDB_DIR = CLI_OPTIONS.leveldbDir + + +# Change the log file to use +ARMORY_LOG_FILE = os.path.join(ARMORY_HOME_DIR, 'armorylog.txt') +ARMCPP_LOG_FILE = os.path.join(ARMORY_HOME_DIR, 'armorycpplog.txt') +if not sys.argv[0] in ['ArmoryQt.py', 'ArmoryQt.exe', 'Armory.exe']: + basename = os.path.basename(sys.argv[0]) + CLI_OPTIONS.logFile = os.path.join(ARMORY_HOME_DIR, '%s.log.txt' % basename) + + +# Change the settings file to use +if CLI_OPTIONS.settingsPath.lower()=='default': + CLI_OPTIONS.settingsPath = os.path.join(ARMORY_HOME_DIR, 'ArmorySettings.txt') + +# Change the log file to use +if CLI_OPTIONS.logFile.lower()=='default': + if sys.argv[0] in ['ArmoryQt.py', 'ArmoryQt.exe', 'Armory.exe']: + CLI_OPTIONS.logFile = os.path.join(ARMORY_HOME_DIR, 'armorylog.txt') + else: + basename = os.path.basename(sys.argv[0]) + CLI_OPTIONS.logFile = os.path.join(ARMORY_HOME_DIR, '%s.log.txt' % basename) + + +SETTINGS_PATH = CLI_OPTIONS.settingsPath +MULT_LOG_FILE = os.path.join(ARMORY_HOME_DIR, 'multipliers.txt') + + +# If this is the first Armory has been run, create directories +if ARMORY_HOME_DIR and not os.path.exists(ARMORY_HOME_DIR): + os.makedirs(ARMORY_HOME_DIR) + + +if not os.path.exists(LEVELDB_DIR): + os.makedirs(LEVELDB_DIR) + +##### MAIN NETWORK IS DEFAULT ##### +if not USE_TESTNET: + # TODO: The testnet genesis tx hash can't be the same...? + BITCOIN_PORT = 8333 + BITCOIN_RPC_PORT = 8332 + ARMORY_RPC_PORT = 8225 + MAGIC_BYTES = '\xf9\xbe\xb4\xd9' + GENESIS_BLOCK_HASH_HEX = '6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000' + GENESIS_BLOCK_HASH = 'o\xe2\x8c\n\xb6\xf1\xb3r\xc1\xa6\xa2F\xaec\xf7O\x93\x1e\x83e\xe1Z\x08\x9ch\xd6\x19\x00\x00\x00\x00\x00' + GENESIS_TX_HASH_HEX = '3ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a' + GENESIS_TX_HASH = ';\xa3\xed\xfdz{\x12\xb2z\xc7,>gv\x8fa\x7f\xc8\x1b\xc3\x88\x8aQ2:\x9f\xb8\xaaK\x1e^J' + ADDRBYTE = '\x00' + P2SHBYTE = '\x05' + PRIVKEYBYTE = '\x80' +else: + BITCOIN_PORT = 18333 + BITCOIN_RPC_PORT = 18332 + ARMORY_RPC_PORT = 18225 + MAGIC_BYTES = '\x0b\x11\x09\x07' + GENESIS_BLOCK_HASH_HEX = '43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000' + GENESIS_BLOCK_HASH = 'CI\x7f\xd7\xf8&\x95q\x08\xf4\xa3\x0f\xd9\xce\xc3\xae\xbay\x97 \x84\xe9\x0e\xad\x01\xea3\t\x00\x00\x00\x00' + GENESIS_TX_HASH_HEX = '3ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a' + GENESIS_TX_HASH = ';\xa3\xed\xfdz{\x12\xb2z\xc7,>gv\x8fa\x7f\xc8\x1b\xc3\x88\x8aQ2:\x9f\xb8\xaaK\x1e^J' + ADDRBYTE = '\x6f' + P2SHBYTE = '\xc4' + PRIVKEYBYTE = '\xef' + +# These are the same regardless of network +# They are the way data is stored in the database which is network agnostic +SCRADDR_P2PKH_BYTE = '\x00' +SCRADDR_P2SH_BYTE = '\x05' +SCRADDR_MULTISIG_BYTE = '\xfe' +SCRADDR_NONSTD_BYTE = '\xff' +SCRADDR_BYTE_LIST = [SCRADDR_P2PKH_BYTE, \ + SCRADDR_P2SH_BYTE, \ + SCRADDR_MULTISIG_BYTE, \ + SCRADDR_NONSTD_BYTE] + +# Copied from cppForSwig/BtcUtils.h::getTxOutScriptTypeInt(script) +CPP_TXOUT_STDHASH160 = 0 +CPP_TXOUT_STDPUBKEY65 = 1 +CPP_TXOUT_STDPUBKEY33 = 2 +CPP_TXOUT_MULTISIG = 3 +CPP_TXOUT_P2SH = 4 +CPP_TXOUT_NONSTANDARD = 5 +CPP_TXOUT_HAS_ADDRSTR = [CPP_TXOUT_STDHASH160, \ + CPP_TXOUT_STDPUBKEY65, + CPP_TXOUT_STDPUBKEY33, + CPP_TXOUT_P2SH] +CPP_TXOUT_STDSINGLESIG = [CPP_TXOUT_STDHASH160, \ + CPP_TXOUT_STDPUBKEY65, + CPP_TXOUT_STDPUBKEY33] + +CPP_TXOUT_SCRIPT_NAMES = ['']*6 +CPP_TXOUT_SCRIPT_NAMES[CPP_TXOUT_STDHASH160] = 'Standard (PKH)' +CPP_TXOUT_SCRIPT_NAMES[CPP_TXOUT_STDPUBKEY65] = 'Standard (PK65)' +CPP_TXOUT_SCRIPT_NAMES[CPP_TXOUT_STDPUBKEY33] = 'Standard (PK33)' +CPP_TXOUT_SCRIPT_NAMES[CPP_TXOUT_MULTISIG] = 'Multi-Signature' +CPP_TXOUT_SCRIPT_NAMES[CPP_TXOUT_P2SH] = 'Standard (P2SH)' +CPP_TXOUT_SCRIPT_NAMES[CPP_TXOUT_NONSTANDARD] = 'Non-Standard' + +# Copied from cppForSwig/BtcUtils.h::getTxInScriptTypeInt(script) +CPP_TXIN_STDUNCOMPR = 0 +CPP_TXIN_STDCOMPR = 1 +CPP_TXIN_COINBASE = 2 +CPP_TXIN_SPENDPUBKEY = 3 +CPP_TXIN_SPENDMULTI = 4 +CPP_TXIN_SPENDP2SH = 5 +CPP_TXIN_NONSTANDARD = 6 + +CPP_TXIN_SCRIPT_NAMES = ['']*7 +CPP_TXIN_SCRIPT_NAMES[CPP_TXIN_STDUNCOMPR] = 'Sig + PubKey65' +CPP_TXIN_SCRIPT_NAMES[CPP_TXIN_STDCOMPR] = 'Sig + PubKey33' +CPP_TXIN_SCRIPT_NAMES[CPP_TXIN_COINBASE] = 'Coinbase' +CPP_TXIN_SCRIPT_NAMES[CPP_TXIN_SPENDPUBKEY] = 'Plain Signature' +CPP_TXIN_SCRIPT_NAMES[CPP_TXIN_SPENDMULTI] = 'Spend Multisig' +CPP_TXIN_SCRIPT_NAMES[CPP_TXIN_SPENDP2SH] = 'Spend P2SH' +CPP_TXIN_SCRIPT_NAMES[CPP_TXIN_NONSTANDARD] = 'Non-Standard' + + +################################################################################ +if not CLI_OPTIONS.satoshiPort == 'DEFAULT': + try: + BITCOIN_PORT = int(CLI_OPTIONS.satoshiPort) + except: + raise TypeError('Invalid port for Bitcoin-Qt, using ' + str(BITCOIN_PORT)) + +if not CLI_OPTIONS.satoshiRpcport == 'DEFAULT': + try: + BITCOIN_RPC_PORT = int(CLI_OPTIONS.satoshiRpcport) + except: + raise TypeError('Invalid rpc port for Bitcoin-Qt, using ' + str(BITCOIN_RPC_PORT)) + +if not CLI_OPTIONS.rpcport == 'DEFAULT': + try: + ARMORY_RPC_PORT = int(CLI_OPTIONS.rpcport) + except: + raise TypeError('Invalid RPC port for armoryd ' + str(ARMORY_RPC_PORT)) + + + +if sys.argv[0]=='ArmoryQt.py': + print '********************************************************************************' + print 'Loading Armory Engine:' + print ' Armory Version: ', getVersionString(BTCARMORY_VERSION) + print ' PyBtcWallet Version:', getVersionString(PYBTCWALLET_VERSION) + print 'Detected Operating system:', OS_NAME + print ' OS Variant :', OS_VARIANT + print ' User home-directory :', USER_HOME_DIR + print ' Satoshi BTC directory :', BTC_HOME_DIR + print ' Armory home dir :', ARMORY_HOME_DIR + print ' LevelDB directory :', LEVELDB_DIR + print ' Armory settings file :', SETTINGS_PATH + print ' Armory log file :', ARMORY_LOG_FILE + print ' Do wallet checking :', DO_WALLET_CHECK + + + +################################################################################ +def launchProcess(cmd, useStartInfo=True, *args, **kwargs): + LOGINFO('Executing popen: %s', str(cmd)) + if not OS_WINDOWS: + from subprocess import Popen, PIPE + return Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, *args, **kwargs) + else: + from subprocess_win import Popen, PIPE, STARTUPINFO, STARTF_USESHOWWINDOW + + if useStartInfo: + startinfo = STARTUPINFO() + startinfo.dwFlags |= STARTF_USESHOWWINDOW + return Popen(cmd, \ + *args, \ + stdin=PIPE, \ + stdout=PIPE, \ + stderr=PIPE, \ + startupinfo=startinfo, \ + **kwargs) + else: + return Popen(cmd, \ + *args, \ + stdin=PIPE, \ + stdout=PIPE, \ + stderr=PIPE, \ + **kwargs) + + +################################################################################ +def killProcess(pid, sig='default'): + # I had to do this, because killing a process in Windows has issues + # when using py2exe (yes, os.kill does not work, for the same reason + # I had to pass stdin/stdout/stderr everywhere... + LOGWARN('Killing process pid=%d', pid) + if not OS_WINDOWS: + import os + sig = signal.SIGKILL if sig=='default' else sig + os.kill(pid, sig) + else: + import sys, os.path, ctypes, ctypes.wintypes + k32 = ctypes.WinDLL('kernel32.dll') + k32.OpenProcess.restype = ctypes.wintypes.HANDLE + k32.TerminateProcess.restype = ctypes.wintypes.BOOL + hProcess = k32.OpenProcess(1, False, pid) + k32.TerminateProcess(hProcess, 1) + k32.CloseHandle(hProcess) + + + +################################################################################ +def subprocess_check_output(*popenargs, **kwargs): + """ + Run command with arguments and return its output as a byte string. + Backported from Python 2.7, because it's stupid useful, short, and + won't exist on systems using Python 2.6 or earlier + """ + from subprocess import CalledProcessError + process = launchProcess(*popenargs, **kwargs) + output, unused_err = process.communicate() + retcode = process.poll() + if retcode: + cmd = kwargs.get("args") + if cmd is None: + cmd = popenargs[0] + error = CalledProcessError(retcode, cmd) + error.output = output + raise error + return output + + +################################################################################ +def killProcessTree(pid): + # In this case, Windows is easier because we know it has the get_children + # call, because have bundled a recent version of psutil. Linux, however, + # does not have that function call in earlier versions. + from subprocess import Popen, PIPE + if not OS_LINUX: + for child in psutil.Process(pid).get_children(): + killProcess(child.pid) + else: + proc = Popen("ps -o pid --ppid %d --noheaders" % pid, shell=True, stdout=PIPE) + out,err = proc.communicate() + for pid_str in out.split("\n")[:-1]: + killProcess(int(pid_str)) + + +################################################################################ +# Similar to subprocess_check_output, but used for long-running commands +def execAndWait(cli_str, timeout=0, useStartInfo=True): + """ + There may actually still be references to this function where check_output + would've been more appropriate. But I didn't know about check_output at + the time... + """ + + process = launchProcess(cli_str, shell=True, useStartInfo=useStartInfo) + pid = process.pid + start = RightNow() + while process.poll() == None: + time.sleep(0.1) + if timeout>0 and (RightNow() - start)>timeout: + print 'Process exceeded timeout, killing it' + killProcess(pid) + out,err = process.communicate() + return [out,err] + + + + +######### INITIALIZE LOGGING UTILITIES ########## +# +# Setup logging to write INFO+ to file, and WARNING+ to console +# In debug mode, will write DEBUG+ to file and INFO+ to console +# + +# Want to get the line in which an error was triggered, but by wrapping +# the logger function (as I will below), the displayed "file:linenum" +# references the logger function, not the function that called it. +# So I use traceback to find the file and line number two up in the +# stack trace, and return that to be displayed instead of default +# [Is this a hack? Yes and no. I see no other way to do this] +def getCallerLine(): + stkTwoUp = traceback.extract_stack()[-3] + filename,method = stkTwoUp[0], stkTwoUp[1] + return '%s:%d' % (os.path.basename(filename),method) + +# When there's an error in the logging function, it's impossible to find! +# These wrappers will print the full stack so that it's possible to find +# which line triggered the error +def LOGDEBUG(msg, *a): + try: + logstr = msg if len(a)==0 else (msg%a) + callerStr = getCallerLine() + ' - ' + logging.debug(callerStr + logstr) + except TypeError: + traceback.print_stack() + raise + +def LOGINFO(msg, *a): + try: + logstr = msg if len(a)==0 else (msg%a) + callerStr = getCallerLine() + ' - ' + logging.info(callerStr + logstr) + except TypeError: + traceback.print_stack() + raise +def LOGWARN(msg, *a): + try: + logstr = msg if len(a)==0 else (msg%a) + callerStr = getCallerLine() + ' - ' + logging.warn(callerStr + logstr) + except TypeError: + traceback.print_stack() + raise +def LOGERROR(msg, *a): + try: + logstr = msg if len(a)==0 else (msg%a) + callerStr = getCallerLine() + ' - ' + logging.error(callerStr + logstr) + except TypeError: + traceback.print_stack() + raise +def LOGCRIT(msg, *a): + try: + logstr = msg if len(a)==0 else (msg%a) + callerStr = getCallerLine() + ' - ' + logging.critical(callerStr + logstr) + except TypeError: + traceback.print_stack() + raise +def LOGEXCEPT(msg, *a): + try: + logstr = msg if len(a)==0 else (msg%a) + callerStr = getCallerLine() + ' - ' + logging.exception(callerStr + logstr) + except TypeError: + traceback.print_stack() + raise + + + +DEFAULT_CONSOLE_LOGTHRESH = logging.WARNING +DEFAULT_FILE_LOGTHRESH = logging.INFO + +DEFAULT_PPRINT_LOGLEVEL = logging.DEBUG +DEFAULT_RAWDATA_LOGLEVEL = logging.DEBUG + +rootLogger = logging.getLogger('') +if CLI_OPTIONS.doDebug or CLI_OPTIONS.netlog or CLI_OPTIONS.mtdebug: + # Drop it all one level: console will see INFO, file will see DEBUG + DEFAULT_CONSOLE_LOGTHRESH -= 20 + DEFAULT_FILE_LOGTHRESH -= 20 + + +def chopLogFile(filename, size): + if not os.path.exists(filename): + print 'Log file doesn\'t exist [yet]' + return + + logfile = open(filename, 'r') + allLines = logfile.readlines() + logfile.close() + + nBytes,nLines = 0,0; + for line in allLines[::-1]: + nBytes += len(line) + nLines += 1 + if nBytes>size: + break + + logfile = open(filename, 'w') + for line in allLines[-nLines:]: + logfile.write(line) + logfile.close() + + +# Cut down the log file to just the most recent 1 MB +chopLogFile(ARMORY_LOG_FILE, 1024*1024) + + +# Now set loglevels +DateFormat = '%Y-%m-%d %H:%M' +logging.getLogger('').setLevel(logging.DEBUG) +fileFormatter = logging.Formatter('%(asctime)s (%(levelname)s) -- %(message)s', \ + datefmt=DateFormat) +fileHandler = logging.FileHandler(ARMORY_LOG_FILE) +fileHandler.setLevel(DEFAULT_FILE_LOGTHRESH) +fileHandler.setFormatter(fileFormatter) +logging.getLogger('').addHandler(fileHandler) + +consoleFormatter = logging.Formatter('(%(levelname)s) %(message)s') +consoleHandler = logging.StreamHandler() +consoleHandler.setLevel(DEFAULT_CONSOLE_LOGTHRESH) +consoleHandler.setFormatter( consoleFormatter ) +logging.getLogger('').addHandler(consoleHandler) + + + +class stringAggregator(object): + def __init__(self): + self.theStr = '' + def getStr(self): + return self.theStr + def write(self, theStr): + self.theStr += theStr + + +# A method to redirect pprint() calls to the log file +# Need a way to take a pprint-able object, and redirect its output to file +# Do this by swapping out sys.stdout temporarily, execute theObj.pprint() +# then set sys.stdout back to the original. +def LOGPPRINT(theObj, loglevel=DEFAULT_PPRINT_LOGLEVEL): + sys.stdout = stringAggregator() + theObj.pprint() + printedStr = sys.stdout.getStr() + sys.stdout = sys.__stdout__ + stkOneUp = traceback.extract_stack()[-2] + filename,method = stkOneUp[0], stkOneUp[1] + methodStr = '(PPRINT from %s:%d)\n' % (filename,method) + logging.log(loglevel, methodStr + printedStr) + +# For super-debug mode, we'll write out raw data +def LOGRAWDATA(rawStr, loglevel=DEFAULT_RAWDATA_LOGLEVEL): + dtype = isLikelyDataType(rawStr) + stkOneUp = traceback.extract_stack()[-2] + filename,method = stkOneUp[0], stkOneUp[1] + methodStr = '(PPRINT from %s:%d)\n' % (filename,method) + pstr = rawStr[:] + if dtype==DATATYPE.Binary: + pstr = binary_to_hex(rawStr) + pstr = prettyHex(pstr, indent=' ', withAddr=False) + elif dtype==DATATYPE.Hex: + pstr = prettyHex(pstr, indent=' ', withAddr=False) + else: + pstr = ' ' + '\n '.join(pstr.split('\n')) + + logging.log(loglevel, methodStr + pstr) + + +cpplogfile = None +if CLI_OPTIONS.logDisable: + print 'Logging is disabled' + rootLogger.disabled = True + + + +def logexcept_override(type, value, tback): + import traceback + import logging + strList = traceback.format_exception(type,value,tback) + logging.error(''.join([s for s in strList])) + # then call the default handler + sys.__excepthook__(type, value, tback) + +sys.excepthook = logexcept_override + + +# If there is a rebuild or rescan flag, let's do the right thing. +fileRedownload = os.path.join(ARMORY_HOME_DIR, 'redownload.flag') +fileRebuild = os.path.join(ARMORY_HOME_DIR, 'rebuild.flag') +fileRescan = os.path.join(ARMORY_HOME_DIR, 'rescan.flag') +fileDelSettings = os.path.join(ARMORY_HOME_DIR, 'delsettings.flag') + +# Flag to remove everything in Bitcoin dir except wallet.dat (if requested) +if os.path.exists(fileRedownload): + # Flag to remove *BITCOIN-QT* databases so it will have to re-download + LOGINFO('Found %s, will delete Bitcoin DBs & redownload' % fileRedownload) + + os.remove(fileRedownload) + + if os.path.exists(fileRebuild): + os.remove(fileRebuild) + + if os.path.exists(fileRescan): + os.remove(fileRescan) + + CLI_OPTIONS.redownload = True + CLI_OPTIONS.rebuild = True + +elif os.path.exists(fileRebuild): + # Flag to remove Armory databases so it will have to rebuild + LOGINFO('Found %s, will destroy and rebuild databases' % fileRebuild) + os.remove(fileRebuild) + + if os.path.exists(fileRescan): + os.remove(fileRescan) + + CLI_OPTIONS.rebuild = True +elif os.path.exists(fileRescan): + LOGINFO('Found %s, will throw out saved history, rescan' % fileRescan) + os.remove(fileRescan) + if os.path.exists(fileRebuild): + os.remove(fileRebuild) + CLI_OPTIONS.rescan = True + + +# Separately, we may want to delete the settings file, which couldn't +# be done easily from the GUI, because it frequently gets rewritten to +# file before shutdown is complete. The best way is to delete it on start. +if os.path.exists(fileDelSettings): + os.remove(SETTINGS_PATH) + os.remove(fileDelSettings) + + + +################################################################################ +def deleteBitcoindDBs(): + if not os.path.exists(BTC_HOME_DIR): + LOGERROR('Could not find Bitcoin-Qt/bitcoind home dir to remove blk data') + LOGERROR(' Does not exist: %s' % BTC_HOME_DIR) + else: + LOGINFO('Found bitcoin home dir, removing blocks and databases') + + # Remove directories + for btcDir in ['blocks', 'chainstate', 'database']: + fullPath = os.path.join(BTC_HOME_DIR, btcDir) + if os.path.exists(fullPath): + LOGINFO(' Removing dir: %s' % fullPath) + shutil.rmtree(fullPath) + + # Remove files + for btcFile in ['DB_CONFIG', 'db.log', 'debug.log', 'peers.dat']: + fullPath = os.path.join(BTC_HOME_DIR, btcFile) + if os.path.exists(fullPath): + LOGINFO(' Removing file: %s' % fullPath) + os.remove(fullPath) + + + +##### +if CLI_OPTIONS.redownload: + deleteBitcoindDBs() + if os.path.exists(fileRedownload): + os.remove(fileRedownload) + + +##### +if CLI_OPTIONS.rebuild and os.path.exists(LEVELDB_DIR): + LOGINFO('Found existing databases dir; removing before rebuild') + shutil.rmtree(LEVELDB_DIR) + os.mkdir(LEVELDB_DIR) + + +#### +if CLI_OPTIONS.testAnnounceCode: + LOGERROR('*'*60) + LOGERROR('You are currently using a developer mode intended for ') + LOGERROR('to help with testing of announcements, which is considered') + LOGERROR('a security risk. ') + LOGERROR('*'*60) + ARMORY_INFO_SIGN_ADDR = '1PpAJyNoocJt38Vcf4AfPffaxo76D4AAEe' + ARMORY_INFO_SIGN_PUBLICKEY = ('04' + '601c891a2cbc14a7b2bb1ecc9b6e42e166639ea4c2790703f8e2ed126fce432c' + '62fe30376497ad3efcd2964aa0be366010c11b8d7fc8209f586eac00bb763015') + + + +################################################################################ +# Load the C++ utilites here +# +# The SWIG/C++ block utilities give us access to the blockchain, fast ECDSA +# operations, and general encryption/secure-binary containers +################################################################################ +try: + import CppBlockUtils as Cpp + from CppBlockUtils import CryptoECDSA, SecureBinaryData + LOGINFO('C++ block utilities loaded successfully') +except: + LOGCRIT('C++ block utilities not available.') + LOGCRIT(' Make sure that you have the SWIG-compiled modules') + LOGCRIT(' in the current directory (or added to the PATH)') + LOGCRIT(' Specifically, you need:') + LOGCRIT(' CppBlockUtils.py and') + if OS_LINUX or OS_MACOSX: + LOGCRIT(' _CppBlockUtils.so') + elif OS_WINDOWS: + LOGCRIT(' _CppBlockUtils.pyd') + else: + LOGCRIT('\n\n... UNKNOWN operating system') + raise + +################################################################################ +# Get system details for logging purposes +class DumbStruct(object): pass +def GetSystemDetails(): + """Checks memory of a given system""" + + out = DumbStruct() + + CPU,COR,X64,MEM = range(4) + sysParam = [None,None,None,None] + out.CpuStr = 'UNKNOWN' + out.Machine = platform.machine().lower() + if OS_LINUX: + # Get total RAM + freeStr = subprocess_check_output('free -m', shell=True) + totalMemory = freeStr.split('\n')[1].split()[1] + out.Memory = int(totalMemory) * 1024 + + # Get CPU name + out.CpuStr = 'Unknown' + cpuinfo = subprocess_check_output(['cat','/proc/cpuinfo']) + for line in cpuinfo.split('\n'): + if line.strip().lower().startswith('model name'): + out.CpuStr = line.split(':')[1].strip() + break + + + elif OS_WINDOWS: + import ctypes + class MEMORYSTATUSEX(ctypes.Structure): + _fields_ = [ + ("dwLength", ctypes.c_ulong), + ("dwMemoryLoad", ctypes.c_ulong), + ("ullTotalPhys", ctypes.c_ulonglong), + ("ullAvailPhys", ctypes.c_ulonglong), + ("ullTotalPageFile", ctypes.c_ulonglong), + ("ullAvailPageFile", ctypes.c_ulonglong), + ("ullTotalVirtual", ctypes.c_ulonglong), + ("ullAvailVirtual", ctypes.c_ulonglong), + ("sullAvailExtendedVirtual", ctypes.c_ulonglong), + ] + def __init__(self): + # have to initialize this to the size of MEMORYSTATUSEX + self.dwLength = ctypes.sizeof(self) + super(MEMORYSTATUSEX, self).__init__() + + stat = MEMORYSTATUSEX() + ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(stat)) + out.Memory = stat.ullTotalPhys/1024. + out.CpuStr = platform.processor() + elif OS_MACOSX: + memsizeStr = subprocess_check_output('sysctl hw.memsize', shell=True) + out.Memory = int(memsizeStr.split(": ")[1]) / 1024 + out.CpuStr = subprocess_check_output('sysctl -n machdep.cpu.brand_string', shell=True) + else: + out.CpuStr = 'Unknown' + raise OSError("Can't get system specs in: %s" % platform.system()) + + out.NumCores = multiprocessing.cpu_count() + out.IsX64 = platform.machine().lower() == 'x86_64' + out.Memory = out.Memory / (1024*1024.) + + def getHddSize(adir): + if OS_WINDOWS: + free_bytes = ctypes.c_ulonglong(0) + ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(adir), \ + None, None, \ + ctypes.pointer(free_bytes)) + return free_bytes.value + else: + s = os.statvfs(adir) + return s.f_bavail * s.f_frsize + out.HddAvailA = getHddSize(ARMORY_HOME_DIR) / (1024**3) + out.HddAvailB = getHddSize(BTC_HOME_DIR) / (1024**3) + return out + +SystemSpecs = None +try: + SystemSpecs = GetSystemDetails() +except: + LOGEXCEPT('Error getting system details:') + LOGERROR('Skipping.') + SystemSpecs = DumbStruct() + SystemSpecs.Memory = -1 + SystemSpecs.CpuStr = 'Unknown' + SystemSpecs.NumCores = -1 + SystemSpecs.IsX64 = 'Unknown' + SystemSpecs.Machine = platform.machine().lower() + SystemSpecs.HddAvailA = -1 + SystemSpecs.HddAvailB = -1 + + +LOGINFO('') +LOGINFO('') +LOGINFO('') +LOGINFO('************************************************************') +LOGINFO('Invoked: ' + ' '.join(sys.argv)) +LOGINFO('************************************************************') +LOGINFO('Loading Armory Engine:') +LOGINFO(' Armory Version : ' + getVersionString(BTCARMORY_VERSION)) +LOGINFO(' PyBtcWallet Version : ' + getVersionString(PYBTCWALLET_VERSION)) +LOGINFO('Detected Operating system: ' + OS_NAME) +LOGINFO(' OS Variant : ' + (OS_VARIANT[0] if OS_MACOSX else '-'.join(OS_VARIANT))) +LOGINFO(' User home-directory : ' + USER_HOME_DIR) +LOGINFO(' Satoshi BTC directory : ' + BTC_HOME_DIR) +LOGINFO(' Armory home dir : ' + ARMORY_HOME_DIR) +LOGINFO('Detected System Specs : ') +LOGINFO(' Total Available RAM : %0.2f GB', SystemSpecs.Memory) +LOGINFO(' CPU ID string : ' + SystemSpecs.CpuStr) +LOGINFO(' Number of CPU cores : %d cores', SystemSpecs.NumCores) +LOGINFO(' System is 64-bit : ' + str(SystemSpecs.IsX64)) +LOGINFO(' Preferred Encoding : ' + locale.getpreferredencoding()) +LOGINFO(' Machine Arch : ' + SystemSpecs.Machine) +LOGINFO(' Available HDD (ARM) : %d GB' % SystemSpecs.HddAvailA) +LOGINFO(' Available HDD (BTC) : %d GB' % SystemSpecs.HddAvailB) +LOGINFO('') +LOGINFO('Network Name: ' + NETWORKS[ADDRBYTE]) +LOGINFO('Satoshi Port: %d', BITCOIN_PORT) +LOGINFO('Do wlt check: %s', str(DO_WALLET_CHECK)) +LOGINFO('Named options/arguments to armoryengine.py:') +for key,val in ast.literal_eval(str(CLI_OPTIONS)).iteritems(): + LOGINFO(' %-16s: %s', key,val) +LOGINFO('Other arguments:') +for val in CLI_ARGS: + LOGINFO(' %s', val) +LOGINFO('************************************************************') + + +def GetExecDir(): + """ + Return the path from where armoryengine was imported. Inspect method + expects a function or module name, it can actually inspect its own + name... + """ + srcfile = inspect.getsourcefile(GetExecDir) + srcpath = os.path.dirname(srcfile) + srcpath = os.path.abspath(srcpath) + return srcpath + + + + +def coin2str(nSatoshi, ndec=8, rJust=True, maxZeros=8): + """ + Converts a raw value (1e-8 BTC) into a formatted string for display + + ndec, guarantees that we get get a least N decimal places in our result + + maxZeros means we will replace zeros with spaces up to M decimal places + in order to declutter the amount field + + """ + + nBtc = float(nSatoshi) / float(ONE_BTC) + s = ('%%0.%df' % ndec) % nBtc + s = s.rjust(18, ' ') + + if maxZeros < ndec: + maxChop = ndec - maxZeros + nChop = min(len(s) - len(str(s.strip('0'))), maxChop) + if nChop>0: + s = s[:-nChop] + nChop*' ' + + if nSatoshi < 10000*ONE_BTC: + s.lstrip() + + if not rJust: + s = s.strip(' ') + + s = s.replace('. ', '') + + return s + + +def coin2strNZ(nSatoshi): + """ Right-justified, minimum zeros, but with padding for alignment""" + return coin2str(nSatoshi, 8, True, 0) + +def coin2strNZS(nSatoshi): + """ Right-justified, minimum zeros, stripped """ + return coin2str(nSatoshi, 8, True, 0).strip() + +def coin2str_approx(nSatoshi, sigfig=3): + posVal = nSatoshi + isNeg = False + if nSatoshi<0: + isNeg = True + posVal *= -1 + + nDig = max(round(math.log(posVal+1, 10)-0.5), 0) + nChop = max(nDig-2, 0 ) + approxVal = round((10**nChop) * round(posVal / (10**nChop))) + return coin2str( (-1 if isNeg else 1)*approxVal, maxZeros=0) + + +def str2coin(theStr, negAllowed=True, maxDec=8, roundHighPrec=True): + coinStr = str(theStr) + if len(coinStr.strip())==0: + raise ValueError + + isNeg = ('-' in coinStr) + coinStrPos = coinStr.replace('-','') + if not '.' in coinStrPos: + if not negAllowed and isNeg: + raise NegativeValueError + return (int(coinStrPos)*ONE_BTC)*(-1 if isNeg else 1) + else: + lhs,rhs = coinStrPos.strip().split('.') + if len(lhs.strip('-'))==0: + lhs='0' + if len(rhs)>maxDec and not roundHighPrec: + raise TooMuchPrecisionError + if not negAllowed and isNeg: + raise NegativeValueError + fullInt = (int(lhs + rhs[:9].ljust(9,'0')) + 5) / 10 + return fullInt*(-1 if isNeg else 1) + + + +################################################################################ +def replacePlurals(txt, *args): + """ + Use this like regular string formatting, but with pairs of strings: + + replacePlurals("I have @{one cat|%d cats}@. @{It is|They are}@ cute!", nCat) + + Then you can supply a single number which will select all supplied pairs. + or one number per @{|}@ object. If you use with format + strings (such as above, with "%d") make sure to replace those strings FIRST, + then call this function. Otherwise the %d will disappear depending on the + plurality and cause an error. Hence why I made the function below: + formatWithPlurals + """ + if len(args)==0: + if ('@{' in txt) and ('}@' in txt): + raise IndexError('Not enough arguments for plural formatting') + return txt + + argList = list(args[::-1]) + n = argList[0] + nRepl = 0 + while '@{' in txt: + idx0 = txt.find('@{') + idx1 = txt.find('}@')+2 + sep = txt.find('|', idx0) + if idx1==1 or sep==-1: + raise TypeError('Invalid replacement format') + + strOne = txt[idx0+2:sep] + strMany = txt[sep+1:idx1-2] + strReplace = txt[idx0:idx1] + + if not len(args) == 1: + try: + n = argList.pop() + except IndexError: + raise IndexError('Not enough arguments for plural formatting') + + txt = txt.replace(strReplace, strOne if n==1 else strMany) + nRepl += 1 + + if (len(args)>1 and len(argList)>0) or (nRepl < len(args)): + raise TypeError('Too many arguments supplied for plural formatting') + + return txt + + + +################################################################################ +def formatWithPlurals(txt, replList=None, pluralList=None): + """ + Where you would normally supply X extra arguments for either regular string + formatting or the plural function, you will instead supply a X-element list + for each one (actually, the two lists are likely to be different sizes). + """ + # Do the string formatting/replacement first, since the post-pluralized + # string may remove some of the replacement objects (i.e. if you have + # "The @{cat|%d cats}@ danced", the %d won't be there if the singular + # is chosen and replaced before applying the string formatting objects). + if replList is not None: + if not isinstance(replList, (list,tuple)): + replList = [replList] + txt = txt % tuple(replList) + + if pluralList is not None: + if not isinstance(pluralList, (list,tuple)): + pluralList = [pluralList] + txt = replacePlurals(txt, *pluralList) + + return txt + + +################################################################################ +# A bunch of convenience methods for converting between: +# -- Raw binary scripts (as seen in the blockchain) +# -- Address strings (exchanged between people for paying each other) +# -- ScrAddr strings (A unique identifier used by the DB) +################################################################################ + +################################################################################ +# Convert a 20-byte hash to a "pay-to-public-key-hash" script to be inserted +# into a TxOut script +def hash160_to_p2pkhash_script(binStr20): + if not len(binStr20)==20: + raise InvalidHashError('Tried to convert non-20-byte str to p2pkh script') + + from Transaction import getOpCode + outScript = ''.join([ getOpCode('OP_DUP' ), \ + getOpCode('OP_HASH160' ), \ + '\x14', \ + binStr20, + getOpCode('OP_EQUALVERIFY'), \ + getOpCode('OP_CHECKSIG' )]) + return outScript + + +################################################################################ +# Convert a 20-byte hash to a "pay-to-script-hash" script to be inserted +# into a TxOut script +def hash160_to_p2sh_script(binStr20): + if not len(binStr20)==20: + raise InvalidHashError('Tried to convert non-20-byte str to p2sh script') + + from Transaction import getOpCode + outScript = ''.join([ getOpCode('OP_HASH160'), \ + '\x14', \ + binStr20, + getOpCode('OP_EQUAL')]) + return outScript + +################################################################################ +# Convert an arbitrary script into a P2SH script +def script_to_p2sh_script(binScript): + scriptHash = hash160(binScript) + return hash160_to_p2sh_script(scriptHash) + + +################################################################################ +# Convert a 33-byte or 65-byte hash to a "pay-to-pubkey" script to be inserted +# into a TxOut script +def pubkey_to_p2pk_script(binStr33or65): + + if not len(binStr33or65) in [33, 65]: + raise KeyDataError('Invalid public key supplied to p2pk script') + + from Transaction import getOpCode + lenByte = int_to_binary(len(binStr33or65), widthBytes=1) + outScript = ''.join([ lenByte, + binStr33or65, + getOpCode('OP_CHECKSIG')]) + return outScript + + +################################################################################ +# Convert a list of public keys to an OP_CHECKMULTISIG script. There will be +# use cases where we require the keys to be sorted lexicographically, so we +# will do that by default. If you require a different order, pre-sort them +# and pass withSort=False. +# +# NOTE: About the hardcoded bytes in here: +# I made a mistake when making the databases, and hardcoded the +# mainnet addrByte and P2SH bytes into DB format. This means that +# that any ScrAddr object will use the mainnet prefix bytes, despite +# being in testnet. I will at some point fix this. +def pubkeylist_to_multisig_script(pkList, M, withSort=True): + + if sum([ (0 if len(pk) in [33,65] else 1) for pk in pkList]) > 0: + raise KeyDataError('Not all strings in pkList are 33 or 65 bytes!') + + from Transaction import getOpCode + opM = getOpCode('OP_%d' % M) + opN = getOpCode('OP_%d' % len(pkList)) + + newPkList = pkList[:] # copy + if withSort: + newPkList = sorted(pkList) + + outScript = opM + for pk in newPkList: + outScript += int_to_binary(len(pk), widthBytes=1) + outScript += pk + outScript += opN + outScript += getOpCode('OP_CHECKMULTISIG') + + return outScript + +################################################################################ +def scrAddr_to_script(scraddr): + """ Convert a scrAddr string (used by BDM) to the correct TxOut script """ + if len(scraddr)==0: + raise BadAddressError('Empty scraddr') + + prefix = scraddr[0] + if not prefix in SCRADDR_BYTE_LIST or not len(scraddr)==21: + LOGERROR('Bad scraddr: "%s"' % binary_to_hex(scraddr)) + raise BadAddressError('Invalid ScrAddress') + + if prefix==SCRADDR_P2PKH_BYTE: + return hash160_to_p2pkhash_script(scraddr[1:]) + elif prefix==SCRADDR_P2SH_BYTE: + return hash160_to_p2sh_script(scraddr[1:]) + else: + LOGERROR('Unsupported scraddr type: "%s"' % binary_to_hex(scraddr)) + raise BadAddressError('Can only convert P2PKH and P2SH scripts') + + +################################################################################ +def script_to_scrAddr(binScript): + """ Convert a binary script to scrAddr string (used by BDM) """ + return Cpp.BtcUtils().getScrAddrForScript(binScript) + +################################################################################ +def script_to_addrStr(binScript): + """ Convert a binary script to scrAddr string (used by BDM) """ + return scrAddr_to_addrStr(script_to_scrAddr(binScript)) + +################################################################################ +def scrAddr_to_addrStr(scrAddr): + if len(scrAddr)==0: + raise BadAddressError('Empty scrAddr') + + prefix = scrAddr[0] + if not prefix in SCRADDR_BYTE_LIST or not len(scrAddr)==21: + LOGERROR('Bad scrAddr: "%s"' % binary_to_hex(scrAddr)) + raise BadAddressError('Invalid ScrAddress') + + if prefix==SCRADDR_P2PKH_BYTE: + return hash160_to_addrStr(scrAddr[1:]) + elif prefix==SCRADDR_P2SH_BYTE: + return hash160_to_p2shStr(scrAddr[1:]) + else: + LOGERROR('Unsupported scrAddr type: "%s"' % binary_to_hex(scrAddr)) + raise BadAddressError('Can only convert P2PKH and P2SH scripts') + +################################################################################ +# We beat around the bush here, to make sure it goes through addrStr which +# triggers errors if this isn't a regular addr or P2SH addr +def scrAddr_to_hash160(scrAddr): + addr = scrAddr_to_addrStr(scrAddr) + atype, a160 = addrStr_to_hash160(addr) + return (atype, a160) + + +################################################################################ +def addrStr_to_scrAddr(addrStr): + if not checkAddrStrValid(addrStr): + BadAddressError('Invalid address: "%s"' % addrStr) + + # Okay this doesn't work because of the issue outlined before, where the + # SCRADDR prefixes don't match the ADDRSTR prefixes. Whoops + #return addrBin[:21] + + atype, a160 = addrStr_to_hash160(addrStr) + if atype==ADDRBYTE: + return SCRADDR_P2PKH_BYTE + a160 + elif atype==P2SHBYTE: + return SCRADDR_P2SH_BYTE + a160 + else: + BadAddressError('Invalid address: "%s"' % addrStr) + + + + + +################################################################################ +# Load the C++ utilites here +# +# The SWIG/C++ block utilities give us access to the blockchain, fast ECDSA +# operations, and general encryption/secure-binary containers +################################################################################ +try: + import CppBlockUtils as Cpp + from CppBlockUtils import CryptoECDSA, SecureBinaryData + LOGINFO('C++ block utilities loaded successfully') +except: + LOGCRIT('C++ block utilities not available.') + LOGCRIT(' Make sure that you have the SWIG-compiled modules') + LOGCRIT(' in the current directory (or added to the PATH)') + LOGCRIT(' Specifically, you need:') + LOGCRIT(' CppBlockUtils.py and') + if OS_LINUX or OS_MACOSX: + LOGCRIT(' _CppBlockUtils.so') + elif OS_WINDOWS: + LOGCRIT(' _CppBlockUtils.pyd') + else: + LOGCRIT('\n\n... UNKNOWN operating system') + raise + + +################################################################################ +# We need to have some methods for casting ASCII<->Unicode<->Preferred +DEFAULT_ENCODING = 'utf-8' + +def isASCII(theStr): + try: + theStr.decode('ascii') + return True + except UnicodeEncodeError: + return False + except UnicodeDecodeError: + return False + except: + LOGEXCEPT('What was passed to this function? %s', theStr) + return False + + +def toBytes(theStr, theEncoding=DEFAULT_ENCODING): + if isinstance(theStr, unicode): + return theStr.encode(theEncoding) + elif isinstance(theStr, str): + return theStr + else: + LOGERROR('toBytes() not been defined for input: %s', str(type(theStr))) + +def toUnicode(theStr, theEncoding=DEFAULT_ENCODING): + if isinstance(theStr, unicode): + return theStr + elif isinstance(theStr, str): + return unicode(theStr, theEncoding) + else: + LOGERROR('toUnicode() not been defined for input: %s', str(type(theStr))) + + +def toPreferred(theStr): + if OS_WINDOWS: + return theStr.encode('utf-8') + else: + return toUnicode(theStr).encode(locale.getpreferredencoding()) + + +def lenBytes(theStr, theEncoding=DEFAULT_ENCODING): + return len(toBytes(theStr, theEncoding)) + +# Stolen from stackoverflow (google "stackoverflow 1809531") +def unicode_truncate(theStr, length, encoding='utf-8'): + encoded = theStr.encode(encoding)[:length] + return encoded.decode(encoding, 'ignore') + +################################################################################ + + + +# This is a sweet trick for create enum-like dictionaries. +# Either automatically numbers (*args), or name-val pairs (**kwargs) +#http://stackoverflow.com/questions/36932/whats-the-best-way-to-implement-an-enum-in-python +def enum(*sequential, **named): + enums = dict(zip(sequential, range(len(sequential))), **named) + return type('Enum', (), enums) + +DATATYPE = enum("Binary", 'Base58', 'Hex') +def isLikelyDataType(theStr, dtype=None): + """ + This really shouldn't be used on short strings. Hence + why it's called "likely" datatype... + """ + ret = None + hexCount = sum([1 if c in BASE16CHARS else 0 for c in theStr]) + b58Count = sum([1 if c in BASE58CHARS else 0 for c in theStr]) + canBeHex = hexCount==len(theStr) + canBeB58 = b58Count==len(theStr) + if canBeHex: + ret = DATATYPE.Hex + elif canBeB58 and not canBeHex: + ret = DATATYPE.Base58 + else: + ret = DATATYPE.Binary + + if dtype==None: + return ret + else: + return dtype==ret + +cpplogfile = None +if CLI_OPTIONS.logDisable: + print 'Logging is disabled' + rootLogger.disabled = True + + + + + +# The database uses prefixes to identify type of address. Until the new +# wallet format is created that supports more than just hash160 addresses +# we have to explicitly add the prefix to any hash160 values that are being +# sent to any of the C++ utilities. For instance, the BlockDataManager (BDM) +# (C++ stuff) tracks regular hash160 addresses, P2SH, multisig, and all +# non-standard scripts. Any such "scrAddrs" (script-addresses) will eventually +# be valid entities for tracking in a wallet. Until then, all of our python +# utilities all use just hash160 values, and we manually add the prefix +# before talking to the BDM. +HASH160PREFIX = '\x00' +P2SHPREFIX = '\x05' +MSIGPREFIX = '\xfe' +NONSTDPREFIX = '\xff' +def CheckHash160(scrAddr): + if not len(scrAddr)==21: + raise BadAddressError("Supplied scrAddr is not a Hash160 value!") + if not scrAddr[0] == HASH160PREFIX: + raise BadAddressError("Supplied scrAddr is not a Hash160 value!") + return scrAddr[1:] + +def Hash160ToScrAddr(a160): + if not len(a160)==20: + LOGERROR('Invalid hash160 value!') + return HASH160PREFIX + a160 + +def HexHash160ToScrAddr(a160): + if not len(a160)==40: + LOGERROR('Invalid hash160 value!') + return HASH160PREFIX + hex_to_binary(a160) + +# Some more constants that are needed to play nice with the C++ utilities +ARMORY_DB_BARE = 0 +ARMORY_DB_LITE = 1 +ARMORY_DB_PARTIAL = 2 +ARMORY_DB_FULL = 3 +ARMORY_DB_SUPER = 4 +DB_PRUNE_ALL = 0 +DB_PRUNE_NONE = 1 + + +# Some time methods (RightNow() return local unix timestamp) +RightNow = time.time +def RightNowUTC(): + return time.mktime(time.gmtime(RightNow())) + +def RightNowStr(fmt=DEFAULT_DATE_FORMAT): + return unixTimeToFormatStr(RightNow(), fmt) + + +# Define all the hashing functions we're going to need. We don't actually +# use any of the first three directly (sha1, sha256, ripemd160), we only +# use hash256 and hash160 which use the first three to create the ONLY hash +# operations we ever do in the bitcoin network +# UPDATE: mini-private-key format requires vanilla sha256... +def sha1(bits): + return hashlib.new('sha1', bits).digest() +def sha256(bits): + return hashlib.new('sha256', bits).digest() +def sha512(bits): + return hashlib.new('sha512', bits).digest() +def ripemd160(bits): + # It turns out that not all python has ripemd160...? + #return hashlib.new('ripemd160', bits).digest() + return Cpp.BtcUtils().ripemd160_SWIG(bits) +def hash256(s): + """ Double-SHA256 """ + return sha256(sha256(s)) +def hash160(s): + """ RIPEMD160( SHA256( binaryStr ) ) """ + return Cpp.BtcUtils().getHash160_SWIG(s) + + +def HMAC(key, msg, hashfunc=sha512, hashsz=None): + """ This is intended to be simple, not fast. For speed, use HDWalletCrypto() """ + hashsz = len(hashfunc('')) if hashsz==None else hashsz + key = (hashfunc(key) if len(key)>hashsz else key) + key = key.ljust(hashsz, '\x00') + okey = ''.join([chr(ord('\x5c')^ord(c)) for c in key]) + ikey = ''.join([chr(ord('\x36')^ord(c)) for c in key]) + return hashfunc( okey + hashfunc(ikey + msg) ) + +HMAC256 = lambda key,msg: HMAC(key, msg, sha256, 32) +HMAC512 = lambda key,msg: HMAC(key, msg, sha512, 64) + + +################################################################################ +def prettyHex(theStr, indent='', withAddr=True, major=8, minor=8): + """ + This is the same as pprintHex(), but returns the string instead of + printing it to console. This is useful for redirecting output to + files, or doing further modifications to the data before display + """ + outStr = '' + sz = len(theStr) + nchunk = int((sz-1)/minor) + 1; + for i in range(nchunk): + if i%major==0: + outStr += '\n' + indent + if withAddr: + locStr = int_to_hex(i*minor/2, widthBytes=2, endOut=BIGENDIAN) + outStr += '0x' + locStr + ': ' + outStr += theStr[i*minor:(i+1)*minor] + ' ' + return outStr + + + + + +################################################################################ +def pprintHex(theStr, indent='', withAddr=True, major=8, minor=8): + """ + This method takes in a long hex string and prints it out into rows + of 64 hex chars, in chunks of 8 hex characters, and with address + markings on each row. This means that each row displays 32 bytes, + which is usually pleasant. + + The format is customizable: you can adjust the indenting of the + entire block, remove address markings, or change the major/minor + grouping size (major * minor = hexCharsPerRow) + """ + print prettyHex(theStr, indent, withAddr, major, minor) + + + +def pprintDiff(str1, str2, indent=''): + if not len(str1)==len(str2): + print 'pprintDiff: Strings are different length!' + return + + byteDiff = [] + for i in range(len(str1)): + if str1[i]==str2[i]: + byteDiff.append('-') + else: + byteDiff.append('X') + + pprintHex(''.join(byteDiff), indent=indent) + + + + +##### Switch endian-ness ##### +def hex_switchEndian(s): + """ Switches the endianness of a hex string (in pairs of hex chars) """ + pairList = [s[i]+s[i+1] for i in xrange(0,len(s),2)] + return ''.join(pairList[::-1]) +def binary_switchEndian(s): + """ Switches the endianness of a binary string """ + return s[::-1] + + +##### INT/HEXSTR ##### +def int_to_hex(i, widthBytes=0, endOut=LITTLEENDIAN): + """ + Convert an integer (int() or long()) to hexadecimal. Default behavior is + to use the smallest even number of hex characters necessary, and using + little-endian. Use the widthBytes argument to add 0-padding where needed + if you are expecting constant-length output. + """ + h = hex(i)[2:] + if isinstance(i,long): + h = h[:-1] + if len(h)%2 == 1: + h = '0'+h + if not widthBytes==0: + nZero = 2*widthBytes - len(h) + if nZero > 0: + h = '0'*nZero + h + if endOut==LITTLEENDIAN: + h = hex_switchEndian(h) + return h + + +def hex_to_int(h, endIn=LITTLEENDIAN): + """ + Convert hex-string to integer (or long). Default behavior is to interpret + hex string as little-endian + """ + hstr = h.replace(' ','') # copies data, no references + if endIn==LITTLEENDIAN: + hstr = hex_switchEndian(hstr) + return( int(hstr, 16) ) + + +##### HEXSTR/BINARYSTR ##### +def hex_to_binary(h, endIn=LITTLEENDIAN, endOut=LITTLEENDIAN): + """ + Converts hexadecimal to binary (in a python string). Endianness is + only switched if (endIn != endOut) + """ + bout = h.replace(' ','') # copies data, no references + if not endIn==endOut: + bout = hex_switchEndian(bout) + return bout.decode('hex_codec') + + +def binary_to_hex(b, endOut=LITTLEENDIAN, endIn=LITTLEENDIAN): + """ + Converts binary to hexadecimal. Endianness is only switched + if (endIn != endOut) + """ + hout = b.encode('hex_codec') + if not endOut==endIn: + hout = hex_switchEndian(hout) + return hout + +##### Shorthand combo of prettyHex and binary_to_hex intended for use in debugging +def ph(binaryInput): + return prettyHex(binary_to_hex(binaryInput)) + +##### INT/BINARYSTR ##### +def int_to_binary(i, widthBytes=0, endOut=LITTLEENDIAN): + """ + Convert integer to binary. Default behavior is use as few bytes + as necessary, and to use little-endian. This can be changed with + the two optional input arguemnts. + """ + h = int_to_hex(i,widthBytes) + return hex_to_binary(h, endOut=endOut) + +def binary_to_int(b, endIn=LITTLEENDIAN): + """ + Converts binary to integer (or long). Interpret as LE by default + """ + h = binary_to_hex(b, endIn, LITTLEENDIAN) + return hex_to_int(h) + +##### INT/BITS ##### + +def int_to_bitset(i, widthBytes=0): + bitsOut = [] + while i>0: + i,r = divmod(i,2) + bitsOut.append(['0','1'][r]) + result = ''.join(bitsOut) + if widthBytes != 0: + result = result.ljust(widthBytes*8,'0') + return result + +def bitset_to_int(bitset): + n = 0 + for i,bit in enumerate(bitset): + n += (0 if bit=='0' else 1) * 2**i + return n + + + +EmptyHash = hex_to_binary('00'*32) + + +################################################################################ +# BINARY/BASE58 CONVERSIONS +def binary_to_base58(binstr): + """ + This method applies the Bitcoin-specific conversion from binary to Base58 + which may includes some extra "zero" bytes, such as is the case with the + main-network addresses. + + This method is labeled as outputting an "addrStr", but it's really this + special kind of Base58 converter, which makes it usable for encoding other + data, such as ECDSA keys or scripts. + """ + padding = 0; + for b in binstr: + if b=='\x00': + padding+=1 + else: + break + + n = 0 + for ch in binstr: + n *= 256 + n += ord(ch) + + b58 = '' + while n > 0: + n, r = divmod (n, 58) + b58 = BASE58CHARS[r] + b58 + return '1'*padding + b58 + + +################################################################################ +def base58_to_binary(addr): + """ + This method applies the Bitcoin-specific conversion from Base58 to binary + which may includes some extra "zero" bytes, such as is the case with the + main-network addresses. + + This method is labeled as inputting an "addrStr", but it's really this + special kind of Base58 converter, which makes it usable for encoding other + data, such as ECDSA keys or scripts. + """ + # Count the zeros ('1' characters) at the beginning + padding = 0; + for c in addr: + if c=='1': + padding+=1 + else: + break + + n = 0 + for ch in addr: + n *= 58 + n += BASE58CHARS.index(ch) + + binOut = '' + while n>0: + d,m = divmod(n,256) + binOut = chr(m) + binOut + n = d + return '\x00'*padding + binOut + + + +################################################################################ +def hash160_to_addrStr(binStr, netbyte=ADDRBYTE): + """ + Converts the 20-byte pubKeyHash to 25-byte binary Bitcoin address + which includes the network byte (prefix) and 4-byte checksum (suffix) + """ + + if not len(binStr) == 20: + raise InvalidHashError('Input string is %d bytes' % len(binStr)) + + addr21 = netbyte + binStr + addr25 = addr21 + hash256(addr21)[:4] + return binary_to_base58(addr25); + +################################################################################ +def hash160_to_p2shStr(binStr): + if not len(binStr) == 20: + raise InvalidHashError('Input string is %d bytes' % len(binStr)) + + addr21 = P2SHBYTE + binStr + addr25 = addr21 + hash256(addr21)[:4] + return binary_to_base58(addr25); + +################################################################################ +def addrStr_is_p2sh(b58Str): + binStr = base58_to_binary(b58Str) + if not len(binStr)==25: + return False + + if not hash256(binStr[:21])[:4] == binStr[-4:]: + return False + + return (binStr[0] == P2SHBYTE) + +################################################################################ +# As of version 0.90.1, this returns the prefix byte with the hash160. This is +# because we need to handle/distinguish regular addresses from P2SH. All code +# using this method must be updated to expect 2 outputs and check the prefix. +def addrStr_to_hash160(b58Str, p2shAllowed=True): + binStr = base58_to_binary(b58Str) + if not p2shAllowed and binStr[0]==P2SHBYTE: + raise P2SHNotSupportedError + if not len(binStr) == 25: + raise BadAddressError('Address string is %d bytes' % len(binStr)) + + if not hash256(binStr[:21])[:4] == binStr[-4:]: + raise ChecksumError('Address string has invalid checksum') + + if not binStr[0] in (ADDRBYTE, P2SHBYTE): + raise BadAddressError('Unknown addr prefix: %s' % binary_to_hex(binStr[0])) + + return (binStr[0], binStr[1:-4]) + + +###### Typing-friendly Base16 ##### +# Implements "hexadecimal" encoding but using only easy-to-type +# characters in the alphabet. Hex usually includes the digits 0-9 +# which can be slow to type, even for good typists. On the other +# hand, by changing the alphabet to common, easily distinguishable, +# lowercase characters, typing such strings will become dramatically +# faster. Additionally, some default encodings of QRCodes do not +# preserve the capitalization of the letters, meaning that Base58 +# is not a feasible options + +NORMALCHARS = '0123 4567 89ab cdef'.replace(' ','') +EASY16CHARS = 'asdf ghjk wert uion'.replace(' ','') +hex_to_base16_map = {} +base16_to_hex_map = {} +for n,b in zip(NORMALCHARS,EASY16CHARS): + hex_to_base16_map[n] = b + base16_to_hex_map[b] = n + +def binary_to_easyType16(binstr): + return ''.join([hex_to_base16_map[c] for c in binary_to_hex(binstr)]) + +# Treat unrecognized characters as 0, to facilitate possibly later recovery of +# their correct values from the checksum. +def easyType16_to_binary(b16str): + return hex_to_binary(''.join([base16_to_hex_map.get(c, '0') for c in b16str])) + + +def makeSixteenBytesEasy(b16): + if not len(b16)==16: + raise ValueError('Must supply 16-byte input') + chk2 = computeChecksum(b16, nBytes=2) + et18 = binary_to_easyType16(b16 + chk2) + nineQuads = [et18[i*4:(i+1)*4] for i in range(9)] + first4 = ' '.join(nineQuads[:4]) + second4 = ' '.join(nineQuads[4:8]) + last1 = nineQuads[8] + return ' '.join([first4, second4, last1]) + +def readSixteenEasyBytes(et18): + b18 = easyType16_to_binary(et18.strip().replace(' ','')) + if len(b18)!=18: + raise ValueError('Must supply 18-byte input') + b16 = b18[:16] + chk = b18[ 16:] + if chk=='': + LOGWARN('Missing checksum when reading EasyType') + return (b16, 'No_Checksum') + b16new = verifyChecksum(b16, chk) + if len(b16new)==0: + return ('','Error_2+') + elif not b16new==b16: + return (b16new,'Fixed_1') + else: + return (b16new,None) + +##### FLOAT/BTC ##### +# https://en.bitcoin.it/wiki/Proper_Money_Handling_(JSON-RPC) +def ubtc_to_floatStr(n): + return '%d.%08d' % divmod (n, ONE_BTC) +def floatStr_to_ubtc(s): + return long(round(float(s) * ONE_BTC)) +def float_to_btc (f): + return long (round(f * ONE_BTC)) + + +##### And a few useful utilities ##### +def unixTimeToFormatStr(unixTime, formatStr=DEFAULT_DATE_FORMAT): + """ + Converts a unix time (like those found in block headers) to a + pleasant, human-readable format + """ + dtobj = datetime.fromtimestamp(unixTime) + dtstr = u'' + dtobj.strftime(formatStr).decode('utf-8') + dtstr = dtstr.encode('ascii', errors='replace') + return dtstr[:-2] + dtstr[-2:].lower() + +def secondsToHumanTime(nSec): + strPieces = [] + floatSec = float(nSec) + if floatSec < 0.9*MINUTE: + strPieces = [floatSec, 'second'] + elif floatSec < 0.9*HOUR: + strPieces = [floatSec/MINUTE, 'minute'] + elif floatSec < 0.9*DAY: + strPieces = [floatSec/HOUR, 'hour'] + elif floatSec < 0.9*WEEK: + strPieces = [floatSec/DAY, 'day'] + elif floatSec < 0.9*MONTH: + strPieces = [floatSec/WEEK, 'week'] + elif floatSec < 0.9*YEAR: + strPieces = [floatSec/MONTH, 'month'] + else: + strPieces = [floatSec/YEAR, 'year'] + + # + if strPieces[0]<1.25: + return '1 '+strPieces[1] + elif strPieces[0]<=1.75: + return '1.5 '+strPieces[1]+'s' + else: + return '%d %ss' % (int(strPieces[0]+0.5), strPieces[1]) + +def bytesToHumanSize(nBytes): + if nBytes0: + if not beQuiet: LOGWARN('fixed!') + return fixStr + else: + # ONE LAST CHECK SPECIFIC TO MY SERIALIZATION SCHEME: + # If the string was originally all zeros, chksum is hash256('') + # ...which is a known value, and frequently used in my files + if chksum==hex_to_binary('5df6e0e2'): + if not beQuiet: LOGWARN('fixed!') + return '' + + + # ID a checksum byte error... + origHash = hashFunc(bin1) + for i in range(len(chksum)): + chkArray = [chksum[j] for j in range(len(chksum))] + for ch in range(256): + chkArray[i] = chr(ch) + if origHash.startswith(''.join(chkArray)): + LOGWARN('***Checksum error! Incorrect byte in checksum!') + return bin1 + + LOGWARN('Checksum fix failed') + return '' + + +# Taken directly from rpc.cpp in reference bitcoin client, 0.3.24 +def binaryBits_to_difficulty(b): + """ Converts the 4-byte binary difficulty string to a float """ + i = binary_to_int(b) + nShift = (i >> 24) & 0xff + dDiff = float(0x0000ffff) / float(i & 0x00ffffff) + while nShift < 29: + dDiff *= 256.0 + nShift += 1 + while nShift > 29: + dDiff /= 256.0 + nShift -= 1 + return dDiff + + +# TODO: I don't actually know how to do this, yet... +def difficulty_to_binaryBits(i): + pass + +################################################################################ +def CreateQRMatrix(dataToEncode, errLevel='L'): + sz=3 + success=False + qrmtrx = [[]] + while sz<20: + try: + errCorrectEnum = getattr(QRErrorCorrectLevel, errLevel.upper()) + qr = QRCode(sz, errCorrectEnum) + qr.addData(dataToEncode) + qr.make() + success=True + break + except TypeError: + sz += 1 + + if not success: + LOGERROR('Unsuccessful attempt to create QR code') + LOGERROR('Data to encode: (Length: %s, isAscii: %s)', \ + len(dataToEncode), isASCII(dataToEncode)) + return [[0]], 1 + + qrmtrx = [] + modCt = qr.getModuleCount() + for r in range(modCt): + tempList = [0]*modCt + for c in range(modCt): + # The matrix is transposed by default, from what we normally expect + tempList[c] = 1 if qr.isDark(c,r) else 0 + qrmtrx.append(tempList) + + return [qrmtrx, modCt] + + +# The following params are for the Bitcoin elliptic curves (secp256k1) +SECP256K1_MOD = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2FL +SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141L +SECP256K1_B = 0x0000000000000000000000000000000000000000000000000000000000000007L +SECP256K1_A = 0x0000000000000000000000000000000000000000000000000000000000000000L +SECP256K1_GX = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798L +SECP256K1_GY = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8L + +################################################################################ +################################################################################ +# START FINITE FIELD OPERATIONS + +class FiniteField(object): + """ + Create a simple, prime-order FiniteField. Because this is used only + to encode data of fixed width, I enforce prime-order by hardcoding + primes, and you just pick the data width (in bytes). If your desired + data width is not here, simply find a prime number very close to 2^N, + and add it to the PRIMES map below. + + This will be used for Shamir's Secret Sharing scheme. Encode your + data as the coeffient of finite-field polynomial, and store points + on that polynomial. The order of the polynomial determines how + many points are needed to recover the original secret. + """ + + # bytes: primeclosetomaxval + PRIMES = { 1: 2**8-5, # mainly for testing + 2: 2**16-39, + 4: 2**32-5, + 8: 2**64-59, + 16: 2**128-797, + 20: 2**160-543, + 24: 2**192-333, + 32: 2**256-357, + 48: 2**384-317, + 64: 2**512-569, + 96: 2**768-825, + 128: 2**1024-105, + 192: 2**1536-3453, + 256: 2**2048-1157 } + + def __init__(self, nbytes): + if not self.PRIMES.has_key(nbytes): + LOGERROR('No primes available for size=%d bytes', nbytes) + self.prime = None + raise FiniteFieldError + self.prime = self.PRIMES[nbytes] + + + def add(self,a,b): + return (a+b) % self.prime + + def subtract(self,a,b): + return (a-b) % self.prime + + def mult(self,a,b): + return (a*b) % self.prime + + def power(self,a,b): + result = 1 + while(b>0): + b,x = divmod(b,2) + result = (result * (a if x else 1)) % self.prime + a = a*a % self.prime + return result + + def powinv(self,a): + """ USE ONLY PRIME MODULUS """ + return self.power(a,self.prime-2) + + def divide(self,a,b): + """ USE ONLY PRIME MODULUS """ + baddinv = self.powinv(b) + return self.mult(a,baddinv) + + def mtrxrmrowcol(self,mtrx,r,c): + if not len(mtrx) == len(mtrx[0]): + LOGERROR('Must be a square matrix!') + return [] + sz = len(mtrx) + return [[mtrx[i][j] for j in range(sz) if not j==c] for i in range(sz) if not i==r] + + + ################################################################################ + def mtrxdet(self,mtrx): + if len(mtrx)==1: + return mtrx[0][0] + + if not len(mtrx) == len(mtrx[0]): + LOGERROR('Must be a square matrix!') + return -1 + + result = 0; + for i in range(len(mtrx)): + mult = mtrx[0][i] * (-1 if i%2==1 else 1) + subdet = self.mtrxdet(self.mtrxrmrowcol(mtrx,0,i)) + result = self.add(result, self.mult(mult,subdet)) + return result + + ################################################################################ + def mtrxmultvect(self,mtrx, vect): + M,N = len(mtrx), len(mtrx[0]) + if not len(mtrx[0])==len(vect): + LOGERROR('Mtrx and vect are incompatible: %dx%d, %dx1', M, N, len(vect)) + return [ sum([self.mult(mtrx[i][j],vect[j]) for j in range(N)])%self.prime for i in range(M) ] + + ################################################################################ + def mtrxmult(self,m1, m2): + M1,N1 = len(m1), len(m1[0]) + M2,N2 = len(m2), len(m2[0]) + if not N1==M2: + LOGERROR('Mtrx and vect are incompatible: %dx%d, %dx%d', M1,N1, M2,N2) + inner = lambda i,j: sum([self.mult(m1[i][k],m2[k][j]) for k in range(N1)]) + return [ [inner(i,j)%self.prime for j in range(N1)] for i in range(M1) ] + + ################################################################################ + def mtrxadjoint(self,mtrx): + sz = len(mtrx) + inner = lambda i,j: self.mtrxdet(self.mtrxrmrowcol(mtrx,i,j)) + return [[((-1 if (i+j)%2==1 else 1)*inner(j,i))%self.prime for j in range(sz)] for i in range(sz)] + + ################################################################################ + def mtrxinv(self,mtrx): + det = self.mtrxdet(mtrx) + adj = self.mtrxadjoint(mtrx) + sz = len(mtrx) + return [[self.divide(adj[i][j],det) for j in range(sz)] for i in range(sz)] + + +################################################################################ +def SplitSecret(secret, needed, pieces, nbytes=None, use_random_x=False): + if not isinstance(secret, basestring): + secret = secret.toBinStr() + + if nbytes==None: + nbytes = len(secret) + + ff = FiniteField(nbytes) + fragments = [] + + # Convert secret to an integer + a = binary_to_int(SecureBinaryData(secret).toBinStr(),BIGENDIAN) + if not a=needed: + LOGERROR('You must create more pieces than needed to reconstruct!') + raise FiniteFieldError + + if needed==1 or needed>8: + LOGERROR('Can split secrets into parts *requiring* at most 8 fragments') + LOGERROR('You can break it into as many optional fragments as you want') + raise FiniteFieldError + + + # We deterministically produce the coefficients so that we always use the + # same polynomial for a given secret + lasthmac = secret[:] + othernum = [] + for i in range(pieces+needed-1): + lasthmac = HMAC512(lasthmac, 'splitsecrets')[:nbytes] + othernum.append(binary_to_int(lasthmac)) + + def poly(x): + polyout = ff.mult(a, ff.power(x,needed-1)) + for i,e in enumerate(range(needed-2,-1,-1)): + term = ff.mult(othernum[i], ff.power(x,e)) + polyout = ff.add(polyout, term) + return polyout + + for i in range(pieces): + x = othernum[i+2] if use_random_x else i+1 + fragments.append( [x, poly(x)] ) + + secret,a = None,None + fragments = [ [int_to_binary(p, nbytes, BIGENDIAN) for p in frag] for frag in fragments] + return fragments + + +################################################################################ +def ReconstructSecret(fragments, needed, nbytes): + + ff = FiniteField(nbytes) + pairs = fragments[:needed] + m = [] + v = [] + for x,y in pairs: + x = binary_to_int(x, BIGENDIAN) + y = binary_to_int(y, BIGENDIAN) + m.append([]) + for i,e in enumerate(range(needed-1,-1,-1)): + m[-1].append( ff.power(x,e) ) + v.append(y) + + minv = ff.mtrxinv(m) + outvect = ff.mtrxmultvect(minv,v) + return int_to_binary(outvect[0], nbytes, BIGENDIAN) + + +################################################################################ +def createTestingSubsets( fragIndices, M, maxTestCount=20): + """ + Returns (IsRandomized, listOfTuplesOfSizeM) + """ + numIdx = len(fragIndices) + + if M>numIdx: + LOGERROR('Insufficent number of fragments') + raise KeyDataError + elif M==numIdx: + LOGINFO('Fragments supplied == needed. One subset to test (%s-of-N)' % M) + return ( False, [tuple(fragIndices)] ) + else: + LOGINFO('Test reconstruct %s-of-N, with %s fragments' % (M, numIdx)) + subs = [] + + # Compute the number of possible subsets. This is stable because we + # shouldn't ever have more than 12 fragments + fact = math.factorial + numCombo = fact(numIdx) / ( fact(M) * fact(numIdx-M) ) + + if numCombo <= maxTestCount: + LOGINFO('Testing all %s combinations...' % numCombo) + for x in xrange(2**numIdx): + bits = int_to_bitset(x) + if not bits.count('1') == M: + continue + + subs.append(tuple([fragIndices[i] for i,b in enumerate(bits) if b=='1'])) + + return (False, sorted(subs)) + else: + LOGINFO('#Subsets > %s, will need to randomize' % maxTestCount) + usedSubsets = set() + while len(subs) < maxTestCount: + sample = tuple(sorted(random.sample(fragIndices, M))) + if not sample in usedSubsets: + usedSubsets.add(sample) + subs.append(sample) + + return (True, sorted(subs)) + + + +################################################################################ +def testReconstructSecrets(fragMap, M, maxTestCount=20): + # If fragMap has X elements, then it will test all X-choose-M subsets of + # the fragMap and return the restored secret for each one. If there's more + # subsets than maxTestCount, then just do a random sampling of the possible + # subsets + fragKeys = [k for k in fragMap.iterkeys()] + isRandom, subs = createTestingSubsets(fragKeys, M, maxTestCount) + nBytes = len(fragMap[fragKeys[0]][1]) + LOGINFO('Testing %d-byte fragments' % nBytes) + + testResults = [] + for subset in subs: + fragSubset = [fragMap[i][:] for i in subset] + + recon = ReconstructSecret(fragSubset, M, nBytes) + testResults.append((subset, recon)) + + return isRandom, testResults + + +################################################################################ +def ComputeFragIDBase58(M, wltIDBin): + mBin4 = int_to_binary(M, widthBytes=4, endOut=BIGENDIAN) + fragBin = hash256(wltIDBin + mBin4)[:4] + fragB58 = str(M) + binary_to_base58(fragBin) + return fragB58 + +################################################################################ +def ComputeFragIDLineHex(M, index, wltIDBin, isSecure=False, addSpaces=False): + fragID = int_to_hex((128+M) if isSecure else M) + fragID += int_to_hex(index+1) + fragID += binary_to_hex(wltIDBin) + + if addSpaces: + fragID = ' '.join([fragID[i*4:(i+1)*4] for i in range(4)]) + + return fragID + + +################################################################################ +def ReadFragIDLineBin(binLine): + doMask = binary_to_int(binLine[0]) > 127 + M = binary_to_int(binLine[0]) & 0x7f + fnum = binary_to_int(binLine[1]) + wltID = binLine[2:] + + idBase58 = ComputeFragIDBase58(M, wltID) + '-#' + str(fnum) + return (M, fnum, wltID, doMask, idBase58) + + +################################################################################ +def ReadFragIDLineHex(hexLine): + return ReadFragIDLineBin( hex_to_binary(hexLine.strip().replace(' ',''))) + +# END FINITE FIELD OPERATIONS +################################################################################ +################################################################################ + + + + + +################################################################################ +def checkAddrType(addrBin): + """ Gets the network byte of the address. Returns -1 if chksum fails """ + first21, chk4 = addrBin[:-4], addrBin[-4:] + chkBytes = hash256(first21) + return addrBin[0] if (chkBytes[:4] == chk4) else -1 + +################################################################################ +def checkAddrBinValid(addrBin, validPrefixes=None): + """ + Checks whether this address is valid for the given network + (set at the top of pybtcengine.py) + """ + if validPrefixes is None: + validPrefixes = [ADDRBYTE, P2SHBYTE] + + if not isinstance(validPrefixes, list): + validPrefixes = [validPrefixes] + + return (checkAddrType(addrBin) in validPrefixes) + + + +################################################################################ +def checkAddrStrValid(addrStr): + """ Check that a Base58 address-string is valid on this network """ + return checkAddrBinValid(base58_to_binary(addrStr)) + + +################################################################################ +def convertKeyDataToAddress(privKey=None, pubKey=None): + """ Returns a hash160 value """ + if not privKey and not pubKey: + raise BadAddressError('No key data supplied for conversion') + elif privKey: + if isinstance(privKey, str): + privKey = SecureBinaryData(privKey) + + if not privKey.getSize()==32: + raise BadAddressError('Invalid private key format!') + else: + pubKey = CryptoECDSA().ComputePublicKey(privKey) + + if isinstance(pubKey,str): + pubKey = SecureBinaryData(pubKey) + return pubKey.getHash160() + + + +################################################################################ +def decodeMiniPrivateKey(keyStr): + """ + Converts a 22, 26 or 30-character Base58 mini private key into a + 32-byte binary private key. + """ + if not len(keyStr) in (22,26,30): + return '' + + keyQ = keyStr + '?' + theHash = sha256(keyQ) + + if binary_to_hex(theHash[0]) == '01': + raise KeyDataError('PBKDF2-based mini private keys not supported!') + elif binary_to_hex(theHash[0]) != '00': + raise KeyDataError('Invalid mini private key... double check the entry') + + return sha256(keyStr) + + +################################################################################ +def parsePrivateKeyData(theStr): + hexChars = '01234567890abcdef' + b58Chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' + + hexCount = sum([1 if c in hexChars else 0 for c in theStr.lower()]) + b58Count = sum([1 if c in b58Chars else 0 for c in theStr]) + canBeHex = hexCount==len(theStr) + canBeB58 = b58Count==len(theStr) + + binEntry = '' + keyType = '' + isMini = False + if canBeB58 and not canBeHex: + if len(theStr) in (22, 30): + # Mini-private key format! + try: + binEntry = decodeMiniPrivateKey(theStr) + except KeyDataError: + raise BadAddressError('Invalid mini-private key string') + keyType = 'Mini Private Key Format' + isMini = True + elif len(theStr) in range(48,53): + binEntry = base58_to_binary(theStr) + keyType = 'Plain Base58' + else: + raise BadAddressError('Unrecognized key data') + elif canBeHex: + binEntry = hex_to_binary(theStr) + keyType = 'Plain Hex' + else: + raise BadAddressError('Unrecognized key data') + + + if len(binEntry)==36 or (len(binEntry)==37 and binEntry[0]==PRIVKEYBYTE): + if len(binEntry)==36: + keydata = binEntry[:32 ] + chk = binEntry[ 32:] + binEntry = verifyChecksum(keydata, chk) + if not isMini: + keyType = 'Raw %s with checksum' % keyType.split(' ')[1] + else: + # Assume leading 0x80 byte, and 4 byte checksum + keydata = binEntry[ :1+32 ] + chk = binEntry[ 1+32:] + binEntry = verifyChecksum(keydata, chk) + binEntry = binEntry[1:] + if not isMini: + keyType = 'Standard %s key with checksum' % keyType.split(' ')[1] + + if binEntry=='': + raise InvalidHashError('Private Key checksum failed!') + elif len(binEntry) in (33, 37) and binEntry[-1]=='\x01': + raise CompressedKeyError('Compressed Public keys not supported!') + return binEntry, keyType + + + +################################################################################ +def encodePrivKeyBase58(privKeyBin): + bin33 = PRIVKEYBYTE + privKeyBin + chk = computeChecksum(bin33) + return binary_to_base58(bin33 + chk) + + + +URI_VERSION_STR = '1.0' + +################################################################################ +def parseBitcoinURI(theStr): + """ Takes a URI string, returns the pieces of it, in a dictionary """ + + # Start by splitting it into pieces on any separator + seplist = ':;?&' + for c in seplist: + theStr = theStr.replace(c,' ') + parts = theStr.split() + + # Now start walking through the parts and get the info out of it + if not parts[0] == 'bitcoin': + return {} + + uriData = {} + + try: + uriData['address'] = parts[1] + for p in parts[2:]: + if not '=' in p: + raise BadURIError('Unrecognized URI field: "%s"'%p) + + # All fields must be "key=value" making it pretty easy to parse + key, value = p.split('=') + + # A few + if key.lower()=='amount': + uriData['amount'] = str2coin(value) + elif key.lower() in ('label','message'): + uriData[key] = uriPercentToReserved(value) + else: + uriData[key] = value + except: + return {} + + return uriData + + +################################################################################ +def uriReservedToPercent(theStr): + """ + Convert from a regular string to a percent-encoded string + """ + #Must replace '%' first, to avoid recursive (and incorrect) replacement! + reserved = "%!*'();:@&=+$,/?#[] " + + for c in reserved: + theStr = theStr.replace(c, '%%%s' % int_to_hex(ord(c))) + return theStr + + +################################################################################ +def uriPercentToReserved(theStr): + """ + This replacement direction is much easier! + Convert from a percent-encoded string to a + """ + + parts = theStr.split('%') + if len(parts)>1: + for p in parts[1:]: + parts[0] += chr( hex_to_int(p[:2]) ) + p[2:] + return parts[0][:] + + +################################################################################ +def createBitcoinURI(addr, amt=None, msg=None): + uriStr = 'bitcoin:%s' % addr + if amt or msg: + uriStr += '?' + + if amt: + uriStr += 'amount=%s' % coin2str(amt, maxZeros=0).strip() + + if amt and msg: + uriStr += '&' + + if msg: + uriStr += 'label=%s' % uriReservedToPercent(msg) + + return uriStr + + +################################################################################ +def createSigScriptFromRS(rBin, sBin): + # Remove all leading zero-bytes + while rBin[0]=='\x00': + rBin = rBin[1:] + while sBin[0]=='\x00': + sBin = sBin[1:] + + if binary_to_int(rBin[0])&128>0: rBin = '\x00'+rBin + if binary_to_int(sBin[0])&128>0: sBin = '\x00'+sBin + rSize = int_to_binary(len(rBin)) + sSize = int_to_binary(len(sBin)) + rsSize = int_to_binary(len(rBin) + len(sBin) + 4) + sigScript = '\x30' + rsSize + \ + '\x02' + rSize + rBin + \ + '\x02' + sSize + sBin + return sigScript + + + + + +################################################################################ +class PyBackgroundThread(threading.Thread): + """ + Wraps a function in a threading.Thread object which will run + that function in a separate thread. Calling self.start() will + return immediately, but will start running that function in + separate thread. You can check its progress later by using + self.isRunning() or self.isFinished(). If the function returns + a value, use self.getOutput(). Use self.getElapsedSeconds() + to find out how long it took. + """ + + def __init__(self, *args, **kwargs): + threading.Thread.__init__(self) + + self.output = None + self.startedAt = UNINITIALIZED + self.finishedAt = UNINITIALIZED + self.errorThrown = None + self.passAsync = None + self.setDaemon(True) + + if len(args)==0: + self.func = lambda: () + else: + if not hasattr(args[0], '__call__'): + raise TypeError('PyBkgdThread ctor arg1 must be a function') + else: + self.setThreadFunction(args[0], *args[1:], **kwargs) + + def setThreadFunction(self, thefunc, *args, **kwargs): + def funcPartial(): + return thefunc(*args, **kwargs) + self.func = funcPartial + + def setDaemon(self, yesno): + if self.isStarted(): + LOGERROR('Must set daemon property before starting thread') + else: + super(PyBackgroundThread, self).setDaemon(yesno) + + def isFinished(self): + return not (self.finishedAt==UNINITIALIZED) + + def isStarted(self): + return not (self.startedAt==UNINITIALIZED) + + def isRunning(self): + return (self.isStarted() and not self.isFinished()) + + def getElapsedSeconds(self): + if not self.isFinished(): + LOGERROR('Thread is not finished yet!') + return None + else: + return self.finishedAt - self.startedAt + + def getOutput(self): + if not self.isFinished(): + if self.isRunning(): + LOGERROR('Cannot get output while thread is running') + else: + LOGERROR('Thread was never .start()ed') + return None + + return self.output + + def didThrowError(self): + return (self.errorThrown is not None) + + def raiseLastError(self): + if self.errorThrown is None: + return + raise self.errorThrown + + def getErrorType(self): + if self.errorThrown is None: + return None + return type(self.errorThrown) + + def getErrorMsg(self): + if self.errorThrown is None: + return '' + return self.errorThrown.args[0] + + + def start(self): + # The prefunc is blocking. Probably preparing something + # that needs to be in place before we start the thread + self.startedAt = RightNow() + super(PyBackgroundThread, self).start() + + def run(self): + # This should not be called manually. Only call start() + try: + self.output = self.func() + except Exception as e: + LOGEXCEPT('Error in pybkgdthread: %s', str(e)) + self.errorThrown = e + self.finishedAt = RightNow() + + if not self.passAsync: return + if hasattr(self.passAsync, '__call__'): + self.passAsync() + + def reset(self): + self.output = None + self.startedAt = UNINITIALIZED + self.finishedAt = UNINITIALIZED + self.errorThrown = None + + def restart(self): + self.reset() + self.start() + + +# Define a decorator that allows the function to be called asynchronously +def AllowAsync(func): + def wrappedFunc(*args, **kwargs): + if not 'async' in kwargs or kwargs['async']==False: + # Run the function normally + if 'async' in kwargs: + del kwargs['async'] + return func(*args, **kwargs) + else: + # Run the function as a background thread + passAsync = kwargs['async'] + del kwargs['async'] + + thr = PyBackgroundThread(func, *args, **kwargs) + thr.passAsync = passAsync + thr.start() + return thr + + return wrappedFunc + + +def emptyFunc(*args, **kwargs): + return + + +def EstimateCumulativeBlockchainSize(blkNum): + # I tried to make a "static" variable here so that + # the string wouldn't be parsed on every call, but + # I botched that, somehow. + # + # It doesn't *have to* be fast, but why not? + # Oh well.. + blksizefile = """ + 0 285 + 20160 4496226 + 40320 9329049 + 60480 16637208 + 80640 31572990 + 82656 33260320 + 84672 35330575 + 86688 36815335 + 88704 38386205 + 100800 60605119 + 102816 64795352 + 104832 68697265 + 108864 79339447 + 112896 92608525 + 116928 116560952 + 120960 140607929 + 124992 170059586 + 129024 217718109 + 133056 303977266 + 137088 405836779 + 141120 500934468 + 145152 593217668 + 149184 673064617 + 153216 745173386 + 157248 816675650 + 161280 886105443 + 165312 970660768 + 169344 1058290613 + 173376 1140721593 + 177408 1240616018 + 179424 1306862029 + 181440 1463634913 + 183456 1639027360 + 185472 1868851317 + 187488 2019397056 + 189504 2173291204 + 191520 2352873908 + 193536 2530862533 + 195552 2744361593 + 197568 2936684028 + 199584 3115432617 + 201600 3282437367 + 203616 3490737816 + 205632 3669806064 + 207648 3848901149 + 209664 4064972247 + 211680 4278148686 + 213696 4557787597 + 215712 4786120879 + 217728 5111707340 + 219744 5419128115 + 221760 5733907456 + 223776 6053668460 + 225792 6407870776 + 227808 6652067986 + 228534 6778529822 + 257568 10838081536 + 259542 11106516992 + 271827 12968787968 + 286296 15619588096 + 290715 16626221056 + """ + strList = [line.strip().split() for line in blksizefile.strip().split('\n')] + BLK_SIZE_LIST = [[int(x[0]), int(x[1])] for x in strList] + + if blkNum < BLK_SIZE_LIST[-1][0]: + # Interpolate + bprev,bcurr = None, None + for i,blkpair in enumerate(BLK_SIZE_LIST): + if blkNum < blkpair[0]: + b0,d0 = BLK_SIZE_LIST[i-1] + b1,d1 = blkpair + ratio = float(blkNum-b0)/float(b1-b0) + return int(ratio*d1 + (1-ratio)*d0) + raise ValueError('Interpolation failed for %d' % blkNum) + + else: + bend, dend = BLK_SIZE_LIST[-1] + bend2, dend2 = BLK_SIZE_LIST[-3] + rate = float(dend - dend2) / float(bend - bend2) # bytes per block + extraOnTop = (blkNum - bend) * rate + return dend+extraOnTop + + + +############################################################################# +def DeriveChaincodeFromRootKey(sbdPrivKey): + return SecureBinaryData( HMAC256( sbdPrivKey.getHash256(), \ + 'Derive Chaincode from Root Key')) + + +################################################################################ +def HardcodedKeyMaskParams(): + paramMap = {} + + # Nothing up my sleeve! Need some hardcoded random numbers to use for + # encryption IV and salt. Using the first 256 digits of Pi for the + # the IV, and first 256 digits of e for the salt (hashed) + digits_pi = ( \ + 'ARMORY_ENCRYPTION_INITIALIZATION_VECTOR_' + '1415926535897932384626433832795028841971693993751058209749445923' + '0781640628620899862803482534211706798214808651328230664709384460' + '9550582231725359408128481117450284102701938521105559644622948954' + '9303819644288109756659334461284756482337867831652712019091456485') + digits_e = ( \ + 'ARMORY_KEY_DERIVATION_FUNCTION_SALT_' + '7182818284590452353602874713526624977572470936999595749669676277' + '2407663035354759457138217852516642742746639193200305992181741359' + '6629043572900334295260595630738132328627943490763233829880753195' + '2510190115738341879307021540891499348841675092447614606680822648') + + paramMap['IV'] = SecureBinaryData( hash256(digits_pi)[:16] ) + paramMap['SALT'] = SecureBinaryData( hash256(digits_e) ) + paramMap['KDFBYTES'] = long(16*MEGABYTE) + + def hardcodeCreateSecurePrintPassphrase(secret): + if isinstance(secret, basestring): + secret = SecureBinaryData(secret) + bin7 = HMAC512(secret.getHash256(), paramMap['SALT'].toBinStr())[:7] + out,bin7 = SecureBinaryData(binary_to_base58(bin7 + hash256(bin7)[0])), None + return out + + def hardcodeCheckPassphrase(passphrase): + if isinstance(passphrase, basestring): + pwd = base58_to_binary(passphrase) + else: + pwd = base58_to_binary(passphrase.toBinStr()) + + isgood,pwd = (hash256(pwd[:7])[0] == pwd[-1]), None + return isgood + + def hardcodeApplyKdf(secret): + if isinstance(secret, basestring): + secret = SecureBinaryData(secret) + kdf = KdfRomix() + kdf.usePrecomputedKdfParams(paramMap['KDFBYTES'], 1, paramMap['SALT']) + return kdf.DeriveKey(secret) + + def hardcodeMask(secret, passphrase=None, ekey=None): + if not ekey: + ekey = hardcodeApplyKdf(passphrase) + return CryptoAES().EncryptCBC(secret, ekey, paramMap['IV']) + + def hardcodeUnmask(secret, passphrase=None, ekey=None): + if not ekey: + ekey = hardcodeApplyKdf(passphrase) + return CryptoAES().DecryptCBC(secret, ekey, paramMap['IV']) + + paramMap['FUNC_PWD'] = hardcodeCreateSecurePrintPassphrase + paramMap['FUNC_KDF'] = hardcodeApplyKdf + paramMap['FUNC_MASK'] = hardcodeMask + paramMap['FUNC_UNMASK'] = hardcodeUnmask + paramMap['FUNC_CHKPWD'] = hardcodeCheckPassphrase + return paramMap + + + + +################################################################################ +################################################################################ +class SettingsFile(object): + """ + This class could be replaced by the built-in QSettings in PyQt, except + that older versions of PyQt do not support the QSettings (or at least + I never figured it out). Easy enough to do it here + + All settings must populated with a simple datatype -- non-simple + datatypes should be broken down into pieces that are simple: numbers + and strings, or lists/tuples of them. + + Will write all the settings to file. Each line will look like: + SingleValueSetting1 | 3824.8 + SingleValueSetting2 | this is a string + Tuple Or List Obj 1 | 12 $ 43 $ 13 $ 33 + Tuple Or List Obj 2 | str1 $ another str + """ + + ############################################################################# + def __init__(self, path=None): + self.settingsPath = path + self.settingsMap = {} + if not path: + self.settingsPath = os.path.join(ARMORY_HOME_DIR, 'ArmorySettings.txt') + + LOGINFO('Using settings file: %s', self.settingsPath) + if os.path.exists(self.settingsPath): + self.loadSettingsFile(path) + + + + ############################################################################# + def pprint(self, nIndent=0): + indstr = indent*nIndent + print indstr + 'Settings:' + for k,v in self.settingsMap.iteritems(): + print indstr + indent + k.ljust(15), v + + + ############################################################################# + def hasSetting(self, name): + return self.settingsMap.has_key(name) + + ############################################################################# + def set(self, name, value): + if isinstance(value, tuple): + self.settingsMap[name] = list(value) + else: + self.settingsMap[name] = value + self.writeSettingsFile() + + ############################################################################# + def extend(self, name, value): + """ Adds/converts setting to list, appends value to the end of it """ + if not self.settingsMap.has_key(name): + if isinstance(value, list): + self.set(name, value) + else: + self.set(name, [value]) + else: + origVal = self.get(name, expectList=True) + if isinstance(value, list): + origVal.extend(value) + else: + origVal.append(value) + self.settingsMap[name] = origVal + self.writeSettingsFile() + + ############################################################################# + def get(self, name, expectList=False): + if not self.hasSetting(name) or self.settingsMap[name]=='': + return ([] if expectList else '') + else: + val = self.settingsMap[name] + if expectList: + if isinstance(val, list): + return val + else: + return [val] + else: + return val + + ############################################################################# + def getAllSettings(self): + return self.settingsMap + + ############################################################################# + def getSettingOrSetDefault(self, name, defaultVal, expectList=False): + output = defaultVal + if self.hasSetting(name): + output = self.get(name) + else: + self.set(name, defaultVal) + + return output + + + + ############################################################################# + def delete(self, name): + if self.hasSetting(name): + del self.settingsMap[name] + self.writeSettingsFile() + + ############################################################################# + def writeSettingsFile(self, path=None): + if not path: + path = self.settingsPath + f = open(path, 'w') + for key,val in self.settingsMap.iteritems(): + try: + # Skip anything that throws an exception + valStr = '' + if isinstance(val, basestring): + valStr = val + elif isinstance(val, int) or \ + isinstance(val, float) or \ + isinstance(val, long): + valStr = str(val) + elif isinstance(val, list) or \ + isinstance(val, tuple): + valStr = ' $ '.join([str(v) for v in val]) + f.write(key.ljust(36)) + f.write(' | ') + f.write(toBytes(valStr)) + f.write('\n') + except: + LOGEXCEPT('Invalid entry in SettingsFile... skipping') + f.close() + + + ############################################################################# + def loadSettingsFile(self, path=None): + if not path: + path = self.settingsPath + + if not os.path.exists(path): + raise FileExistsError('Settings file DNE:' + path) + + f = open(path, 'rb') + sdata = f.read() + f.close() + + # Automatically convert settings to numeric if possible + def castVal(v): + v = v.strip() + a,b = v.isdigit(), v.replace('.','').isdigit() + if a: + return int(v) + elif b: + return float(v) + else: + if v.lower()=='true': + return True + elif v.lower()=='false': + return False + else: + return toUnicode(v) + + + sdata = [line.strip() for line in sdata.split('\n')] + for line in sdata: + if len(line.strip())==0: + continue + + try: + key,vals = line.split('|') + valList = [castVal(v) for v in vals.split('$')] + if len(valList)==1: + self.settingsMap[key.strip()] = valList[0] + else: + self.settingsMap[key.strip()] = valList + except: + LOGEXCEPT('Invalid setting in %s (skipping...)', path) + + + +# Random method for creating +def touchFile(fname): + try: + os.utime(fname, None) + except: + f = open(fname, 'a') + f.flush() + os.fsync(f.fileno()) + f.close() + + +# NOTE: Had to put in this at the eend so it was after the AllowAsync def +# This flag takes into account both CLI_OPTIONs, and availability of the +# BitTornado library (the user can remove the BitTornado dir and/or the +# torrentDL.py files without breaking Armory, it will simply set this +# disable flag to true) +class FakeTDM(object): + def __init__(self): + self.isRunning = lambda: False + self.isStarted = lambda: False + self.isFinished = lambda: False + self.getTDMState = lambda: 'Disabled' + self.removeOldTorrentFile = lambda: None + + +DISABLE_TORRENTDL = CLI_OPTIONS.disableTorrent +TheTDM = FakeTDM() +try: + import torrentDL + TheTDM = torrentDL.TorrentDownloadManager() +except: + LOGEXCEPT('Failed to import torrent downloader') + DISABLE_TORRENTDL = True + +# We only use BITTORRENT for mainnet +if USE_TESTNET: + DISABLE_TORRENTDL = True + + + + diff --git a/armoryengine/BDM.py b/armoryengine/BDM.py new file mode 100644 index 000000000..b6b5aff03 --- /dev/null +++ b/armoryengine/BDM.py @@ -0,0 +1,1458 @@ +################################################################################ +# # +# Copyright (C) 2011-2014, Armory Technologies, Inc. # +# Distributed under the GNU Affero General Public License (AGPL v3) # +# See LICENSE or http://www.gnu.org/licenses/agpl.html # +# # +################################################################################ +import Queue +import os.path +import random +import threading +import traceback + +from armoryengine.ArmoryUtils import * +from SDM import SatoshiDaemonManager +from armoryengine.Timer import TimeThisFunction +import CppBlockUtils as Cpp + +BDMcurrentBlock = [UINT32_MAX, 0] + + +def getCurrTimeAndBlock(): + time0 = long(RightNowUTC()) + if TheBDM.getBDMState()=='BlockchainReady': + if BDMcurrentBlock[1]: return (time0, BDMcurrentBlock[0]) + else: return (time0, TheBDM.getTopBlockHeight()) + else: + return (time0, UINT32_MAX) + +################################################################################ +# Let's create a thread-wrapper for the blockchain utilities. Enable the +# ability for multi-threaded blockchain scanning -- have a main thread and +# a blockchain thread: blockchain can scan, and main thread will check back +# every now and then to see if it's done +BLOCKCHAINMODE = enum('Offline', \ + 'Uninitialized', \ + 'Full', \ + 'Rescanning', \ + 'LiteScanning', \ + 'FullPrune', \ + 'Lite') + +BDMINPUTTYPE = enum('RegisterAddr', \ + 'ZeroConfTxToInsert', \ + 'HeaderRequested', \ + 'TxRequested', \ + 'BlockRequested', \ + 'AddrBookRequested', \ + 'BlockAtHeightRequested', \ + 'HeaderAtHeightRequested', \ + 'ForceRebuild', \ + 'RescanRequested', \ + 'WalletRecoveryScan', \ + 'UpdateWallets', \ + 'ReadBlkUpdate', \ + 'GoOnlineRequested', \ + 'GoOfflineRequested', \ + 'Passthrough', \ + 'Reset', \ + 'Shutdown') + +################################################################################ +class BlockDataManagerThread(threading.Thread): + """ + A note about this class: + + It was mainly created to allow for asynchronous blockchain scanning, + but the act of splitting the BDM into it's own thread meant that ALL + communication with the BDM requires thread-safe access. So basically, + I had to wrap EVERYTHING. And then make it flexible. + + For this reason, any calls not explicitly related to rescanning will + block by default, which could be a long time if the BDM is in the + middle of rescanning. For this reason, you are expected to either + pass wait=False if you just want to queue the function call and move + on in the main thread, or check the BDM state first, to make sure + it's not currently scanning and can expect immediate response. + + This makes using the BDM much more complicated. But comes with the + benefit of all rescanning being able to happen in the background. + If you want to run it like single-threaded, you can use + TheBDM.setBlocking(True) and all calls will block. Always (unless + you pass wait=False explicitly to one of those calls). + + Any calls that retrieve data from the BDM should block, even if you + technically can specify wait=False. This is because the class was + not designed to maintain organization of output data asynchronously. + So a call like TheBDM.getTopBlockHeader() will always block, and you + should check the BDM state if you want to make sure it returns + immediately. Since there is only one main thread, There is really no + way for a rescan to be started between the time you check the state + and the time you call the method (so if you want to access the BDM + from multiple threads, this class will need some redesign). + + + This serves as a layer between the GUI and the Blockchain utilities. + If a request is made to mess with the BDM while it is in the + middle of scanning, it will queue it for when it's done + + All private methods (those starting with two underscores, like __method), + are executed only by the BDM thread. These should never be called + externally, and are only safe to run when the BDM is ready to execute + them. + + You can use any non-private methods at any time, and if you set wait=True, + the main thread will block until that operation is complete. If the BDM + is in the middle of a scan, the main thread could block for minutes until + the scanning is complete and then it processes your request. + + Therefore, use setBlocking(True) to make sure you always wait/block after + every call, if you are interested in simplicity and don't mind waiting. + + Use setBlocking(False) along with wait=False for the appropriate calls + to queue up your request and continue the main thread immediately. You + can finish up something else, and then come back and check whether the + job is finished (usually using TheBDM.getBDMState()=='BlockchainReady') + + Any methods not defined explicitly in this class will "passthrough" the + __getattr__() method, which will then call that exact method name on + the BDM. All calls block by default. All such calls can also include + wait=False if you want to queue it and then continue asynchronously. + + + Implementation notes: + + Before the multi-threaded BDM, there was wallets, and there was the BDM. + We always gave the wallets to the BDM and said "please search the block- + chain for relevant transactions". Now that this is asynchronous, the + calling thread is going to queue the blockchain scan, and then run off + and do other things: which may include address/wallet operations that + would collide with the BDM updating it. + + THEREFORE, the BDM now has a single, master wallet. Any address you add + to any of your wallets, should be added to the master wallet, too. The + PyBtcWallet class does this for you, but if you are using raw BtcWallets + (the C++ equivalent), you need to do: + + cppWallet.addScrAddress_1_(Hash160ToScrAddr(newAddr)) + TheBDM.registerScrAddr(newAddr, isFresh=?) + + This will add the address to the TheBDM.masterCppWallet. Then when you + queue up the TheBDM to do a rescan (if necessary), it will update only + its own wallet. Luckily, I designed the BDM so that data for addresses + in one wallet (the master), can be applied immediately to other/new + wallets that have the same addresses. + + If you say isFresh=False, then the BDM will set isDirty=True. This means + that a full rescan will have to be performed, and wallet information may + not be accurate until it is performed. isFresh=True should be used for + addresses/wallets you just created, and thus there's no reason to rescan, + because there's no chance they could have any history in the blockchain. + + Tying this all together: if you add an address to a PYTHON wallet, you + just add it through an existing call. If you add it with a C++ wallet, + you need to explicitly register it with TheBDM, too. Then you need to + tell the BDM to do a rescan (if isDirty==True), and then call the method + updateWalletsAfterScan( + are ready, you can chec + + """ + ############################################################################# + def __init__(self, isOffline=False, blocking=False): + super(BlockDataManagerThread, self).__init__() + + if isOffline: + self.blkMode = BLOCKCHAINMODE.Offline + self.prefMode = BLOCKCHAINMODE.Offline + else: + self.blkMode = BLOCKCHAINMODE.Uninitialized + self.prefMode = BLOCKCHAINMODE.Full + + self.bdm = Cpp.BlockDataManager().getBDM() + + # These are for communicating with the master (GUI) thread + self.inputQueue = Queue.Queue() + self.outputQueue = Queue.Queue() + + # Flags + self.startBDM = False + self.doShutdown = False + self.aboutToRescan = False + self.errorOut = 0 + + self.setBlocking(blocking) + + self.currentActivity = 'None' + + # Lists of wallets that should be checked after blockchain updates + self.pyWltList = [] # these will be python refs + self.cppWltList = [] # these will be python refs + + # The BlockDataManager is easier to use if you put all your addresses + # into a C++ BtcWallet object, and let it + self.masterCppWallet = Cpp.BtcWallet() + self.bdm.registerWallet(self.masterCppWallet) + + self.btcdir = BTC_HOME_DIR + self.ldbdir = LEVELDB_DIR + self.lastPctLoad = 0 + + + + + ############################################################################# + def __getattr__(self, name): + ''' + Anything that is not explicitly defined in this class should + passthrough to the C++ BlockDataManager class + + This remaps such calls into "passthrough" requests via the input + queue. This makes sure that the requests are processed only when + the BDM is ready. Hopefully, this will prevent multi-threaded + disasters, such as seg faults due to trying to read memory that is + in the process of being updated. + + Specifically, any passthrough call is expected to return output + unless you add 'waitForReturn=False' to the arg list. i.e. all + calls that "passthrough" will always block unless you explicitly + tell it not to. + ''' + + + rndID = int(random.uniform(0,100000000)) + if not hasattr(self.bdm, name): + LOGERROR('No BDM method: %s', name) + raise AttributeError + else: + def passthruFunc(*args, **kwargs): + #LOGDEBUG('External thread requesting: %s (%d)', name, rndID) + waitForReturn = True + if len(kwargs)>0 and \ + kwargs.has_key('wait') and \ + not kwargs['wait']: + waitForReturn = False + + + # If this was ultimately called from the BDM thread, don't go + # through the queue, just do it! + if len(kwargs)>0 and \ + kwargs.has_key('calledFromBDM') and \ + kwargs['calledFromBDM']: + return getattr(self.bdm, name)(*args) + + self.inputQueue.put([BDMINPUTTYPE.Passthrough, rndID, waitForReturn, name] + list(args)) + + + if waitForReturn: + try: + out = self.outputQueue.get(True, self.mtWaitSec) + return out + except Queue.Empty: + LOGERROR('BDM was not ready for your request! Waited %d sec.' % self.mtWaitSec) + LOGERROR(' getattr name: %s', name) + LOGERROR('BDM currently doing: %s (%d)', self.currentActivity,self.currentID ) + LOGERROR('Waiting for completion: ID= %d', rndID) + LOGERROR('Direct traceback') + traceback.print_stack() + self.errorOut += 1 + LOGEXCEPT('Traceback:') + return passthruFunc + + + + ############################################################################# + def waitForOutputIfNecessary(self, expectOutput, rndID=0): + # The get() command will block until the thread puts something there. + # We don't always expect output, but we use this method to + # replace inputQueue.join(). The reason for doing it is so + # that we can guarantee that BDM thread knows whether we are waiting + # for output or not, and any additional requests put on the inputQueue + # won't extend our wait time for this request + if expectOutput: + try: + return self.outputQueue.get(True, self.mtWaitSec) + except Queue.Empty: + stkOneUp = traceback.extract_stack()[-2] + filename,method = stkOneUp[0], stkOneUp[1] + LOGERROR('Waiting for BDM output that didn\'t come after %ds.' % self.mtWaitSec) + LOGERROR('BDM state is currently: %s', self.getBDMState()) + LOGERROR('Called from: %s:%d (%d)', os.path.basename(filename), method, rndID) + LOGERROR('BDM currently doing: %s (%d)', self.currentActivity, self.currentID) + LOGERROR('Direct traceback') + traceback.print_stack() + LOGEXCEPT('Traceback:') + self.errorOut += 1 + else: + return None + + + ############################################################################# + def setBlocking(self, doblock=True, newTimeout=MT_WAIT_TIMEOUT_SEC): + """ + If we want TheBDM to behave as a single-threaded app, we need to disable + the timeouts so that long operations (such as reading the blockchain) do + not crash the process. + + So setting wait=True is NOT identical to setBlocking(True), since using + wait=True with blocking=False will break when the timeout has been reached + """ + if doblock: + self.alwaysBlock = True + self.mtWaitSec = None + else: + self.alwaysBlock = False + self.mtWaitSec = newTimeout + + + ############################################################################# + def Reset(self, wait=None): + expectOutput = False + if not wait==False and (self.alwaysBlock or wait==True): + expectOutput = True + + rndID = int(random.uniform(0,100000000)) + self.inputQueue.put([BDMINPUTTYPE.Reset, rndID, expectOutput] ) + return self.waitForOutputIfNecessary(expectOutput, rndID) + + ############################################################################# + def getBlkMode(self): + return self.blkMode + + ############################################################################# + def getBDMState(self): + if self.blkMode == BLOCKCHAINMODE.Offline: + # BDM will not be able to provide any blockchain data, or scan + return 'Offline' + elif self.blkMode == BLOCKCHAINMODE.Full and not self.aboutToRescan: + # The BDM is idle, waiting for things to do + return 'BlockchainReady' + elif self.blkMode == BLOCKCHAINMODE.LiteScanning and not self.aboutToRescan: + # The BDM is doing some processing but it is expected to be done within + # 0.1s. For instance, readBlkFileUpdate requires processing, but can be + # performed 100/sec. For the outside calling thread, this is not any + # different than BlockchainReady. + return 'BlockchainReady' + elif self.blkMode == BLOCKCHAINMODE.Rescanning or self.aboutToRescan: + # BDM is doing a FULL scan of the blockchain, and expected to take + + return 'Scanning' + elif self.blkMode == BLOCKCHAINMODE.Uninitialized and not self.aboutToRescan: + # BDM wants to be online, but the calling thread never initiated the + # loadBlockchain() call. Usually setOnlineMode, registerWallets, then + # load the blockchain. + return 'Uninitialized' + elif self.blkMode == BLOCKCHAINMODE.FullPrune: + # NOT IMPLEMENTED + return 'FullPrune' + elif self.blkMode == BLOCKCHAINMODE.Lite: + # NOT IMPLEMENTED + return 'Lite' + else: + return '' % self.blkMode + + + ############################################################################# + def predictLoadTime(self): + # Apparently we can't read the C++ state while it's scanning, + # specifically getLoadProgress* methods. Thus we have to resort + # to communicating via files... bleh + bfile = os.path.join(ARMORY_HOME_DIR,'blkfiles.txt') + if not os.path.exists(bfile): + return [-1,-1,-1,-1] + + try: + with open(bfile,'r') as f: + tmtrx = [line.split() for line in f.readlines() if len(line.strip())>0] + phases = [float(row[0]) for row in tmtrx] + currPhase = phases[-1] + startat = [float(row[1]) for row in tmtrx if float(row[0])==currPhase] + sofar = [float(row[2]) for row in tmtrx if float(row[0])==currPhase] + total = [float(row[3]) for row in tmtrx if float(row[0])==currPhase] + times = [float(row[4]) for row in tmtrx if float(row[0])==currPhase] + + todo = total[0] - startat[0] + pct0 = sofar[0] / todo + pct1 = sofar[-1] / todo + t0,t1 = times[0], times[-1] + if (not t1>t0) or todo<0: + return [-1,-1,-1,-1] + rate = (pct1-pct0) / (t1-t0) + tleft = (1-pct1)/rate + totalPct = (startat[-1] + sofar[-1]) / total[-1] + if not self.lastPctLoad == pct1: + LOGINFO('Reading blockchain, pct complete: %0.1f', 100*totalPct) + self.lastPctLoad = totalPct + return (currPhase,totalPct,rate,tleft) + except: + raise + return [-1,-1,-1,-1] + + + + + ############################################################################# + def execCleanShutdown(self, wait=True): + expectOutput = False + if not wait==False and (self.alwaysBlock or wait==True): + expectOutput = True + + rndID = int(random.uniform(0,100000000)) + self.inputQueue.put([BDMINPUTTYPE.Shutdown, rndID, expectOutput]) + return self.waitForOutputIfNecessary(expectOutput, rndID) + + ############################################################################# + def setSatoshiDir(self, newBtcDir): + if not os.path.exists(newBtcDir): + LOGERROR('setSatoshiDir: directory does not exist: %s', newBtcDir) + return + + if not self.blkMode in (BLOCKCHAINMODE.Offline, BLOCKCHAINMODE.Uninitialized): + LOGERROR('Cannot set blockchain/satoshi path after BDM is started') + return + + self.btcdir = newBtcDir + + ############################################################################# + def setLevelDBDir(self, ldbdir): + + if not self.blkMode in (BLOCKCHAINMODE.Offline, BLOCKCHAINMODE.Uninitialized): + LOGERROR('Cannot set blockchain/satoshi path after BDM is started') + return + + if not os.path.exists(ldbdir): + os.makedirs(ldbdir) + + self.ldbdir = ldbdir + + + ############################################################################# + def setOnlineMode(self, goOnline=True, wait=None): + LOGINFO('Setting online mode: %s (wait=%s)' % (str(goOnline), str(wait))) + expectOutput = False + if not wait==False and (self.alwaysBlock or wait==True): + expectOutput = True + + rndID = int(random.uniform(0,100000000)) + + if goOnline: + if TheBDM.getBDMState() in ('Offline','Uninitialized'): + self.inputQueue.put([BDMINPUTTYPE.GoOnlineRequested, rndID, expectOutput]) + else: + if TheBDM.getBDMState() in ('Scanning','BlockchainReady'): + self.inputQueue.put([BDMINPUTTYPE.GoOfflineRequested, rndID, expectOutput]) + + return self.waitForOutputIfNecessary(expectOutput, rndID) + + ############################################################################# + def isScanning(self): + return (self.aboutToRescan or self.blkMode==BLOCKCHAINMODE.Rescanning) + + + ############################################################################# + def readBlkFileUpdate(self, wait=True): + """ + This method can be blocking... it always has been without a problem, + because the block file updates are always fast. But I have to assume + that it theoretically *could* take a while. Consider using wait=False + if you want it to do its thing and not wait for it (this matters, because + you'll want to call TheBDM.updateWalletsAfterScan() when this is + finished to make sure that + """ + expectOutput = False + if not wait==False and (self.alwaysBlock or wait==True): + expectOutput = True + + rndID = int(random.uniform(0,100000000)) + self.inputQueue.put([BDMINPUTTYPE.ReadBlkUpdate, rndID, expectOutput]) + return self.waitForOutputIfNecessary(expectOutput, rndID) + + + ############################################################################# + def isInitialized(self): + return self.blkMode==BLOCKCHAINMODE.Full and self.bdm.isInitialized() + + + ############################################################################# + def isDirty(self): + return self.bdm.isDirty() + + + + ############################################################################# + def rescanBlockchain(self, scanType='AsNeeded', wait=None): + expectOutput = False + if not wait==False and (self.alwaysBlock or wait==True): + expectOutput = True + + self.aboutToRescan = True + + rndID = int(random.uniform(0,100000000)) + self.inputQueue.put([BDMINPUTTYPE.RescanRequested, rndID, expectOutput, scanType]) + LOGINFO('Blockchain rescan requested') + return self.waitForOutputIfNecessary(expectOutput, rndID) + + + ############################################################################# + def updateWalletsAfterScan(self, wait=True): + """ + Be careful with this method: it is asking the BDM thread to update + the wallets in the main thread. If you do this with wait=False, you + need to avoid any wallet operations in the main thread until it's done. + However, this is usually very fast as long as you know the BDM is not + in the middle of a rescan, so you might as well set wait=True. + + In fact, I highly recommend you always use wait=True, in order to + guarantee thread-safety. + + NOTE: If there are multiple wallet-threads, this might not work. It + might require specifying which wallets to update after a scan, + so that other threads don't collide with the BDM updating its + wallet when called from this thread. + """ + expectOutput = False + if not wait==False and (self.alwaysBlock or wait==True): + expectOutput = True + + rndID = int(random.uniform(0,100000000)) + self.inputQueue.put([BDMINPUTTYPE.UpdateWallets, rndID, expectOutput]) + return self.waitForOutputIfNecessary(expectOutput, rndID) + + + ############################################################################# + def startWalletRecoveryScan(self, pywlt, wait=None): + """ + A wallet recovery scan may require multiple, independent rescans. This + is because we don't know how many addresses to pre-calculate for the + initial scan. So, we will calculate the first X addresses in the wallet, + do a scan, and then if any addresses have tx history beyond X/2, calculate + another X and rescan. This will usually only have to be done once, but + may need to be repeated for super-active wallets. + (In the future, I may add functionality to sample the gap between address + usage, so I can more-intelligently determine when we're at the end...) + """ + + + expectOutput = False + if not wait==False and (self.alwaysBlock or wait==True): + expectOutput = True + + self.aboutToRescan = True + + rndID = int(random.uniform(0,100000000)) + self.inputQueue.put([BDMINPUTTYPE.WalletRecoveryScan, rndID, expectOutput, pywlt]) + LOGINFO('Wallet recovery scan requested') + return self.waitForOutputIfNecessary(expectOutput, rndID) + + + + ############################################################################# + def __checkBDMReadyToServeData(self): + if self.blkMode==BLOCKCHAINMODE.Rescanning: + LOGERROR('Requested blockchain data while scanning. Don\'t do this!') + LOGERROR('Check self.getBlkModeStr()==BLOCKCHAINMODE.Full before') + LOGERROR('making requests! Skipping request') + return False + if self.blkMode==BLOCKCHAINMODE.Offline: + LOGERROR('Requested blockchain data while BDM is in offline mode.') + LOGERROR('Please start the BDM using TheBDM.setOnlineMode() before,') + LOGERROR('and then wait for it to complete, before requesting data.') + return False + if not self.bdm.isInitialized(): + LOGERROR('The BDM thread declares the BDM is ready, but the BDM ') + LOGERROR('itself reports that it is not initialized! What is ') + LOGERROR('going on...?') + return False + + + return True + + ############################################################################# + def getTxByHash(self, txHash): + """ + All calls that retrieve blockchain data are blocking calls. You have + no choice in the matter! + """ + #if not self.__checkBDMReadyToServeData(): + #return None + + rndID = int(random.uniform(0,100000000)) + self.inputQueue.put([BDMINPUTTYPE.TxRequested, rndID, True, txHash]) + + try: + result = self.outputQueue.get(True, 10) + if result==None: + LOGERROR('Requested tx does not exist:\n%s', binary_to_hex(txHash)) + return result + except Queue.Empty: + LOGERROR('Waited 10s for tx to be returned. Abort') + LOGERROR('ID: getTxByHash (%d)', rndID) + return None + #LOGERROR('Going to block until we get something...') + #return self.outputQueue.get(True) + + return None + + + ############################################################################ + def getHeaderByHash(self, headHash): + """ + All calls that retrieve blockchain data are blocking calls. You have + no choice in the matter! + """ + #if not self.__checkBDMReadyToServeData(): + #return None + + rndID = int(random.uniform(0,100000000)) + self.inputQueue.put([BDMINPUTTYPE.HeaderRequested, rndID, True, headHash]) + + try: + result = self.outputQueue.get(True, 10) + if result==None: + LOGERROR('Requested header does not exist:\n%s', \ + binary_to_hex(headHash)) + return result + except Queue.Empty: + LOGERROR('Waited 10s for header to be returned. Abort') + LOGERROR('ID: getTxByHash (%d)', rndID) + #LOGERROR('Going to block until we get something...') + #return self.outputQueue.get(True) + + return None + + + ############################################################################# + def getBlockByHash(self,headHash): + """ + All calls that retrieve blockchain data are blocking calls. You have + no choice in the matter! + + This retrives the full block, not just the header, encoded the same + way as it is in the blkXXXX.dat files (including magic bytes and + block 4-byte block size) + """ + #if not self.__checkBDMReadyToServeData(): + #return None + + rndID = int(random.uniform(0,100000000)) + self.inputQueue.put([BDMINPUTTYPE.BlockRequested, rndID, True, headHash]) + + try: + result = self.outputQueue.get(True, 10) + if result==None: + LOGERROR('Requested block does not exist:\n%s', \ + binary_to_hex(headHash)) + return result + except Queue.Empty: + LOGERROR('Waited 10s for block to be returned. Abort') + LOGERROR('ID: getTxByHash (%d)', rndID) + #LOGERROR('Going to block until we get something...') + #return self.outputQueue.get(True) + + return None + + + ############################################################################# + def getAddressBook(self, wlt): + """ + Address books are constructed from Blockchain data, which means this + must be a blocking method. + """ + rndID = int(random.uniform(0,100000000)) + if isinstance(wlt, PyBtcWallet): + self.inputQueue.put([BDMINPUTTYPE.AddrBookRequested, rndID, True, wlt.cppWallet]) + elif isinstance(wlt, Cpp.BtcWallet): + self.inputQueue.put([BDMINPUTTYPE.AddrBookRequested, rndID, True, wlt]) + + try: + result = self.outputQueue.get(True, self.mtWaitSec) + return result + except Queue.Empty: + LOGERROR('Waited %ds for addrbook to be returned. Abort' % self.mtWaitSec) + LOGERROR('ID: getTxByHash (%d)', rndID) + #LOGERROR('Going to block until we get something...') + #return self.outputQueue.get(True) + + return None + + ############################################################################# + def addNewZeroConfTx(self, rawTx, timeRecv, writeToFile, wait=None): + expectOutput = False + if not wait==False and (self.alwaysBlock or wait==True): + expectOutput = True + + rndID = int(random.uniform(0,100000000)) + self.inputQueue.put([BDMINPUTTYPE.ZeroConfTxToInsert, rndID, expectOutput, rawTx, timeRecv]) + return self.waitForOutputIfNecessary(expectOutput, rndID) + + ############################################################################# + def registerScrAddr(self, scrAddr, isFresh=False, wait=None): + """ + This is for a generic address: treat it as imported (requires rescan) + unless specifically specified otherwise + """ + if isFresh: + self.registerNewScrAddr(scrAddr, wait=wait) + else: + self.registerImportedScrAddr(scrAddr, wait=wait) + + + ############################################################################# + def registerNewScrAddr(self, scrAddr, wait=None): + """ + Variable isFresh==True means the address was just [freshly] created, + and we need to watch for transactions with it, but we don't need + to rescan any blocks + """ + expectOutput = False + if not wait==False and (self.alwaysBlock or wait==True): + expectOutput = True + + rndID = int(random.uniform(0,100000000)) + self.inputQueue.put([BDMINPUTTYPE.RegisterAddr, rndID, expectOutput, scrAddr, True]) + + return self.waitForOutputIfNecessary(expectOutput, rndID) + + + + ############################################################################# + def registerImportedScrAddr(self, scrAddr, \ + firstTime=UINT32_MAX, \ + firstBlk=UINT32_MAX, \ + lastTime=0, \ + lastBlk=0, wait=None): + """ + TODO: Need to clean up the first/last blk/time variables. Rather, + I need to make sure they are maintained and applied intelligently + and consistently + """ + expectOutput = False + if not wait==False and (self.alwaysBlock or wait==True): + expectOutput = True + + rndID = int(random.uniform(0,100000000)) + self.inputQueue.put([BDMINPUTTYPE.RegisterAddr, rndID, expectOutput, scrAddr, \ + [firstTime, firstBlk, lastTime, lastBlk]]) + + return self.waitForOutputIfNecessary(expectOutput, rndID) + + + ############################################################################# + def registerWallet(self, wlt, isFresh=False, wait=None): + """ + Will register a C++ wallet or Python wallet + """ + if isinstance(wlt, PyBtcWallet): + scrAddrs = [Hash160ToScrAddr(a.getAddr160()) for a in wlt.getAddrList()] + + if isFresh: + for scrad in scrAddrs: + self.registerNewScrAddr(scrad, wait=wait) + else: + for scrad in scrAddrs: + self.registerImportedScrAddr(scrad, wait=wait) + + if not wlt in self.pyWltList: + self.pyWltList.append(wlt) + + elif isinstance(wlt, Cpp.BtcWallet): + naddr = wlt.getNumScrAddr() + + for a in range(naddr): + self.registerScrAddr(wlt.getScrAddrObjByIndex(a).getScrAddr(), isFresh, wait=wait) + + if not wlt in self.cppWltList: + self.cppWltList.append(wlt) + else: + LOGERROR('Unrecognized object passed to registerWallet function') + + + + + + ############################################################################# + # These bdm_direct methods feel like a hack. They probably are. I need + # find an elegant way to get the code normally run outside the BDM thread, + # to be able to run inside the BDM thread without using the BDM queue (since + # the queue is specifically FOR non-BDM-thread calls). For now, the best + # I can do is create non-private versions of these methods that access BDM + # methods directly, but should not be used under any circumstances, unless + # we know for sure that the BDM ultimately called this method. + def registerScrAddr_bdm_direct(self, scrAddr, timeInfo): + """ + Something went awry calling __registerScrAddrNow from the PyBtcWallet + code (apparently I don't understand __methods). Use this method to + externally bypass the BDM thread queue and register the address + immediately. + + THIS METHOD IS UNSAFE UNLESS CALLED FROM A METHOD RUNNING IN THE BDM THREAD + This method can be called from a non BDM class, but should only do so if + that class method was called by the BDM (thus, no conflicts) + """ + self.__registerScrAddrNow(scrAddr, timeInfo) + + + ############################################################################# + def scanBlockchainForTx_bdm_direct(self, cppWlt, startBlk=0, endBlk=UINT32_MAX): + """ + THIS METHOD IS UNSAFE UNLESS CALLED FROM A METHOD RUNNING IN THE BDM THREAD + This method can be called from a non BDM class, but should only do so if + that class method was called by the BDM (thus, no conflicts) + """ + self.bdm.scanRegisteredTxForWallet(cppWlt, startBlk, endBlk) + + ############################################################################# + def scanRegisteredTxForWallet_bdm_direct(self, cppWlt, startBlk=0, endBlk=UINT32_MAX): + """ + THIS METHOD IS UNSAFE UNLESS CALLED FROM A METHOD RUNNING IN THE BDM THREAD + This method can be called from a non BDM class, but should only do so if + that class method was called by the BDM (thus, no conflicts) + """ + self.bdm.scanRegisteredTxForWallet(cppWlt, startBlk, endBlk) + + ############################################################################# + def getTopBlockHeight_bdm_direct(self): + """ + THIS METHOD IS UNSAFE UNLESS CALLED FROM A METHOD RUNNING IN THE BDM THREAD + This method can be called from a non BDM class, but should only do so if + that class method was called by the BDM (thus, no conflicts) + """ + return self.bdm.getTopBlockHeight() + + + + ############################################################################# + def getLoadProgress(self): + """ + This method does not actually work! The load progress in bytes is not + updated properly while the BDM thread is scanning. It might have to + emit this information explicitly in order to be useful. + """ + return (self.bdm.getLoadProgressBytes(), self.bdm.getTotalBlockchainBytes()) + + + ############################################################################# + def __registerScrAddrNow(self, scrAddr, timeInfo): + """ + Do the registration right now. This should not be called directly + outside of this class. This is only called by the BDM thread when + any previous scans have been completed + """ + + if isinstance(timeInfo, bool): + isFresh = timeInfo + if isFresh: + # We claimed to have just created this ScrAddr...(so no rescan needed) + self.masterCppWallet.addNewScrAddress_1_(scrAddr) + else: + self.masterCppWallet.addScrAddress_1_(scrAddr) + else: + if isinstance(timeInfo, (list,tuple)) and len(timeInfo)==4: + self.masterCppWallet.addScrAddress_5_(scrAddr, *timeInfo) + else: + LOGWARN('Unrecognized time information in register method.') + LOGWARN(' Data: %s', str(timeInfo)) + LOGWARN('Assuming imported key requires full rescan...') + self.masterCppWallet.addScrAddress_1_(scrAddr) + + + + ############################################################################# + @TimeThisFunction + def __startLoadBlockchain(self): + """ + This should only be called by the threaded BDM, and thus there should + never be a conflict. + """ + if self.blkMode == BLOCKCHAINMODE.Rescanning: + LOGERROR('Blockchain is already scanning. Was this called already?') + return + elif self.blkMode == BLOCKCHAINMODE.Full: + LOGERROR('Blockchain has already been loaded -- maybe we meant') + LOGERROR('to call startRescanBlockchain()...?') + return + elif not self.blkMode == BLOCKCHAINMODE.Uninitialized: + LOGERROR('BDM should be in "Uninitialized" mode before starting ') + LOGERROR('the initial scan. If BDM is in offline mode, you should ') + LOGERROR('switch it to online-mode, first, then request the scan.') + LOGERROR('Continuing with the scan, anyway.') + + + # Remove "blkfiles.txt" to make sure we get accurate TGO + bfile = os.path.join(ARMORY_HOME_DIR,'blkfiles.txt') + if os.path.exists(bfile): + os.remove(bfile) + + # Check for the existence of the Bitcoin-Qt directory + if not os.path.exists(self.btcdir): + raise FileExistsError, ('Directory does not exist: %s' % self.btcdir) + + blkdir = os.path.join(self.btcdir, 'blocks') + blk1st = os.path.join(blkdir, 'blk00000.dat') + + # ... and its blk000X.dat files + if not os.path.exists(blk1st): + LOGERROR('Blockchain data not available: %s', blk1st) + self.prefMode = BLOCKCHAINMODE.Offline + raise FileExistsError, ('Blockchain data not available: %s' % self.blk1st) + + # We have the data, we're ready to go + self.blkMode = BLOCKCHAINMODE.Rescanning + self.aboutToRescan = False + + armory_homedir = ARMORY_HOME_DIR + blockdir = blkdir + leveldbdir = self.ldbdir + + if isinstance(ARMORY_HOME_DIR, unicode): + armory_homedir = ARMORY_HOME_DIR.encode('utf8') + if isinstance(blkdir, unicode): + blockdir = blkdir.encode('utf8') + if isinstance(self.ldbdir, unicode): + leveldbdir = self.ldbdir.encode('utf8') + + LOGINFO('Setting Armory Home Dir: %s' % unicode(armory_homedir)) + LOGINFO('Setting BlkFile Dir: %s' % unicode(blockdir)) + LOGINFO('Setting LevelDB Dir: %s' % unicode(leveldbdir)) + + self.bdm.SetDatabaseModes(ARMORY_DB_BARE, DB_PRUNE_NONE); + self.bdm.SetHomeDirLocation(armory_homedir) + self.bdm.SetBlkFileLocation(blockdir) + self.bdm.SetLevelDBLocation(leveldbdir) + self.bdm.SetBtcNetworkParams( GENESIS_BLOCK_HASH, \ + GENESIS_TX_HASH, \ + MAGIC_BYTES) + + # The master wallet contains all addresses of all wallets registered + self.bdm.registerWallet(self.masterCppWallet) + + # Now we actually startup the BDM and run with it + if CLI_OPTIONS.rebuild: + self.bdm.doInitialSyncOnLoad_Rebuild() + elif CLI_OPTIONS.rescan: + self.bdm.doInitialSyncOnLoad_Rescan() + else: + self.bdm.doInitialSyncOnLoad() + + # The above op populates the BDM with all relevent tx, but those tx + # still need to be scanned to collect the wallet ledger and UTXO sets + self.bdm.scanBlockchainForTx(self.masterCppWallet) + self.bdm.saveScrAddrHistories() + + + ############################################################################# + @TimeThisFunction + def __startRescanBlockchain(self, scanType='AsNeeded'): + """ + This should only be called by the threaded BDM, and thus there should + never be a conflict. + + If we don't force a full scan, we let TheBDM figure out how much of the + chain needs to be rescanned. Which may not be very much. We may + force a full scan if we think there's an issue with balances. + """ + if self.blkMode==BLOCKCHAINMODE.Offline: + LOGERROR('Blockchain is in offline mode. How can we rescan?') + elif self.blkMode==BLOCKCHAINMODE.Uninitialized: + LOGERROR('Blockchain was never loaded. Why did we request rescan?') + + # Remove "blkfiles.txt" to make sure we get accurate TGO + bfile = os.path.join(ARMORY_HOME_DIR,'blkfiles.txt') + if os.path.exists(bfile): + os.remove(bfile) + + if not self.isDirty(): + LOGWARN('It does not look like we need a rescan... doing it anyway') + + if scanType=='AsNeeded': + if self.bdm.numBlocksToRescan(self.masterCppWallet) < 144: + LOGINFO('Rescan requested, but <1 day\'s worth of block to rescan') + self.blkMode = BLOCKCHAINMODE.LiteScanning + else: + LOGINFO('Rescan requested, and very large scan is necessary') + self.blkMode = BLOCKCHAINMODE.Rescanning + + + self.aboutToRescan = False + + if scanType=='AsNeeded': + self.bdm.doSyncIfNeeded() + elif scanType=='ForceRescan': + LOGINFO('Forcing full rescan of blockchain') + self.bdm.doFullRescanRegardlessOfSync() + self.blkMode = BLOCKCHAINMODE.Rescanning + elif scanType=='ForceRebuild': + LOGINFO('Forcing full rebuild of blockchain database') + self.bdm.doRebuildDatabases() + self.blkMode = BLOCKCHAINMODE.Rescanning + + # missingBlocks = self.bdm.missingBlockHashes() + + self.bdm.scanBlockchainForTx(self.masterCppWallet) + self.bdm.saveScrAddrHistories() + + + ############################################################################# + @TimeThisFunction + def __startRecoveryRescan(self, pywlt): + """ + This should only be called by the threaded BDM, and thus there should + never be a conflict. + + In order to work cleanly with the threaded BDM, the search code + needed to be integrated directly here, instead of being called + from the PyBtcWallet method. Because that method is normally called + from outside the BDM thread, but this method is only called from + _inside_ the BDM thread. Those calls use the BDM stack which will + deadlock waiting for the itself before it can move on... + + Unfortunately, because of this, we have to break a python-class + privacy rules: we are accessing the PyBtcWallet object as if this + were PyBtcWallet code (accessing properties directly). + """ + if not isinstance(pywlt, PyBtcWallet): + LOGERROR('Only python wallets can be passed for recovery scans') + return + + if self.blkMode==BLOCKCHAINMODE.Offline: + LOGERROR('Blockchain is in offline mode. How can we rescan?') + elif self.blkMode==BLOCKCHAINMODE.Uninitialized: + LOGERROR('Blockchain was never loaded. Why did we request rescan?') + + + self.blkMode = BLOCKCHAINMODE.Rescanning + self.aboutToRescan = False + + ##### + + # Whenever calling PyBtcWallet methods from BDM, set flag + prevCalledFromBDM = pywlt.calledFromBDM + pywlt.calledFromBDM = True + + # Do the scan... + pywlt.freshImportFindHighestIndex() + + # Unset flag when done + pywlt.calledFromBDM = prevCalledFromBDM + + ##### + self.bdm.scanRegisteredTxForWallet(self.masterCppWallet) + + + + ############################################################################# + @TimeThisFunction + def __readBlockfileUpdates(self): + ''' + This method can be blocking... it always has been without a problem, + because the block file updates are always fast. But I have to assume + that it theoretically *could* take a while, and the caller might care. + ''' + if self.blkMode == BLOCKCHAINMODE.Offline: + LOGERROR('Can\'t update blockchain in %s mode!', self.getBDMState()) + return + + self.blkMode = BLOCKCHAINMODE.LiteScanning + nblk = self.bdm.readBlkFileUpdate() + + # On new blocks, re-save the histories + # ACR: This was removed because the histories get saved already on the + # call to TheBDM.updateWalletsAfterScan() + #if nblk > 0: + #self.bdm.saveScrAddrHistories() + + return nblk + + + ############################################################################# + @TimeThisFunction + def __updateWalletsAfterScan(self): + """ + This will actually do a scan regardless of whether it is currently + "after scan", but it will usually only be requested right after a + full rescan + """ + + numToRescan = 0 + for pyWlt in self.pyWltList: + thisNum = self.bdm.numBlocksToRescan(pyWlt.cppWallet) + numToRescan = max(numToRescan, thisNum) + + for cppWlt in self.cppWltList: + thisNum = self.bdm.numBlocksToRescan(cppWlt) + numToRescan = max(numToRescan, thisNum) + + if numToRescan<144: + self.blkMode = BLOCKCHAINMODE.LiteScanning + else: + self.blkMode = BLOCKCHAINMODE.Rescanning + + + for pyWlt in self.pyWltList: + pyWlt.syncWithBlockchain() + + for cppWlt in self.cppWltList: + # The pre-leveldb version of Armory specifically required to call + # + # scanRegisteredTxForWallet (scan already-collected reg tx) + # + # instead of + # + # scanBlockchainForTx (search for reg tx then scan) + # + # Because the second one will induce a full rescan to find all new + # registeredTx, if we recently imported an addr or wallet. If we + # imported but decided not to rescan yet, we wan tthe first one, + # which only scans the registered tx that are already collected + # (including new blocks, but not previous blocks). + # + # However, with the leveldb stuff only supporting super-node, there + # is no rescanning, thus it's safe to always call scanBlockchainForTx, + # which grabs everything from the database almost instantaneously. + # However we may want to re-examine this after we implement new + # database modes of operation + #self.bdm.scanRegisteredTxForWallet(cppWlt) + self.bdm.scanBlockchainForTx(cppWlt) + + # At this point all wallets should be 100% up-to-date, save the histories + # to be reloaded next time + self.bdm.saveScrAddrHistories() + + + + ############################################################################# + def __shutdown(self): + if not self.blkMode == BLOCKCHAINMODE.Rescanning: + self.bdm.saveScrAddrHistories() + + self.__reset() + self.blkMode = BLOCKCHAINMODE.Offline + self.doShutdown = True + + ############################################################################# + def __fullRebuild(self): + self.bdm.destroyAndResetDatabases() + self.__reset() + self.__startLoadBlockchain() + + ############################################################################# + def __reset(self): + LOGERROR('Resetting BDM and all wallets') + self.bdm.Reset() + + if self.blkMode in (BLOCKCHAINMODE.Full, BLOCKCHAINMODE.Rescanning): + # Uninitialized means we want to be online, but haven't loaded yet + self.blkMode = BLOCKCHAINMODE.Uninitialized + elif not self.blkMode==BLOCKCHAINMODE.Offline: + return + + self.bdm.resetRegisteredWallets() + + # Flags + self.startBDM = False + #self.btcdir = BTC_HOME_DIR + + # Lists of wallets that should be checked after blockchain updates + self.pyWltList = [] # these will be python refs + self.cppWltList = [] # these will be C++ refs + + + # The BlockDataManager is easier to use if you put all your addresses + # into a C++ BtcWallet object, and let it + self.masterCppWallet = Cpp.BtcWallet() + self.bdm.registerWallet(self.masterCppWallet) + + + ############################################################################# + def __getFullBlock(self, headerHash): + headerObj = self.bdm.getHeaderByHash(headerHash) + if not headerObj: + return None + + rawTxList = [] + txList = headerObj.getTxRefPtrList() + for txref in txList: + tx = txref.getTxCopy() + rawTxList.append(tx.serialize()) + + numTxVarInt = len(rawTxList) + blockBytes = 80 + len(numTxVarInt) + sum([len(tx) for tx in rawTxList]) + + rawBlock = MAGIC_BYTES + rawBlock += int_to_hex(blockBytes, endOut=LITTLEENDIAN, widthBytes=4) + rawBlock += headerObj.serialize() + rawBlock += packVarInt(numTxVarInt) + rawBlock += ''.join(rawTxList) + return rawBlock + + + ############################################################################# + def getBDMInputName(self, i): + for name in dir(BDMINPUTTYPE): + if getattr(BDMINPUTTYPE, name)==i: + return name + + ############################################################################# + @TimeThisFunction + def createAddressBook(self, cppWlt): + return cppWlt.createAddressBook() + + def run(self): + """ + This thread runs in an infinite loop, waiting for things to show up + on the self.inputQueue, and then processing those entries. If there + are no requests to the BDM from the main thread, this thread will just + sit idle (in a CPU-friendly fashion) until something does. + """ + + while not self.doShutdown: + # If there were any errors, we will have that many extra output + # entries on the outputQueue. We clear them off so that this + # thread can be re-sync'd with the main thread + try: + while self.errorOut>0: + self.outputQueue.get_nowait() + self.errorOut -= 1 + except Queue.Empty: + LOGERROR('ErrorOut var over-represented number of errors!') + self.errorOut = 0 + + + # Now start the main + try: + try: + inputTuple = self.inputQueue.get_nowait() + # If we don't error out, we have stuff to process right now + except Queue.Empty: + # We only switch to offline/full/uninitialzed when the queue + # is empty. After that, then we block in a CPU-friendly way + # until data shows up on the Queue + if self.prefMode==BLOCKCHAINMODE.Full: + if self.bdm.isInitialized(): + self.blkMode = BLOCKCHAINMODE.Full + else: + self.blkMode = BLOCKCHAINMODE.Uninitialized + else: + self.blkMode = BLOCKCHAINMODE.Offline + + self.currentActivity = 'None' + + # Block until something shows up. + inputTuple = self.inputQueue.get() + except: + LOGERROR('Unknown error in BDM thread') + + + + # The first list element is always the BDMINPUTTYPE (command) + # The second argument is whether the caller will be waiting + # for the output: which means even if it's None, we need to + # put something on the output queue. + cmd = inputTuple[0] + rndID = inputTuple[1] + expectOutput = inputTuple[2] + output = None + + # Some variables that can be queried externally to figure out + # what the BDM is currently doing + self.currentActivity = self.getBDMInputName(inputTuple[0]) + self.currentID = rndID + + if cmd == BDMINPUTTYPE.RegisterAddr: + scrAddr,timeInfo = inputTuple[3:] + self.__registerScrAddrNow(scrAddr, timeInfo) + + elif cmd == BDMINPUTTYPE.ZeroConfTxToInsert: + rawTx = inputTuple[3] + timeIn = inputTuple[4] + if isinstance(rawTx, PyTx): + rawTx = rawTx.serialize() + self.bdm.addNewZeroConfTx(rawTx, timeIn, True) + + elif cmd == BDMINPUTTYPE.HeaderRequested: + headHash = inputTuple[3] + rawHeader = self.bdm.getHeaderByHash(headHash) + if rawHeader: + output = rawHeader + else: + output = None + + elif cmd == BDMINPUTTYPE.TxRequested: + txHash = inputTuple[3] + rawTx = self.bdm.getTxByHash(txHash) + if rawTx: + output = rawTx + else: + output = None + + elif cmd == BDMINPUTTYPE.BlockRequested: + headHash = inputTuple[3] + rawBlock = self.__getFullBlock(headHash) + if rawBlock: + output = rawBlock + else: + output = None + LOGERROR('Requested header does not exist:\n%s', \ + binary_to_hex(headHash)) + + elif cmd == BDMINPUTTYPE.HeaderAtHeightRequested: + height = inputTuple[3] + rawHeader = self.bdm.getHeaderByHeight(height) + if rawHeader: + output = rawHeader + else: + output = None + LOGERROR('Requested header does not exist:\nHeight=%s', height) + + elif cmd == BDMINPUTTYPE.BlockAtHeightRequested: + height = inputTuple[3] + rawBlock = self.__getFullBlock(height) + if rawBlock: + output = rawBlock + else: + output = None + LOGERROR('Requested header does not exist:\nHeight=%s', height) + + elif cmd == BDMINPUTTYPE.AddrBookRequested: + cppWlt = inputTuple[3] + output = self.createAddressBook(cppWlt) + + elif cmd == BDMINPUTTYPE.UpdateWallets: + self.__updateWalletsAfterScan() + + elif cmd == BDMINPUTTYPE.RescanRequested: + scanType = inputTuple[3] + if not scanType in ('AsNeeded', 'ForceRescan', 'ForceRebuild'): + LOGERROR('Invalid scan type for rescanning: ' + scanType) + scanType = 'AsNeeded' + self.__startRescanBlockchain(scanType) + + elif cmd == BDMINPUTTYPE.WalletRecoveryScan: + LOGINFO('Wallet Recovery Scan Requested') + pywlt = inputTuple[3] + self.__startRecoveryRescan(pywlt) + + elif cmd == BDMINPUTTYPE.ReadBlkUpdate: + output = self.__readBlockfileUpdates() + + elif cmd == BDMINPUTTYPE.Passthrough: + # If the caller is waiting, then it is notified by output + funcName = inputTuple[3] + funcArgs = inputTuple[4:] + output = getattr(self.bdm, funcName)(*funcArgs) + + elif cmd == BDMINPUTTYPE.Shutdown: + LOGINFO('Shutdown Requested') + self.__shutdown() + + elif cmd == BDMINPUTTYPE.ForceRebuild: + LOGINFO('Rebuild databases requested') + self.__fullRebuild() + + elif cmd == BDMINPUTTYPE.Reset: + LOGINFO('Reset Requested') + self.__reset() + + elif cmd == BDMINPUTTYPE.GoOnlineRequested: + LOGINFO('Go online requested') + # This only sets the blkMode to what will later be + # recognized as online-requested, or offline + self.prefMode = BLOCKCHAINMODE.Full + if self.bdm.isInitialized(): + # The BDM was started and stopped at one point, without + # being reset. It can safely pick up from where it + # left off + self.__readBlockfileUpdates() + else: + self.blkMode = BLOCKCHAINMODE.Uninitialized + self.__startLoadBlockchain() + + elif cmd == BDMINPUTTYPE.GoOfflineRequested: + LOGINFO('Go offline requested') + self.prefMode = BLOCKCHAINMODE.Offline + + self.inputQueue.task_done() + if expectOutput: + self.outputQueue.put(output) + + except Queue.Empty: + continue + except: + inputName = self.getBDMInputName(inputTuple[0]) + LOGERROR('Error processing BDM input') + #traceback.print_stack() + LOGERROR('Received inputTuple: ' + inputName + ' ' + str(inputTuple)) + LOGERROR('Error processing ID (%d)', rndID) + LOGEXCEPT('ERROR:') + if expectOutput: + self.outputQueue.put('BDM_REQUEST_ERROR') + self.inputQueue.task_done() + continue + + LOGINFO('BDM is shutdown.') + + + + + +################################################################################ +# Make TheBDM reference the asyncrhonous BlockDataManager wrapper if we are +# running +TheBDM = None +TheSDM = None +if CLI_OPTIONS.offline: + LOGINFO('Armory loaded in offline-mode. Will not attempt to load ') + LOGINFO('blockchain without explicit command to do so.') + TheBDM = BlockDataManagerThread(isOffline=True, blocking=False) + TheBDM.start() + + # Also create the might-be-needed SatoshiDaemonManager + TheSDM = SatoshiDaemonManager() + +else: + # NOTE: "TheBDM" is sometimes used in the C++ code to reference the + # singleton BlockDataManager_LevelDB class object. Here, + # "TheBDM" refers to a python BlockDataManagerThead class + # object that wraps the C++ version. It implements some of + # it's own methods, and then passes through anything it + # doesn't recognize to the C++ object. + LOGINFO('Using the asynchronous/multi-threaded BlockDataManager.') + LOGINFO('Blockchain operations will happen in the background. ') + LOGINFO('Devs: check TheBDM.getBDMState() before asking for data.') + LOGINFO('Registering addresses during rescans will queue them for ') + LOGINFO('inclusion after the current scan is completed.') + TheBDM = BlockDataManagerThread(isOffline=False, blocking=False) + TheBDM.setDaemon(True) + TheBDM.start() + + #if CLI_OPTIONS.doDebug or CLI_OPTIONS.netlog or CLI_OPTIONS.mtdebug: + cppLogFile = os.path.join(ARMORY_HOME_DIR, 'armorycpplog.txt') + + cpplf = cppLogFile + if getattr(sys, 'frozen', False): + cpplf = cppLogFile.encode('utf8') + + TheBDM.StartCppLogging(cpplf, 4) + TheBDM.EnableCppLogStdOut() + + # 32-bit linux has an issue with max open files. Rather than modifying + # the system, we can tell LevelDB to take it easy with max files to open + if OS_LINUX and not SystemSpecs.IsX64: + LOGINFO('Lowering max-open-files parameter in LevelDB for 32-bit linux') + TheBDM.setMaxOpenFiles(75) + + # Override the above if they explicitly specify it as CLI arg + if CLI_OPTIONS.maxOpenFiles > 0: + LOGINFO('Overriding max files via command-line arg') + TheBDM.setMaxOpenFiles( CLI_OPTIONS.maxOpenFiles ) + + #LOGINFO('LevelDB max-open-files is %d', TheBDM.getMaxOpenFiles()) + + # Also load the might-be-needed SatoshiDaemonManager + TheSDM = SatoshiDaemonManager() + + +# Put the import at the end to avoid circular reference problem +from armoryengine.PyBtcWallet import PyBtcWallet +from armoryengine.Transaction import PyTx + diff --git a/armoryengine/BinaryPacker.py b/armoryengine/BinaryPacker.py new file mode 100644 index 000000000..379fcb53c --- /dev/null +++ b/armoryengine/BinaryPacker.py @@ -0,0 +1,84 @@ +################################################################################ +# +# Copyright (C) 2011-2014, Armory Technologies, Inc. +# Distributed under the GNU Affero General Public License (AGPL v3) +# See LICENSE or http://www.gnu.org/licenses/agpl.html +# +################################################################################ +# +# Project: Armory +# Author: Alan Reiner +# Website: www.bitcoinarmory.com +# Orig Date: 20 November, 2011 +# +################################################################################ +from armoryengine.ArmoryUtils import LITTLEENDIAN, int_to_binary, packVarInt +UINT8, UINT16, UINT32, UINT64, INT8, INT16, INT32, INT64, VAR_INT, VAR_STR, FLOAT, BINARY_CHUNK = range(12) +from struct import pack, unpack + +class PackerError(Exception): pass + +class BinaryPacker(object): + + """ + Class for helping load binary data into a stream. Typical usage is + >> binpack = BinaryPacker() + >> bup.put(UINT32, 12) + >> bup.put(VAR_INT, 78) + >> bup.put(BINARY_CHUNK, '\x9f'*10) + >> ...etc... + >> result = bup.getBinaryString() + """ + def __init__(self): + self.binaryConcat = [] + + def getSize(self): + return sum([len(a) for a in self.binaryConcat]) + + def getBinaryString(self): + return ''.join(self.binaryConcat) + + def __str__(self): + return self.getBinaryString() + + + def put(self, varType, theData, width=None, endianness=LITTLEENDIAN): + """ + Need to supply the argument type you are put'ing into the stream. + Values of BINARY_CHUNK will automatically detect the size as necessary + + Use width=X to include padding of BINARY_CHUNKs w/ 0x00 bytes + """ + E = endianness + if varType == UINT8: + self.binaryConcat += int_to_binary(theData, 1, endianness) + elif varType == UINT16: + self.binaryConcat += int_to_binary(theData, 2, endianness) + elif varType == UINT32: + self.binaryConcat += int_to_binary(theData, 4, endianness) + elif varType == UINT64: + self.binaryConcat += int_to_binary(theData, 8, endianness) + elif varType == INT8: + self.binaryConcat += pack(E+'b', theData) + elif varType == INT16: + self.binaryConcat += pack(E+'h', theData) + elif varType == INT32: + self.binaryConcat += pack(E+'i', theData) + elif varType == INT64: + self.binaryConcat += pack(E+'q', theData) + elif varType == VAR_INT: + self.binaryConcat += packVarInt(theData)[0] + elif varType == VAR_STR: + self.binaryConcat += packVarInt(len(theData))[0] + self.binaryConcat += theData + elif varType == FLOAT: + self.binaryConcat += pack(E+'f', theData) + elif varType == BINARY_CHUNK: + if width==None: + self.binaryConcat += theData + else: + if len(theData)>width: + raise PackerError, 'Too much data to fit into fixed width field' + self.binaryConcat += theData.ljust(width, '\x00') + else: + raise PackerError, "Var type not recognized! VarType="+str(varType) diff --git a/armoryengine/BinaryUnpacker.py b/armoryengine/BinaryUnpacker.py new file mode 100644 index 000000000..2eac6ea06 --- /dev/null +++ b/armoryengine/BinaryUnpacker.py @@ -0,0 +1,130 @@ +################################################################################ +# +# Copyright (C) 2011-2014, Armory Technologies, Inc. +# Distributed under the GNU Affero General Public License (AGPL v3) +# See LICENSE or http://www.gnu.org/licenses/agpl.html +# +################################################################################ +# +# Project: Armory +# Author: Alan Reiner +# Website: www.bitcoinarmory.com +# Orig Date: 20 November, 2011 +# +################################################################################ + + + + +################################################################################ +################################################################################ +# Classes for reading and writing large binary objects +################################################################################ +################################################################################ +from struct import pack, unpack +from BinaryPacker import UINT8, UINT16, UINT32, UINT64, INT8, INT16, INT32, INT64, VAR_INT, VAR_STR, FLOAT, BINARY_CHUNK +from armoryengine.ArmoryUtils import LITTLEENDIAN, unpackVarInt, LOGERROR + +class UnpackerError(Exception): pass + +# Seed this object with binary data, then read in its pieces sequentially +class BinaryUnpacker(object): + """ + Class for helping unpack binary streams of data. Typical usage is + >> bup = BinaryUnpacker(myBinaryData) + >> int32 = bup.get(UINT32) + >> int64 = bup.get(VAR_INT) + >> bytes10 = bup.get(BINARY_CHUNK, 10) + >> ...etc... + """ + def __init__(self, binaryStr): + self.binaryStr = binaryStr + self.pos = 0 + + def getSize(self): return len(self.binaryStr) + def getRemainingSize(self): return len(self.binaryStr) - self.pos + def getBinaryString(self): return self.binaryStr + def getRemainingString(self): return self.binaryStr[self.pos:] + def append(self, binaryStr): self.binaryStr += binaryStr + def advance(self, bytesToAdvance): self.pos += bytesToAdvance + def rewind(self, bytesToRewind): self.pos -= bytesToRewind + def resetPosition(self, toPos=0): self.pos = toPos + def getPosition(self): return self.pos + + def get(self, varType, sz=0, endianness=LITTLEENDIAN): + """ + First argument is the data-type: UINT32, VAR_INT, etc. + If BINARY_CHUNK, need to supply a number of bytes to read, as well + """ + def sizeCheck(sz): + if self.getRemainingSize() 1: + hashes = self.merkleTree[-sz:] + mod2 = sz%2 + for i in range(sz/2): + self.merkleTree.append( hash256(hashes[2*i] + hashes[2*i+1]) ) + if mod2==1: + self.merkleTree.append( hash256(hashes[-1] + hashes[-1]) ) + sz = (sz+1) / 2 + self.merkleRoot = self.merkleTree[-1] + return self.merkleRoot + + def printMerkleTree(self, reverseHash=False, indent=''): + print indent + 'Printing Merkle Tree:' + if reverseHash: + print indent + '(hashes will be reversed, like shown on BlockExplorer.com)' + root = self.getMerkleRoot() + print indent + 'Merkle Root:', binary_to_hex(root) + for h in self.merkleTree: + phash = binary_to_hex(h) if not reverseHash else binary_to_hex(h, endOut=BIGENDIAN) + print indent + '\t' + phash + + + def pprint(self, nIndent=0, endian=BIGENDIAN): + indstr = indent*nIndent + print indstr + 'BlockData:' + print indstr + indent + 'MerkleRoot: ', binary_to_hex(self.getMerkleRoot(), endian), \ + '(BE)' if endian==BIGENDIAN else '(LE)' + print indstr + indent + 'NumTx: ', self.numTx + for tx in self.txList: + tx.pprint(nIndent+1, endian=endian) + + +################################################################################ +################################################################################ +class PyBlock(object): + def __init__(self, prevHeader=None, txlist=[]): + self.blockHeader = PyBlockHeader() + self.blockData = PyBlockData() + if prevHeader: + self.setPrevHeader(prevHeader) + if txlist: + self.setTxList(txlist) + + def serialize(self): + assert( not self.blockHeader == UNINITIALIZED ) + binOut = BinaryPacker() + binOut.put(BINARY_CHUNK, self.blockHeader.serialize()) + binOut.put(BINARY_CHUNK, self.blockData.serialize()) + return binOut.getBinaryString() + + def unserialize(self, toUnpack): + if isinstance(toUnpack, BinaryUnpacker): + blkData = toUnpack + else: + blkData = BinaryUnpacker( toUnpack ) + + self.txList = [] + self.blockHeader = PyBlockHeader().unserialize(blkData) + self.blockData = PyBlockData().unserialize(blkData) + return self + + def getNumTx(self): + return len(self.blockData.txList) + + def getSize(self): + return len(self.serialize()) + + # Not sure how useful these manual block-construction methods + # are. For now, I just need something with non-ridiculous vals + def setPrevHeader(self, prevHeader, copyAttr=True): + self.blockHeader.prevBlkHash = prevHeader.theHash + self.blockHeader.nonce = 0 + if copyAttr: + self.blockHeader.version = prevHeader.version + self.blockHeader.timestamp = prevHeader.timestamp+600 + self.blockHeader.diffBits = prevHeader.diffBits + + def setTxList(self, txlist): + self.blockData = PyBlockData(txlist) + if not self.blockHeader == UNINITIALIZED: + self.blockHeader.merkleRoot = self.blockData.getMerkleRoot() + + def tx(self, idx): + return self.blockData.txList[idx] + + def pprint(self, nIndent=0, endian=BIGENDIAN): + indstr = indent*nIndent + print indstr + 'Block:' + self.blockHeader.pprint(nIndent+1, endian=endian) + self.blockData.pprint(nIndent+1, endian=endian) + diff --git a/armoryengine/CoinSelection.py b/armoryengine/CoinSelection.py new file mode 100644 index 000000000..a01ffcc36 --- /dev/null +++ b/armoryengine/CoinSelection.py @@ -0,0 +1,744 @@ +################################################################################ +# # +# Copyright (C) 2011-2014, Armory Technologies, Inc. # +# Distributed under the GNU Affero General Public License (AGPL v3) # +# See LICENSE or http://www.gnu.org/licenses/agpl.html # +# # +################################################################################ +################################################################################ +################################################################################ +# +# SelectCoins algorithms +# +# The following methods define multiple ways that one could select coins +# for a given transaction. However, the "best" solution is extremely +# dependent on the variety of unspent outputs, and also the preferences +# of the user. Things to take into account when selecting coins: +# +# - Number of inputs: If we have a lot of inputs in this transaction +# from different addresses, then all those addresses +# have now been linked together. We want to use +# as few outputs as possible +# +# - Tx Fess/Size: The bigger the transaction, in bytes, the more +# fee we're going to have to pay to the miners +# +# - Priority: Low-priority transactions might require higher +# fees and/or take longer to make it into the +# blockchain. Priority is the sum of TxOut +# priorities: (NumConfirm * NumBTC / SizeKB) +# We especially want to avoid 0-confirmation txs +# +# - Output values: In almost every transaction, we must return +# change to ourselves. This means there will +# be two outputs, one to the recipient, one to +# us. We prefer that both outputs be about the +# same size, so that it's not clear which is the +# recipient, which is the change. But we don't +# want to use too many inputs to do this. +# +# - Sustainability: We should pick a strategy that tends to leave our +# wallet containing a variety of TxOuts that are +# well-suited for future transactions to benefit. +# For instance, always favoring the single TxOut +# with a value close to the target, will result +# in a future wallet full of tiny TxOuts. This +# guarantees that in the future, we're going to +# have to do 10+ inputs for a single Tx. +# +# +# The strategy is to execute a half dozen different types of SelectCoins +# algorithms, each with a different goal in mind. Then we examine each +# of the results and evaluate a "select-score." Use the one with the +# best score. In the future, we could make the scoring algorithm based +# on user preferences. We expect that depending on what the availble +# list looks like, some of these algorithms could produce perfect results, +# and in other instances *terrible* results. +# +################################################################################ +################################################################################ +import math +import random + +from armoryengine.ArmoryUtils import CheckHash160, binary_to_hex, coin2str, \ + hash160_to_addrStr, ONE_BTC, CENT, int_to_binary, MIN_RELAY_TX_FEE, MIN_TX_FEE +from armoryengine.Timer import TimeThisFunction +from armoryengine.Transaction import * + + +################################################################################ +# These would normally be defined by C++ and fed in, but I've recreated +# the C++ class here... it's really just a container, anyway +# +# TODO: LevelDB upgrade: had to upgrade this class to use arbitrary +# ScrAddress "notation", even though everything else on the python +# side expects pure hash160 values. For now, it looks like it can +# handle arbitrary scripts, but the CheckHash160() calls will +# (correctly) throw errors if you don't. We can upgrade this in +# the future. +class PyUnspentTxOut(object): + def __init__(self, scrAddr='', val=-1, numConf=-1): + pass + #self.scrAddr = scrAddr + #self.val = long(val*ONE_BTC) + #self.conf = numConf + def createFromCppUtxo(self, cppUtxo): + self.scrAddr = cppUtxo.getRecipientScrAddr() + self.val = cppUtxo.getValue() + self.conf = cppUtxo.getNumConfirm() + # For now, this will throw errors unless we always use hash160 scraddrs + self.binScript = '\x76\xa9\x14' + CheckHash160(self.scrAddr) + '\x88\xac' + self.txHash = cppUtxo.getTxHash() + self.txOutIndex = cppUtxo.getTxOutIndex() + return self + def getTxHash(self): + return self.txHash + def getTxOutIndex(self): + return self.txOutIndex + def getValue(self): + return self.val + def getNumConfirm(self): + return self.conf + def getScript(self): + return self.binScript + def getRecipientScrAddr(self): + return self.scrAddr + def getRecipientHash160(self): + return CheckHash160(self.scrAddr) + def prettyStr(self, indent=''): + pstr = [indent] + pstr.append(binary_to_hex(self.scrAddr[:8])) + pstr.append(coin2str(self.val)) + pstr.append(str(self.conf).rjust(8,' ')) + return ' '.join(pstr) + def pprint(self, indent=''): + print self.prettyStr(indent) + + +################################################################################ +def sumTxOutList(txoutList): + return sum([u.getValue() for u in txoutList]) + +################################################################################ +# This is really just for viewing a TxOut list -- usually for debugging +def pprintUnspentTxOutList(utxoList, headerLine='Coin Selection: '): + totalSum = sum([u.getValue() for u in utxoList]) + print headerLine, '(Total = %s BTC)' % coin2str(totalSum) + print ' ','Owner Address'.ljust(34), + print ' ','TxOutValue'.rjust(18), + print ' ','NumConf'.rjust(8), + print ' ','PriorityFactor'.rjust(16) + for utxo in utxoList: + a160 = CheckHash160(utxo.getRecipientScrAddr()) + print ' ',hash160_to_addrStr(a160).ljust(34), + print ' ',(coin2str(utxo.getValue()) + ' BTC').rjust(18), + print ' ',str(utxo.getNumConfirm()).rjust(8), + print ' ', ('%0.2f' % (utxo.getValue()*utxo.getNumConfirm()/(ONE_BTC*144.))).rjust(16) + + +################################################################################ +# Sorting currently implemented in C++, but we implement a different kind, here +def PySortCoins(unspentTxOutInfo, sortMethod=1): + """ + Here we define a few different ways to sort a list of unspent TxOut objects. + Most of them are simple, some are more complex. In particular, the last + method (4) tries to be intelligent, by grouping together inputs from the + same address. + + The goal is not to do the heavy lifting for SelectCoins... we simply need + a few different ways to sort coins so that the SelectCoins algorithms has + a variety of different inputs to play with. Each sorting method is useful + for some types of unspent-TxOut lists, so as long as we have one good + sort, the PyEvalCoinSelect method will pick it out. + + As a precaution we send all the zero-confirmation UTXO's to the back + of the list, so that they will only be used if absolutely necessary. + """ + zeroConfirm = [] + + if sortMethod==0: + priorityFn = lambda a: a.getValue() * a.getNumConfirm() + return sorted(unspentTxOutInfo, key=priorityFn, reverse=True) + if sortMethod==1: + priorityFn = lambda a: (a.getValue() * a.getNumConfirm())**(1/3.) + return sorted(unspentTxOutInfo, key=priorityFn, reverse=True) + if sortMethod==2: + priorityFn = lambda a: (math.log(a.getValue()*a.getNumConfirm()+1)+4)**4 + return sorted(unspentTxOutInfo, key=priorityFn, reverse=True) + if sortMethod==3: + priorityFn = lambda a: a.getValue() if a.getNumConfirm()>0 else 0 + return sorted(unspentTxOutInfo, key=priorityFn, reverse=True) + if sortMethod==4: + addrMap = {} + zeroConfirm = [] + for utxo in unspentTxOutInfo: + if utxo.getNumConfirm() == 0: + zeroConfirm.append(utxo) + else: + addr = script_to_addrStr(utxo.getScript()) + if not addrMap.has_key(addr): + addrMap[addr] = [utxo] + else: + addrMap[addr].append(utxo) + + priorityUTXO = (lambda a: (a.getNumConfirm()*a.getValue()**0.333)) + for addr,txoutList in addrMap.iteritems(): + txoutList.sort(key=priorityUTXO, reverse=True) + + priorityGrp = lambda a: max([priorityUTXO(utxo) for utxo in a]) + finalSortedList = [] + for utxo in sorted(addrMap.values(), key=priorityGrp, reverse=True): + finalSortedList.extend(utxo) + + finalSortedList.extend(zeroConfirm) + return finalSortedList + if sortMethod in (5, 6, 7): + utxoSorted = PySortCoins(unspentTxOutInfo, 1) + # Rotate the top 1,2 or 3 elements to the bottom of the list + for i in range(sortMethod-4): + utxoSorted.append(utxoSorted[0]) + del utxoSorted[0] + return utxoSorted + + # TODO: Add a semi-random sort method: it will favor putting high-priority + # outputs at the front of the list, but will not be deterministic + # This should give us some high-fitness variation compared to sorting + # uniformly + if sortMethod==8: + utxosNoZC = filter(lambda a: a.getNumConfirm()!=0, unspentTxOutInfo) + random.shuffle(utxosNoZC) + utxosNoZC.extend(filter(lambda a: a.getNumConfirm()==0, unspentTxOutInfo)) + return utxosNoZC + if sortMethod==9: + utxoSorted = PySortCoins(unspentTxOutInfo, 1) + sz = len(filter(lambda a: a.getNumConfirm()!=0, utxoSorted)) + # swap 1/3 of the values at random + topsz = int(min(max(round(sz/3), 5), sz)) + for i in range(topsz): + pick1 = int(random.uniform(0,topsz)) + pick2 = int(random.uniform(0,sz-topsz)) + utxoSorted[pick1], utxoSorted[pick2] = utxoSorted[pick2], utxoSorted[pick1] + return utxoSorted + + + + +################################################################################ +# Now we try half a dozen different selection algorithms +################################################################################ + + + +################################################################################ +def PySelectCoins_SingleInput_SingleValue( \ + unspentTxOutInfo, targetOutVal, minFee=0): + """ + This method should usually be called with a small number added to target val + so that a tx can be constructed that has room for user to add some extra fee + if necessary. + + However, we must also try calling it with the exact value, in case the user + is trying to spend exactly their remaining balance. + """ + target = targetOutVal + minFee + bestMatchVal = 2**64 + bestMatchUtxo = None + for utxo in unspentTxOutInfo: + if target <= utxo.getValue() < bestMatchVal: + bestMatchVal = utxo.getValue() + bestMatchUtxo = utxo + + closeness = bestMatchVal - target + if 0 < closeness <= CENT: + # If we're going to have a change output, make sure it's above CENT + # to avoid a mandatory fee + try2Val = 2**64 + try2Utxo = None + for utxo in unspentTxOutInfo: + if target+CENT < utxo.getValue() < try2Val: + try2Val = utxo.getValue() + try2Val = utxo + if not try2Utxo==None: + bestMatchUtxo = try2Utxo + + + if bestMatchUtxo==None: + return [] + else: + return [bestMatchUtxo] + +################################################################################ +def PySelectCoins_MultiInput_SingleValue( \ + unspentTxOutInfo, targetOutVal, minFee=0): + """ + This method should usually be called with a small number added to target val + so that a tx can be constructed that has room for user to add some extra fee + if necessary. + + However, we must also try calling it with the exact value, in case the user + is trying to spend exactly their remaining balance. + """ + target = targetOutVal + minFee + outList = [] + sumVal = 0 + for utxo in unspentTxOutInfo: + sumVal += utxo.getValue() + outList.append(utxo) + if sumVal>=target: + break + + return outList + + + +################################################################################ +def PySelectCoins_SingleInput_DoubleValue( \ + unspentTxOutInfo, targetOutVal, minFee=0): + """ + We will look for a single input that is within 30% of the target + In case the tx value is tiny rel to the fee: the minTarget calc + may fail to exceed the actual tx size needed, so we add an extra + + We restrain the search to 25%. If there is no one output in this + range, then we will return nothing, and the SingleInput_SingleValue + method might return a usable result + """ + idealTarget = 2*targetOutVal + minFee + + # check to make sure we're accumulating enough + minTarget = long(0.75 * idealTarget) + minTarget = max(minTarget, targetOutVal+minFee) + maxTarget = long(1.25 * idealTarget) + + if sum([u.getValue() for u in unspentTxOutInfo]) < minTarget: + return [] + + bestMatch = 2**64-1 + bestUTXO = None + for txout in unspentTxOutInfo: + if minTarget <= txout.getValue() <= maxTarget: + if abs(txout.getValue()-idealTarget) < bestMatch: + bestMatch = abs(txout.getValue()-idealTarget) + bestUTXO = txout + + if bestUTXO==None: + return [] + else: + return [bestUTXO] + +################################################################################ +def PySelectCoins_MultiInput_DoubleValue( \ + unspentTxOutInfo, targetOutVal, minFee=0): + + idealTarget = 2.0 * targetOutVal + minTarget = long(0.80 * idealTarget) + minTarget = max(minTarget, targetOutVal+minFee) + if sum([u.getValue() for u in unspentTxOutInfo]) < minTarget: + return [] + + outList = [] + lastDiff = 2**64-1 + sumVal = 0 + for utxo in unspentTxOutInfo: + sumVal += utxo.getValue() + outList.append(utxo) + currDiff = abs(sumVal - idealTarget) + # should switch from decreasing to increasing when best match + if sumVal>=minTarget and currDiff>lastDiff: + del outList[-1] + break + lastDiff = currDiff + + return outList + + + + +################################################################################ +def getSelectCoinsScores(utxoSelectList, targetOutVal, minFee): + """ + Define a metric for scoring the output of SelectCoints. The output of + this method is a tuple of scores which identify a few different factors + of a txOut selection that users might care about in a selectCoins algorithm. + + This method only returns an absolute score, usually between 0 and 1 for + each factor. It is up to the person calling this method to decide how + much "weight" they want to give each one. You could even use the scores + as multiplicative factors if you wanted, though they were designed with + the following equation in mind: finalScore = sum(WEIGHT[i] * SCORE[i]) + + TODO: I need to recalibrate some of these factors, and modify them to + represent more directly what the user would be concerned about -- + such as PayFeeFactor, AnonymityFactor, etc. The information is + indirectly available with the current set of factors here + """ + + # Need to calculate how much the change will be returned to sender on this tx + totalIn = sum([utxo.getValue() for utxo in utxoSelectList]) + totalChange = totalIn - (targetOutVal+minFee) + + # Abort if this is an empty list (negative score) or not enough coins + if len(utxoSelectList)==0 or totalIn 0) + # + # On the other hand, if we have 1.832 and 10.00, and the 10.000 is the + # change, we don't really care that they're not close, it's still + # damned good/deceptive output anonymity (so: only execute + # the following block if outAnonFactor <= 1) + if 0 < outAnonFactor <= 1 and not totalChange==0: + outValDiff = abs(totalChange - targetOutVal) + diffPct = (outValDiff / max(totalChange, targetOutVal)) + if diffPct < 0.20: + outAnonFactor *= 1 + elif diffPct < 0.50: + outAnonFactor *= 0.7 + elif diffPct < 1.0: + outAnonFactor *= 0.3 + else: + outAnonFactor = 0 + + + ################## + # Tx size: we don't have signatures yet, but we assume that each txin is + # about 180 Bytes, TxOuts are 35, and 10 other bytes in the Tx + numBytes = 10 + numBytes += 180 * len(utxoSelectList) + numBytes += 35 * (1 if totalChange==0 else 2) + txSizeFactor = 0 + numKb = int(numBytes / 1000) + # Will compute size factor after we see this tx priority and AllowFree + # results. If the tx qualifies for free, we don't need to penalize + # a 3 kB transaction vs one that is 0.5 kB + + + ################## + # Priority: If our priority is above the 1-btc-after-1-day threshold + # then we might be allowed a free tx. But, if its priority + # isn't much above this thresh, it might take a couple blocks + # to be included + dPriority = 0 + anyZeroConfirm = False + for utxo in utxoSelectList: + if utxo.getNumConfirm() == 0: + anyZeroConfirm = True + else: + dPriority += utxo.getValue() * utxo.getNumConfirm() + + dPriority = dPriority / numBytes + priorityThresh = ONE_BTC * 144 / 250 + if dPriority < priorityThresh: + priorityFactor = 0 + elif dPriority < 10.0*priorityThresh: + priorityFactor = 0.7 + elif dPriority < 100.0*priorityThresh: + priorityFactor = 0.9 + else: + priorityFactor = 1.0 + + + ################## + # AllowFree: If three conditions are met, then the tx can be sent safely + # without a tx fee. Granted, it may not be included in the + # current block if the free space is full, but definitely in + # the next one + isFreeAllowed = 0 + haveDustOutputs = (0= priorityThresh and \ + numBytes <= 10000): + isFreeAllowed = 1 + + + ################## + # Finish size-factor calculation -- if free is allowed, kB is irrelevant + txSizeFactor = 0 + if isFreeAllowed or numKb<1: + txSizeFactor = 1 + else: + if numKb < 2: + txSizeFactor=0.2 + elif numKb<3: + txSizeFactor=0.1 + elif numKb<4: + txSizeFactor=0 + else: + txSizeFactor=-1 #if this is huge, actually subtract score + + return (isFreeAllowed, noZeroConf, priorityFactor, numAddrFactor, txSizeFactor, outAnonFactor) + + +################################################################################ +# We define default preferences for weightings. Weightings are used to +# determine the "priorities" for ranking various SelectCoins results +# By setting the weights to different orders of magnitude, you are essentially +# defining a sort-order: order by FactorA, then sub-order by FactorB... +################################################################################ +# TODO: ADJUST WEIGHTING! +IDX_ALLOWFREE = 0 +IDX_NOZEROCONF = 1 +IDX_PRIORITY = 2 +IDX_NUMADDR = 3 +IDX_TXSIZE = 4 +IDX_OUTANONYM = 5 +WEIGHTS = [None]*6 +WEIGHTS[IDX_ALLOWFREE] = 100000 +WEIGHTS[IDX_NOZEROCONF] = 1000000 # let's avoid zero-conf if possible +WEIGHTS[IDX_PRIORITY] = 50 +WEIGHTS[IDX_NUMADDR] = 100000 +WEIGHTS[IDX_TXSIZE] = 100 +WEIGHTS[IDX_OUTANONYM] = 30 + + +################################################################################ +def PyEvalCoinSelect(utxoSelectList, targetOutVal, minFee, weights=WEIGHTS): + """ + Use a specified set of weightings and sub-scores for a unspentTxOut list, + to assign an absolute "fitness" of this particular selection. The goal of + getSelectCoinsScores() is to produce weighting-agnostic subscores -- then + this method applies the weightings to these scores to get a final answer. + + If list A has a higher score than list B, then it's a better selection for + that transaction. If you the two scores don't look right to you, then you + probably just need to adjust the weightings to your liking. + + These weightings may become user-configurable in the future -- likely as an + option of coin-selection profiles -- such as "max anonymity", "min fee", + "balanced", etc). + """ + scores = getSelectCoinsScores(utxoSelectList, targetOutVal, minFee) + if scores==-1: + return -1 + + # Combine all the scores + theScore = 0 + theScore += weights[IDX_NOZEROCONF] * scores[IDX_NOZEROCONF] + theScore += weights[IDX_PRIORITY] * scores[IDX_PRIORITY] + theScore += weights[IDX_NUMADDR] * scores[IDX_NUMADDR] + theScore += weights[IDX_TXSIZE] * scores[IDX_TXSIZE] + theScore += weights[IDX_OUTANONYM] * scores[IDX_OUTANONYM] + + # If we're already paying a fee, why bother including this weight? + if minFee < 0.0005: + theScore += weights[IDX_ALLOWFREE] * scores[IDX_ALLOWFREE] + + return theScore + + +################################################################################ +@TimeThisFunction +def PySelectCoins(unspentTxOutInfo, targetOutVal, minFee=0, numRand=10, margin=CENT): + """ + Intense algorithm for coin selection: computes about 30 different ways to + select coins based on the desired target output and the min tx fee. Then + ranks the various solutions and picks the best one + """ + + if sum([u.getValue() for u in unspentTxOutInfo]) < targetOutVal: + return [] + + targExact = targetOutVal + targMargin = targetOutVal+margin + + selectLists = [] + + # Start with the intelligent solutions with different sortings + for sortMethod in range(8): + diffSortList = PySortCoins(unspentTxOutInfo, sortMethod) + selectLists.append(PySelectCoins_SingleInput_SingleValue( diffSortList, targExact, minFee )) + selectLists.append(PySelectCoins_MultiInput_SingleValue( diffSortList, targExact, minFee )) + selectLists.append(PySelectCoins_SingleInput_SingleValue( diffSortList, targMargin, minFee )) + selectLists.append(PySelectCoins_MultiInput_SingleValue( diffSortList, targMargin, minFee )) + selectLists.append(PySelectCoins_SingleInput_DoubleValue( diffSortList, targExact, minFee )) + selectLists.append(PySelectCoins_MultiInput_DoubleValue( diffSortList, targExact, minFee )) + selectLists.append(PySelectCoins_SingleInput_DoubleValue( diffSortList, targMargin, minFee )) + selectLists.append(PySelectCoins_MultiInput_DoubleValue( diffSortList, targMargin, minFee )) + + # Throw in a couple random solutions, maybe we get lucky + # But first, make a copy before in-place shuffling + # NOTE: using list[:] like below, really causes a swig::vector to freak out! + #utxos = unspentTxOutInfo[:] + #utxos = list(unspentTxOutInfo) + for method in range(8,10): + for i in range(numRand): + utxos = PySortCoins(unspentTxOutInfo, method) + selectLists.append(PySelectCoins_MultiInput_SingleValue(utxos, targExact, minFee)) + selectLists.append(PySelectCoins_MultiInput_DoubleValue(utxos, targExact, minFee)) + selectLists.append(PySelectCoins_MultiInput_SingleValue(utxos, targMargin, minFee)) + selectLists.append(PySelectCoins_MultiInput_DoubleValue(utxos, targMargin, minFee)) + + # Now we define PyEvalCoinSelect as our sorting metric, and find the best solution + scoreFunc = lambda ulist: PyEvalCoinSelect(ulist, targetOutVal, minFee) + finalSelection = max(selectLists, key=scoreFunc) + SCORES = getSelectCoinsScores(finalSelection, targetOutVal, minFee) + if len(finalSelection)==0: + return [] + + # If we selected a list that has only one or two inputs, and we have + # other, tiny, unspent outputs from the same addresses, we should + # throw one or two of them in to help clear them out. However, we + # only do so if a plethora of conditions exist: + # + # First, we only consider doing this if the tx has <5 inputs already. + # Also, we skip this process if the current tx doesn't have excessive + # priority already -- we don't want to risk de-prioritizing a tx for + # this purpose. + # + # Next we sort by LOWEST value, because we really benefit from this most + # by clearing out tiny outputs. Along those lines, we don't even do + # unless it has low priority -- don't want to take a high-priority utxo + # and convert it to one that will be low-priority to start. + # + # Finally, we shouldn't do this if a high score was assigned to output + # anonymity: this extra output may cause a tx with good output anonymity + # to no longer possess this property + IDEAL_NUM_INPUTS = 5 + if len(finalSelection) < IDEAL_NUM_INPUTS and \ + SCORES[IDX_OUTANONYM] == 0: + + utxoToHash160 = lambda a: CheckHash160(a.getRecipientScrAddr()) + getPriority = lambda a: a.getValue() * a.getNumConfirm() + getUtxoID = lambda a: a.getTxHash() + int_to_binary(a.getTxOutIndex()) + + alreadyUsedAddr = set( [utxoToHash160(utxo) for utxo in finalSelection] ) + utxoSmallToLarge = sorted(unspentTxOutInfo, key=getPriority) + utxoSmToLgIDs = [getUtxoID(utxo) for utxo in utxoSmallToLarge] + finalSelectIDs = [getUtxoID(utxo) for utxo in finalSelection] + + for other in utxoSmallToLarge: + + # Skip it if it is already selected + if getUtxoID(other) in finalSelectIDs: + continue + + # We only consider UTXOs that won't link any new addresses together + if not utxoToHash160(other) in alreadyUsedAddr: + continue + + # Avoid zero-conf inputs altogether + if other.getNumConfirm() == 0: + continue + + # Don't consider any inputs that are high priority already + if getPriority(other) > ONE_BTC*144: + continue + + finalSelection.append(other) + if len(finalSelection)>=IDEAL_NUM_INPUTS: + break + return finalSelection + + +################################################################################ +def calcMinSuggestedFees(selectCoinsResult, targetOutVal, preSelectedFee, + numRecipients): + """ + Returns two fee options: one for relay, one for include-in-block. + In general, relay fees are required to get your block propagated + (since most nodes are Satoshi clients), but there's no guarantee + it will be included in a block -- though I'm sure there's plenty + of miners out there will include your tx for sub-standard fee. + However, it's virtually guaranteed that a miner will accept a fee + equal to the second return value from this method. + + We have to supply the fee that was used in the selection algorithm, + so that we can figure out how much change there will be. Without + this information, we might accidentally declare a tx to be freeAllow + when it actually is not. + """ + + if len(selectCoinsResult)==0: + return [-1,-1] + + paid = targetOutVal + preSelectedFee + change = sum([u.getValue() for u in selectCoinsResult]) - paid + + # Calc approx tx size + numBytes = 10 + numBytes += 180 * len(selectCoinsResult) + numBytes += 35 * (numRecipients + (1 if change>0 else 0)) + numKb = int(numBytes / 1000) + + if numKb>10: + return [(1+numKb)*MIN_RELAY_TX_FEE, (1+numKb)*MIN_TX_FEE] + + # Compute raw priority of tx + prioritySum = 0 + for utxo in selectCoinsResult: + prioritySum += utxo.getValue() * utxo.getNumConfirm() + prioritySum = prioritySum / numBytes + + # Any tiny/dust outputs? + haveDustOutputs = (0= ONE_BTC * 144 / 250. and \ + numBytes < 10000): + return [0,0] + + # This cannot be a free transaction. + minFeeMultiplier = (1 + numKb) + + # At the moment this condition never triggers + if minFeeMultiplier<1.0 and haveDustOutputs: + minFeeMultiplier = 1.0 + + + return [minFeeMultiplier * MIN_RELAY_TX_FEE, \ + minFeeMultiplier * MIN_TX_FEE] + + + + diff --git a/armoryengine/Decorators.py b/armoryengine/Decorators.py new file mode 100644 index 000000000..021bd84d8 --- /dev/null +++ b/armoryengine/Decorators.py @@ -0,0 +1,63 @@ +################################################################################ +# +# Copyright (C) 2011-2014, Armory Technologies, Inc. +# Distributed under the GNU Affero General Public License (AGPL v3) +# See LICENSE or http://www.gnu.org/licenses/agpl.html +# +################################################################################ +# +# Project: Armory +# Author: Alan Reiner +# Website: www.bitcoinarmory.com +# Orig Date: 20 November, 2011 +# +################################################################################ +from armoryengine.ArmoryUtils import LOGWARN, LOGERROR + + +import smtplib +import os +from email.MIMEMultipart import MIMEMultipart +from email.MIMEBase import MIMEBase +from email.MIMEText import MIMEText +from email.Utils import COMMASPACE, formatdate +from email import Encoders +import functools + +def send_email(send_from, password, send_to, subject, text): + if not type(send_to) == list: + raise AssertionError + msg = MIMEMultipart() + msg['From'] = send_from + msg['To'] = COMMASPACE.join(send_to) + msg['Date'] = formatdate(localtime=True) + msg['Subject'] = subject + msg.attach(MIMEText(text)) + mailServer = smtplib.SMTP('smtp.gmail.com', 587) + mailServer.ehlo() + mailServer.starttls() + mailServer.ehlo() + mailServer.login(send_from, password) + mailServer.sendmail(send_from, send_to, msg.as_string()) + mailServer.close() + +# Following this pattern to allow arguments to be passed to this decorator: +# http://stackoverflow.com/questions/10176226/how-to-pass-extra-arguments-to-python-decorator +def EmailOutput(send_from, password, send_to, subject='Armory Output'): + def ActualEmailOutputDecorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + ret = func(*args, **kwargs) + if ret and send_from and password and send_to: + send_email(send_from, password, send_to, subject, ret) + return ret + return wrapper + return ActualEmailOutputDecorator + + + + + + + + diff --git a/armoryengine/Networking.py b/armoryengine/Networking.py new file mode 100644 index 000000000..2ebe4baab --- /dev/null +++ b/armoryengine/Networking.py @@ -0,0 +1,1075 @@ +################################################################################ +# # +# Copyright (C) 2011-2014, Armory Technologies, Inc. # +# Distributed under the GNU Affero General Public License (AGPL v3) # +# See LICENSE or http://www.gnu.org/licenses/agpl.html # +# # +################################################################################ +################################################################################ +# +# Armory Networking: +# +# This is where I will define all the network operations needed for +# Armory to operate, using python-twisted. There are "better" +# ways to do this with "reusable" code structures (i.e. using huge +# deferred callback chains), but this is not the central "creative" +# part of the Bitcoin protocol. I need just enough to broadcast tx +# and receive new tx that aren't in the blockchain yet. Beyond that, +# I'll just be ignoring everything else. +# +################################################################################ + +import os.path +import random + +from twisted.internet.defer import Deferred +from twisted.internet.protocol import Protocol, ReconnectingClientFactory + +from armoryengine.ArmoryUtils import LOGINFO, RightNow, getVersionString, \ + BTCARMORY_VERSION, NetworkIDError, LOGERROR, BLOCKCHAINS, CLI_OPTIONS, LOGDEBUG, \ + binary_to_hex, BIGENDIAN, LOGRAWDATA, ARMORY_HOME_DIR, ConnectionError, \ + MAGIC_BYTES, hash256, verifyChecksum, NETWORKENDIAN, int_to_bitset, \ + bitset_to_int, unixTimeToFormatStr +from armoryengine.BDM import TheBDM +from armoryengine.BinaryPacker import BinaryPacker, BINARY_CHUNK, UINT32, UINT64, \ + UINT16, VAR_INT, INT32, INT64, VAR_STR +from armoryengine.BinaryUnpacker import BinaryUnpacker, UnpackerError +from armoryengine.Block import PyBlockHeader +from armoryengine.Transaction import PyTx, indent + + +class ArmoryClient(Protocol): + """ + This is where all the Bitcoin-specific networking stuff goes. + In the Twisted way, you need to inject your own chains of + callbacks through the factory in order to get this class to do + the right thing on the various events. + """ + + ############################################################ + def __init__(self): + self.recvData = '' + self.gotVerack = False + self.sentVerack = False + self.sentHeadersReq = True + self.peer = [] + + ############################################################ + def connectionMade(self): + """ + Construct the initial version message and send it right away. + Everything else will be handled by dataReceived. + """ + LOGINFO('Connection initiated. Start handshake') + addrTo = str_to_quad(self.transport.getPeer().host) + portTo = self.transport.getPeer().port + addrFrom = str_to_quad(self.transport.getHost().host) + portFrom = self.transport.getHost().port + + self.peer = [addrTo, portTo] + + services = '0'*16 + msgVersion = PayloadVersion() + msgVersion.version = 40000 # TODO: this is what my Satoshi client says + msgVersion.services = services + msgVersion.time = long(RightNow()) + msgVersion.addrRecv = PyNetAddress(0, services, addrTo, portTo ) + msgVersion.addrFrom = PyNetAddress(0, services, addrFrom, portFrom) + msgVersion.nonce = random.randint(2**60, 2**64-1) + msgVersion.subver = 'Armory:%s' % getVersionString(BTCARMORY_VERSION) + msgVersion.height0 = -1 + self.sendMessage( msgVersion ) + self.factory.func_madeConnect() + + + ############################################################ + def dataReceived(self, data): + """ + Called by the reactor when data is received over the connection. + This method will do nothing if we don't receive a full message. + """ + + + #print '\n\nData Received:', + #pprintHex(binary_to_hex(data), withAddr=False) + + # Put the current buffer into an unpacker, process until empty + self.recvData += data + buf = BinaryUnpacker(self.recvData) + + messages = [] + while True: + try: + # recvData is only modified if the unserialize succeeds + # Had a serious issue with references, so I had to convert + # messages to strings to guarantee that copies were being + # made! (yes, hacky...) + thisMsg = PyMessage().unserialize(buf) + messages.append( thisMsg.serialize() ) + self.recvData = buf.getRemainingString() + except NetworkIDError: + LOGERROR('Message for a different network!' ) + if BLOCKCHAINS.has_key(self.recvData[:4]): + LOGERROR( '(for network: %s)', BLOCKCHAINS[self.recvData[:4]]) + # Before raising the error, we should've finished reading the msg + # So pop it off the front of the buffer + self.recvData = buf.getRemainingString() + return + except UnpackerError: + # Expect this error when buffer isn't full enough for a whole msg + break + + # We might've gotten here without anything to process -- if so, bail + if len(messages)==0: + return + + + # Finally, we have some message to process, let's do it + for msgStr in messages: + msg = PyMessage().unserialize(msgStr) + cmd = msg.cmd + + # Log the message if netlog option + if CLI_OPTIONS.netlog: + LOGDEBUG( 'DataReceived: %s', msg.payload.command) + if msg.payload.command == 'tx': + LOGDEBUG('\t' + binary_to_hex(msg.payload.tx.thisHash)) + elif msg.payload.command == 'block': + LOGDEBUG('\t' + msg.payload.header.getHashHex()) + elif msg.payload.command == 'inv': + for inv in msg.payload.invList: + LOGDEBUG(('\tBLOCK: ' if inv[0]==2 else '\tTX : ') + \ + binary_to_hex(inv[1])) + + + # We process version and verackk only if we haven't yet + if cmd=='version' and not self.sentVerack: + self.peerInfo = {} + self.peerInfo['version'] = msg.payload.version + self.peerInfo['subver'] = msg.payload.subver + self.peerInfo['time'] = msg.payload.time + self.peerInfo['height'] = msg.payload.height0 + LOGINFO('Received version message from peer:') + LOGINFO(' Version: %s', str(self.peerInfo['version'])) + LOGINFO(' SubVersion: %s', str(self.peerInfo['subver'])) + LOGINFO(' TimeStamp: %s', str(self.peerInfo['time'])) + LOGINFO(' StartHeight: %s', str(self.peerInfo['height'])) + self.sentVerack = True + self.sendMessage( PayloadVerack() ) + elif cmd=='verack': + self.gotVerack = True + self.factory.handshakeFinished(self) + #self.startHeaderDL() + + #################################################################### + # Don't process any other messages unless the handshake is finished + if self.gotVerack and self.sentVerack: + self.processMessage(msg) + + + ############################################################ + #def connectionLost(self, reason): + #""" + #Try to reopen connection (not impl yet) + #""" + #self.factory.connectionFailed(self, reason) + + + ############################################################ + def processMessage(self, msg): + # TODO: when I start expanding this class to be more versatile, + # I'll consider chaining/setting callbacks from the calling + # application. For now, it's pretty static. + #msg.payload.pprint(nIndent=2) + if msg.cmd=='inv': + invobj = msg.payload + getdataMsg = PyMessage('getdata') + for inv in invobj.invList: + if inv[0]==MSG_INV_BLOCK: + if self.factory.bdm and (self.factory.bdm.getBDMState()=='Scanning' or \ + self.factory.bdm.hasHeaderWithHash(inv[1])): + continue + getdataMsg.payload.invList.append(inv) + if inv[0]==MSG_INV_TX: + if self.factory.bdm and (self.factory.bdm.getBDMState()=='Scanning' or \ + self.factory.bdm.hasTxWithHash(inv[1])): + continue + getdataMsg.payload.invList.append(inv) + + # Now send the full request + if self.factory.bdm and not self.factory.bdm.getBDMState()=='Scanning': + self.sendMessage(getdataMsg) + + if msg.cmd=='tx': + pytx = msg.payload.tx + self.factory.func_newTx(pytx) + elif msg.cmd=='inv': + invList = msg.payload.invList + self.factory.func_inv(invList) + elif msg.cmd=='block': + pyHeader = msg.payload.header + pyTxList = msg.payload.txList + LOGINFO('Received new block. %s', binary_to_hex(pyHeader.getHash(), BIGENDIAN)) + self.factory.func_newBlock(pyHeader, pyTxList) + + + + ############################################################ + def startHeaderDL(self): + numList = self.createBlockLocatorNumList(self.topBlk) + msg = PyMessage('getheaders') + msg.payload.version = 1 + if self.factory.bdm: + msg.payload.hashList = [self.factory.bdm.getHeaderByHeight(i).getHash() for i in numList] + else: + msg.payload.hashList = [] + msg.payload.hashStop = '\x00'*32 + + self.sentHeadersReq = True + + + + ############################################################ + def startBlockDL(self): + numList = self.createBlockLocatorNumList(self.topBlk) + msg = PyMessage('getblocks') + msg.payload.version = 1 + if self.factory.bdm: + msg.payload.hashList = [self.factory.bdm.getHeaderByHeight(i).getHash() for i in numList] + else: + msg.payload.hashList = [] + msg.payload.hashStop = '\x00'*32 + + + ############################################################ + def sendMessage(self, msg): + """ + Must pass in a PyMessage, or one of the Payload types, which + will be converted to a PyMessage -- and then sent to the peer. + If you have a fully-serialized message (with header) already, + easy enough to user PyMessage().unserialize(binMsg) + """ + + if isinstance(msg, PyMessage): + #print '\n\nSending Message:', msg.payload.command.upper() + #pprintHex(binary_to_hex(msg.serialize()), indent=' ') + if CLI_OPTIONS.netlog: + LOGDEBUG( 'SendMessage: %s', msg.payload.command) + LOGRAWDATA( msg.serialize() ) + self.transport.write(msg.serialize()) + else: + msg = PyMessage(payload=msg) + #print '\n\nSending Message:', msg.payload.command.upper() + #pprintHex(binary_to_hex(msg.serialize()), indent=' ') + if CLI_OPTIONS.netlog: + LOGDEBUG( 'SendMessage: %s', msg.payload.command) + LOGRAWDATA( msg.serialize() ) + self.transport.write(msg.serialize()) + + + ############################################################ + def sendTx(self, txObj): + """ + This is a convenience method for the special case of sending + a locally-constructed transaction. Pass in either a PyTx + object, or a binary serialized tx. It will be converted to + a PyMessage and forwarded to our peer(s) + """ + LOGINFO('sendTx called...') + if isinstance(txObj, PyMessage): + self.sendMessage( txObj ) + elif isinstance(txObj, PyTx): + self.sendMessage( PayloadTx(txObj)) + elif isinstance(txObj, str): + self.sendMessage( PayloadTx(PyTx().unserialize(txObj)) ) + + + + + + + + +################################################################################ +################################################################################ +class ArmoryClientFactory(ReconnectingClientFactory): + """ + Spawns Protocol objects used for communicating over the socket. All such + objects (ArmoryClients) can share information through this factory. + However, at the moment, this class is designed to only create a single + connection -- to localhost. + """ + protocol = ArmoryClient + lastAlert = 0 + + ############################################################################# + def __init__(self, \ + bdm, + def_handshake=None, \ + func_loseConnect=(lambda: None), \ + func_madeConnect=(lambda: None), \ + func_newTx=(lambda x: None), \ + func_newBlock=(lambda x,y: None), \ + func_inv=(lambda x: None)): + """ + Initialize the ReconnectingClientFactory with a deferred for when the handshake + finishes: there should be only one handshake, and thus one firing + of the handshake-finished callback + """ + self.bdm = bdm + self.lastAlert = 0 + self.deferred_handshake = forceDeferred(def_handshake) + self.fileMemPool = os.path.join(ARMORY_HOME_DIR, 'mempool.bin') + + # All other methods will be regular callbacks: we plan to have a very + # static set of behaviors for each message type + # (NOTE: The logic for what I need right now is so simple, that + # I finished implementing it in a few lines of code. When I + # need to expand the versatility of this class, I'll start + # doing more OOP/deferreds/etc + self.func_loseConnect = func_loseConnect + self.func_madeConnect = func_madeConnect + self.func_newTx = func_newTx + self.func_newBlock = func_newBlock + self.func_inv = func_inv + self.proto = None + + + + ############################################################################# + def addTxToMemoryPool(self, pytx): + if self.bdm and not self.bdm.getBDMState()=='Offline': + self.bdm.addNewZeroConfTx(pytx.serialize(), long(RightNow()), True) + + + + ############################################################################# + def handshakeFinished(self, protoObj): + LOGINFO('Handshake finished, connection open!') + self.proto = protoObj + if self.deferred_handshake: + d, self.deferred_handshake = self.deferred_handshake, None + d.callback(protoObj) + + + ############################################################################# + def clientConnectionLost(self, connector, reason): + LOGERROR('***Connection to Satoshi client LOST! Attempting to reconnect...') + self.func_loseConnect() + ReconnectingClientFactory.clientConnectionLost(self,connector,reason) + + + ############################################################################# + def connectionFailed(self, protoObj, reason): + LOGERROR('***Initial connection to Satoshi client failed! Retrying...') + ReconnectingClientFactory.connectionFailed(self, protoObj, reason) + + + ############################################################################# + def sendTx(self, pytxObj): + if self.proto: + self.proto.sendTx(pytxObj) + else: + raise ConnectionError, 'Connection to localhost DNE.' + + + ############################################################################# + def sendMessage(self, msgObj): + if self.proto: + self.proto.sendMessage(msgObj) + else: + raise ConnectionError, 'Connection to localhost DNE.' + + + +############################################################################### +############################################################################### +# +# Networking Objects +# +############################################################################### +############################################################################### + +def quad_to_str( addrQuad): + return '.'.join([str(a) for a in addrQuad]) + +def quad_to_binary( addrQuad): + return ''.join([chr(a) for a in addrQuad]) + +def binary_to_quad(addrBin): + return [ord(a) for a in addrBin] + +def str_to_quad(addrBin): + return [int(a) for a in addrBin.split('.')] + +def str_to_binary(addrBin): + """ I should come up with a better name for this -- it's net-addr only """ + return ''.join([chr(int(a)) for a in addrBin.split('.')]) + +def parseNetAddress(addrObj): + if isinstance(addrObj, str): + if len(addrObj)==4: + return binary_to_quad(addrObj) + else: + return str_to_quad(addrObj) + # Probably already in the right form + return addrObj + + + +MSG_INV_ERROR = 0 +MSG_INV_TX = 1 +MSG_INV_BLOCK = 2 + + +################################################################################ +class PyMessage(object): + """ + All payload objects have a serialize and unserialize method, making them + easy to attach to PyMessage objects + """ + def __init__(self, cmd='', payload=None): + """ + Can create a message by the command name, or the payload (or neither) + """ + self.magic = MAGIC_BYTES + self.cmd = cmd + self.payload = payload + + if payload: + self.cmd = payload.command + elif cmd: + self.payload = PayloadMap[self.cmd]() + + + + def serialize(self): + bp = BinaryPacker() + bp.put(BINARY_CHUNK, self.magic, width= 4) + bp.put(BINARY_CHUNK, self.cmd.ljust(12, '\x00'), width=12) + payloadBin = self.payload.serialize() + bp.put(UINT32, len(payloadBin)) + bp.put(BINARY_CHUNK, hash256(payloadBin)[:4], width= 4) + bp.put(BINARY_CHUNK, payloadBin) + return bp.getBinaryString() + + def unserialize(self, toUnpack): + if isinstance(toUnpack, BinaryUnpacker): + msgData = toUnpack + else: + msgData = BinaryUnpacker( toUnpack ) + + + self.magic = msgData.get(BINARY_CHUNK, 4) + self.cmd = msgData.get(BINARY_CHUNK, 12).strip('\x00') + length = msgData.get(UINT32) + chksum = msgData.get(BINARY_CHUNK, 4) + payload = msgData.get(BINARY_CHUNK, length) + payload = verifyChecksum(payload, chksum) + + self.payload = PayloadMap[self.cmd]().unserialize(payload) + + if self.magic != MAGIC_BYTES: + raise NetworkIDError, 'Message has wrong network bytes!' + return self + + + def pprint(self, nIndent=0): + indstr = indent*nIndent + print '' + print indstr + 'Bitcoin-Network-Message -- ' + self.cmd.upper() + print indstr + indent + 'Magic: ' + binary_to_hex(self.magic) + print indstr + indent + 'Command: ' + self.cmd + print indstr + indent + 'Payload: ' + str(len(self.payload.serialize())) + ' bytes' + self.payload.pprint(nIndent+1) + + +################################################################################ +class PyNetAddress(object): + + def __init__(self, time=-1, svcs='0'*16, netaddrObj=[], port=-1): + """ + For our client we will ALWAYS use svcs=0 (NODE_NETWORK=0) + + time is stored as a unix timestamp + services is stored as a bitset -- a string of 16 '0's or '1's + addrObj is stored as a list/tuple of four UINT8s + port is a regular old port number... + """ + self.time = time + self.services = svcs + self.addrQuad = parseNetAddress(netaddrObj) + self.port = port + + def unserialize(self, toUnpack, hasTimeField=True): + if isinstance(toUnpack, BinaryUnpacker): + addrData = toUnpack + else: + addrData = BinaryUnpacker( toUnpack ) + + if hasTimeField: + self.time = addrData.get(UINT32) + + self.services = addrData.get(UINT64) + self.addrQuad = addrData.get(BINARY_CHUNK,16)[-4:] + self.port = addrData.get(UINT16, endianness=NETWORKENDIAN) + + self.services = int_to_bitset(self.services) + self.addrQuad = binary_to_quad(self.addrQuad) + return self + + def serialize(self, withTimeField=True): + bp = BinaryPacker() + if withTimeField: + bp.put(UINT32, self.time) + bp.put(UINT64, bitset_to_int(self.services)) + bp.put(BINARY_CHUNK, quad_to_binary(self.addrQuad).rjust(16,'\x00')) + bp.put(UINT16, self.port, endianness=NETWORKENDIAN) + return bp.getBinaryString() + + def pprint(self, nIndent=0): + indstr = indent*nIndent + print '' + print indstr + 'Network-Address:', + print indstr + indent + 'Time: ' + unixTimeToFormatStr(self.time) + print indstr + indent + 'Svcs: ' + self.services + print indstr + indent + 'IPv4: ' + quad_to_str(self.addrQuad) + print indstr + indent + 'Port: ' + self.port + + def pprintShort(self): + print quad_to_str(self.addrQuad) + ':' + str(self.port) + +################################################################################ +################################################################################ +class PayloadAddr(object): + + command = 'addr' + + def __init__(self, addrList=[]): + self.addrList = addrList # PyNetAddress objs + + def unserialize(self, toUnpack): + if isinstance(toUnpack, BinaryUnpacker): + addrData = toUnpack + else: + addrData = BinaryUnpacker( toUnpack ) + + self.addrList = [] + naddr = addrData.get(VAR_INT) + for i in range(naddr): + self.addrList.append( PyNetAddress().unserialize(addrData) ) + return self + + def serialize(self): + bp = BinaryPacker() + bp.put(VAR_INT, len(self.addrList)) + for netaddr in self.addrList: + bp.put(BINARY_CHUNK, netaddr.serialize(), width=30) + return bp.getBinaryString() + + def pprint(self, nIndent=0): + indstr = indent*nIndent + print '' + print indstr + 'Message(addr):', + for a in self.addrList: + a.pprintShort() + + def pprintShort(self): + for a in self.addrList: + print '[' + quad_to_str(a.pprintShort()) + '], ' + +################################################################################ +################################################################################ +class PayloadPing(object): + """ + All payload objects have a serialize and unserialize method, making them + easy to attach to PyMessage objects + """ + command = 'ping' + + def __init__(self): + pass + + def unserialize(self, toUnpack): + return self + + def serialize(self): + return '' + + + def pprint(self, nIndent=0): + indstr = indent*nIndent + print '' + print indstr + 'Message(ping)' + + +################################################################################ +################################################################################ +class PayloadVersion(object): + + command = 'version' + + def __init__(self, version=0, svcs='0'*16, tstamp=-1, addrRcv=PyNetAddress(), \ + addrFrm=PyNetAddress(), nonce=-1, sub=-1, height=-1): + self.version = version + self.services = svcs + self.time = tstamp + self.addrRecv = addrRcv + self.addrFrom = addrFrm + self.nonce = nonce + self.subver = sub + self.height0 = height + + def unserialize(self, toUnpack): + if isinstance(toUnpack, BinaryUnpacker): + verData = toUnpack + else: + verData = BinaryUnpacker( toUnpack ) + + self.version = verData.get(INT32) + self.services = int_to_bitset(verData.get(UINT64), widthBytes=8) + self.time = verData.get(INT64) + self.addrRecv = PyNetAddress().unserialize(verData, hasTimeField=False) + self.addrFrom = PyNetAddress().unserialize(verData, hasTimeField=False) + self.nonce = verData.get(UINT64) + self.subver = verData.get(VAR_STR) + self.height0 = verData.get(INT32) + return self + + def serialize(self): + bp = BinaryPacker() + bp.put(INT32, self.version ) + bp.put(UINT64, bitset_to_int(self.services)) + bp.put(INT64, self.time ) # todo, should this really be int64? + bp.put(BINARY_CHUNK, self.addrRecv.serialize(withTimeField=False)) + bp.put(BINARY_CHUNK, self.addrFrom.serialize(withTimeField=False)) + bp.put(UINT64, self.nonce ) + bp.put(VAR_STR, self.subver ) + bp.put(INT32, self.height0 ) + return bp.getBinaryString() + + def pprint(self, nIndent=0): + indstr = indent*nIndent + print '' + print indstr + 'Message(version):' + print indstr + indent + 'Version: ' + str(self.version) + print indstr + indent + 'Services: ' + self.services + print indstr + indent + 'Time: ' + unixTimeToFormatStr(self.time) + print indstr + indent + 'AddrTo: ',; self.addrRecv.pprintShort() + print indstr + indent + 'AddrFrom:',; self.addrFrom.pprintShort() + print indstr + indent + 'Nonce: ' + str(self.nonce) + print indstr + indent + 'SubVer: ', self.subver + print indstr + indent + 'StartHgt: ' + str(self.height0) + +################################################################################ +class PayloadVerack(object): + """ + All payload objects have a serialize and unserialize method, making them + easy to attach to PyMessage objects + """ + + command = 'verack' + + def __init__(self): + pass + + def unserialize(self, toUnpack): + return self + + def serialize(self): + return '' + + def pprint(self, nIndent=0): + indstr = indent*nIndent + print '' + print indstr + 'Message(verack)' + + + +################################################################################ +################################################################################ +class PayloadInv(object): + """ + All payload objects have a serialize and unserialize method, making them + easy to attach to PyMessage objects + """ + + command = 'inv' + + def __init__(self): + self.invList = [] # list of (type, hash) pairs + + def unserialize(self, toUnpack): + if isinstance(toUnpack, BinaryUnpacker): + invData = toUnpack + else: + invData = BinaryUnpacker( toUnpack ) + + numInv = invData.get(VAR_INT) + for i in range(numInv): + invType = invData.get(UINT32) + invHash = invData.get(BINARY_CHUNK, 32) + self.invList.append( [invType, invHash] ) + return self + + def serialize(self): + bp = BinaryPacker() + bp.put(VAR_INT, len(self.invList)) + for inv in self.invList: + bp.put(UINT32, inv[0]) + bp.put(BINARY_CHUNK, inv[1], width=32) + return bp.getBinaryString() + + + def pprint(self, nIndent=0): + indstr = indent*nIndent + print '' + print indstr + 'Message(inv):' + for inv in self.invList: + print indstr + indent + ('BLOCK: ' if inv[0]==2 else 'TX : ') + \ + binary_to_hex(inv[1]) + + + +################################################################################ +################################################################################ +class PayloadGetData(object): + """ + All payload objects have a serialize and unserialize method, making them + easy to attach to PyMessage objects + """ + + command = 'getdata' + + def __init__(self, invList=[]): + if invList: + self.invList = invList + else: + self.invList = [] + + + def unserialize(self, toUnpack): + if isinstance(toUnpack, BinaryUnpacker): + invData = toUnpack + else: + invData = BinaryUnpacker( toUnpack ) + + numInv = invData.get(VAR_INT) + for i in range(numInv): + invType = invData.get(UINT32) + invHash = invData.get(BINARY_CHUNK, 32) + self.invList.append( [invType, invHash] ) + return self + + def serialize(self): + bp = BinaryPacker() + bp.put(VAR_INT, len(self.invList)) + for inv in self.invList: + bp.put(UINT32, inv[0]) + bp.put(BINARY_CHUNK, inv[1], width=32) + return bp.getBinaryString() + + + def pprint(self, nIndent=0): + indstr = indent*nIndent + print '' + print indstr + 'Message(getdata):' + for inv in self.invList: + print indstr + indent + ('BLOCK: ' if inv[0]==2 else 'TX : ') + \ + binary_to_hex(inv[1]) + + +################################################################################ +################################################################################ +class PayloadGetHeaders(object): + command = 'getheaders' + + def __init__(self, hashStartList=[], hashStop=''): + self.version = 1 + self.hashList = hashStartList + self.hashStop = hashStop + + + def unserialize(self, toUnpack): + if isinstance(toUnpack, BinaryUnpacker): + ghData = toUnpack + else: + ghData = BinaryUnpacker( toUnpack ) + + self.version = ghData.get(UINT32) + nhash = ghData.get(VAR_INT) + for i in range(nhash): + self.hashList.append(ghData.get(BINARY_CHUNK, 32)) + self.hashStop = ghData.get(BINARY_CHUNK, 32) + return self + + def serialize(self): + nhash = len(self.hashList) + bp = BinaryPacker() + bp.put(UINT32, self.version) + bp.put(VAR_INT, nhash) + for i in range(nhash): + bp.put(BINARY_CHUNK, self.hashList[i], width=32) + bp.put(BINARY_CHUNK, self.hashStop, width=32) + return bp.getBinaryString() + + def pprint(self, nIndent=0): + indstr = indent*nIndent + print '' + print indstr + 'Message(getheaders):' + print indstr + indent + 'HashList(s) :' + binary_to_hex(self.hashList[0]) + for i in range(1,len(self.hashList)): + print indstr + indent + ' :' + binary_to_hex(self.hashList[i]) + print indstr + indent + 'HashStop :' + binary_to_hex(self.hashStop) + + + +################################################################################ +################################################################################ +class PayloadGetBlocks(object): + command = 'getblocks' + + def __init__(self, version=1, startCt=-1, hashStartList=[], hashStop=''): + self.version = 1 + self.hashList = hashStartList + self.hashStop = hashStop + + + def unserialize(self, toUnpack): + if isinstance(toUnpack, BinaryUnpacker): + gbData = toUnpack + else: + gbData = BinaryUnpacker( toUnpack ) + + self.version = gbData.get(UINT32) + nhash = gbData.get(VAR_INT) + for i in range(nhash): + self.hashList.append(gbData.get(BINARY_CHUNK, 32)) + self.hashStop = gbData.get(BINARY_CHUNK, 32) + return self + + def serialize(self): + nhash = len(self.hashList) + bp = BinaryPacker() + bp.put(UINT32, self.version) + bp.put(VAR_INT, nhash) + for i in range(nhash): + bp.put(BINARY_CHUNK, self.hashList[i], width=32) + bp.put(BINARY_CHUNK, self.hashList, width=32) + return bp.getBinaryString() + + def pprint(self, nIndent=0): + indstr = indent*nIndent + print '' + print indstr + 'Message(getheaders):' + print indstr + indent + 'Version :' + str(self.version) + print indstr + indent + 'HashList(s) :' + binary_to_hex(self.hashList[0]) + for i in range(1,len(self.hashList)): + print indstr + indent + ' :' + binary_to_hex(self.hashList[i]) + print indstr + indent + 'HashStop :' + binary_to_hex(self.hashStop) + + +################################################################################ +################################################################################ +class PayloadTx(object): + command = 'tx' + + def __init__(self, tx=PyTx()): + self.tx = tx + + def unserialize(self, toUnpack): + self.tx.unserialize(toUnpack) + return self + + def serialize(self): + return self.tx.serialize() + + def pprint(self, nIndent=0): + indstr = indent*nIndent + print '' + print indstr + 'Message(tx):' + self.tx.pprint(nIndent+1) + + +################################################################################ +################################################################################ +class PayloadHeaders(object): + command = 'headers' + + def __init__(self, header=PyBlockHeader(), headerlist=[]): + self.header = header + self.headerList = headerlist + + + def unserialize(self, toUnpack): + if isinstance(toUnpack, BinaryUnpacker): + headerData = toUnpack + else: + headerData = BinaryUnpacker( toUnpack ) + + self.headerList = [] + self.header.unserialize(headerData) + numHeader = headerData.get(VAR_INT) + for i in range(numHeader): + self.headerList.append(PyBlockHeader().unserialize(headerData)) + headerData.get(VAR_INT) # Not sure if this is even used, ever + return self + + def serialize(self): + bp = BinaryPacker() + bp.put(BINARY_CHUNK, self.header.serialize()) + bp.put(VAR_INT, len(self.headerList)) + for header in self.headerList: + bp.put(BINARY_CHUNK, header.serialize()) + bp.put(VAR_INT, 0) + return bp.getBinaryString() + + def pprint(self, nIndent=0): + indstr = indent*nIndent + print '' + print indstr + 'Message(headers):' + self.header.pprint(nIndent+1) + for header in self.headerList: + print indstr + indent + 'Header:', header.getHash() + + +################################################################################ +################################################################################ +class PayloadBlock(object): + command = 'block' + + def __init__(self, header=PyBlockHeader(), txlist=[]): + self.header = header + self.txList = txlist + + + def unserialize(self, toUnpack): + if isinstance(toUnpack, BinaryUnpacker): + blkData = toUnpack + else: + blkData = BinaryUnpacker( toUnpack ) + + self.txList = [] + self.header.unserialize(blkData) + numTx = blkData.get(VAR_INT) + for i in range(numTx): + self.txList.append(PyTx().unserialize(blkData)) + return self + + def serialize(self): + bp = BinaryPacker() + bp.put(BINARY_CHUNK, self.header.serialize()) + bp.put(VAR_INT, len(self.txList)) + for tx in self.txList: + bp.put(BINARY_CHUNK, tx.serialize()) + return bp.getBinaryString() + + def pprint(self, nIndent=0): + indstr = indent*nIndent + print '' + print indstr + 'Message(block):' + self.header.pprint(nIndent+1) + for tx in self.txList: + print indstr + indent + 'Tx:', tx.getHashHex() + + +################################################################################ +class PayloadAlert(object): + command = 'alert' + + def __init__(self): + self.version = 1 + self.relayUntil = 0 + self.expiration = 0 + self.uniqueID = 0 + self.cancelVal = 0 + self.cancelSet = [] + self.minVersion = 0 + self.maxVersion = 0 + self.subVerSet = [] + self.comment = '' + self.statusBar = '' + self.reserved = '' + self.signature = '' + + + def unserialize(self, toUnpack): + if isinstance(toUnpack, BinaryUnpacker): + blkData = toUnpack + else: + blkData = BinaryUnpacker( toUnpack ) + + return self + + def serialize(self): + bp = BinaryPacker() + return bp.getBinaryString() + + + def pprint(self, nIndent=0): + print nIndent*'\t' + 'ALERT(...)' + +################################################################################ +# Use this map to figure out which object to serialize/unserialize from a cmd +PayloadMap = { + 'ping': PayloadPing, + 'tx': PayloadTx, + 'inv': PayloadInv, + 'version': PayloadVersion, + 'verack': PayloadVerack, + 'addr': PayloadAddr, + 'getdata': PayloadGetData, + 'getheaders': PayloadGetHeaders, + 'getblocks': PayloadGetBlocks, + 'block': PayloadBlock, + 'headers': PayloadHeaders, + 'alert': PayloadAlert } + + +class FakeClientFactory(ReconnectingClientFactory): + """ + A fake class that has the same methods as an ArmoryClientFactory, + but doesn't do anything. If there is no internet, then we want + to be able to use the same calls + """ + ############################################################################# + def __init__(self, \ + def_handshake=None, \ + func_loseConnect=(lambda: None), \ + func_madeConnect=(lambda: None), \ + func_newTx=(lambda x: None), \ + func_newBlock=(lambda x,y: None), \ + func_inv=(lambda x: None)): pass + def addTxToMemoryPool(self, pytx): pass + def handshakeFinished(self, protoObj): pass + def clientConnectionLost(self, connector, reason): pass + def connectionFailed(self, protoObj, reason): pass + def sendTx(self, pytxObj): pass + +################################################################################ +# It seems we need to do this frequently when downloading headers & blocks +# This only returns a list of numbers, but one list-comprehension to get hashes +def createBlockLocatorNumList(topblk): + blockNumList = [] + n,step,niter = topblk,1,0 + while n>0: + blockNumList.append(n) + if niter >= 10: + step *= 2 + n -= step + niter += 1 + blockNumList.append(0) + return blockNumList + + +################################################################################ +def forceDeferred(callbk): + if callbk: + if isinstance(callbk, Deferred): + return callbk + else: + d = Deferred() + d.addCallback(callbk) + return d + +# kate: indent-width 3; replace-tabs on; diff --git a/armoryengine/PyBtcAddress.py b/armoryengine/PyBtcAddress.py new file mode 100644 index 000000000..f8bec854f --- /dev/null +++ b/armoryengine/PyBtcAddress.py @@ -0,0 +1,1339 @@ +################################################################################ +# # +# Copyright (C) 2011-2014, Armory Technologies, Inc. # +# Distributed under the GNU Affero General Public License (AGPL v3) # +# See LICENSE or http://www.gnu.org/licenses/agpl.html # +# # +################################################################################ +from CppBlockUtils import SecureBinaryData, CryptoAES, CryptoECDSA +from armoryengine.ArmoryUtils import ADDRBYTE, hash256, binary_to_base58, \ + KeyDataError, RightNow, LOGERROR, ChecksumError, convertKeyDataToAddress, \ + verifyChecksum, WalletLockError, createSigScriptFromRS, binary_to_int, computeChecksum, \ + getVersionInt, PYBTCWALLET_VERSION, bitset_to_int, LOGDEBUG, Hash160ToScrAddr, \ + int_to_bitset, UnserializeError, hash160_to_addrStr, int_to_binary, BIGENDIAN, \ + BadAddressError, checkAddrStrValid, binary_to_hex +from armoryengine.BinaryPacker import BinaryPacker, UINT8, UINT16, UINT32, UINT64, \ + INT8, INT16, INT32, INT64, VAR_INT, VAR_STR, FLOAT, BINARY_CHUNK +from armoryengine.BinaryUnpacker import BinaryUnpacker +from armoryengine.Timer import TimeThisFunction +import CppBlockUtils as Cpp + + +############################################################################# +def calcWalletIDFromRoot(root, chain): + """ Helper method for computing a wallet ID """ + root = PyBtcAddress().createFromPlainKeyData(SecureBinaryData(root)) + root.chaincode = SecureBinaryData(chain) + first = root.extendAddressChain() + return binary_to_base58((ADDRBYTE + first.getAddr160()[:5])[::-1]) + +class PyBtcAddress(object): + """ + PyBtcAddress -- + + This class encapsulated EVERY kind of address object: + -- Plaintext private-key-bearing addresses + -- Encrypted private key addresses, with AES locking and unlocking + -- Watching-only public-key addresses + -- Address-only storage, representing someone else's key + -- Deterministic address generation from previous addresses + -- Serialization and unserialization of key data under all conditions + -- Checksums on all serialized fields to protect against HDD byte errors + + For deterministic wallets, new addresses will be created from a chaincode + and the previous address. What is implemented here is a special kind of + deterministic calculation that actually allows the user to securely + generate new addresses even if they don't have the private key. This + method uses Diffie-Hellman shared-secret calculations to produce the new + keys, and has the same level of security as all other ECDSA operations. + There's a lot of fantastic benefits to doing this: + + (1) If all addresses in wallet are chained, then you only need to backup + your wallet ONCE -- when you first create it. Print it out, put it + in a safety-deposit box, or tattoo the generator key to the inside + of your eyelid: it will never change. + + (2) You can keep your private keys on an offline machine, and keep a + watching-only wallet online. You will be able to generate new + keys/addresses, and verify incoming transactions, without ever + requiring your private key to touch the internet. + + (3) If your friend has the chaincode and your first public key, they + too can generate new addresses for you -- allowing them to send + you money multiple times, with different addresses, without ever + needing to specifically request the addresses. + (the downside to this is if the chaincode is compromised, all + chained addresses become de-anonymized -- but is only a loss of + privacy, not security) + + However, we do require some fairly complicated logic, due to the fact + that a user with a full, private-key-bearing wallet, may try to generate + a new key/address without supplying a passphrase. If this happens, the + wallet logic gets very complicated -- we don't want to reject the request + to generate a new address, but we can't compute the private key until the + next time the user unlocks their wallet. Thus, we have to save off the + data they will need to create the key, to be applied on next unlock. + """ + + ############################################################################# + def __init__(self): + """ + We use SecureBinaryData objects to store pub, priv and IV objects, + because that is what is required by the C++ code. See EncryptionUtils.h + to see that available methods. + """ + self.addrStr20 = '' + self.binPublicKey65 = SecureBinaryData() # 0x04 X(BE) Y(BE) + self.binPrivKey32_Encr = SecureBinaryData() # BIG-ENDIAN + self.binPrivKey32_Plain = SecureBinaryData() + self.binInitVect16 = SecureBinaryData() + self.isLocked = False + self.useEncryption = False + self.isInitialized = False + self.keyChanged = False # ...since last key encryption + self.walletByteLoc = -1 + self.chaincode = SecureBinaryData() + self.chainIndex = 0 + + # Information to be used by C++ to know where to search for transactions + # in the blockchain (disabled in favor of a better search method) + self.timeRange = [2**32-1, 0] + self.blkRange = [2**32-1, 0] + + # This feels like a hack, but it's the only way I can think to handle + # the case of generating new, chained addresses, even without the + # private key currently in memory. i.e. - If we can't unlock the priv + # key when creating a new chained priv key, we will simply extend the + # public key, and store the last-known chain info, so that it can be + # generated the next time the address is unlocked + self.createPrivKeyNextUnlock = False + self.createPrivKeyNextUnlock_IVandKey = [None, None] # (IV,Key) + self.createPrivKeyNextUnlock_ChainDepth = -1 + + ############################################################################# + def isInitialized(self): + """ Keep track of whether this address has been initialized """ + return self.isInitialized + + ############################################################################# + def hasPrivKey(self): + """ + We have a private key if either the plaintext, or ciphertext private-key + fields are non-empty. We also consider ourselves to "have" the private + key if this address was chained from a key that has the private key, even + if we haven't computed it yet (due to not having unlocked the private key + before creating the new address). + """ + return (self.binPrivKey32_Encr.getSize() != 0 or \ + self.binPrivKey32_Plain.getSize() != 0 or \ + self.createPrivKeyNextUnlock) + + ############################################################################# + def hasPubKey(self): + return (self.binPublicKey65.getSize() != 0) + + ############################################################################# + def getAddrStr(self, netbyte=ADDRBYTE): + chksum = hash256(netbyte + self.addrStr20)[:4] + return binary_to_base58(netbyte + self.addrStr20 + chksum) + + ############################################################################# + def getAddr160(self): + if len(self.addrStr20)!=20: + raise KeyDataError, 'PyBtcAddress does not have an address string!' + return self.addrStr20 + + + ############################################################################# + def isCompressed(self): + # Armory wallets (v1.35) do not support compressed keys + return False + + + ############################################################################# + def touch(self, unixTime=None, blkNum=None): + """ + Just like "touching" a file, this makes sure that the firstSeen and + lastSeen fields for this address are updated to include "now" + + If we include only a block number, we will fill in the timestamp with + the unix-time for that block (if the BlockDataManager is availabled) + """ + if self.blkRange[0]==0: + self.blkRange[0]=2**32-1 + if self.timeRange[0]==0: + self.timeRange[0]=2**32-1 + + if blkNum==None: + if TheBDM.getBDMState()=='BlockchainReady': + topBlk = TheBDM.getTopBlockHeight() + self.blkRange[0] = long(min(self.blkRange[0], topBlk)) + self.blkRange[1] = long(max(self.blkRange[1], topBlk)) + else: + self.blkRange[0] = long(min(self.blkRange[0], blkNum)) + self.blkRange[1] = long(max(self.blkRange[1], blkNum)) + + if unixTime==None and TheBDM.getBDMState()=='BlockchainReady': + unixTime = TheBDM.getHeaderByHeight(blkNum).getTimestamp() + + if unixTime==None: + unixTime = RightNow() + + self.timeRange[0] = long(min(self.timeRange[0], unixTime)) + self.timeRange[1] = long(max(self.timeRange[1], unixTime)) + + + + ############################################################################# + def copy(self): + newAddr = PyBtcAddress().unserialize(self.serialize()) + newAddr.binPrivKey32_Plain = self.binPrivKey32_Plain.copy() + newAddr.binPrivKey32_Encr = self.binPrivKey32_Encr.copy() + newAddr.binPublicKey65 = self.binPublicKey65.copy() + newAddr.binInitVect16 = self.binInitVect16.copy() + newAddr.isLocked = self.isLocked + newAddr.useEncryption = self.useEncryption + newAddr.isInitialized = self.isInitialized + newAddr.keyChanged = self.keyChanged + newAddr.walletByteLoc = self.walletByteLoc + newAddr.chaincode = self.chaincode + newAddr.chainIndex = self.chainIndex + return newAddr + + + + ############################################################################# + def getTimeRange(self): + return self.timeRange + + ############################################################################# + def getBlockRange(self): + return self.blkRange + + ############################################################################# + def serializePublicKey(self): + """Converts the SecureBinaryData public key to a 65-byte python string""" + return self.binPublicKey65.toBinStr() + + ############################################################################# + def serializeEncryptedPrivateKey(self): + """Converts SecureBinaryData encrypted private key to python string""" + return self.binPrivKey32_Encr.toBinStr() + + ############################################################################# + # NOTE: This method should rarely be used, unless we are only printing it + # to the screen. Actually, it will be used for unencrypted wallets + def serializePlainPrivateKey(self): + return self.binPrivKey32_Plain.toBinStr() + + def serializeInitVector(self): + return self.binInitVect16.toBinStr() + + + ############################################################################# + def verifyEncryptionKey(self, secureKdfOutput): + """ + Determine if this data is the decryption key for this encrypted address + """ + if not self.useEncryption or not self.hasPrivKey(): + return False + + if self.useEncryption and not secureKdfOutput: + LOGERROR('No encryption key supplied to verifyEncryption!') + return False + + + decryptedKey = CryptoAES().DecryptCFB( self.binPrivKey32_Encr, \ + SecureBinaryData(secureKdfOutput), \ + self.binInitVect16) + verified = False + + if not self.isLocked: + if decryptedKey==self.binPrivKey32_Plain: + verified = True + else: + computedPubKey = CryptoECDSA().ComputePublicKey(decryptedKey) + if self.hasPubKey(): + verified = (self.binPublicKey65==computedPubKey) + else: + verified = (computedPubKey.getHash160()==self.addrStr20) + if verified: + self.binPublicKey65 = computedPubKey + + decryptedKey.destroy() + return verified + + + + ############################################################################# + def setInitializationVector(self, IV16=None, random=False, force=False): + """ + Either set the IV through input arg, or explicitly call random=True + Returns the IV -- which is especially important if it is randomly gen + + This method is mainly for PREVENTING you from changing an existing IV + without meaning to. Losing the IV for encrypted data is almost as bad + as losing the encryption key. Caller must use force=True in order to + override this warning -- otherwise this method will abort. + """ + if self.binInitVect16.getSize()==16: + if self.isLocked: + LOGERROR('Address already locked with different IV.') + LOGERROR('Changing IV may cause loss of keydata.') + else: + LOGERROR('Address already contains an initialization') + LOGERROR('vector. If you change IV without updating') + LOGERROR('the encrypted storage, you may permanently') + LOGERROR('lose the encrypted data') + + if not force: + LOGERROR('If you really want to do this, re-execute this call with force=True') + return '' + + if IV16: + self.binInitVect16 = SecureBinaryData(IV16) + elif random==True: + self.binInitVect16 = SecureBinaryData().GenerateRandom(16) + else: + raise KeyDataError, 'setInitVector: set IV data, or random=True' + return self.binInitVect16 + + ############################################################################# + def enableKeyEncryption(self, IV16=None, generateIVIfNecessary=False): + """ + setIV method will raise error is we don't specify any args, but it is + acceptable HERE to not specify any args just to enable encryption + """ + self.useEncryption = True + if IV16: + self.setInitializationVector(IV16) + elif generateIVIfNecessary and self.binInitVect16.getSize()<16: + self.setInitializationVector(random=True) + + + ############################################################################# + def isKeyEncryptionEnabled(self): + return self.useEncryption + + ############################################################################# + def createFromEncryptedKeyData(self, addr20, encrPrivKey32, IV16, \ + chkSum=None, pubKey=None): + # We expect both private key and IV to the right size + assert(encrPrivKey32.getSize()==32) + assert(IV16.getSize()==16) + self.__init__() + self.addrStr20 = addr20 + self.binPrivKey32_Encr = SecureBinaryData(encrPrivKey32) + self.setInitializationVector(IV16) + self.isLocked = True + self.useEncryption = True + self.isInitialized = True + if chkSum and not self.binPrivKey32_Encr.getHash256().startswith(chkSum): + raise ChecksumError, "Checksum doesn't match encrypted priv key data!" + if pubKey: + self.binPublicKey65 = SecureBinaryData(pubKey) + if not self.binPublicKey65.getHash160()==self.addrStr20: + raise KeyDataError, "Public key does not match supplied address" + + return self + + + ############################################################################# + def createFromPlainKeyData(self, plainPrivKey, addr160=None, willBeEncr=False, \ + generateIVIfNecessary=False, IV16=None, \ + chksum=None, publicKey65=None, \ + skipCheck=False, skipPubCompute=False): + + assert(plainPrivKey.getSize()==32) + + if not addr160: + addr160 = convertKeyDataToAddress(privKey=plainPrivKey) + + self.__init__() + self.addrStr20 = addr160 + self.isInitialized = True + self.binPrivKey32_Plain = SecureBinaryData(plainPrivKey) + self.isLocked = False + + if willBeEncr: + self.enableKeyEncryption(IV16, generateIVIfNecessary) + elif IV16: + self.binInitVect16 = IV16 + + if chksum and not verifyChecksum(self.binPrivKey32_Plain.toBinStr(), chksum): + raise ChecksumError, "Checksum doesn't match plaintext priv key!" + if publicKey65: + self.binPublicKey65 = SecureBinaryData(publicKey65) + if not self.binPublicKey65.getHash160()==self.addrStr20: + raise KeyDataError, "Public key does not match supplied address" + if not skipCheck: + if not CryptoECDSA().CheckPubPrivKeyMatch(self.binPrivKey32_Plain,\ + self.binPublicKey65): + raise KeyDataError, 'Supplied pub and priv key do not match!' + elif not skipPubCompute: + # No public key supplied, but we do want to calculate it + self.binPublicKey65 = CryptoECDSA().ComputePublicKey(plainPrivKey) + + return self + + + ############################################################################# + def createFromPublicKeyData(self, publicKey65, chksum=None): + + assert(publicKey65.getSize()==65) + self.__init__() + self.addrStr20 = publicKey65.getHash160() + self.binPublicKey65 = publicKey65 + self.isInitialized = True + self.isLocked = False + self.useEncryption = False + + if chksum and not verifyChecksum(self.binPublicKey65.toBinStr(), chksum): + raise ChecksumError, "Checksum doesn't match supplied public key!" + + return self + + + + ############################################################################# + def safeExtendPrivateKey(self, privKey, chn, pubKey=None): + # We do this computation twice, in case one is somehow corrupted + # (Must be ultra paranoid with computing keys) + logMult1 = SecureBinaryData() + logMult2 = SecureBinaryData() + a160hex = '' + + # Can provide a pre-computed public key to skip that part of the compute + if pubKey is None: + pubKey = SecureBinaryData(0) + else: + a160hex = binary_to_hex(pubKey.getHash160()) + + newPriv1 = CryptoECDSA().ComputeChainedPrivateKey(privKey, chn, pubKey, logMult1) + newPriv2 = CryptoECDSA().ComputeChainedPrivateKey(privKey, chn, pubKey, logMult2) + + if newPriv1==newPriv2: + newPriv2.destroy() + with open(MULT_LOG_FILE,'a') as f: + f.write('PrvChain (pkh, mult): %s,%s\n' % (a160hex,logMult1.toHexStr())) + return newPriv1 + + else: + LOGCRIT('Chaining failed! Computed keys are different!') + LOGCRIT('Recomputing chained key 3 times; bail if they do not match') + newPriv1.destroy() + newPriv2.destroy() + logMult3 = SecureBinaryData() + newPriv1 = CryptoECDSA().ComputeChainedPrivateKey(privKey, chn, pubKey, logMult1) + newPriv2 = CryptoECDSA().ComputeChainedPrivateKey(privKey, chn, pubKey, logMult2) + newPriv3 = CryptoECDSA().ComputeChainedPrivateKey(privKey, chn, pubKey, logMult3) + LOGCRIT(' Multiplier1: ' + logMult1.toHexStr()) + LOGCRIT(' Multiplier2: ' + logMult2.toHexStr()) + LOGCRIT(' Multiplier3: ' + logMult3.toHexStr()) + + if newPriv1==newPriv2 and newPriv1==newPriv3: + newPriv2.destroy() + newPriv3.destroy() + with open(MULT_LOG_FILE,'a') as f: + f.write('PrvChain (pkh, mult): %s,%s\n' % (a160hex,logMult1.toHexStr())) + return newPriv1 + else: + LOGCRIT('Chaining failed again! Returning empty private key.') + newPriv1.destroy() + newPriv2.destroy() + newPriv3.destroy() + # This should crash just about any process that would try to use it + # without checking for empty private key. + return SecureBinaryData(0) + + + ############################################################################# + def safeExtendPublicKey(self, pubKey, chn): + # We do this computation twice, in case one is somehow corrupted + # (Must be ultra paranoid with computing keys) + a160hex = binary_to_hex(pubKey.getHash160()) + logMult1 = SecureBinaryData() + logMult2 = SecureBinaryData() + newPub1 = CryptoECDSA().ComputeChainedPublicKey(pubKey, chn, logMult1) + newPub2 = CryptoECDSA().ComputeChainedPublicKey(pubKey, chn, logMult2) + + if newPub1==newPub2: + newPub2.destroy() + with open(MULT_LOG_FILE,'a') as f: + f.write('PubChain (pkh, mult): %s,%s\n' % (a160hex, logMult1.toHexStr())) + return newPub1 + else: + LOGCRIT('Chaining failed! Computed keys are different!') + LOGCRIT('Recomputing chained key 3 times; bail if they do not match') + newPub1.destroy() + newPub2.destroy() + logMult3 = SecureBinaryData() + newPub1 = CryptoECDSA().ComputeChainedPublicKey(pubKey, chn, logMult1) + newPub2 = CryptoECDSA().ComputeChainedPublicKey(pubKey, chn, logMult2) + newPub3 = CryptoECDSA().ComputeChainedPublicKey(pubKey, chn, logMult3) + LOGCRIT(' Multiplier1: ' + logMult1.toHexStr()) + LOGCRIT(' Multiplier2: ' + logMult2.toHexStr()) + LOGCRIT(' Multiplier3: ' + logMult3.toHexStr()) + + if newPub1==newPub2 and newPub1==newPub3: + newPub2.destroy() + newPub3.destroy() + with open(MULT_LOG_FILE,'a') as f: + f.write('PubChain (pkh, mult): %s,%s\n' % (a160hex, logMult1.toHexStr())) + return newPub1 + else: + LOGCRIT('Chaining failed again! Returning empty public key.') + newPub1.destroy() + newPub2.destroy() + newPub3.destroy() + # This should crash just about any process that would try to use it + # without checking for empty public key. + return SecureBinaryData(0) + + ############################################################################# + def lock(self, secureKdfOutput=None, generateIVIfNecessary=False): + # We don't want to destroy the private key if it's not supposed to be + # encrypted. Similarly, if we haven't actually saved the encrypted + # version, let's not lock it + newIV = False + if not self.useEncryption or not self.hasPrivKey(): + # This isn't supposed to be encrypted, or there's no privkey to encrypt + return + else: + if self.binPrivKey32_Encr.getSize()==32 and not self.keyChanged: + # Addr should be encrypted, and we already have encrypted priv key + self.binPrivKey32_Plain.destroy() + self.isLocked = True + elif self.binPrivKey32_Plain.getSize()==32: + # Addr should be encrypted, but haven't computed encrypted value yet + if secureKdfOutput!=None: + # We have an encryption key, use it + if self.binInitVect16.getSize() < 16: + if not generateIVIfNecessary: + raise KeyDataError, 'No Initialization Vector available' + else: + self.binInitVect16 = SecureBinaryData().GenerateRandom(16) + newIV = True + + # Finally execute the encryption + self.binPrivKey32_Encr = CryptoAES().EncryptCFB( \ + self.binPrivKey32_Plain, \ + SecureBinaryData(secureKdfOutput), \ + self.binInitVect16) + # Destroy the unencrypted key, reset the keyChanged flag + self.binPrivKey32_Plain.destroy() + self.isLocked = True + self.keyChanged = False + else: + # Can't encrypt the addr because we don't have encryption key + raise WalletLockError, ("\n\tTrying to destroy plaintext key, but no" + "\n\tencrypted key data is available, and no" + "\n\tencryption key provided to encrypt it.") + + + # In case we changed the IV, we should let the caller know this + return self.binInitVect16 if newIV else SecureBinaryData() + + + ############################################################################# + def unlock(self, secureKdfOutput, skipCheck=False): + """ + This method knows nothing about a key-derivation function. It simply + takes in an AES key and applies it to decrypt the data. However, it's + best if that AES key is actually derived from "heavy" key-derivation + function. + """ + if not self.useEncryption or not self.isLocked: + # Bail out if the wallet is unencrypted, or already unlocked + self.isLocked = False + return + + + if self.createPrivKeyNextUnlock: + # This is SPECIFICALLY for the case that we didn't have the encr key + # available when we tried to extend our deterministic wallet, and + # generated a new address anyway + self.binPrivKey32_Plain = CryptoAES().DecryptCFB( \ + self.createPrivKeyNextUnlock_IVandKey[1], \ + SecureBinaryData(secureKdfOutput), \ + self.createPrivKeyNextUnlock_IVandKey[0]) + + for i in range(self.createPrivKeyNextUnlock_ChainDepth): + #self.binPrivKey32_Plain = CryptoECDSA().ComputeChainedPrivateKey( \ + #self.binPrivKey32_Plain, \ + #self.chaincode) + + self.binPrivKey32_Plain = self.safeExtendPrivateKey( \ + self.binPrivKey32_Plain, \ + self.chaincode) + + + # IV should have already been randomly generated, before + self.isLocked = False + self.createPrivKeyNextUnlock = False + self.createPrivKeyNextUnlock_IVandKey = [] + self.createPrivKeyNextUnlock_ChainDepth = 0 + + # Lock/Unlock to make sure encrypted private key is filled + self.lock(secureKdfOutput,generateIVIfNecessary=True) + self.unlock(secureKdfOutput) + + else: + + if not self.binPrivKey32_Encr.getSize()==32: + raise WalletLockError, 'No encrypted private key to decrypt!' + + if not self.binInitVect16.getSize()==16: + raise WalletLockError, 'Initialization Vect (IV) is missing!' + + self.binPrivKey32_Plain = CryptoAES().DecryptCFB( \ + self.binPrivKey32_Encr, \ + secureKdfOutput, \ + self.binInitVect16) + + self.isLocked = False + + if not skipCheck: + if not self.hasPubKey(): + self.binPublicKey65 = CryptoECDSA().ComputePublicKey(\ + self.binPrivKey32_Plain) + else: + # We should usually check that keys match, but may choose to skip + # if we have a lot of keys to load + # NOTE: I run into this error if I fill the keypool without first + # unlocking the wallet. I'm not sure why it doesn't work + # when locked (it should), but this wallet format has been + # working flawless for almost a year... and will be replaced + # soon, so I won't sweat it. + if not CryptoECDSA().CheckPubPrivKeyMatch(self.binPrivKey32_Plain, \ + self.binPublicKey65): + raise KeyDataError, "Stored public key does not match priv key!" + + + + ############################################################################# + def changeEncryptionKey(self, secureOldKey, secureNewKey): + """ + We will use None to specify "no encryption", either for old or new. Of + course we throw an error is old key is "None" but the address is actually + encrypted. + """ + if not self.hasPrivKey(): + raise KeyDataError, 'No private key available to re-encrypt' + + if not secureOldKey and self.useEncryption and self.isLocked: + raise WalletLockError, 'Need old encryption key to unlock private keys' + + wasLocked = self.isLocked + + # Decrypt the original key + if self.isLocked: + self.unlock(secureOldKey, skipCheck=False) + + # Keep the old IV if we are changing the key. IV reuse is perfectly + # fine for a new key, and might save us from disaster if we otherwise + # generated a new one and then forgot to take note of it. + self.keyChanged = True + if not secureNewKey: + # If we chose not to re-encrypt, make sure we clear the encryption + self.binInitVect16 = SecureBinaryData() + self.binPrivKey32_Encr = SecureBinaryData() + self.isLocked = False + self.useEncryption = False + else: + # Re-encrypt with new key (using same IV) + self.useEncryption = True + self.lock(secureNewKey) # do this to make sure privKey_Encr filled + if wasLocked: + self.isLocked = True + else: + self.unlock(secureNewKey) + self.isLocked = False + + + + + ############################################################################# + # This is more of a static method + def checkPubPrivKeyMatch(self, securePriv, securePub): + CryptoECDSA().CheckPubPrivKeyMatch(securePriv, securePub) + + + + ############################################################################# + @TimeThisFunction + def generateDERSignature(self, binMsg, secureKdfOutput=None): + """ + This generates a DER signature for this address using the private key. + Obviously, if we don't have the private key, we throw an error. Or if + the wallet is locked and no encryption key was provided. + + If an encryption key IS provided, then we unlock the address just long + enough to sign the message and then re-lock it + """ + + if not self.hasPrivKey(): + raise KeyDataError, 'Cannot sign for address without private key!' + + if self.isLocked: + if secureKdfOutput==None: + raise WalletLockError, "Cannot sign Tx when private key is locked!" + else: + # Wallet is locked but we have a decryption key + self.unlock(secureKdfOutput, skipCheck=False) + + try: + secureMsg = SecureBinaryData(binMsg) + sig = CryptoECDSA().SignData(secureMsg, self.binPrivKey32_Plain) + sigstr = sig.toBinStr() + + rBin = sigstr[:32 ] + sBin = sigstr[ 32:] + return createSigScriptFromRS(rBin, sBin) + + except: + LOGERROR('Failed signature generation') + finally: + # Always re-lock/cleanup after unlocking, even after an exception. + # If locking triggers an error too, we will just skip it. + try: + if secureKdfOutput!=None: + self.lock(secureKdfOutput) + except: + LOGERROR('Error re-locking address') + pass + + + + + ############################################################################# + @TimeThisFunction + def verifyDERSignature(self, binMsgVerify, derSig): + if not self.hasPubKey(): + raise KeyDataError, 'No public key available for this address!' + + if not isinstance(derSig, str): + # In case this is a SecureBinaryData object... + derSig = derSig.toBinStr() + + codeByte = derSig[0] + nBytes = binary_to_int(derSig[1]) + rsStr = derSig[2:2+nBytes] + assert(codeByte == '\x30') + assert(nBytes == len(rsStr)) + # Read r + codeByte = rsStr[0] + rBytes = binary_to_int(rsStr[1]) + r = rsStr[2:2+rBytes] + assert(codeByte == '\x02') + sStr = rsStr[2+rBytes:] + # Read s + codeByte = sStr[0] + sBytes = binary_to_int(sStr[1]) + s = sStr[2:2+sBytes] + assert(codeByte == '\x02') + # Now we have the (r,s) values of the + + secMsg = SecureBinaryData(binMsgVerify) + secSig = SecureBinaryData(r[-32:] + s[-32:]) + secPubKey = SecureBinaryData(self.binPublicKey65) + return CryptoECDSA().VerifyData(secMsg, secSig, secPubKey) + + ############################################################################# + def markAsRootAddr(self, chaincode): + if not chaincode.getSize()==32: + raise KeyDataError, 'Chaincode must be 32 bytes' + else: + self.chainIndex = -1 + self.chaincode = chaincode + + + ############################################################################# + def isAddrChainRoot(self): + return (self.chainIndex==-1) + + ############################################################################# + @TimeThisFunction + def extendAddressChain(self, secureKdfOutput=None, newIV=None): + """ + We require some fairly complicated logic here, due to the fact that a + user with a full, private-key-bearing wallet, may try to generate a new + key/address without supplying a passphrase. If this happens, the wallet + logic gets mucked up -- we don't want to reject the request to + generate a new address, but we can't compute the private key until the + next time the user unlocks their wallet. Thus, we have to save off the + data they will need to create the key, to be applied on next unlock. + """ + if not self.chaincode.getSize() == 32: + raise KeyDataError, 'No chaincode has been defined to extend chain' + + newAddr = PyBtcAddress() + privKeyAvailButNotDecryptable = (self.hasPrivKey() and \ + self.isLocked and \ + not secureKdfOutput ) + + + if self.hasPrivKey() and not privKeyAvailButNotDecryptable: + # We are extending a chain using private key data + wasLocked = self.isLocked + if self.useEncryption and self.isLocked: + if not secureKdfOutput: + raise WalletLockError, 'Cannot create new address without passphrase' + self.unlock(secureKdfOutput) + if not newIV: + newIV = SecureBinaryData().GenerateRandom(16) + + if self.hasPubKey(): + #newPriv = CryptoECDSA().ComputeChainedPrivateKey( \ + #self.binPrivKey32_Plain, \ + #self.chaincode, \ + #self.binPublicKey65) + newPriv = self.safeExtendPrivateKey( \ + self.binPrivKey32_Plain, \ + self.chaincode, \ + self.binPublicKey65) + else: + #newPriv = CryptoECDSA().ComputeChainedPrivateKey( \ + #self.binPrivKey32_Plain, \ + #self.chaincode) + newPriv = self.safeExtendPrivateKey( \ + self.binPrivKey32_Plain, \ + self.chaincode) + + newPub = CryptoECDSA().ComputePublicKey(newPriv) + newAddr160 = newPub.getHash160() + newAddr.createFromPlainKeyData(newPriv, newAddr160, \ + IV16=newIV, publicKey65=newPub) + + newAddr.addrStr20 = newPub.getHash160() + newAddr.useEncryption = self.useEncryption + newAddr.isInitialized = True + newAddr.chaincode = self.chaincode + newAddr.chainIndex = self.chainIndex+1 + + # We can't get here without a secureKdfOutput (I think) + if newAddr.useEncryption: + newAddr.lock(secureKdfOutput) + if not wasLocked: + newAddr.unlock(secureKdfOutput) + self.unlock(secureKdfOutput) + return newAddr + else: + # We are extending the address based solely on its public key + if not self.hasPubKey(): + raise KeyDataError, 'No public key available to extend chain' + + #newAddr.binPublicKey65 = CryptoECDSA().ComputeChainedPublicKey( \ + #self.binPublicKey65, self.chaincode) + newAddr.binPublicKey65 = self.safeExtendPublicKey( \ + self.binPublicKey65, self.chaincode) + + newAddr.addrStr20 = newAddr.binPublicKey65.getHash160() + newAddr.useEncryption = self.useEncryption + newAddr.isInitialized = True + newAddr.chaincode = self.chaincode + newAddr.chainIndex = self.chainIndex+1 + + + if privKeyAvailButNotDecryptable: + # *** store what is needed to recover key on next addr unlock *** + newAddr.isLocked = True + newAddr.useEncryption = True + if not newIV: + newIV = SecureBinaryData().GenerateRandom(16) + newAddr.binInitVect16 = newIV + newAddr.createPrivKeyNextUnlock = True + newAddr.createPrivKeyNextUnlock_IVandKey = [None,None] + if self.createPrivKeyNextUnlock: + # We are chaining from address also requiring gen on next unlock + newAddr.createPrivKeyNextUnlock_IVandKey[0] = \ + self.createPrivKeyNextUnlock_IVandKey[0].copy() + newAddr.createPrivKeyNextUnlock_IVandKey[1] = \ + self.createPrivKeyNextUnlock_IVandKey[1].copy() + newAddr.createPrivKeyNextUnlock_ChainDepth = \ + self.createPrivKeyNextUnlock_ChainDepth+1 + else: + # The address from which we are extending has already been generated + newAddr.createPrivKeyNextUnlock_IVandKey[0] = self.binInitVect16.copy() + newAddr.createPrivKeyNextUnlock_IVandKey[1] = self.binPrivKey32_Encr.copy() + newAddr.createPrivKeyNextUnlock_ChainDepth = 1 + return newAddr + + + def serialize(self): + """ + We define here a binary serialization scheme that will write out ALL + information needed to completely reconstruct address data from file. + This method returns a string, but presumably will be used to write addr + data to file. The following format is used. + + Address160 (20 bytes) : The 20-byte hash of the public key + This must always be the first field + AddressChk ( 4 bytes) : Checksum to make sure no error in addr160 + AddrVersion ( 4 bytes) : Early version don't specify encrypt params + Flags ( 8 bytes) : Addr-specific info, including encrypt params + + ChainCode (32 bytes) : For extending deterministic wallets + ChainChk ( 4 bytes) : Checksum for chaincode + ChainIndex ( 8 bytes) : Index in chain if deterministic addresses + ChainDepth ( 8 bytes) : How deep addr is in chain beyond last + computed private key (if base address was + locked when we tried to extend/chain it) + + InitVect (16 bytes) : Initialization vector for encryption + InitVectChk ( 4 bytes) : Checksum for IV + PrivKey (32 bytes) : Private key data (may be encrypted) + PrivKeyChk ( 4 bytes) : Checksum for private key data + + PublicKey (65 bytes) : Public key for this address + PubKeyChk ( 4 bytes) : Checksum for private key data + + + FirstTime ( 8 bytes) : The first time addr was seen in blockchain + LastTime ( 8 bytes) : The last time addr was seen in blockchain + FirstBlock ( 4 bytes) : The first block addr was seen in blockchain + LastBlock ( 4 bytes) : The last block addr was seen in blockchain + """ + + serializeWithEncryption = self.useEncryption + + if self.useEncryption and \ + self.binPrivKey32_Encr.getSize()==0 and \ + self.binPrivKey32_Plain.getSize()>0: + LOGERROR('') + LOGERROR('***WARNING: you have chosen to serialize a key you hope to be') + LOGERROR(' encrypted, but have not yet chosen a passphrase for') + LOGERROR(' it. The only way to serialize this address is with ') + LOGERROR(' the plaintext keys. Please lock this address at') + LOGERROR(' least once in order to enable encrypted output.') + serializeWithEncryption = False + + # Before starting, let's construct the flags for this address + nFlagBytes = 8 + flags = [False]*nFlagBytes*8 + flags[0] = self.hasPrivKey() + flags[1] = self.hasPubKey() + flags[2] = serializeWithEncryption + flags[3] = self.createPrivKeyNextUnlock + flags = ''.join([('1' if f else '0') for f in flags]) + + def raw(a): + if isinstance(a, str): + return a + else: + return a.toBinStr() + + def chk(a): + if isinstance(a, str): + return computeChecksum(a,4) + else: + return computeChecksum(a.toBinStr(),4) + + # Use BinaryPacker "width" fields to guaranteee BINARY_CHUNK width. + # Sure, if we have malformed data we might cut some of it off instead + # of writing it to the binary stream. But at least we'll ALWAYS be + # able to determine where each field is, and will never corrupt the + # whole wallet so badly we have to go hex-diving to figure out what + # happened. + binOut = BinaryPacker() + binOut.put(BINARY_CHUNK, self.addrStr20, width=20) + binOut.put(BINARY_CHUNK, chk(self.addrStr20), width= 4) + binOut.put(UINT32, getVersionInt(PYBTCWALLET_VERSION)) + binOut.put(UINT64, bitset_to_int(flags)) + + # Write out address-chaining parameters (for deterministic wallets) + binOut.put(BINARY_CHUNK, raw(self.chaincode), width=32) + binOut.put(BINARY_CHUNK, chk(self.chaincode), width= 4) + binOut.put(INT64, self.chainIndex) + binOut.put(INT64, self.createPrivKeyNextUnlock_ChainDepth) + + # Write out whatever is appropriate for private-key data + # Binary-unpacker will write all 0x00 bytes if empty values are given + if serializeWithEncryption: + if self.createPrivKeyNextUnlock: + binOut.put(BINARY_CHUNK, raw(self.createPrivKeyNextUnlock_IVandKey[0]), width=16) + binOut.put(BINARY_CHUNK, chk(self.createPrivKeyNextUnlock_IVandKey[0]), width= 4) + binOut.put(BINARY_CHUNK, raw(self.createPrivKeyNextUnlock_IVandKey[1]), width=32) + binOut.put(BINARY_CHUNK, chk(self.createPrivKeyNextUnlock_IVandKey[1]), width= 4) + else: + binOut.put(BINARY_CHUNK, raw(self.binInitVect16), width=16) + binOut.put(BINARY_CHUNK, chk(self.binInitVect16), width= 4) + binOut.put(BINARY_CHUNK, raw(self.binPrivKey32_Encr), width=32) + binOut.put(BINARY_CHUNK, chk(self.binPrivKey32_Encr), width= 4) + else: + binOut.put(BINARY_CHUNK, raw(self.binInitVect16), width=16) + binOut.put(BINARY_CHUNK, chk(self.binInitVect16), width= 4) + binOut.put(BINARY_CHUNK, raw(self.binPrivKey32_Plain), width=32) + binOut.put(BINARY_CHUNK, chk(self.binPrivKey32_Plain), width= 4) + + binOut.put(BINARY_CHUNK, raw(self.binPublicKey65), width=65) + binOut.put(BINARY_CHUNK, chk(self.binPublicKey65), width= 4) + + binOut.put(UINT64, self.timeRange[0]) + binOut.put(UINT64, self.timeRange[1]) + binOut.put(UINT32, self.blkRange[0]) + binOut.put(UINT32, self.blkRange[1]) + + return binOut.getBinaryString() + + ############################################################################# + def scanBlockchainForAddress(self, abortIfBDMBusy=False): + """ + This method will return null output if the BDM is currently in the + middle of a scan. You can use waitAsLongAsNecessary=True if you + want to wait for the previous scan AND the next scan. Otherwise, + you can check for bal==-1 and then try again later... + + This is particularly relevant if you know that an address has already + been scanned, and you expect this method to return immediately. Thus, + you don't want to wait for any scan at all... + + This one-stop-shop method has to be blocking. You might want to + register the address and rescan asynchronously, skipping this method + entirely: + + cppWlt = Cpp.BtcWallet() + cppWlt.addScrAddress_1_(Hash160ToScrAddr(self.getAddr160())) + TheBDM.registerScrAddr(Hash160ToScrAddr(self.getAddr160())) + TheBDM.rescanBlockchain(wait=False) + + <... do some other stuff ...> + + if TheBDM.getBDMState()=='BlockchainReady': + TheBDM.updateWalletsAfterScan(wait=True) # fast after a rescan + bal = cppWlt.getBalance('Spendable') + utxoList = cppWlt.getUnspentTxOutList() + else: + <...come back later...> + + """ + if TheBDM.getBDMState()=='BlockchainReady' or \ + (TheBDM.isScanning() and not abortIfBDMBusy): + LOGDEBUG('Scanning blockchain for address') + + # We are expecting this method to return balance + # and UTXO data, so we must make sure we're blocking. + cppWlt = Cpp.BtcWallet() + cppWlt.addScrAddress_1_(Hash160ToScrAddr(self.getAddr160())) + TheBDM.registerWallet(cppWlt, wait=True) + TheBDM.scanBlockchainForTx(cppWlt, wait=True) + + utxoList = cppWlt.getUnspentTxOutList() + bal = cppWlt.getSpendableBalance(-1, IGNOREZC) + return (bal, utxoList) + else: + return (-1, []) + + ############################################################################# + def unserialize(self, toUnpack): + """ + We reconstruct the address from a serialized version of it. See the help + text for "serialize()" for information on what fields need to + be included and the binary mapping + + We verify all checksums, correct for one byte errors, and raise exceptions + for bigger problems that can't be fixed. + """ + if isinstance(toUnpack, BinaryUnpacker): + serializedData = toUnpack + else: + serializedData = BinaryUnpacker( toUnpack ) + + + def chkzero(a): + """ + Due to fixed-width fields, we will get lots of zero-bytes + even when the binary data container was empty + """ + if a.count('\x00')==len(a): + return '' + else: + return a + + + # Start with a fresh new address + self.__init__() + + self.addrStr20 = serializedData.get(BINARY_CHUNK, 20) + chkAddr20 = serializedData.get(BINARY_CHUNK, 4) + + addrVerInt = serializedData.get(UINT32) + flags = serializedData.get(UINT64) + self.addrStr20 = verifyChecksum(self.addrStr20, chkAddr20) + flags = int_to_bitset(flags, widthBytes=8) + + # Interpret the flags + containsPrivKey = (flags[0]=='1') + containsPubKey = (flags[1]=='1') + self.useEncryption = (flags[2]=='1') + self.createPrivKeyNextUnlock = (flags[3]=='1') + + addrChkError = False + if len(self.addrStr20)==0: + addrChkError = True + if not containsPrivKey and not containsPubKey: + raise UnserializeError, 'Checksum mismatch in addrStr' + + + + # Write out address-chaining parameters (for deterministic wallets) + self.chaincode = chkzero(serializedData.get(BINARY_CHUNK, 32)) + chkChaincode = serializedData.get(BINARY_CHUNK, 4) + self.chainIndex = serializedData.get(INT64) + depth = serializedData.get(INT64) + self.createPrivKeyNextUnlock_ChainDepth = depth + + # Correct errors, convert to secure container + self.chaincode = SecureBinaryData(verifyChecksum(self.chaincode, chkChaincode)) + + + # Write out whatever is appropriate for private-key data + # Binary-unpacker will write all 0x00 bytes if empty values are given + iv = chkzero(serializedData.get(BINARY_CHUNK, 16)) + chkIv = serializedData.get(BINARY_CHUNK, 4) + privKey = chkzero(serializedData.get(BINARY_CHUNK, 32)) + chkPriv = serializedData.get(BINARY_CHUNK, 4) + iv = SecureBinaryData(verifyChecksum(iv, chkIv)) + privKey = SecureBinaryData(verifyChecksum(privKey, chkPriv)) + + # If this is SUPPOSED to contain a private key... + if containsPrivKey: + if privKey.getSize()==0: + raise UnserializeError, 'Checksum mismatch in PrivateKey '+\ + '('+hash160_to_addrStr(self.addrStr20)+')' + + if self.useEncryption: + if iv.getSize()==0: + raise UnserializeError, 'Checksum mismatch in IV ' +\ + '('+hash160_to_addrStr(self.addrStr20)+')' + if self.createPrivKeyNextUnlock: + self.createPrivKeyNextUnlock_IVandKey[0] = iv.copy() + self.createPrivKeyNextUnlock_IVandKey[1] = privKey.copy() + else: + self.binInitVect16 = iv.copy() + self.binPrivKey32_Encr = privKey.copy() + else: + self.binInitVect16 = iv.copy() + self.binPrivKey32_Plain = privKey.copy() + + pubKey = chkzero(serializedData.get(BINARY_CHUNK, 65)) + chkPub = serializedData.get(BINARY_CHUNK, 4) + pubKey = SecureBinaryData(verifyChecksum(pubKey, chkPub)) + + if containsPubKey: + if not pubKey.getSize()==65: + if self.binPrivKey32_Plain.getSize()==32: + pubKey = CryptoAES().ComputePublicKey(self.binPrivKey32_Plain) + else: + raise UnserializeError, 'Checksum mismatch in PublicKey ' +\ + '('+hash160_to_addrStr(self.addrStr20)+')' + + self.binPublicKey65 = pubKey + + if addrChkError: + self.addrStr20 = self.binPublicKey65.getHash160() + + self.timeRange[0] = serializedData.get(UINT64) + self.timeRange[1] = serializedData.get(UINT64) + self.blkRange[0] = serializedData.get(UINT32) + self.blkRange[1] = serializedData.get(UINT32) + + self.isInitialized = True + return self + + + + ############################################################################# + # The following methods are the SIMPLE address operations that can be used + # to juggle address data without worrying at all about encryption details. + # The addresses created here can later be endowed with encryption. + ############################################################################# + def createFromPrivateKey(self, privKey, pubKey=None, skipCheck=False): + """ + Creates address from a user-supplied random INTEGER. + This method DOES perform elliptic-curve operations + """ + if isinstance(privKey, str) and len(privKey)==32: + self.binPrivKey32_Plain = SecureBinaryData(privKey) + elif isinstance(privKey, int) or isinstance(privKey, long): + binPriv = int_to_binary(privKey, widthBytes=32, endOut=BIGENDIAN) + self.binPrivKey32_Plain = SecureBinaryData(binPriv) + else: + raise KeyDataError, 'Unknown private key format' + + if pubKey==None: + self.binPublicKey65 = CryptoECDSA().ComputePublicKey(self.binPrivKey32_Plain) + else: + self.binPublicKey65 = SecureBinaryData(pubKey) + + if not skipCheck: + assert(CryptoECDSA().CheckPubPrivKeyMatch( \ + self.binPrivKey32_Plain, \ + self.binPublicKey65)) + + self.addrStr20 = self.binPublicKey65.getHash160() + + self.isInitialized = True + return self + + + + ############################################################################# + def createFromPublicKey(self, pubkey): + """ + Creates address from a user-supplied ECDSA public key. + + The key can be supplied as an (x,y) pair of integers, an EC_Point + as defined in the lisecdsa class, or as a 65-byte binary string + (the 64 public key bytes with a 0x04 prefix byte) + + This method will fail if the supplied pair of points is not + on the secp256k1 curve. + """ + if isinstance(pubkey, tuple) and len(pubkey)==2: + # We are given public-key (x,y) pair + binXBE = int_to_binary(pubkey[0], widthBytes=32, endOut=BIGENDIAN) + binYBE = int_to_binary(pubkey[1], widthBytes=32, endOut=BIGENDIAN) + self.binPublicKey65 = SecureBinaryData('\x04' + binXBE + binYBE) + if not CryptoECDSA().VerifyPublicKeyValid(self.binPublicKey65): + raise KeyDataError, 'Supplied public key is not on secp256k1 curve' + elif isinstance(pubkey, str) and len(pubkey)==65: + self.binPublicKey65 = SecureBinaryData(pubkey) + if not CryptoECDSA().VerifyPublicKeyValid(self.binPublicKey65): + raise KeyDataError, 'Supplied public key is not on secp256k1 curve' + else: + raise KeyDataError, 'Unknown public key format!' + + # TODO: I should do a test to see which is faster: + # 1) Compute the hash directly like this + # 2) Get the string, hash it in python + self.addrStr20 = self.binPublicKey65.getHash160() + self.isInitialized = True + return self + + + def createFromPublicKeyHash160(self, pubkeyHash160, netbyte=ADDRBYTE): + """ + Creates an address from just the 20-byte binary hash of a public key. + + In binary form without a chksum, there is no protection against byte + errors, since there's no way to distinguish an invalid address from + a valid one (they both look like random data). + + If you are creating an address using 20 bytes you obtained in an + unreliable manner (such as manually typing them in), you should + double-check the input before sending money using the address created + here -- the tx will appear valid and be accepted by the network, + but will be permanently tied up in the network + """ + self.__init__() + self.addrStr20 = pubkeyHash160 + self.isInitialized = True + return self + + def createFromAddrStr(self, addrStr): + """ + Creates an address from a Base58 address string. Since the address + string includes a checksum, this method will fail if there was any + errors entering/copying the address + """ + self.__init__() + self.addrStr = addrStr + if not self.checkAddressValid(): + raise BadAddressError, 'Invalid address string: '+addrStr + self.isInitialized = True + return self + + def calculateAddrStr(self, netbyte=ADDRBYTE): + """ + Forces a recalculation of the address string from the public key + """ + if not self.hasPubKey(): + raise KeyDataError, 'Cannot compute address without PublicKey' + keyHash = self.binPublicKey65.getHash160() + chksum = hash256(netbyte + keyHash)[:4] + return binary_to_base58(netbyte + keyHash + chksum) + + + + def checkAddressValid(self): + return checkAddrStrValid(self.addrStr); + + + def pprint(self, withPrivKey=True, indent=''): + def pp(x, nchar=1000): + if x.getSize()==0: + return '--'*32 + else: + return x.toHexStr()[:nchar] + print indent + 'BTC Address :', self.getAddrStr() + print indent + 'Hash160[BE] :', binary_to_hex(self.getAddr160()) + print indent + 'Wallet Location :', self.walletByteLoc + print indent + 'Chained Address :', self.chainIndex >= -1 + print indent + 'Have (priv,pub) : (%s,%s)' % \ + (str(self.hasPrivKey()), str(self.hasPubKey())) + print indent + 'First/Last Time : (%s,%s)' % \ + (str(self.timeRange[0]), str(self.timeRange[1])) + print indent + 'First/Last Block : (%s,%s)' % \ + (str(self.blkRange[0]), str(self.blkRange[1])) + if self.hasPubKey(): + print indent + 'PubKeyX(BE) :', \ + binary_to_hex(self.binPublicKey65.toBinStr()[1:33 ]) + print indent + 'PubKeyY(BE) :', \ + binary_to_hex(self.binPublicKey65.toBinStr()[ 33:]) + print indent + 'Encryption parameters:' + print indent + ' UseEncryption :', self.useEncryption + print indent + ' IsLocked :', self.isLocked + print indent + ' KeyChanged :', self.keyChanged + print indent + ' ChainIndex :', self.chainIndex + print indent + ' Chaincode :', pp(self.chaincode) + print indent + ' InitVector :', pp(self.binInitVect16) + if withPrivKey and self.hasPrivKey(): + print indent + 'PrivKeyPlain(BE) :', pp(self.binPrivKey32_Plain) + print indent + 'PrivKeyCiphr(BE) :', pp(self.binPrivKey32_Encr) + else: + print indent + 'PrivKeyPlain(BE) :', pp(SecureBinaryData()) + print indent + 'PrivKeyCiphr(BE) :', pp(SecureBinaryData()) + if self.createPrivKeyNextUnlock: + print indent + ' ***** :', 'PrivKeys available on next unlock' + + def toString(self, withPrivKey=True, indent=''): + def pp(x, nchar=1000): + if x.getSize()==0: + return '--'*32 + else: + return x.toHexStr()[:nchar] + result = ''.join([indent + 'BTC Address :', self.getAddrStr()]) + result = ''.join([result, '\n', indent + 'Hash160[BE] :', binary_to_hex(self.getAddr160())]) + result = ''.join([result, '\n', indent + 'Wallet Location :', str(self.walletByteLoc)]) + result = ''.join([result, '\n', indent + 'Chained Address :', str(self.chainIndex >= -1)]) + result = ''.join([result, '\n', indent + 'Have (priv,pub) : (%s,%s)' % \ + (str(self.hasPrivKey()), str(self.hasPubKey()))]) + result = ''.join([result, '\n', indent + 'First/Last Time : (%s,%s)' % \ + (str(self.timeRange[0]), str(self.timeRange[1]))]) + result = ''.join([result, '\n', indent + 'First/Last Block : (%s,%s)' % \ + (str(self.blkRange[0]), str(self.blkRange[1]))]) + if self.hasPubKey(): + result = ''.join([result, '\n', indent + 'PubKeyX(BE) :', \ + binary_to_hex(self.binPublicKey65.toBinStr()[1:33 ])]) + result = ''.join([result, '\n', indent + 'PubKeyY(BE) :', \ + binary_to_hex(self.binPublicKey65.toBinStr()[ 33:])]) + result = ''.join([result, '\n', indent + 'Encryption parameters:']) + result = ''.join([result, '\n', indent + ' UseEncryption :', str(self.useEncryption)]) + result = ''.join([result, '\n', indent + ' IsLocked :', str(self.isLocked)]) + result = ''.join([result, '\n', indent + ' KeyChanged :', str(self.keyChanged)]) + result = ''.join([result, '\n', indent + ' ChainIndex :', str(self.chainIndex)]) + result = ''.join([result, '\n', indent + ' Chaincode :', pp(self.chaincode)]) + result = ''.join([result, '\n', indent + ' InitVector :', pp(self.binInitVect16)]) + if withPrivKey and self.hasPrivKey(): + result = ''.join([result, '\n', indent + 'PrivKeyPlain(BE) :', pp(self.binPrivKey32_Plain)]) + result = ''.join([result, '\n', indent + 'PrivKeyCiphr(BE) :', pp(self.binPrivKey32_Encr)]) + else: + result = ''.join([result, '\n', indent + 'PrivKeyPlain(BE) :', pp(SecureBinaryData())]) + result = ''.join([result, '\n', indent + 'PrivKeyCiphr(BE) :', pp(SecureBinaryData())]) + if self.createPrivKeyNextUnlock: + result = ''.join([result, '\n', indent + ' ***** :', 'PrivKeys available on next unlock']) + return result + +# Put the import at the end to avoid circular reference problem +from armoryengine.BDM import * diff --git a/armoryengine/PyBtcWallet.py b/armoryengine/PyBtcWallet.py new file mode 100644 index 000000000..23593f349 --- /dev/null +++ b/armoryengine/PyBtcWallet.py @@ -0,0 +1,2918 @@ +################################################################################ +# # +# Copyright (C) 2011-2014, Armory Technologies, Inc. # +# Distributed under the GNU Affero General Public License (AGPL v3) # +# See LICENSE or http://www.gnu.org/licenses/agpl.html # +# # +################################################################################ +import os.path +import shutil + +from CppBlockUtils import SecureBinaryData, KdfRomix, CryptoAES, CryptoECDSA +import CppBlockUtils as Cpp +from armoryengine.ArmoryUtils import * +from armoryengine.BinaryPacker import * +from armoryengine.BinaryUnpacker import * +from armoryengine.Timer import * +# This import is causing a circular import problem when used by findpass and promokit +# it is imported at the end of the file. Do not add it back at the begining +# from armoryengine.Transaction import * + + +BLOCKCHAIN_READONLY = 0 +BLOCKCHAIN_READWRITE = 1 +BLOCKCHAIN_DONOTUSE = 2 + +WLT_UPDATE_ADD = 0 +WLT_UPDATE_MODIFY = 1 + +WLT_DATATYPE_KEYDATA = 0 +WLT_DATATYPE_ADDRCOMMENT = 1 +WLT_DATATYPE_TXCOMMENT = 2 +WLT_DATATYPE_OPEVAL = 3 +WLT_DATATYPE_DELETED = 4 + +DEFAULT_COMPUTE_TIME_TARGET = 0.25 +DEFAULT_MAXMEM_LIMIT = 32*1024*1024 + +class PyBtcWallet(object): + """ + This class encapsulates all the concepts and variables in a "wallet", + and maintains the passphrase protection, key stretching, encryption, + etc, required to maintain the wallet. This class also includes the + file I/O methods for storing and loading wallets. + + ***NOTE: I have ONLY implemented deterministic wallets, using ECDSA + Diffie-Hellman shared-secret crypto operations. This allows + one to actually determine the next PUBLIC KEY in the address + chain without actually having access to the private keys. + This makes it possible to synchronize online-offline computers + once and never again. + + You can import random keys into your wallet, but if it is + encrypted, you will have to supply a passphrase to make sure + it can be encrypted as well. + + Presumably, wallets will be used for one of three purposes: + + (1) Spend money and receive payments + (2) Watching-only wallets - have the private keys, just not on this computer + (3) May be watching *other* people's addrs. There's a variety of reasons + we might want to watch other peoples' addresses, but most them are not + relevant to a "basic" BTC user. Nonetheless it should be supported to + watch money without considering it part of our own assets + + This class is included in the combined-python-cpp module, because we really + need to maintain a persistent Cpp.BtcWallet if this class is to be useful + (we don't want to have to rescan the entire blockchain every time we do any + wallet operations). + + The file format was designed from the outset with lots of unused space to + allow for expansion without having to redefine the file format and break + previous wallets. Luckily, wallet information is cheap, so we don't have + to stress too much about saving space (100,000 addresses should take 15 MB) + + This file is NOT for storing Tx-related information. I want this file to + be the minimal amount of information you need to secure and backup your + entire wallet. Tx information can always be recovered from examining the + blockchain... your private keys cannot be. + + We track version numbers, just in case. We start with 1.0 + + Version 1.0: + --- + fileID -- (8) '\xbaWALLET\x00' for wallet files + version -- (4) getVersionInt(PYBTCWALLET_VERSION) + magic bytes -- (4) defines the blockchain for this wallet (BTC, NMC) + wlt flags -- (8) 64 bits/flags representing info about wallet + binUniqueID -- (6) first 5 bytes of first address in wallet + (rootAddr25Bytes[:5][::-1]), reversed + This is not intended to look like the root addr str + and is reversed to avoid having all wallet IDs start + with the same characters (since the network byte is front) + create date -- (8) unix timestamp of when this wallet was created + (actually, the earliest creation date of any addr + in this wallet -- in the case of importing addr + data). This is used to improve blockchain searching + Short Name -- (32) Null-terminated user-supplied short name for wlt + Long Name -- (256) Null-terminated user-supplied description for wlt + Highest Used-- (8) The chain index of the highest used address + --- + Crypto/KDF -- (512) information identifying the types and parameters + of encryption used to secure wallet, and key + stretching used to secure your passphrase. + Includes salt. (the breakdown of this field will + be described separately) + KeyGenerator-- (237) The base address for a determinstic wallet. + Just a serialized PyBtcAddress object. + --- + UNUSED -- (1024) unused space for future expansion of wallet file + --- + Remainder of file is for key storage and various other things. Each + "entry" will start with a 4-byte code identifying the entry type, then + 20 bytes identifying what address the data is for, and finally then + the subsequent data . So far, I have three types of entries that can + be included: + + \x01 -- Address/Key data (as of PyBtcAddress version 1.0, 237 bytes) + \x02 -- Address comments (variable-width field) + \x03 -- Address comments (variable-width field) + \x04 -- OP_EVAL subscript (when this is enabled, in the future) + + Please see PyBtcAddress for information on how key data is serialized. + Comments (\x02) are var-width, and if a comment is changed to + something longer than the existing one, we'll just blank out the old + one and append a new one to the end of the file. It looks like + + 02000000 01 4f This comment is enabled (01) with 4f characters + + + For file syncing, we protect against corrupted wallets by doing atomic + operations before even telling the user that new data has been added. + We do this by copying the wallet file, and creating a walletUpdateFailed + file. We then modify the original, verify its integrity, and then delete + the walletUpdateFailed file. Then we create a backupUpdateFailed flag, + do the identical update on the backup file, and delete the failed flag. + This guaranatees that no matter which nanosecond the power goes out, + there will be an uncorrupted wallet and we know which one it is. + + We never let the user see any data until the atomic write-to-file operation + has completed + + + Additionally, we implement key locking and unlocking, with timeout. These + key locking features are only DEFINED here, not actually enforced (because + this is a library, not an application). You can set the default/temporary + time that the KDF key is maintained in memory after the passphrase is + entered, and this class will keep track of when the wallet should be next + locked. It is up to the application to check whether the current time + exceeds the lock time. This will probably be done in a kind of heartbeat + method, which checks every few seconds for all sorts of things -- including + wallet locking. + """ + + ############################################################################# + def __init__(self): + self.fileTypeStr = '\xbaWALLET\x00' + self.magicBytes = MAGIC_BYTES + self.version = PYBTCWALLET_VERSION # (Major, Minor, Minor++, even-more-minor) + self.eofByte = 0 + self.cppWallet = None # Mirror of PyBtcWallet in C++ object + self.cppInfo = {} # Extra info about each address to help sync + self.watchingOnly = False + self.wltCreateDate = 0 + + # Three dictionaries hold all data + self.addrMap = {} # maps 20-byte addresses to PyBtcAddress objects + self.commentsMap = {} # maps 20-byte addresses to user-created comments + self.commentLocs = {} # map comment keys to wallet file locations + self.opevalMap = {} # maps 20-byte addresses to OP_EVAL data (future) + self.labelName = '' + self.labelDescr = '' + self.linearAddr160List = [] + self.chainIndexMap = {} + self.txAddrMap = {} # cache for getting tx-labels based on addr search + if USE_TESTNET: + self.addrPoolSize = 10 # this makes debugging so much easier! + else: + self.addrPoolSize = CLI_OPTIONS.keypool + + # For file sync features + self.walletPath = '' + self.doBlockchainSync = BLOCKCHAIN_READONLY + self.lastSyncBlockNum = 0 + + # Private key encryption details + self.useEncryption = False + self.kdf = None + self.crypto = None + self.kdfKey = None + self.defaultKeyLifetime = 10 # seconds after unlock, that key is discarded + self.lockWalletAtTime = 0 # seconds after unlock, that key is discarded + self.isLocked = False + self.testedComputeTime=None + + # Deterministic wallet, need a root key. Though we can still import keys. + # The unique ID contains the network byte (id[-1]) but is not intended to + # resemble the address of the root key + self.uniqueIDBin = '' + self.uniqueIDB58 = '' # Base58 version of reversed-uniqueIDBin + self.lastComputedChainAddr160 = '' + self.lastComputedChainIndex = 0 + self.highestUsedChainIndex = 0 + + # All PyBtcAddress serializations are exact same size, figure it out now + self.pybtcaddrSize = len(PyBtcAddress().serialize()) + + + # All BDM calls by default go on the multi-thread-queue. But if the BDM + # is the one calling the PyBtcWallet methods, it will deadlock if it uses + # the queue. Therefore, the BDM will set this flag before making any + # calls, which will tell PyBtcWallet to use __direct methods. + self.calledFromBDM = False + + # Finally, a bunch of offsets that tell us where data is stored in the + # file: this can be generated automatically on unpacking (meaning it + # doesn't require manually updating offsets if I change the format), and + # will save us a couple lines of code later, when we need to update things + self.offsetWltFlags = -1 + self.offsetLabelName = -1 + self.offsetLabelDescr = -1 + self.offsetTopUsed = -1 + self.offsetRootAddr = -1 + self.offsetKdfParams = -1 + self.offsetCrypto = -1 + + # These flags are ONLY for unit-testing the walletFileSafeUpdate function + self.interruptTest1 = False + self.interruptTest2 = False + self.interruptTest3 = False + + #flags the wallet if it has off chain imports (from a consistency repair) + self.hasNegativeImports = False + + + ############################################################################# + def getWalletVersion(self): + return (getVersionInt(self.version), getVersionString(self.version)) + + ############################################################################# + def getTimeRangeForAddress(self, addr160): + if not self.addrMap.has_key(addr160): + return None + else: + return self.addrMap[addr160].getTimeRange() + + ############################################################################# + def getBlockRangeForAddress(self, addr160): + if not self.addrMap.has_key(addr160): + return None + else: + return self.addrMap[addr160].getBlockRange() + + ############################################################################# + def setBlockchainSyncFlag(self, syncYes=True): + self.doBlockchainSync = syncYes + + ############################################################################# + @TimeThisFunction + def syncWithBlockchain(self, startBlk=None): + """ + Will block until getTopBlockHeader() returns, which could be a while. + If you don't want to wait, check TheBDM.getBDMState()=='BlockchainReady' + before calling this method. If you expect the blockchain will have to + be rescanned, then call TheBDM.rescanBlockchain or TheBDM.loadBlockchain + + If this method is called from the BDM itself, calledFromBDM will signal + to use the BDM methods directly, not the queue. This will deadlock + otherwise. + """ + if TheBDM.getBDMState() in ('Offline', 'Uninitialized'): + LOGWARN('Called syncWithBlockchain but BDM is %s', TheBDM.getBDMState()) + return + + if not self.doBlockchainSync==BLOCKCHAIN_DONOTUSE: + if startBlk==None: + startBlk = self.lastSyncBlockNum + 1 + + # calledFromBDM means that ultimately the BDM itself called this + # method and is blocking waiting for it. So we can't use the + # BDM-thread queue, must call its methods directly + if self.calledFromBDM: + TheBDM.scanBlockchainForTx_bdm_direct(self.cppWallet, startBlk) + self.lastSyncBlockNum = TheBDM.getTopBlockHeight_bdm_direct() + else: + TheBDM.scanBlockchainForTx(self.cppWallet, startBlk, wait=True) + self.lastSyncBlockNum = TheBDM.getTopBlockHeight(wait=True) + else: + LOGERROR('Blockchain-sync requested, but current wallet') + LOGERROR('is set to BLOCKCHAIN_DONOTUSE') + + ############################################################################# + @TimeThisFunction + def syncWithBlockchainLite(self, startBlk=None): + """ + This is just like a regular sync, but it won't rescan the whole blockchain + if the wallet is dirty -- if addresses were imported recently, it will + still only scan what the blockchain picked up on the last scan. Use the + non-lite version to allow a full scan. + """ + + if TheBDM.getBDMState() in ('Offline', 'Uninitialized'): + LOGWARN('Called syncWithBlockchainLite but BDM is %s', TheBDM.getBDMState()) + return + + if not self.doBlockchainSync==BLOCKCHAIN_DONOTUSE: + if startBlk==None: + if self.lastSyncBlockNum is not None: + startBlk = self.lastSyncBlockNum + 1 + + # calledFromBDM means that ultimately the BDM itself called this + # method and is blocking waiting for it. So we can't use the + # BDM-thread queue, must call its methods directly + + if self.calledFromBDM: + TheBDM.scanRegisteredTxForWallet_bdm_direct(self.cppWallet, startBlk) + self.lastSyncBlockNum = TheBDM.getTopBlockHeight_bdm_direct() + else: + TheBDM.scanRegisteredTxForWallet(self.cppWallet, startBlk, wait=True) + self.lastSyncBlockNum = TheBDM.getTopBlockHeight(wait=True) + + wltLE = self.cppWallet.getTxLedgerForComments() + for le in wltLE: + txHash = le.getTxHash() + if not self.txAddrMap.has_key(txHash): + self.txAddrMap[txHash] = [] + scrAddr = SecureBinaryData(le.getScrAddr()) + try: + addrStr = scrAddr_to_addrStr(scrAddr.toBinStr()) + addr160 = addrStr_to_hash160(addrStr)[1] + if addr160 not in self.txAddrMap[txHash]: + self.txAddrMap[txHash].append(addr160) + except: + continue + else: + LOGERROR('Blockchain-sync requested, but current wallet') + LOGERROR('is set to BLOCKCHAIN_DONOTUSE') + + ############################################################################# + def getCommentForAddrBookEntry(self, abe): + comment = self.getComment(abe.getAddr160()) + if len(comment)>0: + return comment + + # SWIG BUG! + # http://sourceforge.net/tracker/?func=detail&atid=101645&aid=3403085&group_id=1645 + # Apparently, using the -threads option when compiling the swig module + # causes the "for i in vector<...>:" mechanic to sometimes throw seg faults! + # For this reason, this method was replaced with the one below: + for regTx in abe.getTxList(): + comment = self.getComment(regTx.getTxHash()) + if len(comment)>0: + return comment + + return '' + + ############################################################################# + def getCommentForTxList(self, a160, txhashList): + comment = self.getComment(a160) + if len(comment)>0: + return comment + + for txHash in txhashList: + comment = self.getComment(txHash) + if len(comment)>0: + return comment + + return '' + + ############################################################################# + def printAddressBook(self): + addrbook = self.cppWallet.createAddressBook() + for abe in addrbook: + print hash160_to_addrStr(abe.getAddr160()), + txlist = abe.getTxList() + print len(txlist) + for rtx in txlist: + print '\t', binary_to_hex(rtx.getTxHash(), BIGENDIAN) + + ############################################################################# + def hasAnyImported(self): + for a160,addr in self.addrMap.iteritems(): + if addr.chainIndex == -2: + return True + return False + + + ############################################################################# + # The IGNOREZC args on the get*Balance calls determine whether unconfirmed + # change (sent-to-self) will be considered spendable or unconfirmed. This + # was added after the malleability issues cropped up in Feb 2014. Zero-conf + # change was always deprioritized, but using --nospendzeroconfchange makes + # it totally unspendable + def getBalance(self, balType="Spendable"): + if not TheBDM.getBDMState()=='BlockchainReady' and not self.calledFromBDM: + return -1 + else: + currBlk = TheBDM.getTopBlockHeight(calledFromBDM=self.calledFromBDM) + if balType.lower() in ('spendable','spend'): + return self.cppWallet.getSpendableBalance(currBlk, IGNOREZC) + elif balType.lower() in ('unconfirmed','unconf'): + return self.cppWallet.getUnconfirmedBalance(currBlk, IGNOREZC) + elif balType.lower() in ('total','ultimate','unspent','full'): + return self.cppWallet.getFullBalance() + else: + raise TypeError('Unknown balance type! "' + balType + '"') + + + ############################################################################# + def getAddrBalance(self, addr160, balType="Spendable", currBlk=UINT32_MAX): + if (not TheBDM.getBDMState()=='BlockchainReady' and not self.calledFromBDM) or \ + not self.hasAddr(addr160): + return -1 + else: + addr = self.cppWallet.getScrAddrObjByKey(Hash160ToScrAddr(addr160)) + if balType.lower() in ('spendable','spend'): + return addr.getSpendableBalance(currBlk, IGNOREZC) + elif balType.lower() in ('unconfirmed','unconf'): + return addr.getUnconfirmedBalance(currBlk, IGNOREZC) + elif balType.lower() in ('ultimate','unspent','full'): + return addr.getFullBalance() + else: + raise TypeError('Unknown balance type!') + + ############################################################################# + def getTxLedger(self, ledgType='Full'): + """ + Gets the ledger entries for the entire wallet, from C++/SWIG data structs + """ + if not TheBDM.getBDMState()=='BlockchainReady' and not self.calledFromBDM: + return [] + else: + ledgBlkChain = self.cppWallet.getTxLedger() + ledgZeroConf = self.cppWallet.getZeroConfLedger() + if ledgType.lower() in ('full','all','ultimate'): + ledg = [] + ledg.extend(ledgBlkChain) + ledg.extend(ledgZeroConf) + return ledg + elif ledgType.lower() in ('blk', 'blkchain', 'blockchain'): + return ledgBlkChain + elif ledgType.lower() in ('zeroconf', 'zero'): + return ledgZeroConf + else: + raise TypeError('Unknown ledger type! "' + ledgType + '"') + + + + + ############################################################################# + def getAddrTxLedger(self, addr160, ledgType='Full'): + """ + Gets the ledger entries for the entire wallet, from C++/SWIG data structs + """ + if (not TheBDM.getBDMState()=='BlockchainReady' and not self.calledFromBDM) or \ + not self.hasAddr(addr160): + return [] + else: + scrAddr = Hash160ToScrAddr(addr160) + ledgBlkChain = self.cppWallet.getScrAddrObjByKey(scrAddr).getTxLedger() + ledgZeroConf = self.cppWallet.getScrAddrObjByKey(scrAddr).getZeroConfLedger() + if ledgType.lower() in ('full','all','ultimate'): + ledg = [] + ledg.extend(ledgBlkChain) + ledg.extend(ledgZeroConf) + return ledg + elif ledgType.lower() in ('blk', 'blkchain', 'blockchain'): + return ledgBlkChain + elif ledgType.lower() in ('zeroconf', 'zero'): + return ledgZeroConf + else: + raise TypeError('Unknown balance type! "' + ledgType + '"') + + + ############################################################################# + def getTxOutList(self, txType='Spendable'): + """ Returns UnspentTxOut/C++ objects """ + if TheBDM.getBDMState()=='BlockchainReady' and \ + not self.doBlockchainSync==BLOCKCHAIN_DONOTUSE: + + currBlk = TheBDM.getTopBlockHeight(calledFromBDM=self.calledFromBDM) + self.syncWithBlockchain() + if txType.lower() in ('spend', 'spendable'): + return self.cppWallet.getSpendableTxOutList(currBlk, IGNOREZC); + elif txType.lower() in ('full', 'all', 'unspent', 'ultimate'): + return self.cppWallet.getFullTxOutList(currBlk); + else: + raise TypeError('Unknown balance type! ' + txType) + else: + LOGERROR('***Blockchain is not available for accessing wallet-tx data') + return [] + + ############################################################################# + def getAddrTxOutList(self, addr160, txType='Spendable'): + """ Returns UnspentTxOut/C++ objects """ + if TheBDM.getBDMState()=='BlockchainReady' and \ + self.hasAddr(addr160) and \ + not self.doBlockchainSync==BLOCKCHAIN_DONOTUSE: + + currBlk = TheBDM.getTopBlockHeight(calledFromBDM=self.calledFromBDM) + self.syncWithBlockchain() + scrAddrStr = Hash160ToScrAddr(addr160) + cppAddr = self.cppWallet.getScrAddrObjByKey(scrAddrStr) + if txType.lower() in ('spend', 'spendable'): + return cppAddr.getSpendableTxOutList(currBlk, IGNOREZC); + elif txType.lower() in ('full', 'all', 'unspent', 'ultimate'): + return cppAddr.getFullTxOutList(currBlk); + else: + raise TypeError('Unknown TxOutList type! ' + txType) + else: + LOGERROR('***Blockchain is not available for accessing wallet-tx data') + return [] + + + ############################################################################# + def getAddrByHash160(self, addr160): + return (None if not self.hasAddr(addr160) else self.addrMap[addr160]) + + ############################################################################# + def hasAddr(self, addrData): + if isinstance(addrData, str): + if len(addrData) == 20: + return self.addrMap.has_key(addrData) + elif isLikelyDataType(addrData)==DATATYPE.Base58: + return self.addrMap.has_key(addrStr_to_hash160(addrData)[1]) + else: + return False + elif isinstance(addrData, PyBtcAddress): + return self.addrMap.has_key(addrData.getAddr160()) + else: + return False + + + ############################################################################# + def setDefaultKeyLifetime(self, newlifetime): + """ Set a new default lifetime for holding the unlock key. Min 2 sec """ + self.defaultKeyLifetime = max(newlifetime, 2) + + ############################################################################# + def checkWalletLockTimeout(self): + if not self.isLocked and self.kdfKey and RightNow()>self.lockWalletAtTime: + self.lock() + if self.kdfKey: + self.kdfKey.destroy() + self.kdfKey = None + + if self.useEncryption: + self.isLocked = True + + + + ############################################################################# + def lockTxOutsOnNewTx(self, pytxObj): + for txin in pytxObj.inputs: + self.cppWallet.lockTxOutSwig(txin.outpoint.txHash, \ + txin.outpoint.txOutIndex) + + + ############################################################################# + # THIS WAS CREATED ORIGINALLY TO SUPPORT BITSAFE INTEGRATION INTO ARMORY + # But it's also a good first step into general BIP 32 support + def getChildExtPubFromRoot(self, i): + root = self.addrMap['ROOT'] + ekey = ExtendedKey().CreateFromPublic(root.binPublicKey65, root.chaincode) + newKey = HDWalletCrypto().ChildKeyDeriv(ekey, i) + newKey.setIndex(i) + return newKey + #newAddr = PyBtcAddress().createFromExtendedPublicKey(newKey) + + ############################################################################# + #def createFromExtendedPublicKey(self, ekey): + #pub65 = ekey.getPub() + #chain = ekey.getChain() + #newAddr = self.createFromPublicKeyData(pub65, chain) + #newAddr.chainIndex = newAddr.getIndex() + #return newAddr + + ############################################################################# + #def deriveChildPublicKey(self, i): + #newKey = HDWalletCrypto().ChildKeyDeriv(self.getExtendedPublicKey(), i) + #newAddr = PyBtcAddress().createFromExtendedPublicKey(newKey) + + ############################################################################# + # Copy the wallet file to backup + def backupWalletFile(self, backupPath = None): + walletFileBackup = self.getWalletPath('backup') if backupPath == None \ + else backupPath + shutil.copy(self.walletPath, walletFileBackup) + + ############################################################################# + # THIS WAS CREATED ORIGINALLY TO SUPPORT BITSAFE INTEGRATION INTO ARMORY + # But it's also a good first step into general BIP 32 support + def createWalletFromMasterPubKey(self, masterHex, \ + isActuallyNew=True, \ + doRegisterWithBDM=True): + # This function eats hex inputs, not sure why I chose to do that... + p0 = masterHex.index('4104') + 2 + pubkey = SecureBinaryData(hex_to_binary(masterHex[p0:p0+130])) + c0 = masterHex.index('1220') + 4 + chain = SecureBinaryData(hex_to_binary(masterHex[c0:c0+64])) + + # Create the root address object + rootAddr = PyBtcAddress().createFromPublicKeyData( pubkey ) + rootAddr.markAsRootAddr(chain) + self.addrMap['ROOT'] = rootAddr + + ekey = self.getChildExtPubFromRoot(0) + firstAddr = PyBtcAddress().createFromPublicKeyData(ekey.getPub()) + firstAddr.chaincode = ekey.getChain() + firstAddr.chainIndex = 0 + first160 = firstAddr.getAddr160() + + # Update wallet object with the new data + # NEW IN WALLET VERSION 1.35: unique ID is now based on + # the first chained address: this guarantees that the unique ID + # is based not only on the private key, BUT ALSO THE CHAIN CODE + self.useEncryption = False + self.addrMap[firstAddr.getAddr160()] = firstAddr + self.uniqueIDBin = (ADDRBYTE + firstAddr.getAddr160()[:5])[::-1] + self.uniqueIDB58 = binary_to_base58(self.uniqueIDBin) + self.labelName = 'BitSafe Demo Wallet' + self.labelDescr = 'We\'ll be lucky if this works!' + self.lastComputedChainAddr160 = first160 + self.lastComputedChainIndex = firstAddr.chainIndex + self.highestUsedChainIndex = firstAddr.chainIndex-1 + self.wltCreateDate = long(RightNow()) + self.linearAddr160List = [first160] + self.chainIndexMap[firstAddr.chainIndex] = first160 + self.watchingOnly = True + + # We don't have to worry about atomic file operations when + # creating the wallet: so we just do it naively here. + newWalletFilePath = os.path.join(ARMORY_HOME_DIR, 'bitsafe_demo_%s.wallet' % self.uniqueIDB58) + self.walletPath = newWalletFilePath + if not newWalletFilePath: + shortName = self.labelName .replace(' ','_') + # This was really only needed when we were putting name in filename + #for c in ',?;:\'"?/\\=+-|[]{}<>': + #shortName = shortName.replace(c,'_') + newName = 'armory_%s_.wallet' % self.uniqueIDB58 + self.walletPath = os.path.join(ARMORY_HOME_DIR, newName) + + LOGINFO(' New wallet will be written to: %s', self.walletPath) + newfile = open(self.walletPath, 'wb') + fileData = BinaryPacker() + + # packHeader method writes KDF params and root address + headerBytes = self.packHeader(fileData) + + # We make sure we have byte locations of the two addresses, to start + self.addrMap[first160].walletByteLoc = headerBytes + 21 + + fileData.put(BINARY_CHUNK, '\x00' + first160 + firstAddr.serialize()) + + + # Store the current localtime and blocknumber. Block number is always + # accurate if available, but time may not be exactly right. Whenever + # basing anything on time, please assume that it is up to one day off! + time0,blk0 = getCurrTimeAndBlock() if isActuallyNew else (0,0) + + # Don't forget to sync the C++ wallet object + self.cppWallet = Cpp.BtcWallet() + self.cppWallet.addAddress_5_(rootAddr.getAddr160(), time0,blk0,time0,blk0) + self.cppWallet.addAddress_5_(first160, time0,blk0,time0,blk0) + + # We might be holding the wallet temporarily and not ready to register it + if doRegisterWithBDM: + TheBDM.registerWallet(self.cppWallet, isFresh=isActuallyNew) # new wallet + + newfile.write(fileData.getBinaryString()) + newfile.close() + + walletFileBackup = self.getWalletPath('backup') + shutil.copy(self.walletPath, walletFileBackup) + + + # Let's fill the address pool while we are unlocked + # It will get a lot more expensive if we do it on the next unlock + if doRegisterWithBDM: + self.fillAddressPool(self.addrPoolSize, isActuallyNew=isActuallyNew) + + return self + + + + + ############################################################################# + def createNewWallet(self, newWalletFilePath=None, \ + plainRootKey=None, chaincode=None, \ + withEncrypt=True, IV=None, securePassphrase=None, \ + kdfTargSec=DEFAULT_COMPUTE_TIME_TARGET, \ + kdfMaxMem=DEFAULT_MAXMEM_LIMIT, \ + shortLabel='', longLabel='', isActuallyNew=True, \ + doRegisterWithBDM=True, skipBackupFile=False, \ + extraEntropy=None, Progress=emptyFunc): + """ + This method will create a new wallet, using as much customizability + as you want. You can enable encryption, and set the target params + of the key-derivation function (compute-time and max memory usage). + The KDF parameters will be experimentally determined to be as hard + as possible for your computer within the specified time target + (default, 0.25s). It will aim for maximizing memory usage and using + only 1 or 2 iterations of it, but this can be changed by scaling + down the kdfMaxMem parameter (default 32 MB). + + If you use encryption, don't forget to supply a 32-byte passphrase, + created via SecureBinaryData(pythonStr). This method will apply + the passphrase so that the wallet is "born" encrypted. + + The field plainRootKey could be used to recover a written backup + of a wallet, since all addresses are deterministically computed + from the root address. This obviously won't reocver any imported + keys, but does mean that you can recover your ENTIRE WALLET from + only those 32 plaintext bytes AND the 32-byte chaincode. + + We skip the atomic file operations since we don't even have + a wallet file yet to safely update. + + DO NOT CALL THIS FROM BDM METHOD. IT MAY DEADLOCK. + """ + + + if self.calledFromBDM: + LOGERROR('Called createNewWallet() from BDM method!') + LOGERROR('Don\'t do this!') + return None + + if securePassphrase: + securePassphrase = SecureBinaryData(securePassphrase) + if plainRootKey: + plainRootKey = SecureBinaryData(plainRootKey) + if chaincode: + chaincode = SecureBinaryData(chaincode) + + if withEncrypt and not securePassphrase: + raise EncryptionError('Cannot create encrypted wallet without passphrase') + + LOGINFO('***Creating new deterministic wallet') + + # Set up the KDF + if not withEncrypt: + self.kdfKey = None + else: + LOGINFO('(with encryption)') + self.kdf = KdfRomix() + LOGINFO('Target (time,RAM)=(%0.3f,%d)', kdfTargSec, kdfMaxMem) + (mem,niter,salt) = self.computeSystemSpecificKdfParams( \ + kdfTargSec, kdfMaxMem) + self.kdf.usePrecomputedKdfParams(mem, niter, salt) + self.kdfKey = self.kdf.DeriveKey(securePassphrase) + + if not plainRootKey: + # TODO: We should find a source for injecting extra entropy + # At least, Crypto++ grabs from a few different sources, itself + if not extraEntropy: + extraEntropy = SecureBinaryData(0) + plainRootKey = SecureBinaryData().GenerateRandom(32, extraEntropy) + + if not chaincode: + #chaincode = SecureBinaryData().GenerateRandom(32) + # For wallet 1.35a, derive chaincode deterministically from root key + # The root key already has 256 bits of entropy which is excessive, + # anyway. And my original reason for having the chaincode random is + # no longer valid. + chaincode = DeriveChaincodeFromRootKey(plainRootKey) + + + + # Create the root address object + rootAddr = PyBtcAddress().createFromPlainKeyData( \ + plainRootKey, \ + IV16=IV, \ + willBeEncr=withEncrypt, \ + generateIVIfNecessary=True) + rootAddr.markAsRootAddr(chaincode) + + # This does nothing if no encryption + rootAddr.lock(self.kdfKey) + rootAddr.unlock(self.kdfKey) + + firstAddr = rootAddr.extendAddressChain(self.kdfKey) + first160 = firstAddr.getAddr160() + + # Update wallet object with the new data + # NEW IN WALLET VERSION 1.35: unique ID is now based on + # the first chained address: this guarantees that the unique ID + # is based not only on the private key, BUT ALSO THE CHAIN CODE + self.useEncryption = withEncrypt + self.addrMap['ROOT'] = rootAddr + self.addrMap[firstAddr.getAddr160()] = firstAddr + self.uniqueIDBin = (ADDRBYTE + firstAddr.getAddr160()[:5])[::-1] + self.uniqueIDB58 = binary_to_base58(self.uniqueIDBin) + self.labelName = shortLabel[:32] + self.labelDescr = longLabel[:256] + self.lastComputedChainAddr160 = first160 + self.lastComputedChainIndex = firstAddr.chainIndex + self.highestUsedChainIndex = firstAddr.chainIndex-1 + self.wltCreateDate = long(RightNow()) + self.linearAddr160List = [first160] + self.chainIndexMap[firstAddr.chainIndex] = first160 + + # We don't have to worry about atomic file operations when + # creating the wallet: so we just do it naively here. + self.walletPath = newWalletFilePath + if not newWalletFilePath: + shortName = self.labelName .replace(' ','_') + # This was really only needed when we were putting name in filename + #for c in ',?;:\'"?/\\=+-|[]{}<>': + #shortName = shortName.replace(c,'_') + newName = 'armory_%s_.wallet' % self.uniqueIDB58 + self.walletPath = os.path.join(ARMORY_HOME_DIR, newName) + + LOGINFO(' New wallet will be written to: %s', self.walletPath) + newfile = open(self.walletPath, 'wb') + fileData = BinaryPacker() + + # packHeader method writes KDF params and root address + headerBytes = self.packHeader(fileData) + + # We make sure we have byte locations of the two addresses, to start + self.addrMap[first160].walletByteLoc = headerBytes + 21 + + fileData.put(BINARY_CHUNK, '\x00' + first160 + firstAddr.serialize()) + + + # Store the current localtime and blocknumber. Block number is always + # accurate if available, but time may not be exactly right. Whenever + # basing anything on time, please assume that it is up to one day off! + time0,blk0 = getCurrTimeAndBlock() if isActuallyNew else (0,0) + + # Don't forget to sync the C++ wallet object + self.cppWallet = Cpp.BtcWallet() + self.cppWallet.addScrAddress_5_(Hash160ToScrAddr(rootAddr.getAddr160()), \ + time0,blk0,time0,blk0) + self.cppWallet.addScrAddress_5_(Hash160ToScrAddr(first160), \ + time0,blk0,time0,blk0) + + # We might be holding the wallet temporarily and not ready to register it + if doRegisterWithBDM: + TheBDM.registerWallet(self.cppWallet, isFresh=isActuallyNew) # new wallet + + + newfile.write(fileData.getBinaryString()) + newfile.close() + + if not skipBackupFile: + walletFileBackup = self.getWalletPath('backup') + shutil.copy(self.walletPath, walletFileBackup) + + # Lock/unlock to make sure encrypted keys are computed and written to file + if self.useEncryption: + self.unlock(secureKdfOutput=self.kdfKey, Progress=Progress) + + # Let's fill the address pool while we are unlocked + # It will get a lot more expensive if we do it on the next unlock + if doRegisterWithBDM: + self.fillAddressPool(self.addrPoolSize, isActuallyNew=isActuallyNew, + Progress=Progress) + + if self.useEncryption: + self.lock() + + return self + + ############################################################################# + def advanceHighestIndex(self, ct=1): + topIndex = self.highestUsedChainIndex + ct + topIndex = min(topIndex, self.lastComputedChainIndex) + topIndex = max(topIndex, 0) + + self.highestUsedChainIndex = topIndex + self.walletFileSafeUpdate( [[WLT_UPDATE_MODIFY, self.offsetTopUsed, \ + int_to_binary(self.highestUsedChainIndex, widthBytes=8)]]) + self.fillAddressPool() + + ############################################################################# + def rewindHighestIndex(self, ct=1): + self.advanceHighestIndex(-ct) + + + ############################################################################# + def peekNextUnusedAddr160(self): + try: + return self.getAddress160ByChainIndex(self.highestUsedChainIndex+1) + except: + # Not sure why we'd fail, maybe addrPoolSize==0? + return '' + + ############################################################################# + def getNextUnusedAddress(self): + if self.lastComputedChainIndex - self.highestUsedChainIndex < \ + max(self.addrPoolSize-1,1): + self.fillAddressPool(self.addrPoolSize) + + self.advanceHighestIndex(1) + new160 = self.getAddress160ByChainIndex(self.highestUsedChainIndex) + self.addrMap[new160].touch() + self.walletFileSafeUpdate( [[WLT_UPDATE_MODIFY, \ + self.addrMap[new160].walletByteLoc, \ + self.addrMap[new160].serialize()]] ) + return self.addrMap[new160] + + + ############################################################################# + def computeNextAddress(self, addr160=None, isActuallyNew=True, doRegister=True): + """ + Use this to extend the chain beyond the last-computed address. + + We will usually be computing the next address from the tip of the + chain, but I suppose someone messing with the file format may + leave gaps in the chain requiring some to be generated in the middle + (then we can use the addr160 arg to specify which address to extend) + """ + if not addr160: + addr160 = self.lastComputedChainAddr160 + + newAddr = self.addrMap[addr160].extendAddressChain(self.kdfKey) + new160 = newAddr.getAddr160() + newDataLoc = self.walletFileSafeUpdate( \ + [[WLT_UPDATE_ADD, WLT_DATATYPE_KEYDATA, new160, newAddr]]) + self.addrMap[new160] = newAddr + self.addrMap[new160].walletByteLoc = newDataLoc[0] + 21 + + if newAddr.chainIndex > self.lastComputedChainIndex: + self.lastComputedChainAddr160 = new160 + self.lastComputedChainIndex = newAddr.chainIndex + + self.linearAddr160List.append(new160) + self.chainIndexMap[newAddr.chainIndex] = new160 + + # In the future we will enable first/last seen, but not yet + time0,blk0 = getCurrTimeAndBlock() if isActuallyNew else (0,0) + self.cppWallet.addScrAddress_5_(Hash160ToScrAddr(new160), \ + time0,blk0,time0,blk0) + + # For recovery rescans, this method will be called directly by + # the BDM, which may cause a deadlock if we go through the + # thread queue. The calledFromBDM is "permission" to access the + # BDM private methods directly + if doRegister: + if self.calledFromBDM: + TheBDM.registerScrAddr_bdm_direct(Hash160ToScrAddr(new160), timeInfo=isActuallyNew) + else: + # This uses the thread queue, which means the address will be + # registered next time the BDM is not busy + TheBDM.registerScrAddr(Hash160ToScrAddr(new160), isFresh=isActuallyNew) + + return new160 + + ############################################################################# + def fillAddressPool(self, numPool=None, isActuallyNew=True, + doRegister=True, Progress=emptyFunc): + """ + Usually, when we fill the address pool, we are generating addresses + for the first time, and thus there is no chance it's ever seen the + blockchain. However, this method is also used for recovery/import + of wallets, where the address pool has addresses that probably have + transactions already in the blockchain. + """ + if not numPool: + numPool = self.addrPoolSize + + gap = self.lastComputedChainIndex - self.highestUsedChainIndex + numToCreate = max(numPool - gap, 0) + for i in range(numToCreate): + Progress(i+1, numToCreate) + self.computeNextAddress(isActuallyNew=isActuallyNew, + doRegister=doRegister) + #dlgPrg.UpdateHBar(i+1) + + return self.lastComputedChainIndex + + ############################################################################# + def setAddrPoolSize(self, newSize): + if newSize<5: + LOGERROR('Will not allow address pool sizes smaller than 5...') + return + + self.addrPoolSize = newSize + self.fillAddressPool(newSize) + + + ############################################################################# + def getHighestUsedIndex(self): + """ + This only retrieves the stored value, but it may not be correct if, + for instance, the wallet was just imported but has been used before. + """ + return self.highestUsedChainIndex + + + ############################################################################# + def getHighestComputedIndex(self): + """ + This only retrieves the stored value, but it may not be correct if, + for instance, the wallet was just imported but has been used before. + """ + return self.lastComputedChainIndex + + + + ############################################################################# + def detectHighestUsedIndex(self, writeResultToWallet=False, fullscan=False): + """ + This method is used to find the highestUsedChainIndex value of the + wallet WITHIN its address pool. It will NOT extend its address pool + in this search, because it is assumed that the wallet couldn't have + used any addresses it had not calculated yet. + + If you have a wallet IMPORT, though, or a wallet that has been used + before but does not have this information stored with it, then you + should be using the next method: + + self.freshImportFindHighestIndex() + + which will actually extend the address pool as necessary to find the + highest address used. + """ + if not TheBDM.getBDMState()=='BlockchainReady' and not self.calledFromBDM: + LOGERROR('Cannot detect any usage information without the blockchain') + return -1 + + oldSync = self.doBlockchainSync + self.doBlockchainSync = BLOCKCHAIN_READONLY + if fullscan: + # Will initiate rescan if wallet is dirty + self.syncWithBlockchain(self.lastSyncBlockNum) + else: + # Will only use data already scanned, even if wallet is dirty + self.syncWithBlockchainLite(self.lastSyncBlockNum) + self.doBlockchainSync = oldSync + + highestIndex = max(self.highestUsedChainIndex, 0) + for addr in self.getLinearAddrList(withAddrPool=True): + a160 = addr.getAddr160() + if len(self.getAddrTxLedger(a160)) > 0: + highestIndex = max(highestIndex, addr.chainIndex) + + if writeResultToWallet: + self.highestUsedChainIndex = highestIndex + self.walletFileSafeUpdate( [[WLT_UPDATE_MODIFY, self.offsetTopUsed, \ + int_to_binary(highestIndex, widthBytes=8)]]) + + + return highestIndex + + + + + ############################################################################# + @TimeThisFunction + def freshImportFindHighestIndex(self, stepSize=None): + """ + This is much like detectHighestUsedIndex, except this will extend the + address pool as necessary. It assumes that you have a fresh wallet + that has been used before, but was deleted and restored from its root + key and chaincode, and thus we don't know if only 10 or 10,000 addresses + were used. + + If this was an exceptionally active wallet, it's possible that we + may need to manually increase the step size to be sure we find + everything. In fact, there is no way to tell FOR SURE what is the + last addressed used: one must make an assumption that the wallet + never calculated more than X addresses without receiving a payment... + """ + if not stepSize: + stepSize = self.addrPoolSize + + topCompute = 0 + topUsed = 0 + oldPoolSize = self.addrPoolSize + self.addrPoolSize = stepSize + # When we hit the highest address, the topCompute value will extend + # out [stepsize] addresses beyond topUsed, and the topUsed will not + # change, thus escaping the while loop + nWhile = 0 + while topCompute - topUsed < 0.9*stepSize: + topCompute = self.fillAddressPool(stepSize, isActuallyNew=False) + topUsed = self.detectHighestUsedIndex(True) + nWhile += 1 + if nWhile>10000: + raise WalletAddressError('Escaping inf loop in freshImport...') + + + self.addrPoolSize = oldPoolSize + return topUsed + + + ############################################################################# + def writeFreshWalletFile(self, path, newName='', newDescr=''): + newFile = open(path, 'wb') + bp = BinaryPacker() + self.packHeader(bp) + newFile.write(bp.getBinaryString()) + + for addr160,addrObj in self.addrMap.iteritems(): + if not addr160=='ROOT': + newFile.write('\x00' + addr160 + addrObj.serialize()) + + for hashVal,comment in self.commentsMap.iteritems(): + twoByteLength = int_to_binary(len(comment), widthBytes=2) + if len(hashVal)==20: + typestr = int_to_binary(WLT_DATATYPE_ADDRCOMMENT) + newFile.write(typestr + hashVal + twoByteLength + comment) + elif len(hashVal)==32: + typestr = int_to_binary(WLT_DATATYPE_TXCOMMENT) + newFile.write(typestr + hashVal + twoByteLength + comment) + + newFile.close() + + + ############################################################################# + def makeUnencryptedWalletCopy(self, newPath, securePassphrase=None): + + self.writeFreshWalletFile(newPath) + if not self.useEncryption: + return True + + if self.isLocked: + if not securePassphrase: + LOGERROR('Attempted to make unencrypted copy without unlocking') + return False + else: + self.unlock(securePassphrase=SecureBinaryData(securePassphrase)) + + newWlt = PyBtcWallet().readWalletFile(newPath) + newWlt.unlock(self.kdfKey) + newWlt.changeWalletEncryption(None) + + + walletFileBackup = newWlt.getWalletPath('backup') + if os.path.exists(walletFileBackup): + LOGINFO('New wallet created, deleting backup file') + os.remove(walletFileBackup) + return True + + + ############################################################################# + def makeEncryptedWalletCopy(self, newPath, securePassphrase=None): + """ + Unlike the previous method, I can't just copy it if it's unencrypted, + because the target device probably shouldn't be exposed to the + unencrypted wallet. So for that case, we will encrypt the wallet + in place, copy, then remove the encryption. + """ + + if self.useEncryption: + # Encrypted->Encrypted: Easy! + self.writeFreshWalletFile(newPath) + return True + + if not securePassphrase: + LOGERROR("Tried to make encrypted copy, but no passphrase supplied") + return False + + # If we're starting unencrypted...encrypt it in place + (mem,nIter,salt) = self.computeSystemSpecificKdfParams(0.25) + self.changeKdfParams(mem, nIter, salt) + self.changeWalletEncryption(securePassphrase=securePassphrase) + + # Write the encrypted wallet to the target directory + self.writeFreshWalletFile(newPath) + + # Unencrypt the wallet now + self.unlock(securePassphrase=securePassphrase) + self.changeWalletEncryption(None) + return True + + + + + + ############################################################################# + def forkOnlineWallet(self, newWalletFile, shortLabel='', longLabel=''): + """ + Make a copy of this wallet that contains no private key data + """ + if not self.addrMap['ROOT'].hasPrivKey(): + LOGWARN('This wallet is already void of any private key data!') + LOGWARN('Aborting wallet fork operation.') + + onlineWallet = PyBtcWallet() + onlineWallet.fileTypeStr = self.fileTypeStr + onlineWallet.version = self.version + onlineWallet.magicBytes = self.magicBytes + onlineWallet.wltCreateDate = self.wltCreateDate + onlineWallet.useEncryption = False + onlineWallet.watchingOnly = True + + if not shortLabel: + shortLabel = self.labelName + if not longLabel: + longLabel = self.labelDescr + + onlineWallet.labelName = (shortLabel + ' (Watch)')[:32] + onlineWallet.labelDescr = (longLabel + ' (Watching-only copy)')[:256] + + newAddrMap = {} + for addr160,addrObj in self.addrMap.iteritems(): + onlineWallet.addrMap[addr160] = addrObj.copy() + onlineWallet.addrMap[addr160].binPrivKey32_Encr = SecureBinaryData() + onlineWallet.addrMap[addr160].binPrivKey32_Plain = SecureBinaryData() + onlineWallet.addrMap[addr160].binInitVector16 = SecureBinaryData() + onlineWallet.addrMap[addr160].useEncryption = False + onlineWallet.addrMap[addr160].createPrivKeyNextUnlock = False + + onlineWallet.commentsMap = self.commentsMap + onlineWallet.opevalMap = self.opevalMap + + onlineWallet.uniqueIDBin = self.uniqueIDBin + onlineWallet.highestUsedChainIndex = self.highestUsedChainIndex + onlineWallet.lastComputedChainAddr160 = self.lastComputedChainAddr160 + onlineWallet.lastComputedChainIndex = self.lastComputedChainIndex + + onlineWallet.writeFreshWalletFile(newWalletFile, shortLabel, longLabel) + return onlineWallet + + + ############################################################################# + def supplyRootKeyForWatchingOnlyWallet(self, securePlainRootKey32, \ + permanent=False): + """ + If you have a watching only wallet, you might want to upgrade it to a + full wallet by supplying the 32-byte root private key. Generally, this + will be used to make a 'permanent' upgrade to your wallet, and the new + keys will be written to file ( NOTE: you should setup encryption just + after doing this, to make sure that the plaintext keys get wiped from + your wallet file). + + On the other hand, if you don't want this to be a permanent upgrade, + this could potentially be used to maintain a watching only wallet on your + harddrive, and actually plug in your plaintext root key instead of an + encryption password whenever you want sign transactions. + """ + pass + + + ############################################################################# + def touchAddress(self, addr20): + """ + Use this to update your wallet file to recognize the first/last times + seen for the address. This information will improve blockchain search + speed, if it knows not to search transactions that happened before they + were created. + """ + pass + + ############################################################################# + def testKdfComputeTime(self): + """ + Experimentally determines the compute time required by this computer + to execute with the current key-derivation parameters. This may be + useful for when you transfer a wallet to a new computer that has + different speed/memory characteristic. + """ + testPassphrase = SecureBinaryData('This is a simple passphrase') + start = RightNow() + self.kdf.DeriveKey(testPassphrase) + self.testedComputeTime = (RightNow()-start) + return self.testedComputeTime + + ############################################################################# + def serializeKdfParams(self, kdfObj=None, binWidth=256): + """ + Pack key-derivation function parameters into a binary stream. + As of wallet version 1.0, there is only one KDF technique used + in these wallets, and thus we only need to store the parameters + of this KDF. In the future, we may have multiple KDFs and have + to store the selection in this serialization. + """ + if not kdfObj: + kdfObj = self.kdf + + if not kdfObj: + return '\x00'*binWidth + + binPacker = BinaryPacker() + binPacker.put(UINT64, kdfObj.getMemoryReqtBytes()) + binPacker.put(UINT32, kdfObj.getNumIterations()) + binPacker.put(BINARY_CHUNK, kdfObj.getSalt().toBinStr(), width=32) + + kdfStr = binPacker.getBinaryString() + binPacker.put(BINARY_CHUNK, computeChecksum(kdfStr,4), width=4) + padSize = binWidth - binPacker.getSize() + binPacker.put(BINARY_CHUNK, '\x00'*padSize) + + return binPacker.getBinaryString() + + + + ############################################################################# + def unserializeKdfParams(self, toUnpack, binWidth=256): + + if isinstance(toUnpack, BinaryUnpacker): + binUnpacker = toUnpack + else: + binUnpacker = BinaryUnpacker(toUnpack) + + + + allKdfData = binUnpacker.get(BINARY_CHUNK, 44) + kdfChksum = binUnpacker.get(BINARY_CHUNK, 4) + kdfBytes = len(allKdfData) + len(kdfChksum) + padding = binUnpacker.get(BINARY_CHUNK, binWidth-kdfBytes) + + if allKdfData=='\x00'*44: + return None + + fixedKdfData = verifyChecksum(allKdfData, kdfChksum) + if len(fixedKdfData)==0: + raise UnserializeError('Corrupted KDF params, could not fix') + elif not fixedKdfData==allKdfData: + self.walletFileSafeUpdate( \ + [[WLT_UPDATE_MODIFY, self.offsetKdfParams, fixedKdfData]]) + allKdfData = fixedKdfData + LOGWARN('KDF params in wallet were corrupted, but fixed') + + kdfUnpacker = BinaryUnpacker(allKdfData) + mem = kdfUnpacker.get(UINT64) + nIter = kdfUnpacker.get(UINT32) + salt = kdfUnpacker.get(BINARY_CHUNK, 32) + + kdf = KdfRomix(mem, nIter, SecureBinaryData(salt)) + return kdf + + + ############################################################################# + def serializeCryptoParams(self, binWidth=256): + """ + As of wallet version 1.0, all wallets use the exact same encryption types, + so there is nothing to serialize or unserialize. The 256 bytes here may + be used in the future, though. + """ + return '\x00'*binWidth + + ############################################################################# + def unserializeCryptoParams(self, toUnpack, binWidth=256): + """ + As of wallet version 1.0, all wallets use the exact same encryption types, + so there is nothing to serialize or unserialize. The 256 bytes here may + be used in the future, though. + """ + if isinstance(toUnpack, BinaryUnpacker): + binUnpacker = toUnpack + else: + binUnpacker = BinaryUnpacker(toUnpack) + + binUnpacker.get(BINARY_CHUNK, binWidth) + return CryptoAES() + + ############################################################################# + def verifyPassphrase(self, securePassphrase): + """ + Verify a user-submitted passphrase. This passphrase goes into + the key-derivation function to get actual encryption key, which + is what actually needs to be verified + + Since all addresses should have the same encryption, we only need + to verify correctness on the root key + """ + kdfOutput = self.kdf.DeriveKey(securePassphrase) + try: + isValid = self.addrMap['ROOT'].verifyEncryptionKey(kdfOutput) + return isValid + finally: + kdfOutput.destroy() + + + ############################################################################# + def verifyEncryptionKey(self, secureKdfOutput): + """ + Verify the underlying encryption key (from KDF). + Since all addresses should have the same encryption, + we only need to verify correctness on the root key. + """ + return self.addrMap['ROOT'].verifyEncryptionKey(secureKdfOutput) + + + ############################################################################# + def computeSystemSpecificKdfParams(self, targetSec=0.25, maxMem=32*1024*1024): + """ + WARNING!!! DO NOT CHANGE KDF PARAMS AFTER ALREADY ENCRYPTED THE WALLET + By changing them on an already-encrypted wallet, we are going + to lose the original AES256-encryption keys -- which are + uniquely determined by (numIter, memReqt, salt, passphrase) + + Only use this method before you have encrypted your wallet, + in order to determine good KDF parameters based on your + computer's specific speed/memory capabilities. + """ + kdf = KdfRomix() + kdf.computeKdfParams(targetSec, long(maxMem)) + + mem = kdf.getMemoryReqtBytes() + nIter = kdf.getNumIterations() + salt = SecureBinaryData(kdf.getSalt().toBinStr()) + return (mem, nIter, salt) + + ############################################################################# + def restoreKdfParams(self, mem, numIter, secureSalt): + """ + This method should only be used when we are loading an encrypted wallet + from file. DO NOT USE THIS TO CHANGE KDF PARAMETERS. Doing so may + result in data loss! + """ + self.kdf = KdfRomix(mem, numIter, secureSalt) + + + ############################################################################# + def changeKdfParams(self, mem, numIter, salt, securePassphrase=None): + """ + Changing KDF changes the wallet encryption key which means that a KDF + change is essentially the same as an encryption key change. As such, + the wallet must be unlocked if you intend to change an already- + encrypted wallet with KDF. + + TODO: this comment doesn't belong here...where does it go? : + If the KDF is NOT yet setup, this method will do it. Supply the target + compute time, and maximum memory requirements, and the underlying C++ + code will experimentally determine the "hardest" key-derivation params + that will run within the specified time and memory usage on the system + executing this method. You should set the max memory usage very low + (a few kB) for devices like smartphones, which have limited memory + availability. The KDF will then use less memory but more iterations + to achieve the same compute time. + """ + if self.useEncryption: + if not securePassphrase: + LOGERROR('') + LOGERROR('You have requested changing the key-derivation') + LOGERROR('parameters on an already-encrypted wallet, which') + LOGERROR('requires modifying the encryption on this wallet.') + LOGERROR('Please unlock your wallet before attempting to') + LOGERROR('change the KDF parameters.') + raise WalletLockError('Cannot change KDF without unlocking wallet') + elif not self.verifyPassphrase(securePassphrase): + LOGERROR('Incorrect passphrase to unlock wallet') + raise PassphraseError('Incorrect passphrase to unlock wallet') + + secureSalt = SecureBinaryData(salt) + newkdf = KdfRomix(mem, numIter, secureSalt) + bp = BinaryPacker() + bp.put(BINARY_CHUNK, self.serializeKdfParams(newkdf), width=256) + updList = [[WLT_UPDATE_MODIFY, self.offsetKdfParams, bp.getBinaryString()]] + + if not self.useEncryption: + # We may be setting the kdf params before enabling encryption + self.walletFileSafeUpdate(updList) + else: + # Must change the encryption key: and we won't get here unless + # we have a passphrase to use. This call will take the + self.changeWalletEncryption(securePassphrase=securePassphrase, \ + extraFileUpdates=updList, kdfObj=newkdf) + + self.kdf = newkdf + + ############################################################################# + def changeWalletEncryption(self, secureKdfOutput=None, \ + securePassphrase=None, \ + extraFileUpdates=[], + kdfObj=None, Progress=emptyFunc): + """ + Supply the passphrase you would like to use to encrypt this wallet + (or supply the KDF output directly, to skip the passphrase part). + This method will attempt to re-encrypt with the new passphrase. + This fails if the wallet is already locked with a different passphrase. + If encryption is already enabled, please unlock the wallet before + calling this method. + + Make sure you set up the key-derivation function (KDF) before changing + from an unencrypted to an encrypted wallet. An error will be thrown + if you don't. You can use something like the following + + # For a target of 0.05-0.1s compute time: + (mem,nIter,salt) = wlt.computeSystemSpecificKdfParams(0.1) + wlt.changeKdfParams(mem, nIter, salt) + + Use the extraFileUpdates to pass in other changes that need to be + written to the wallet file in the same atomic operation as the + encryption key modifications. + """ + + if not kdfObj: + kdfObj = self.kdf + + oldUsedEncryption = self.useEncryption + if securePassphrase or secureKdfOutput: + newUsesEncryption = True + else: + newUsesEncryption = False + + oldKdfKey = None + if oldUsedEncryption: + if self.isLocked: + raise WalletLockError('Must unlock wallet to change passphrase') + else: + oldKdfKey = self.kdfKey.copy() + + + if newUsesEncryption and not self.kdf: + raise EncryptionError('KDF must be setup before encrypting wallet') + + # Prep the file-update list with extras passed in as argument + walletUpdateInfo = list(extraFileUpdates) + + # Derive the new KDF key if a passphrase was supplied + newKdfKey = secureKdfOutput + if securePassphrase: + newKdfKey = self.kdf.DeriveKey(securePassphrase) + + if oldUsedEncryption and newUsesEncryption and self.verifyEncryptionKey(newKdfKey): + LOGWARN('Attempting to change encryption to same passphrase!') + return # Wallet is encrypted with the new passphrase already + + + # With unlocked key data, put the rest in a try/except/finally block + # To make sure we destroy the temporary kdf outputs + try: + # If keys were previously unencrypted, they will be not have + # initialization vectors and need to be generated before encrypting. + # This is why we have the enableKeyEncryption() call + + if not oldUsedEncryption==newUsesEncryption: + # If there was an encryption change, we must change the flags + # in the wallet file in the same atomic operation as changing + # the stored keys. We can't let them get out of sync. + self.useEncryption = newUsesEncryption + walletUpdateInfo.append(self.createChangeFlagsEntry()) + self.useEncryption = oldUsedEncryption + # Restore the old flag just in case the file write fails + + newAddrMap = {} + i=1 + nAddr = len(self.addrMap) + + for addr160,addr in self.addrMap.iteritems(): + Progress(i, nAddr) + i = i +1 + + newAddrMap[addr160] = addr.copy() + newAddrMap[addr160].enableKeyEncryption(generateIVIfNecessary=True) + newAddrMap[addr160].changeEncryptionKey(oldKdfKey, newKdfKey) + newAddrMap[addr160].walletByteLoc = addr.walletByteLoc + walletUpdateInfo.append( \ + [WLT_UPDATE_MODIFY, addr.walletByteLoc, newAddrMap[addr160].serialize()]) + + + # Try to update the wallet file with the new encrypted key data + updateSuccess = self.walletFileSafeUpdate( walletUpdateInfo ) + + if updateSuccess: + # Finally give the new data to the user + for addr160,addr in newAddrMap.iteritems(): + self.addrMap[addr160] = addr.copy() + + self.useEncryption = newUsesEncryption + if newKdfKey: + self.lock() + self.unlock(newKdfKey, Progress=Progress) + + finally: + # Make sure we always destroy the temporary passphrase results + if newKdfKey: newKdfKey.destroy() + if oldKdfKey: oldKdfKey.destroy() + + ############################################################################# + def getWalletPath(self, nameSuffix=None): + fpath = self.walletPath + + if self.walletPath=='': + fpath = os.path.join(ARMORY_HOME_DIR, 'armory_%s_.wallet' % self.uniqueIDB58) + + if not nameSuffix==None: + pieces = os.path.splitext(fpath) + if not pieces[0].endswith('_'): + fpath = pieces[0] + '_' + nameSuffix + pieces[1] + else: + fpath = pieces[0] + nameSuffix + pieces[1] + return fpath + + + + ############################################################################# + def getCommentForAddress(self, addr160): + if self.commentsMap.has_key(addr160): + return self.commentsMap[addr160] + else: + return '' + + ############################################################################# + def getComment(self, hashVal): + """ + This method is used for both address comments, as well as tx comments + In the first case, use the 20-byte binary pubkeyhash. Use 32-byte tx + hash for the tx-comment case. + """ + if self.commentsMap.has_key(hashVal): + return self.commentsMap[hashVal] + else: + return '' + + ############################################################################# + def setComment(self, hashVal, newComment): + """ + This method is used for both address comments, as well as tx comments + In the first case, use the 20-byte binary pubkeyhash. Use 32-byte tx + hash for the tx-comment case. + """ + updEntry = [] + isNewComment = False + if self.commentsMap.has_key(hashVal): + # If there is already a comment for this address, overwrite it + oldCommentLen = len(self.commentsMap[hashVal]) + oldCommentLoc = self.commentLocs[hashVal] + # The first 23 bytes are the datatype, hashVal, and 2-byte comment size + offset = 1 + len(hashVal) + 2 + updEntry.append([WLT_UPDATE_MODIFY, oldCommentLoc+offset, '\x00'*oldCommentLen]) + else: + isNewComment = True + + + dtype = WLT_DATATYPE_ADDRCOMMENT + if len(hashVal)>20: + dtype = WLT_DATATYPE_TXCOMMENT + + updEntry.append([WLT_UPDATE_ADD, dtype, hashVal, newComment]) + newCommentLoc = self.walletFileSafeUpdate(updEntry) + self.commentsMap[hashVal] = newComment + + # If there was a wallet overwrite, it's location is the first element + self.commentLocs[hashVal] = newCommentLoc[-1] + + + + ############################################################################# + def getAddrCommentIfAvail(self, txHash): + if not TheBDM.getBDMState()=='BlockchainReady': + return self.getComment(txHash) + + # If we haven't extracted relevant addresses for this tx, yet -- do it + if not self.txAddrMap.has_key(txHash): + self.txAddrMap[txHash] = [] + tx = TheBDM.getTxByHash(txHash) + if tx.isInitialized(): + for i in range(tx.getNumTxOut()): + txout = tx.getTxOutCopy(i) + stype = getTxOutScriptType(txout.getScript()) + scrAddr = tx.getScrAddrForTxOut(i) + + if stype in CPP_TXOUT_HAS_ADDRSTR: + addrStr = scrAddr_to_addrStr(scrAddr) + addr160 = addrStr_to_hash160(addrStr)[1] + if self.hasAddr(addr160): + self.txAddrMap[txHash].append(addr160) + else: + LOGERROR("Unrecognized scraddr: " + binary_to_hex(scrAddr)) + + + + addrComments = [] + for a160 in self.txAddrMap[txHash]: + if self.commentsMap.has_key(a160) and '[[' not in self.commentsMap[a160]: + addrComments.append(self.commentsMap[a160]) + + return '; '.join(addrComments) + + + ############################################################################# + def getCommentForLE(self, le): + # Smart comments for LedgerEntry objects: get any direct comments ... + # if none, then grab the one for any associated addresses. + txHash = le.getTxHash() + if self.commentsMap.has_key(txHash): + comment = self.commentsMap[txHash] + else: + # [[ COMMENTS ]] are not meant to be displayed on main ledger + comment = self.getAddrCommentIfAvail(txHash) + if comment.startswith('[[') and comment.endswith(']]'): + comment = '' + + return comment + + + + + + ############################################################################# + def setWalletLabels(self, lshort, llong=''): + self.labelName = lshort + self.labelDescr = llong + toWriteS = lshort.ljust( 32, '\x00') + toWriteL = llong.ljust(256, '\x00') + + updList = [] + updList.append([WLT_UPDATE_MODIFY, self.offsetLabelName, toWriteS]) + updList.append([WLT_UPDATE_MODIFY, self.offsetLabelDescr, toWriteL]) + self.walletFileSafeUpdate(updList) + + + ############################################################################# + def packWalletFlags(self, binPacker): + nFlagBytes = 8 + flags = [False]*nFlagBytes*8 + flags[0] = self.useEncryption + flags[1] = self.watchingOnly + flagsBitset = ''.join([('1' if f else '0') for f in flags]) + binPacker.put(UINT64, bitset_to_int(flagsBitset)) + + ############################################################################# + def createChangeFlagsEntry(self): + """ + Packs up the wallet flags and returns a update-entry that can be included + in a walletFileSafeUpdate call. + """ + bp = BinaryPacker() + self.packWalletFlags(bp) + toWrite = bp.getBinaryString() + return [WLT_UPDATE_MODIFY, self.offsetWltFlags, toWrite] + + ############################################################################# + def unpackWalletFlags(self, toUnpack): + if isinstance(toUnpack, BinaryUnpacker): + flagData = toUnpack + else: + flagData = BinaryUnpacker( toUnpack ) + + wltflags = flagData.get(UINT64, 8) + wltflags = int_to_bitset(wltflags, widthBytes=8) + self.useEncryption = (wltflags[0]=='1') + self.watchingOnly = (wltflags[1]=='1') + + + ############################################################################# + def packHeader(self, binPacker): + if not self.addrMap['ROOT']: + raise WalletAddressError('Cannot serialize uninitialzed wallet!') + + startByte = binPacker.getSize() + + binPacker.put(BINARY_CHUNK, self.fileTypeStr, width=8) + binPacker.put(UINT32, getVersionInt(self.version)) + binPacker.put(BINARY_CHUNK, self.magicBytes, width=4) + + # Wallet info flags + self.offsetWltFlags = binPacker.getSize() - startByte + self.packWalletFlags(binPacker) + + # Binary Unique ID (firstAddr25bytes[:5][::-1]) + binPacker.put(BINARY_CHUNK, self.uniqueIDBin, width=6) + + # Unix time of wallet creations + binPacker.put(UINT64, self.wltCreateDate) + + # User-supplied wallet label (short) + self.offsetLabelName = binPacker.getSize() - startByte + binPacker.put(BINARY_CHUNK, self.labelName , width=32) + + # User-supplied wallet label (long) + self.offsetLabelDescr = binPacker.getSize() - startByte + binPacker.put(BINARY_CHUNK, self.labelDescr, width=256) + + # Highest used address: + self.offsetTopUsed = binPacker.getSize() - startByte + binPacker.put(INT64, self.highestUsedChainIndex) + + # Key-derivation function parameters + self.offsetKdfParams = binPacker.getSize() - startByte + binPacker.put(BINARY_CHUNK, self.serializeKdfParams(), width=256) + + # Wallet encryption parameters (currently nothing to put here) + self.offsetCrypto = binPacker.getSize() - startByte + binPacker.put(BINARY_CHUNK, self.serializeCryptoParams(), width=256) + + # Address-chain root, (base-address for deterministic wallets) + self.offsetRootAddr = binPacker.getSize() - startByte + self.addrMap['ROOT'].walletByteLoc = self.offsetRootAddr + binPacker.put(BINARY_CHUNK, self.addrMap['ROOT'].serialize()) + + # In wallet version 1.0, this next kB is unused -- may be used in future + binPacker.put(BINARY_CHUNK, '\x00'*1024) + return binPacker.getSize() - startByte + + + + + ############################################################################# + def unpackHeader(self, binUnpacker): + """ + Unpacking the header information from a wallet file. See the help text + on the base class, PyBtcWallet, for more information on the wallet + serialization. + """ + self.fileTypeStr = binUnpacker.get(BINARY_CHUNK, 8) + self.version = readVersionInt(binUnpacker.get(UINT32)) + self.magicBytes = binUnpacker.get(BINARY_CHUNK, 4) + + # Decode the bits to get the flags + self.offsetWltFlags = binUnpacker.getPosition() + self.unpackWalletFlags(binUnpacker) + + # This is the first 4 bytes of the 25-byte address-chain-root address + # This includes the network byte (i.e. main network, testnet, namecoin) + self.uniqueIDBin = binUnpacker.get(BINARY_CHUNK, 6) + self.uniqueIDB58 = binary_to_base58(self.uniqueIDBin) + self.wltCreateDate = binUnpacker.get(UINT64) + + # We now have both the magic bytes and network byte + if not self.magicBytes == MAGIC_BYTES: + LOGERROR('Requested wallet is for a different blockchain!') + LOGERROR('Wallet is for: %s ', BLOCKCHAINS[self.magicBytes]) + LOGERROR('ArmoryEngine: %s ', BLOCKCHAINS[MAGIC_BYTES]) + return -1 + if not self.uniqueIDBin[-1] == ADDRBYTE: + LOGERROR('Requested wallet is for a different network!') + LOGERROR('ArmoryEngine: %s ', NETWORKS[ADDRBYTE]) + return -2 + + # User-supplied description/name for wallet + self.offsetLabelName = binUnpacker.getPosition() + self.labelName = binUnpacker.get(BINARY_CHUNK, 32).strip('\x00') + + + # Longer user-supplied description/name for wallet + self.offsetLabelDescr = binUnpacker.getPosition() + self.labelDescr = binUnpacker.get(BINARY_CHUNK, 256).strip('\x00') + + + self.offsetTopUsed = binUnpacker.getPosition() + self.highestUsedChainIndex = binUnpacker.get(INT64) + + + # Read the key-derivation function parameters + self.offsetKdfParams = binUnpacker.getPosition() + self.kdf = self.unserializeKdfParams(binUnpacker) + + # Read the crypto parameters + self.offsetCrypto = binUnpacker.getPosition() + self.crypto = self.unserializeCryptoParams(binUnpacker) + + # Read address-chain root address data + self.offsetRootAddr = binUnpacker.getPosition() + + + rawAddrData = binUnpacker.get(BINARY_CHUNK, self.pybtcaddrSize) + self.addrMap['ROOT'] = PyBtcAddress().unserialize(rawAddrData) + fixedAddrData = self.addrMap['ROOT'].serialize() + if not rawAddrData==fixedAddrData: + self.walletFileSafeUpdate([ \ + [WLT_UPDATE_MODIFY, self.offsetRootAddr, fixedAddrData]]) + + self.addrMap['ROOT'].walletByteLoc = self.offsetRootAddr + if self.useEncryption: + self.addrMap['ROOT'].isLocked = True + self.isLocked = True + + # In wallet version 1.0, this next kB is unused -- may be used in future + binUnpacker.advance(1024) + + # TODO: automatic conversion if the code uses a newer wallet + # version than the wallet... got a manual script, but it + # would be nice to autodetect and correct + #convertVersion + + return 0 #success + + ############################################################################# + def unpackNextEntry(self, binUnpacker): + dtype = binUnpacker.get(UINT8) + hashVal = '' + binData = '' + if dtype==WLT_DATATYPE_KEYDATA: + hashVal = binUnpacker.get(BINARY_CHUNK, 20) + binData = binUnpacker.get(BINARY_CHUNK, self.pybtcaddrSize) + elif dtype==WLT_DATATYPE_ADDRCOMMENT: + hashVal = binUnpacker.get(BINARY_CHUNK, 20) + commentLen = binUnpacker.get(UINT16) + binData = binUnpacker.get(BINARY_CHUNK, commentLen) + elif dtype==WLT_DATATYPE_TXCOMMENT: + hashVal = binUnpacker.get(BINARY_CHUNK, 32) + commentLen = binUnpacker.get(UINT16) + binData = binUnpacker.get(BINARY_CHUNK, commentLen) + elif dtype==WLT_DATATYPE_OPEVAL: + raise NotImplementedError('OP_EVAL not support in wallet yet') + elif dtype==WLT_DATATYPE_DELETED: + deletedLen = binUnpacker.get(UINT16) + binUnpacker.advance(deletedLen) + + + return (dtype, hashVal, binData) + + ############################################################################# + @TimeThisFunction + def readWalletFile(self, wltpath, verifyIntegrity=True, doScanNow=False): + if not os.path.exists(wltpath): + raise FileExistsError("No wallet file:"+wltpath) + + self.__init__() + self.walletPath = wltpath + + if verifyIntegrity: + try: + nError = self.doWalletFileConsistencyCheck() + except KeyDataError, errmsg: + LOGEXCEPT('***ERROR: Wallet file had unfixable errors.') + raise KeyDataError(errmsg) + + + wltfile = open(wltpath, 'rb') + wltdata = BinaryUnpacker(wltfile.read()) + wltfile.close() + + self.cppWallet = Cpp.BtcWallet() + self.unpackHeader(wltdata) + + self.lastComputedChainIndex = -UINT32_MAX + self.lastComputedChainAddr160 = None + while wltdata.getRemainingSize()>0: + byteLocation = wltdata.getPosition() + dtype, hashVal, rawData = self.unpackNextEntry(wltdata) + if dtype==WLT_DATATYPE_KEYDATA: + newAddr = PyBtcAddress() + newAddr.unserialize(rawData) + newAddr.walletByteLoc = byteLocation + 21 + # Fix byte errors in the address data + fixedAddrData = newAddr.serialize() + + if not rawData==fixedAddrData: + self.walletFileSafeUpdate([ \ + [WLT_UPDATE_MODIFY, newAddr.walletByteLoc, fixedAddrData]]) + if newAddr.useEncryption: + newAddr.isLocked = True + self.addrMap[hashVal] = newAddr + if newAddr.chainIndex > self.lastComputedChainIndex: + self.lastComputedChainIndex = newAddr.chainIndex + self.lastComputedChainAddr160 = newAddr.getAddr160() + + if newAddr.chainIndex < -2: + newAddr.chainIndex = -2 + self.hasNegativeImports = True + + self.linearAddr160List.append(newAddr.getAddr160()) + self.chainIndexMap[newAddr.chainIndex] = newAddr.getAddr160() + + # Update the parallel C++ object that scans the blockchain for us + timeRng = newAddr.getTimeRange() + blkRng = newAddr.getBlockRange() + self.cppWallet.addScrAddress_5_(Hash160ToScrAddr(hashVal), \ + timeRng[0], blkRng[0], \ + timeRng[1], blkRng[1]) + + if dtype in (WLT_DATATYPE_ADDRCOMMENT, WLT_DATATYPE_TXCOMMENT): + self.commentsMap[hashVal] = rawData # actually ASCII data, here + self.commentLocs[hashVal] = byteLocation + if dtype==WLT_DATATYPE_OPEVAL: + raise NotImplementedError('OP_EVAL not support in wallet yet') + if dtype==WLT_DATATYPE_DELETED: + pass + + + if (not doScanNow or \ + not TheBDM.getBDMState()=='BlockchainReady' or \ + self.doBlockchainSync==BLOCKCHAIN_DONOTUSE): + pass + else: + self.syncWithBlockchain() + + + ### Update the wallet version if necessary ### + if getVersionInt(self.version) < getVersionInt(PYBTCWALLET_VERSION): + LOGERROR('Wallets older than version 1.35 no longer supported!') + return + + return self + + + + ############################################################################# + def walletFileSafeUpdate(self, updateList): + + """ + The input "toAddDataList" should be a list of triplets, such as: + [ + [WLT_DATA_ADD, WLT_DATATYPE_KEYDATA, addr160_1, PyBtcAddrObj1] + [WLT_DATA_ADD, WLT_DATATYPE_KEYDATA, addr160_2, PyBtcAddrObj2] + [WLT_DATA_MODIFY, modifyStartByte1, binDataForOverwrite1 ] + [WLT_DATA_ADD, WLT_DATATYPE_ADDRCOMMENT, addr160_3, 'Long-term savings'] + [WLT_DATA_MODIFY, modifyStartByte2, binDataForOverwrite2 ] + ] + + The return value is the list of new file byte offsets (from beginning of + the file), that specify the start of each modification made to the + wallet file. For MODIFY fields, this just returns the modifyStartByte + field that was provided as input. For adding data, it specifies the + starting byte of the new field (the DATATYPE byte). We keep this data + in PyBtcAddress objects so that we know where to apply modifications in + case we need to change something, like converting from unencrypted to + encrypted private keys. + + If this method fails, we simply return an empty list. We can check for + an empty list to know if the file update succeeded. + + WHY IS THIS SO COMPLICATED? -- Because it's atomic! + + When we want to add data to the wallet file, we will do so in a completely + recoverable way. We define this method to make sure a backup exists when + we start modifying the file, and keep a flag to identify when the wallet + might be corrupt. If we ever try to load the wallet file and see another + file with the _update_unsuccessful suffix, we should instead just restore + from backup. + + Similarly, we have to update the backup file after updating the main file + so we will use a similar technique with the backup_unsuccessful suffix. + We don't want to rely on a backup if somehow *the backup* got corrupted + and the original file is fine. THEREFORE -- this is implemented in such + a way that the user should know two things: + + (1) No matter when the power goes out, we ALWAYS have a uncorrupted + wallet file, and know which one it is. Either the backup is safe, + or the original is safe. Based on the flag files, we know which + one is guaranteed to be not corrupted. + (2) ALWAYS DO YOUR FILE OPERATIONS BEFORE SETTING DATA IN MEMORY + You must write it to disk FIRST using this SafeUpdate method, + THEN give the new data to the user -- never give it to them + until you are sure that it was written safely to disk. + + Number (2) is easy to screw up because you plan to write the file just + AFTER the data is created and stored in local memory. But an error + might be thrown halfway which is handled higher up, and instead the data + never made it to file. Then there is a risk that the user uses their + new address that never made it into the wallet file. + """ + + if not os.path.exists(self.walletPath): + raise FileExistsError('No wallet file exists to be updated!') + + if len(updateList)==0: + return [] + + # Make sure that the primary and backup files are synced before update + self.doWalletFileConsistencyCheck() + + walletFileBackup = self.getWalletPath('backup') + mainUpdateFlag = self.getWalletPath('update_unsuccessful') + backupUpdateFlag = self.getWalletPath('backup_unsuccessful') + + + # Will be passing back info about all data successfully added + oldWalletSize = os.path.getsize(self.walletPath) + updateLocations = [] + dataToChange = [] + toAppend = BinaryPacker() + + try: + for entry in updateList: + modType = entry[0] + updateInfo = entry[1:] + + if(modType==WLT_UPDATE_ADD): + dtype = updateInfo[0] + updateLocations.append(toAppend.getSize()+oldWalletSize) + if dtype==WLT_DATATYPE_KEYDATA: + if len(updateInfo[1])!=20 or not isinstance(updateInfo[2], PyBtcAddress): + raise Exception('Data type does not match update type') + toAppend.put(UINT8, WLT_DATATYPE_KEYDATA) + toAppend.put(BINARY_CHUNK, updateInfo[1]) + toAppend.put(BINARY_CHUNK, updateInfo[2].serialize()) + + elif dtype in (WLT_DATATYPE_ADDRCOMMENT, WLT_DATATYPE_TXCOMMENT): + if not isinstance(updateInfo[2], str): + raise Exception('Data type does not match update type') + toAppend.put(UINT8, dtype) + toAppend.put(BINARY_CHUNK, updateInfo[1]) + toAppend.put(UINT16, len(updateInfo[2])) + toAppend.put(BINARY_CHUNK, updateInfo[2]) + + elif dtype==WLT_DATATYPE_OPEVAL: + raise Exception('OP_EVAL not support in wallet yet') + + elif(modType==WLT_UPDATE_MODIFY): + updateLocations.append(updateInfo[0]) + dataToChange.append( updateInfo ) + else: + LOGERROR('Unknown wallet-update type!') + raise Exception('Unknown wallet-update type!') + except Exception: + LOGEXCEPT('Bad input to walletFileSafeUpdate') + return [] + + binaryToAppend = toAppend.getBinaryString() + + # We need to safely modify both the main wallet file and backup + # Start with main wallet + touchFile(mainUpdateFlag) + + try: + wltfile = open(self.walletPath, 'ab') + wltfile.write(binaryToAppend) + wltfile.close() + + # This is for unit-testing the atomic-wallet-file-update robustness + if self.interruptTest1: raise InterruptTestError + + wltfile = open(self.walletPath, 'r+b') + for loc,replStr in dataToChange: + wltfile.seek(loc) + wltfile.write(replStr) + wltfile.close() + + except IOError: + LOGEXCEPT('Could not write data to wallet. Permissions?') + shutil.copy(walletFileBackup, self.walletPath) + os.remove(mainUpdateFlag) + return [] + + # Write backup flag before removing main-update flag. If we see + # both flags, we know file IO was interrupted RIGHT HERE + touchFile(backupUpdateFlag) + + # This is for unit-testing the atomic-wallet-file-update robustness + if self.interruptTest2: raise InterruptTestError + + os.remove(mainUpdateFlag) + + # Modify backup + try: + # This is for unit-testing the atomic-wallet-file-update robustness + if self.interruptTest3: raise InterruptTestError + + backupfile = open(walletFileBackup, 'ab') + backupfile.write(binaryToAppend) + backupfile.close() + + backupfile = open(walletFileBackup, 'r+b') + for loc,replStr in dataToChange: + backupfile.seek(loc) + backupfile.write(replStr) + backupfile.close() + + except IOError: + LOGEXCEPT('Could not write backup wallet. Permissions?') + shutil.copy(self.walletPath, walletFileBackup) + os.remove(mainUpdateFlag) + return [] + + os.remove(backupUpdateFlag) + + return updateLocations + + + + ############################################################################# + def doWalletFileConsistencyCheck(self, onlySyncBackup=True): + """ + First we check the file-update flags (files we touched/removed during + file modification operations), and then restore the primary wallet file + and backup file to the exact same state -- we know that at least one of + them is guaranteed to not be corrupt, and we know based on the flags + which one that is -- so we execute the appropriate copy operation. + + ***NOTE: For now, the remaining steps are untested and unused! + + After we have guaranteed that main wallet and backup wallet are the + same, we want to do a check that the data is consistent. We do this + by simply reading in the key-data from the wallet, unserializing it + and reserializing it to see if it matches -- this works due to the + way the PyBtcAddress::unserialize() method works: it verifies the + checksums in the address data, and corrects errors automatically! + And it's part of the unit-tests that serialize/unserialize round-trip + is guaranteed to match for all address types if there's no byte errors. + + If an error is detected, we do a safe-file-modify operation to re-write + the corrected information to the wallet file, in-place. We DO NOT + check comment fields, since they do not have checksums, and are not + critical to protect against byte errors. + """ + + + + if not os.path.exists(self.walletPath): + raise FileExistsError('No wallet file exists to be checked!') + + walletFileBackup = self.getWalletPath('backup') + mainUpdateFlag = self.getWalletPath('update_unsuccessful') + backupUpdateFlag = self.getWalletPath('backup_unsuccessful') + + if not os.path.exists(walletFileBackup): + # We haven't even created a backup file, yet + LOGDEBUG('Creating backup file %s', walletFileBackup) + touchFile(backupUpdateFlag) + shutil.copy(self.walletPath, walletFileBackup) + os.remove(backupUpdateFlag) + + if os.path.exists(backupUpdateFlag) and os.path.exists(mainUpdateFlag): + # Here we actually have a good main file, but backup never succeeded + LOGWARN('***WARNING: error in backup file... how did that happen?') + shutil.copy(self.walletPath, walletFileBackup) + os.remove(mainUpdateFlag) + os.remove(backupUpdateFlag) + elif os.path.exists(mainUpdateFlag): + LOGWARN('***WARNING: last file operation failed! Restoring wallet from backup') + # main wallet file might be corrupt, copy from backup + shutil.copy(walletFileBackup, self.walletPath) + os.remove(mainUpdateFlag) + elif os.path.exists(backupUpdateFlag): + LOGWARN('***WARNING: creation of backup was interrupted -- fixing') + shutil.copy(self.walletPath, walletFileBackup) + os.remove(backupUpdateFlag) + + if onlySyncBackup: + return 0 + + + + + + + ############################################################################# + #def getAddrByIndex(self, i): + #return self.addrMap.values()[i] + + ############################################################################# + def deleteImportedAddress(self, addr160): + """ + We want to overwrite a particular key in the wallet. Before overwriting + the data looks like this: + [ \x00 | <20-byte addr160> | <237-byte keydata> ] + And we want it to look like: + [ \x04 | <2-byte length> | \x00\x00\x00... ] + So we need to construct a wallet-update vector to modify the data + starting at the first byte, replace it with 0x04, specifies how many + bytes are in the deleted entry, and then actually overwrite those + bytes with 0s + """ + + if not self.addrMap[addr160].chainIndex==-2: + raise WalletAddressError('You can only delete imported addresses!') + + overwriteLoc = self.addrMap[addr160].walletByteLoc - 21 + overwriteLen = 20 + self.pybtcaddrSize - 2 + + overwriteBin = '' + overwriteBin += int_to_binary(WLT_DATATYPE_DELETED, widthBytes=1) + overwriteBin += int_to_binary(overwriteLen, widthBytes=2) + overwriteBin += '\x00'*overwriteLen + + self.walletFileSafeUpdate([[WLT_UPDATE_MODIFY, overwriteLoc, overwriteBin]]) + + # IMPORTANT: we need to update the wallet structures to reflect the + # new state of the wallet. This will actually be easiest + # if we just "forget" the current wallet state and re-read + # the wallet from file + wltPath = self.walletPath + self.readWalletFile(wltPath, doScanNow=True) + + + ############################################################################# + def importExternalAddressData(self, privKey=None, privChk=None, \ + pubKey=None, pubChk=None, \ + addr20=None, addrChk=None, \ + firstTime=UINT32_MAX, firstBlk=UINT32_MAX, \ + lastTime=0, lastBlk=0): + """ + This wallet fully supports importing external keys, even though it is + a deterministic wallet: determinism only adds keys to the pool based + on the address-chain, but there's nothing wrong with adding new keys + not on the chain. + + We don't know when this address was created, so we have to set its + first/last-seen times to 0, to make sure we search the whole blockchain + for tx related to it. This data will be updated later after we've done + the search and know for sure when it is "relevant". + (alternatively, if you know it's first-seen time for some reason, you + can supply it as an input, but this seems rare: we don't want to get it + wrong or we could end up missing wallet-relevant transactions) + + DO NOT CALL FROM A BDM THREAD FUNCTION. IT MAY DEADLOCK. + """ + + if self.calledFromBDM: + LOGERROR('Called importExternalAddressData() from BDM method!') + LOGERROR('Don\'t do this!') + return '' + + if not privKey and not self.watchingOnly: + LOGERROR('') + LOGERROR('This wallet is strictly for addresses that you') + LOGERROR('own. You cannot import addresses without the') + LOGERROR('the associated private key. Instead, use a') + LOGERROR('watching-only wallet to import this address.') + LOGERROR('(actually, this is currently, completely disabled)') + raise WalletAddressError('Cannot import non-private-key addresses') + + + + # First do all the necessary type conversions and error corrections + computedPubKey = None + computedAddr20 = None + if privKey: + if isinstance(privKey, str): + privKey = SecureBinaryData(privKey) + + if privChk: + privKey = SecureBinaryData(verifyChecksum(privKey.toBinStr(), privChk)) + + computedPubkey = CryptoECDSA().ComputePublicKey(privKey) + computedAddr20 = convertKeyDataToAddress(pubKey=computedPubkey) + + # If public key is provided, we prep it so we can verify Pub/Priv match + if pubKey: + if isinstance(pubKey, str): + pubKey = SecureBinaryData(pubKey) + if pubChk: + pubKey = SecureBinaryData(verifyChecksum(pubKey.toBinStr(), pubChk)) + + if not computedAddr20: + computedAddr20 = convertKeyDataToAddress(pubKey=pubKey) + + # The 20-byte address (pubkey hash160) should always be a python string + if addr20: + if not isinstance(pubKey, str): + addr20 = addr20.toBinStr() + if addrChk: + addr20 = verifyChecksum(addr20, addrChk) + + + # Now a few sanity checks + if self.addrMap.has_key(addr20): + LOGWARN('This address is already in your wallet!') + return + + #if pubKey and not computedPubkey==pubKey: + #raise ECDSA_Error('Private and public keys to be imported do not match!') + #if addr20 and not computedAddr20==addr20: + #raise ECDSA_Error('Supplied address hash does not match key data!') + + addr20 = computedAddr20 + + if self.addrMap.has_key(addr20): + return None + + # If a private key is supplied and this wallet is encrypted&locked, then + # we have no way to secure the private key without unlocking the wallet. + if self.useEncryption and privKey and not self.kdfKey: + raise WalletLockError('Cannot import private key when wallet is locked!') + + + if privKey: + # For priv key, lots of extra encryption and verification options + newAddr = PyBtcAddress().createFromPlainKeyData( addr160=addr20, \ + plainPrivKey=privKey, publicKey65=computedPubkey, \ + willBeEncr=self.useEncryption, \ + generateIVIfNecessary=self.useEncryption, \ + skipCheck=True, skipPubCompute=True) + if self.useEncryption: + newAddr.lock(self.kdfKey) + newAddr.unlock(self.kdfKey) + elif pubKey: + securePubKey = SecureBinaryData(pubKey) + newAddr = PyBtcAddress().createFromPublicKeyData(securePubKey) + else: + newAddr = PyBtcAddress().createFromPublicKeyHash160(addr20) + + + newAddr.chaincode = SecureBinaryData('\xff'*32) + newAddr.chainIndex = -2 + newAddr.timeRange = [firstTime, lastTime] + newAddr.blkRange = [firstBlk, lastBlk ] + #newAddr.binInitVect16 = SecureBinaryData().GenerateRandom(16) + newAddr160 = newAddr.getAddr160() + + newDataLoc = self.walletFileSafeUpdate( \ + [[WLT_UPDATE_ADD, WLT_DATATYPE_KEYDATA, newAddr160, newAddr]]) + self.addrMap[newAddr160] = newAddr.copy() + self.addrMap[newAddr160].walletByteLoc = newDataLoc[0] + 21 + self.linearAddr160List.append(newAddr160) + if self.useEncryption and self.kdfKey: + self.addrMap[newAddr160].lock(self.kdfKey) + if not self.isLocked: + self.addrMap[newAddr160].unlock(self.kdfKey) + + self.cppWallet.addScrAddress_5_(Hash160ToScrAddr(newAddr160), \ + firstTime, firstBlk, lastTime, lastBlk) + + # The following line MAY deadlock if this method is called from the BDM + # thread. Do not write any BDM methods that calls this method! + TheBDM.registerImportedScrAddr(Hash160ToScrAddr(newAddr160), + firstTime, firstBlk, lastTime, lastBlk) + + + return newAddr160 + + + ############################################################################# + def bulkImportAddresses(self, textBlock, privKeyEndian=BIGENDIAN, \ + sepList=":;'[]()=-_*&^%$#@!,./?\n"): + """ + Attempts to import plaintext key data stored in a file. This method + expects all data to be in hex or Base58: + + 20 bytes / 40 hex chars -- public key hashes + 25 bytes / 50 hex chars -- full binary addresses + 65 bytes / 130 hex chars -- public key + 32 bytes / 64 hex chars -- private key + + 33 or 34 Base58 chars -- address strings + 50 to 52 Base58 chars -- base58-encoded private key + + Since this is python, I don't have to require any particular format: + I can pretty easily break apart the entire file into individual strings, + search for addresses and public keys, then, search for private keys that + correspond to that data. Obviously, simpler is better, but as long as + the data is encoded as in the above list and separated by whitespace or + punctuation, this method should succeed. + + We must throw an error if this is NOT a watching-only address and we + find an address without a private key. We will need to create a + separate watching-only wallet in order to import these keys. + + TODO: will finish this later + """ + + """ + STUB: (AGAIN) I just can't make this work out to be as stupid-proof + as I originally planned. I'll have to put it on hold. + self.__init__() + + newfile = open(filename,'rb') + newdata = newfile.read() + newfile.close() + + # Change all punctuation to the same char so split() works easier + for ch in sepList: + newdata.replace(ch, ' ') + + newdata = newdata.split() + hexChars = '01234567890abcdef' + b58Chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' + DATATYPES = enum( 'UNKNOWN', \ + 'Addr_Hex_20', \ + 'Addr_B58_25', \ + 'PubX_Hex_32', \ + 'PubY_Hex_32', \ + 'PubK_Hex_65', \ + 'Priv_Hex_32', \ + 'Priv_Hex_36', \ + 'Priv_Hex_37', \ + 'Priv_B58_32', \ + 'Priv_B58_37', \ + 'Priv_MiniPriv', \ + 'PubK_Hex_33_Compressed', \ + 'Priv_Hex_33_Compressed') + + DTYPES = enum('Unknown', 'Hash160', 'PubKey', 'PrivKey', 'Byte32', 'Byte33') + + + lastAddr = None + lastPubK = None + lastPriv = None + for theStr in newdata: + if len(theStr)<20: + continue + + hexCount = sum([1 if c in hexChars else 0 for c in theStr]) + b58Count = sum([1 if c in b58Chars else 0 for c in theStr]) + canBeHex = hexCount==len(theStr) + canBeB58 = b58Count==len(theStr) + isHex = canBeHex + isB58 = canBeB58 and not canBeHex + isStr = not isHex and not isB58 + + dataAndType = [DTYPES.Unknown, ''] + if isHex: + binData = hex_to_binary(theStr) + sz = len(binData) + + if sz==20: + dataAndType = [DTYPES.Hash160, binData] + elif sz==25: + dataAndType = [DTYPES.Hash160, binData[1:21]] + elif sz==32: + dataAndType = [DTYPES., binData[1:21]] + elif isB58: + binData = base58_to_binary(theStr) + sz = len(binData) + + + if isHex and sz==40: + elif isHex and sz==50: + dataAndType = [DTYPES.Hash160, hex_to_binary(theStr)[1:21]] + elif isB58 and sz>=31 and sz<=35: + dataAndType = [DTYPES.Hash160, addrStr_to_hash160(theStr)] + elif isHex is sz==130: + dataAndType = [DTYPES.PubKey, hex_to_binary(theStr)] + elif isHex is sz==128: + dataAndType = [DTYPES.PubKey, '\x04'+hex_to_binary(theStr)] + elif isHex is sz==128: + + + + potentialKey = SecureBinaryData('\x04' + piece) + isValid = CryptoECDSA().VerifyPublicKeyValid(potentialKey) + """ + pass + + + + + + ############################################################################# + def checkIfRescanRequired(self): + """ + Returns true is we have to go back to disk/mmap and rescan more than two + weeks worth of blocks + + DO NOT CALL FROM A BDM METHOD. Instead, call directly: + self.bdm.numBlocksToRescan(pywlt.cppWallet) > 2016 + """ + if self.calledFromBDM: + LOGERROR('Called checkIfRescanRequired() from BDM method!') + LOGERROR('Don\'t do this!') + + if TheBDM.getBDMState()=='BlockchainReady': + return (TheBDM.numBlocksToRescan(self.cppWallet) > 2016) + else: + return False + + + + ############################################################################# + def signTxDistProposal(self, txdp, hashcode=1): + if not hashcode==1: + LOGERROR('hashcode!=1 is not supported at this time!') + return + + # If the wallet is locked, we better bail now + if self.isLocked is True and self.kdfKey is None: + raise WalletLockError('Cannot sign tx without unlocking wallet') + + numInputs = len(txdp.pytxObj.inputs) + wltAddr = [] + for index,txin in enumerate(txdp.pytxObj.inputs): + scrType = txdp.scriptTypes[index] + if scrType in CPP_TXOUT_STDSINGLESIG: + scrAddr = txdp.inScrAddrList[index] + addr160 = scrAddr[1:] + if self.hasAddr(addr160) and self.addrMap[addr160].hasPrivKey(): + wltAddr.append( (self.addrMap[addr160], index, 0)) + elif scrType==CPP_TXOUT_MULTISIG: + # Basically the same check but multiple addresses to consider + # STUB -- this branch has never been tested + addrList = getMultisigScriptInfo(txdp.txOutScripts[index])[2] + for addrIdx, addr in enumerate(addrList): + if self.hasAddr(addr) and self.addrMap[addr].hasPrivKey(): + wltAddr.append( (self.addrMap[addr], index, addrIdx) ) + break + + # WltAddr now contains a list of every input we can sign for, and the + # PyBtcAddress object that can be used to sign it. Let's do it. + numMyAddr = len(wltAddr) + LOGDEBUG('Total number of inputs in transaction: %d', numInputs) + LOGDEBUG('Number of inputs that you can sign for: %d', numMyAddr) + + + # Unlock the wallet if necessary, sign inputs + maxChainIndex = -1 + for addrObj,idx, sigIdx in wltAddr: + maxChainIndex = max(maxChainIndex, addrObj.chainIndex) + if addrObj.isLocked: + if self.kdfKey: + if addrObj.createPrivKeyNextUnlock: + self.unlock(self.kdfKey) + else: + addrObj.unlock(self.kdfKey) + else: + self.lock() + raise WalletLockError('Cannot sign tx without unlocking wallet') + + if not addrObj.hasPubKey(): + # Make sure the public key is available for this address + addrObj.binPublicKey65 = \ + CryptoECDSA().ComputePublicKey(addrObj.binPrivKey32_Plain) + + # Copy the script, blank out out all other scripts (assume hashcode==1) + txCopy = PyTx().unserialize(txdp.pytxObj.serialize()) + for i in range(len(txCopy.inputs)): + if not i==idx: + txCopy.inputs[i].binScript = '' + else: + txCopy.inputs[i].binScript = txdp.txOutScripts[i] + + hashCode1 = int_to_binary(hashcode, widthBytes=1) + hashCode4 = int_to_binary(hashcode, widthBytes=4) + preHashMsg = txCopy.serialize() + hashCode4 + signature = addrObj.generateDERSignature(preHashMsg) + hashCode1 + + # Now we attach a binary signature or full script, depending on the type + p2shScript = txdp.p2shScripts[idx] + p2shAppend = '' + if len(p2shScript) > 0: + LOGWARN('Signing for P2SH input') + p2shAppend = serializeBytesWithPushData(p2shScript) + + scrType = txdp.scriptTypes[idx] + if scrType in [CPP_TXOUT_STDPUBKEY33, CPP_TXOUT_STDPUBKEY65]: + # Only need the signature to complete coinbase TxOut + serSignature = serializeBytesWithPushData(signature) + txdp.signatures[idx][0] = serSignature + p2shAppend + elif scrType==CPP_TXOUT_STDHASH160: + # Gotta include the public key, too, for standard TxOuts + pubkey = addrObj.binPublicKey65.toBinStr() + serSig = serializeBytesWithPushData(signature) + serPubKey = serializeBytesWithPushData(pubkey) + txdp.signatures[idx][0] = serSig + serPubKey + p2shAppend + elif txdp.scriptTypes[idx]==TXOUT_SCRIPT_MULTISIG: + # We attach just the sig for multi-sig transactions + serSignature = serializeBytesWithPushData(signature) + txdp.signatures[idx][sigIdx] = serSig + else: + LOGERROR('Unknown txOut script type') + + self.lock() + + prevHighestIndex = self.highestUsedChainIndex + if prevHighestIndex 0 and addrObjPrev.chainIndex > -1: + addrObj.createPrivKeyNextUnlock_IVandKey[0] = \ + addrObjPrev.binInitVect16.copy() + addrObj.createPrivKeyNextUnlock_IVandKey[1] = \ + addrObjPrev.binPrivKey32_Encr.copy() + + addrObj.createPrivKeyNextUnlock_ChainDepth = ChainDepth + + addrObj.unlock(self.kdfKey) + if addrObj.chainIndex > -1: addrObjPrev = addrObj + + if needToSaveAddrAfterUnlock: + updateLoc = addrObj.walletByteLoc + self.walletFileSafeUpdate( [[WLT_UPDATE_MODIFY, + addrObj.walletByteLoc, + addrObj.serialize()]]) + + self.isLocked = False + LOGDEBUG('Unlock succeeded: %s', self.uniqueIDB58) + + ############################################################################ + def lock(self, Progress=emptyFunc): + """ + We assume that we have already set all encryption parameters (such as + IVs for each key) and thus all we need to do is call the "lock" method + on each PyBtcAddress object. + + If wallet is unlocked, try to re-lock addresses, regardless of whether + we have a kdfKey or not. In some circumstances (such as when the addrs + have never been locked before) we will need the key to encrypt them. + However, in most cases, the encrypted versions are already available + and the PyBtcAddress objects can destroy the plaintext keys without + ever needing access to the encryption keys. + + ANY METHOD THAT CALLS THIS MUST CATCH WALLETLOCKERRORS UNLESS YOU ARE + POSITIVE THAT THE KEYS HAVE ALREADY BEEN ENCRYPTED BEFORE, OR ARE + ALREADY SITTING IN THE ENCRYPTED WALLET FILE. PyBtcAddress objects + were designed to do this, but in case of a bug, you don't want the + program crashing with money-bearing private keys sitting in memory only. + + TODO: If things like IVs are not set properly, we should implement + a way to check for this, correct it, and update the wallet + file if necessary + """ + + # Wallet is unlocked, will try to re-lock addresses, regardless of whether + # we have a kdfKey or not. If a key is required, we will throw a + # WalletLockError, and the caller can get the passphrase from the user, + # unlock the wallet, then try locking again. + # NOTE: If we don't have kdfKey, it is set to None, which is the default + # input for PyBtcAddress::lock for "I don't have it". In most + # cases, it is actually possible to lock the wallet without the + # kdfKey because we saved the encrypted versions before unlocking + LOGDEBUG('Attempting to lock wallet: %s', self.uniqueIDB58) + i=1 + nAddr = len(self.addrMap) + try: + for addr160,addrObj in self.addrMap.iteritems(): + Progress(i, nAddr) + i = i +1 + + self.addrMap[addr160].lock(self.kdfKey) + + if self.kdfKey: + self.kdfKey.destroy() + self.kdfKey = None + self.isLocked = True + except WalletLockError: + LOGERROR('Locking wallet requires encryption key. This error') + LOGERROR('Usually occurs on newly-encrypted wallets that have') + LOGERROR('never been encrypted before.') + raise WalletLockError('Unlock with passphrase before locking again') + LOGDEBUG('Wallet locked: %s', self.uniqueIDB58) + + ############################################################################# + def getAddrListSortedByChainIndex(self, withRoot=False): + """ Returns Addr160 list """ + addrList = [] + for addr160 in self.linearAddr160List: + addr=self.addrMap[addr160] + addrList.append( [addr.chainIndex, addr160, addr] ) + + addrList.sort(key=lambda x: x[0]) + return addrList + + ############################################################################# + def getAddrList(self): + """ Returns list of PyBtcAddress objects """ + addrList = [] + for addr160,addrObj in self.addrMap.iteritems(): + if addr160=='ROOT': + continue + # I assume these will be references, not copies + addrList.append( addrObj ) + return addrList + + + ############################################################################# + def getLinearAddrList(self, withImported=True, withAddrPool=False): + """ + Retrieves a list of addresses, by hash, in the order they + appear in the wallet file. Can ignore the imported addresses + to get only chained addresses, if necessary. + + I could do this with one list comprehension, but it would be long. + I'm resisting the urge... + """ + addrList = [] + for a160 in self.linearAddr160List: + addr = self.addrMap[a160] + if not a160=='ROOT' and (withImported or addr.chainIndex>=0): + # Either we want imported addresses, or this isn't one + if (withAddrPool or addr.chainIndex<=self.highestUsedChainIndex): + addrList.append(addr) + + return addrList + + + ############################################################################# + def getAddress160ByChainIndex(self, desiredIdx): + """ + It should be safe to assume that if the index is less than the highest + computed, it will be in the chainIndexMap, but I don't like making such + assumptions. Perhaps something went wrong with the wallet, or it was + manually reconstructed and has holes in the chain. We will regenerate + addresses up to that point, if necessary (but nothing past the value + self.lastComputedChainIndex. + """ + if desiredIdx>self.lastComputedChainIndex or desiredIdx<0: + # I removed the option for fillPoolIfNecessary, because of the risk + # that a bug may lead to generation of billions of addresses, which + # would saturate the system's resources and fill the HDD. + raise WalletAddressError('Chain index is out of range') + + + if self.chainIndexMap.has_key(desiredIdx): + return self.chainIndexMap[desiredIdx] + else: + # Somehow the address isn't here, even though it is less than the + # last computed index + closestIdx = 0 + for idx,addr160 in self.chainIndexMap.iteritems(): + if closestIdx- Building log file...
    ' + Progress(self.UIreport) + + if errorCode < 0: + if errorCode == -1: + errorstr = \ + 'ERROR: Invalid path, or file is not a valid Armory wallet\r\n' + elif errorCode == -2: + errorstr = \ + 'ERROR: file I/O failure. Do you have proper credentials?\r\n' + elif errorCode == -3: + errorstr = \ + 'ERROR: This wallet file is for another network/blockchain\r\n' + elif errorCode == -4: + errorstr = \ + 'ERROR: invalid or missing passphrase for encrypted wallet\r\n' + elif errorCode == -10: + errorstr = 'ERROR: no kdf parameters available\r\n' + elif errorCode == -12: + errorstr = 'ERROR: failed to unlock root key\r\n' + + self.strOutput.append(' %s' % (errorstr)) + + self.UIreport = self.UIreport + errorstr + Progress(self.UIreport) + return self.FinalizeLog(errorCode, Progress, returnError) + + + if returnError == 'Dict': + errors = {} + errors['byteError'] = self.byteError + errors['brokenSequence'] = self.brokenSequence + errors['sequenceGaps'] = self.sequenceGaps + errors['forkedPublicKeyChain'] = self.forkedPublicKeyChain + errors['chainCodeCorruption'] = self.chainCodeCorruption + errors['invalidPubKey'] = self.invalidPubKey + errors['missingPubKey'] = self.missingPubKey + errors['hashValMismatch'] = self.hashValMismatch + errors['unmatchedPair'] = self.unmatchedPair + errors['misc'] = self.misc + errors['importedErr'] = self.importedErr + errors['negativeImports'] = self.negativeImports + errors['nErrors'] = nErrors + errors['privMult'] = self.privKeyMultipliers + + return errors + + + if self.newwalletPath != None: + self.LogPath = self.newwalletPath + ".log" + else: + self.LogPath = self.WalletPath + ".log" + basename = os.path.basename(self.WalletPath) + + if self.smode == RECOVERMODE.Check: + self.strOutput.append('Checking wallet %s (ID: %s) on %s \r\n' % \ + ('\'' + self.labelName + '\'' \ + if len(self.labelName) != 0 else basename, \ + self.UID, ctime())) + else: + self.strOutput.append('Analyzing wallet %s (ID: %s) on %s \r\n' % \ + ('\'' + self.labelName + '\'' if \ + len(self.labelName) != 0 else basename, \ + self.UID, ctime())) + self.strOutput.append('Using recovery mode: %d\r\n' % (self.smode)) + + if self.WO: + self.strOutput.append('Wallet is Watch Only\r\n') + else: + self.strOutput.append('Wallet contains private keys ') + if self.useEnc == 0: + self.strOutput.append('and doesn\'t use encryption\r\n') + else: + self.strOutput.append('and uses encryption\r\n') + + # If all we have is these logs, should know num used, if avail + self.strOutput.append('Highest used index: %d\r\n' % self.highestUsed) + + + if self.smode == RECOVERMODE.Stripped and not self.WO: + self.strOutput.append(' Recovered root key and chaincode, stripped recovery done.') + return self.FinalizeLog(errorCode, Progress, returnError) + + self.strOutput.append('The wallet file is %d bytes, of which %d bytes were read\r\n' % \ + (self.fileSize, self.dataLastOffset)) + self.strOutput.append('%d chain addresses, %d imported keys and %d comments were found\r\n' % \ + (self.naddress, self.nImports, self.ncomments)) + + nErrors = 0 + #### chained keys + self.strOutput.append('Found %d chained address entries\r\n' \ + % (self.naddress)) + + if len(self.byteError) == 0: + self.strOutput.append('No byte errors were found in the wallet file\r\n') + else: + nErrors = nErrors + len(self.byteError) + self.strOutput.append('%d byte errors were found in the wallet file:\r\n' % (len(self.byteError))) + for i in range(0, len(self.byteError)): + self.strOutput.append(' chainIndex %s at file offset %s\r\n' \ + % (self.byteError[i][0], self.byteError[i][1])) + + + if len(self.brokenSequence) == 0: + self.strOutput.append('All chained addresses were arranged sequentially in the wallet file\r\n') + else: + #nErrors = nErrors + len(self.brokenSequence) + self.strOutput.append('The following %d addresses were not arranged sequentially in the wallet file:\r\n' % \ + (len(self.brokenSequence))) + for i in range(0, len(self.brokenSequence)): + self.strOutput.append(' chainIndex %s at file offset %s\r\n' % \ + (self.brokenSequence[i][0], self.brokenSequence[i][1])) + + if len(self.sequenceGaps) == 0: + self.strOutput.append('There are no gaps in the address chain\r\n') + else: + nErrors = nErrors + len(self.sequenceGaps) + self.strOutput.append('Found %d gaps in the address chain:\r\n' % \ + (len(self.sequenceGaps))) + for i in range(0, len(self.sequenceGaps)): + self.strOutput.append(' from chainIndex %s to %s\r\n' % \ + (self.sequenceGaps[i][0], self.sequenceGaps[i][1])) + + if len(self.forkedPublicKeyChain) == 0: + self.strOutput.append('No chained address fork was found\r\n') + else: + nErrors = nErrors + len(self.forkedPublicKeyChain) + self.strOutput.append('Found %d forks within the address chain:\r\n' \ + % (len(self.forkedPublicKeyChain))) + for i in range(0, len(self.forkedPublicKeyChain)): + self.strOutput.append(' at chainIndex %s, file offset %s\r\n' \ + % (self.forkedPublicKeyChain[i][0], \ + self.forkedPublicKeyChain[i][1])) + + if len(self.chainCodeCorruption) == 0: + self.strOutput.append('No chaincode corruption was found\r\n') + else: + nErrors = nErrors + len(self.chainCodeCorruption) + self.strOutput.append(' \ + Found %d instances of chaincode corruption:\r\n' % \ + (len(self.chainCodeCorruption))) + for i in range(0, len(self.chainCodeCorruption)): + self.strOutput.append(' at chainIndex %s, file offset %s\r\n' % (self.chainCodeCorruption[i][0], \ + self.chainCodeCorruption[i][1])) + + if len(self.invalidPubKey) == 0: + self.strOutput.append('All chained public keys are valid EC points\r\n') + else: + nErrors = nErrors + len(self.invalidPubKey) + self.strOutput.append('%d chained public keys are invalid EC points:\r\n' % (len(self.invalidPubKey))) + for i in range(0, len(self.invalidPubKey)): + self.strOutput.append(' at chainIndex %s, file offset %s' % \ + (self.invalidPubKey[i][0], \ + self.invalidPubKey[i][1])) + + if len(self.missingPubKey) == 0: + self.strOutput.append('No chained public key is missing\r\n') + else: + nErrors = nErrors + len(self.missingPubKey) + self.strOutput.append('%d chained public keys are missing:\r\n' % \ + (len(self.missingPubKey))) + for i in range(0, len(self.missingPubKey)): + self.strOutput.append(' at chainIndex %s, file offset %s' % \ + (self.missingPubKey[i][0], \ + self.missingPubKey[i][1])) + + if len(self.hashValMismatch) == 0: + self.strOutput.append('All entries were saved under their matching hashVal\r\n') + else: + nErrors = nErrors + len(self.hashValMismatch) + self.strOutput.append('%d address entries were saved under an erroneous hashVal:\r\n' % \ + (len(self.hashValMismatch))) + for i in range(0, len(self.hashValMismatch)): + self.strOutput.append(' at chainIndex %s, file offset %s\r\n' \ + % (self.hashValMismatch[i][0], \ + self.hashValMismatch[i][1])) + + if not self.WO: + if len(self.unmatchedPair) == 0: + self.strOutput.append('All chained public keys match their respective private keys\r\n') + else: + nErrors = nErrors + len(self.unmatchedPair) + self.strOutput.append('%d public keys do not match their respective private key:\r\n' % \ + (len(self.unmatchedPair))) + for i in range(0, len(self.unmatchedPair)): + self.strOutput.append(' at chainIndex %s, file offset %s\r\n' \ + % (self.unmatchedPair[i][0], + self.unmatchedPair[i][1])) + + if len(self.misc) > 0: + nErrors = nErrors + len(self.misc) + self.strOutput.append('%d miscalleneous errors were found:\r\n' % \ + (len(self.misc))) + for i in range(0, len(self.misc)): + self.strOutput.append(' %s\r\n' % self.misc[i]) + + #### imported keys + self.strOutput.append('Found %d imported address entries\r\n' % \ + (self.nImports)) + + if self.nImports > 0: + if len(self.importedErr) == 0: + self.strOutput.append('No errors were found within the imported address entries\r\n') + else: + nErrors = nErrors + len(self.importedErr) + self.strOutput.append('%d errors were found within the imported address entries:\r\n' % \ + (len(self.importedErr))) + for i in range(0, len(self.importedErr)): + self.strOutput.append(' %s\r\n' % (self.importedErr[i])) + + if len(self.privKeyMultipliers) > 0: + self.strOutput.append('Inconsistent private keys were found!\r\n') + self.strOutput.append('Logging Multipliers (no private key data):\r\n') + + for i in range(0, len(self.privKeyMultipliers)): + self.strOutput.append(' %s\r\n' % (self.privKeyMultipliers[i])) + + ####TODO: comments error log + self.strOutput.append('%d errors were found\r\n' % (nErrors)) + #self.UIreport += '- %d errors were found
    ' % \ + #( ' style="color: red;"' if nErrors else '', nErrors) + return self.FinalizeLog(errorCode, Progress, returnError) + + + ############################################################################ + def FinalizeLog(self, errorcode, Progress, returnError=False): + + self.EndLog = '' + + if errorcode < 0: + self.strOutput.append( \ + 'Recovery failed: error code %d\r\n\r\n\r\n' % (errorcode)) + + self.EndLog = '- Recovery failed: error code %d
    ' % \ + (errorcode) + Progress(self.UIreport + self.EndLog) + return errorcode + else: + + self.strOutput.append('Recovery done\r\n\r\n\r\n') + self.EndLog = self.EndLog + '- Recovery done
    ' + if self.newwalletPath: self.EndLog = self.EndLog + \ + '
    Recovered wallet saved at:
    - %s
    ' % \ + (self.newwalletPath) + Progress(self.UIreport + self.EndLog) + + self.strOutput.append('\r\n\r\n\r\n') + + if not returnError: + self.EndLog = self.EndLog + '
    Recovery log saved at:
    - %s
    ' \ + % (self.LogPath) + Progress(self.UIreport + self.EndLog, True) + + self.logfile = open(self.LogPath, 'ab') + + for s in self.strOutput: + self.logfile.write(s) + + self.logfile.close() + + return errorcode + else: + return [errorcode, self.strOutput] + + ############################################################################ + def RecoverWallet(self, WalletPath, Passphrase=None, Mode=RECOVERMODE.Bare, + returnError=False, Progress=emptyFunc): + + return self.ProcessWallet(WalletPath, None, Passphrase, Mode, None, + returnError, async=True, Progress=Progress) + + ############################################################################ + @AllowAsync + def ProcessWallet(self, WalletPath=None, Wallet=None, Passphrase=None, + Mode=RECOVERMODE.Stripped, prgAt=None, + returnError=False, Progress=emptyFunc): + + self.__init__() + + if not WalletPath: + if not Wallet: return -1 + WalletPath = Wallet.walletPath + + self.WalletPath = WalletPath + + RecoveredWallet = None + SecurePassphrase = None + + self.naddress = 0 + #holds address chain sequentially, ordered by chainIndex, as lists: + #[addrEntry, hashVal, naddress, byteLocation, rawData] + #validChainDict uses the same list format, and is used to hold computed + #valid chain address entries + addrDict = {} + validChainDict = {} + + self.nImports = 0 + #holds imported address, by order of apparition, as lists: + #[addrEntry, hashVal, byteLocation, rawData] + importedDict = {} + + self.ncomments = 0 + #holds all comments entries, as lists: [rawData, hashVal, dtype] + commentDict = {} + + #in meta mode, the wallet's short and long labels are saved in entries + #shortLabel and longLabel, pointing to a single str object + + rmode = Mode + self.smode = Mode + if Mode == RECOVERMODE.Meta: + self.WO = True + + self.fileSize=0 + if not os.path.exists(WalletPath): + return self.BuildLogFile(-1, Progress, returnError) + else: self.fileSize = os.path.getsize(WalletPath) + + toRecover = PyBtcWallet() + toRecover.walletPath = WalletPath + + #consistency check + try: + toRecover.doWalletFileConsistencyCheck() + except: + #I expect 99% of errors raised here would be by Python's "os" module + #failing an I/O operations, mainly for lack of credentials. + LOGEXCEPT('') + return self.BuildLogFile(-2, Progress, returnError) + + #fetch wallet content + wltfile = open(WalletPath, 'rb') + wltdata = BinaryUnpacker(wltfile.read()) + wltfile.close() + + #unpack header + try: + returned = toRecover.unpackHeader(wltdata) + except: + LOGEXCEPT('') + #Raises here come from invalid header parsing, meaning the file isn't + #an Armory wallet to begin with, or the header is fubar + return self.BuildLogFile(-1, Progress, returnError) + + self.UID = toRecover.uniqueIDB58 + self.labelName = toRecover.labelName + self.highestUsed = toRecover.highestUsedChainIndex + #TODO: try to salvage broken header + # compare uniqueIDB58 with recovered wallet + + self.UIreport = 'Analyzing wallet: %s
    ' % (toRecover.labelName \ + if len(toRecover.labelName) != 0 \ + else os.path.basename(WalletPath)) + Progress(self.UIreport) + + if returned < 0: return self.BuildLogFile(-3, Progress, returnError) + + self.useEnc=0 + rootAddr = toRecover.addrMap['ROOT'] + + #check for private keys (watch only?) + if toRecover.watchingOnly is True: + self.WO = True + + if not self.WO: + #check if wallet is encrypted + if toRecover.isLocked==True and rmode != RECOVERMODE.Meta: + ''' + Passphrase can one of be 3 things: + 1) str + 2) SecureBinaryData + 3) a function that will return the passphrase (think user prompt) + ''' + if isinstance(Passphrase, str): + SecurePassphrase = SecureBinaryData(Passphrase) + Passphrase = '' + elif isinstance(Passphrase, SecureBinaryData): + SecurePassphrase = Passphrase.copy() + elif hasattr(Passphrase, '__call__'): + getPassphrase = Passphrase(toRecover) + + if isinstance(getPassphrase, SecureBinaryData): + SecurePassphrase = getPassphrase.copy() + getPassphrase.destroy() + else: + if rmode==RECOVERMODE.Check: + self.WO = True + else: + return self.BuildLogFile(-4, Progress, returnError) + else: + if rmode==RECOVERMODE.Check: + self.WO = True + else: + return self.BuildLogFile(-4, Progress, returnError) + + #if the wallet uses encryption, unlock ROOT and verify it + if toRecover.isLocked and not self.WO: + self.useEnc=1 + if not toRecover.kdf: + SecurePassphrase.destroy() + return self.BuildLogFile(-10, Progress, returnError) + + secureKdfOutput = toRecover.kdf.DeriveKey(SecurePassphrase) + + if not toRecover.verifyEncryptionKey(secureKdfOutput): + SecurePassphrase.destroy() + secureKdfOutput.destroy() + return self.BuildLogFile(-4, Progress, returnError) + + #DlgUnlockWallet may have filled kdfKey. Since this code can be + #called with no UI and just the passphrase, gotta make sure this + #member is cleaned up before setting it + if isinstance(toRecover.kdfKey, SecureBinaryData): + toRecover.kdfKey.destroy() + toRecover.kdfKey = secureKdfOutput + + try: + rootAddr.unlock(toRecover.kdfKey) + except: + LOGEXCEPT('') + SecurePassphrase.destroy() + return self.BuildLogFile(-12, Progress, returnError) + else: + SecurePassphrase = None + + #stripped recovery, we're done + if rmode == RECOVERMODE.Stripped: + RecoveredWallet = self.createRecoveredWallet(toRecover, rootAddr, \ + SecurePassphrase, Progress, returnError) + rootAddr.lock() + if SecurePassphrase: SecurePassphrase.destroy() + + if not isinstance(RecoveredWallet, PyBtcWallet): + return RecoveredWallet + + if isinstance(toRecover.kdfKey, SecureBinaryData): + toRecover.kdfKey.destroy() + if isinstance(RecoveredWallet.kdfKey, SecureBinaryData): + RecoveredWallet.kdfKey.destroy() + + #stripped recovery, we are done + return self.BuildLogFile(1, Progress, returnError) + + if rmode == RECOVERMODE.Meta: + commentDict["shortLabel"] = toRecover.labelName + commentDict["longLabel"] = toRecover.labelDescr + + ''' + address entries may not be saved sequentially. To check the address + chain is valid, all addresses will be unserialized and saved by + chainIndex in addrDict. Then all addresses will be checked for + consistency and proper chaining. Imported private keys and comments + will be added at the tail of the file. + ''' + + UIupdate = "" + self.misc = [] #miscellaneous errors + self.rawError = [] #raw binary errors' + + if prgAt: + prgAt_in = prgAt[0] + prgAt[0] = prgAt_in +prgAt[1]*0.01 + + + #move on to wallet body + toRecover.lastComputedChainIndex = -UINT32_MAX + toRecover.lastComputedChainAddr160 = None + while wltdata.getRemainingSize()>0: + byteLocation = wltdata.getPosition() + + + UIupdate = '- Reading wallet: %0.1f/%0.1f kB
    ' % \ + (float(byteLocation)/KILOBYTE, float(self.fileSize)/KILOBYTE) + if Progress(self.UIreport + UIupdate) == 0: + if SecurePassphrase: SecurePassphrase.destroy() + if toRecover.kdfKey: toRecover.kdfKey.destroy() + rootAddr.lock() + return 0 + + newAddr = None + try: + dtype, hashVal, rawData = toRecover.unpackNextEntry(wltdata) + except NotImplementedError: + self.misc.append('Found OPEVAL data entry at offest: %d' % \ + (byteLocation)) + pass + except: + LOGEXCEPT('') + #Error in the binary file content. Try to skip an entry size amount + #of bytes to find a valid entry. + self.rawError.append('Raw binary error found at offset: %d' \ + % (byteLocation)) + + dtype, hashVal, rawData, dataList = self.LookForFurtherEntry( \ + wltdata, byteLocation) + + if dtype is None: + #could not find anymore valid data + self.rawError.append('Could not find anymore valid data past \ + offset: %d' % (byteLocation)) + break + + byteLocation = dataList[1] + self.rawError.append(' Found a valid data entry at offset: %d' \ + % (byteLocation)) + + if dataList[0] == 0: + #found an address entry, but it has checksum errors + newAddr = dataList[2] + + if dtype==WLT_DATATYPE_KEYDATA: + if rmode != RECOVERMODE.Meta: + if newAddr is None: + newAddr = PyBtcAddress() + try: + newAddr.unserialize(rawData) + except: + LOGEXCEPT('') + #unserialize error, try to recover the entry + self.rawError.append( \ + ' Found checksum errors in address entry starting at offset: %d' \ + % (byteLocation)) + + try: + newAddr, chksumError = \ + self.addrEntry_unserialize_recover(rawData) + self.rawError.append(' Recovered damaged entry') + except: + LOGEXCEPT('') + #failed to recover the entry + self.rawError.append( \ + ' Could not recover damaged entry') + newAddr = None + + if newAddr is not None: + newAddr.walletByteLoc = byteLocation + 21 + + if newAddr.useEncryption: + newAddr.isLocked = True + + #save address entry count in the file, to check + #for entry sequence + if newAddr.chainIndex > -2 : + addrDict[newAddr.chainIndex] = \ + [newAddr, hashVal, self.naddress, byteLocation, rawData] + self.naddress = self.naddress +1 + else: + importedDict[self.nImports] = \ + [newAddr, hashVal, byteLocation, rawData] + self.nImports = self.nImports +1 + + else: self.naddress = self.naddress +1 + + + elif dtype in (WLT_DATATYPE_ADDRCOMMENT, WLT_DATATYPE_TXCOMMENT): + #if rmode > 2: + if rmode in [RECOVERMODE.Full, RECOVERMODE.Meta, RECOVERMODE.Check]: + commentDict[self.ncomments] = [rawData, hashVal, dtype] + self.ncomments = self.ncomments +1 + + elif dtype==WLT_DATATYPE_OPEVAL: + self.misc.append('Found OPEVAL data entry at offest: %d' % \ + (byteLocation)) + pass + elif dtype==WLT_DATATYPE_DELETED: + pass + else: + self.misc.append('Found unknown data entry type at offset: %d' % \ + (byteLocation)) + #TODO: try same trick as recovering from unpack errors? + + self.dataLastOffset = wltdata.getPosition() + UIupdate = '- Reading wallet: %0.1f/%0.1f kB
    ' % \ + (float(self.dataLastOffset)/KILOBYTE, float(self.fileSize)/KILOBYTE) + self.UIreport = self.UIreport + UIupdate + + #verify the root address is derived from the root key + if not self.WO: + testroot = PyBtcAddress().createFromPlainKeyData( \ + rootAddr.binPrivKey32_Plain, None, None, \ + generateIVIfNecessary=True) + if rootAddr.addrStr20 != testroot.addrStr20: + self.rawError.append( \ + ' root address was not derived from the root key') + + + #verify chainIndex 0 was derived from the root address + firstAddr = rootAddr.extendAddressChain(toRecover.kdfKey) + if firstAddr.addrStr20 != addrDict[0][0].addrStr20: + self.rawError.append(' chainIndex 0 was not derived from the \ + root address') + + testroot.lock() + + if rmode != RECOVERMODE.Meta: + currSequence = addrDict[0][2] + chaincode = addrDict[0][0].chaincode.toHexStr() + else: + currSequence = None + chaincode = None + commentDict['naddress'] = self.naddress + self.naddress = 0 + commentDict['ncomments'] = self.ncomments + + if prgAt: + prgTotal = len(addrDict) + len(importedDict) + len(commentDict) + + + + #chained key pairs. for rmode is 4, no need to skip this part, + #naddress will be 0 + n=0 + for i in addrDict: + entrylist = [] + entrylist = list(addrDict[i]) + newAddr = entrylist[0] + rawData = entrylist[4] + byteLocation = entrylist[3] + + n = n+1 + UIupdate = '- Processing address entries: %d/%d
    ' % \ + (n, self.naddress) + if Progress(self.UIreport + UIupdate) == 0: + if SecurePassphrase: SecurePassphrase.destroy() + if toRecover.kdfKey: toRecover.kdfKey.destroy() + rootAddr.lock() + return 0 + if prgAt: + prgAt[0] = prgAt_in + (0.01 + 0.99*n/prgTotal)*prgAt[1] + + # Fix byte errors in the address data + fixedAddrData = newAddr.serialize() + if not rawData==fixedAddrData: + self.byteError.append([newAddr.chainIndex, byteLocation]) + fixedAddr = PyBtcAddress() + fixedAddr.unserialize(fixedAddrData) + newAddr = PyBtcAddress() + newAddr.unserialize(fixedAddrData) + entrylist[0] = newAddr + addrDict[i] = entrylist + + #check public key is a valid EC point + if newAddr.hasPubKey(): + if not CryptoECDSA().VerifyPublicKeyValid(newAddr.binPublicKey65): + self.invalidPubKey.append([newAddr.chainIndex, byteLocation]) + else: self.missingPubKey.append([newAddr.chainIndex, byteLocation]) + + #check chaincode consistency + newCC = newAddr.chaincode.toHexStr() + if newCC != chaincode: + self.chainCodeCorruption.append([newAddr.chainIndex, byteLocation]) + + #check the address entry sequence + nextSequence = entrylist[2] + if nextSequence != currSequence: + if (nextSequence - currSequence) != 1: + self.brokenSequence.append([newAddr.chainIndex, byteLocation]) + currSequence = nextSequence + + #check for gaps in the sequence + isPubForked = False + if newAddr.chainIndex > 0: + seq = newAddr.chainIndex -1 + prevEntry = [] + while seq > -1: + if seq in addrDict: break + seq = seq -1 + + prevEntry = list(addrDict[seq]) + prevAddr = prevEntry[0] + + gap = newAddr.chainIndex - seq + if gap > 1: + self.sequenceGaps.append([seq, newAddr.chainIndex]) + + #check public address chain + if newAddr.hasPubKey(): + cid = 0 + extended = prevAddr.binPublicKey65 + while cid < gap: + extended = CryptoECDSA().ComputeChainedPublicKey( \ + extended, prevAddr.chaincode) + cid = cid +1 + + if extended.toHexStr() != newAddr.binPublicKey65.toHexStr(): + self.forkedPublicKeyChain.append([newAddr.chainIndex, \ + byteLocation]) + isPubForked = True + + + if not self.WO: + #not a watch only wallet, check private/public key chaining and + #integrity + + if newAddr.useEncryption != toRecover.useEncryption: + if newAddr.useEncryption: + self.misc.append('Encrypted address entry in a non encrypted \ + wallet at chainIndex %d in wallet %s' % \ + (newAddr.chainIndex, os.path.basename( \ + WalletPath))) + else: + self.misc.append('Unencrypted address entry in an encrypted wallet at chainIndex %d in wallet %s' % \ + (newAddr.chainIndex, os.path.basename( \ + WalletPath))) + + keymismatch=0 + """ + 0: public key matches private key + 1: public key doesn't match private key + 2: private key is missing (encrypted) + 3: public key is missing + 4: private key is missing (unencrypted) + """ + if not newAddr.hasPrivKey(): + #entry has no private key + keymismatch=2 + + if not newAddr.useEncryption: + #uncomputed private key in a non encrypted wallet? + #definitely not supposed to happen + keymismatch = 4 + self.misc.append('Uncomputed private key in unencrypted wallet at chainIndex %d in wallet %s' \ + % (newAddr.chainIndex, os.path.basename \ + (WalletPath))) + else: + self.misc.append('Missing private key is not flagged for computation at chainIndex %d in wallet %s'\ + % (newAddr.chainIndex, os.path.basename \ + (WalletPath))) + + else: + if newAddr.createPrivKeyNextUnlock: + #have to build the private key on unlock; we can use prevAddr + #for that purpose, used to chain the public key off of + newAddr.createPrivKeyNextUnlock_IVandKey[0] = \ + prevAddr.binInitVect16.copy() + newAddr.createPrivKeyNextUnlock_IVandKey[1] = \ + prevAddr.binPrivKey32_Encr.copy() + + newAddr.createPrivKeyNextUnlock_ChainDepth = \ + newAddr.chainIndex - prevAddr.chainIndex + + + #unlock if necessary + if keymismatch == 0: + if newAddr.isLocked: + try: + newAddr.unlock(toRecover.kdfKey) + keymismatch = 0 + except KeyDataError: + keymismatch = 1 + + isPrivForked = False + validAddr = None + if newAddr.chainIndex > 0 and keymismatch != 2: + #if the wallet has the private key, derive it from the + #chainIndex and compare. If they mismatch, save the bad + #private key as index -3 in the saved wallet. Additionally, + #derive the private key in case it is missing (keymismatch==4) + + gap = newAddr.chainIndex + prevkey = None + + if prevAddr.useEncryption: + if prevAddr.binPrivKey32_Encr.getSize() == 32: + gap = newAddr.chainIndex - prevAddr.chainIndex + prevkey = CryptoAES().DecryptCFB( \ + prevAddr.binPrivKey32_Encr, \ + SecureBinaryData(toRecover.kdfKey), \ + prevAddr.binInitVect16) + else: + if prevAddr.binPrivKey32_Plain.getSize() == 32: + gap = newAddr.chainIndex - prevAddr.chainIndex + prevkey = prevAddr.binPrivKey32_Plain + + if gap == newAddr.chainIndex: + #coudln't get a private key from prevAddr, + #derive from root addr + prevAddr = addrDict[0][0] + + if prevAddr.useEncryption: + prevkey = CryptoAES().DecryptCFB( \ + prevAddr.binPrivKey32_Encr, \ + SecureBinaryData(toRecover.kdfKey), \ + prevAddr.binInitVect16) + else: + prevkey = prevAddr.binPrivKey32_Plain + + for t in range(0, gap): + prevkey = prevAddr.safeExtendPrivateKey( \ + prevkey, \ + prevAddr.chaincode) + + if keymismatch != 4: + if prevkey.toHexStr() != \ + newAddr.binPrivKey32_Plain.toHexStr(): + """ + Special case: The private key saved in the wallet doesn't + match the extended private key. + + 2 things to do: + 1) Save the current address entry as an import, + as -chainIndex -3 + 2) After the address entry has been analyzed, replace it + with a valid one, to keep on checking the chain. + """ + isPrivForked = True + validAddr = newAddr.copy() + validAddr.binPrivKey32_Plain = prevkey.copy() + validAddr.binPublicKey65 = CryptoECDSA().ComputePublicKey(\ + validAddr.binPrivKey32_Plain) + validAddr.chainCode = prevAddr.chaincode.copy() + validAddr.keyChanged = True + + if validAddr.useEncryption: + validAddr.lock() + + if isPubForked is not True: + self.forkedPublicKeyChain.append([newAddr.chainIndex, \ + byteLocation]) + + if isPrivForked is False: + chID = newAddr.chainIndex + validchID = 0 + for ids in range(chID -1, 0, -1): + if ids in validChainDict: + validchID = ids + break + + validChainAddr = validChainDict[validchID] + if validChainAddr.useEncryption: + validPrivKey = CryptoAES().DecryptCFB( \ + validChainAddr.binPrivKey32_Encr, \ + SecureBinaryData(toRecover.kdfKey), \ + validChainAddr.binInitVect16) + else: + validPrivKey = validChainAddr.binPrivKey32_Plain.copy() + + gap = chID - validchID + for t in range(0, gap): + validPrivKey = validChainAddr.safeExtendPrivateKey( \ + validPrivKey, \ + validChainAddr.chaincode) + + if prevkey.toHexStr() != validPrivKey.toHexStr(): + isPrivForked = True + validAddr = newAddr.copy() + validAddr.binPrivKey32_Plain = validPrivKey.copy() + validAddr.binPublicKey65 = \ + CryptoECDSA().ComputePublicKey( \ + validAddr.binPrivKey32_Plain) + + validAddr.chainCode = validChainAddr.chaincode.copy() + validAddr.keyChanged = True + + if validAddr.useEncryption: + validAddr.lock() + + validPrivKey.destroy() + + else: + newAddr.binPrivKey32_Plain = prevkey.copy() + + prevkey.destroy() + + if validAddr is None: + validChainDict[i] = newAddr + else: + validChainDict[i] = validAddr + + + #deal with mismatch scenarios + if keymismatch == 1: + self.unmatchedPair.append([newAddr.chainIndex, byteLocation]) + + #TODO: needs better handling for keymismatch == 2 + elif keymismatch == 2: + self.misc.append('no private key at chainIndex %d in wallet %s'\ + % (newAddr.chainIndex, WalletPath)) + + elif keymismatch == 3: + newAddr.binPublicKey65 = \ + CryptoECDSA().ComputePublicKey(newAddr.binPrivKey32_Plain) + newAddr.addrStr20 = newAddr.binPublicKey65.getHash160() + + #if we have clear possible mismatches (or there were none), + #proceed to consistency checks + if keymismatch == 0: + if not CryptoECDSA().CheckPubPrivKeyMatch( \ + newAddr.binPrivKey32_Plain, \ + newAddr.binPublicKey65): + self.unmatchedPair.append([newAddr.chainIndex, byteLocation]) + + if newAddr.addrStr20 != entrylist[1]: + self.hashValMismatch.append([newAddr.chainIndex, byteLocation]) + + + + if isPrivForked: + negImport = newAddr.copy() + negImport.chainIndex = -3 -newAddr.chainIndex + + if negImport.useEncryption: + negImport.lock() + + importedDict[self.nImports] = [negImport, 0, 0, 0] + self.nImports = self.nImports +1 + + if newAddr.useEncryption: + newAddr.lock() + + if self.naddress > 0: self.UIreport = self.UIreport + UIupdate + + #imported addresses + if not self.WO: + for i in range(0, self.nImports): + entrylist = [] + entrylist = list(importedDict[i]) + newAddr = entrylist[0] + rawData = entrylist[3] + + UIupdate = '- Processing imported address entries: \ + %d/%d
    ' % (i +1, self.nImports) + if Progress(self.UIreport + UIupdate) == 0: + if SecurePassphrase: SecurePassphrase.destroy() + if toRecover.kdfKey: toRecover.kdfKey.destroy() + rootAddr.lock() + return 0 + if prgAt: + prgAt[0] = prgAt_in + (0.01 + 0.99*(newAddr.chainIndex +1) \ + /prgTotal)*prgAt[1] + + if newAddr.chainIndex < -2: + self.negativeImports.append(newAddr.addrStr20) + elif newAddr.chainIndex == -2: + # Fix byte errors in the address data + fixedAddrData = newAddr.serialize() + if not rawData==fixedAddrData: + self.importedErr.append('found byte error in imported \ + address %d at file offset %d' % (i, entrylist[2])) + newAddr = PyBtcAddress() + newAddr.unserialize(fixedAddrData) + entrylist[0] = newAddr + importedDict[i] = entrylist + + #marked forked imports + + + #check public key is a valid EC point + if newAddr.hasPubKey(): + if not CryptoECDSA().VerifyPublicKeyValid( \ + newAddr.binPublicKey65): + self.importedErr.append('invalid pub key for imported \ + address %d at file offset %d\r\n' % (i, entrylist[2])) + else: + self.importedErr.append('missing pub key for imported \ + address %d at file offset %d\r\n' % (i, entrylist[2])) + + #if there a private key in the entry, check for consistency + if not newAddr.hasPrivKey(): + self.importedErr.append('missing private key for imported \ + address %d at file offset %d\r\n' % (i, entrylist[2])) + else: + + if newAddr.useEncryption != toRecover.useEncryption: + if newAddr.useEncryption: + self.importedErr.append('Encrypted address entry in \ + a non encrypted wallet for imported address %d at \ + file offset %d\r\n' % (i, entrylist[2])) + else: + self.importedErr.append('Unencrypted address entry in \ + an encrypted wallet for imported address %d at file \ + offset %d\r\n' % (i, entrylist[2])) + + keymismatch = 0 + if newAddr.isLocked: + try: + newAddr.unlock(toRecover.kdfKey) + except KeyDataError: + keymismatch = 1 + self.importedErr.append('pub key doesnt match private \ + key for imported address %d at file offset %d\r\n' \ + % (i, entrylist[2])) + + + if keymismatch == 0: + #pubkey is present, check against priv key + if not CryptoECDSA().CheckPubPrivKeyMatch( \ + newAddr.binPrivKey32_Plain, newAddr.binPublicKey65): + keymismatch = 1 + self.importedErr.append('pub key doesnt match private \ + key for imported address %d at file offset %d\r\n' \ + % (i, entrylist[2])) + + if keymismatch == 1: + #compute missing/invalid pubkey + newAddr.binPublicKey65 = CryptoECDSA().ComputePublicKey( \ + newAddr.binPrivKey32_Plain) + + #check hashVal + if newAddr.addrStr20 != entrylist[1]: + newAddr.addrStr20 = newAddr.binPublicKey65.getHash160() + self.importedErr.append('hashVal doesnt match addrStr20 \ + for imported address %d at file offset %d\r\n' \ + % (i, entrylist[2])) + + #if the entry was encrypted, lock it back with the new wallet + #kdfkey + if newAddr.useEncryption: + newAddr.lock() + + + if self.nImports > 0: self.UIreport = self.UIreport + UIupdate + #TODO: check comments consistency + + nerrors = len(self.rawError) + len(self.byteError) + \ + len(self.sequenceGaps) + len(self.forkedPublicKeyChain) + \ + len(self.chainCodeCorruption) + len(self.invalidPubKey) + \ + len(self.missingPubKey) + len(self.hashValMismatch) + \ + len(self.unmatchedPair) + len(self.importedErr) + len(self.misc) + + if nerrors: + if not self.WO or rmode == RECOVERMODE.Full: + if rmode < RECOVERMODE.Meta: + + #create recovered wallet + RecoveredWallet = self.createRecoveredWallet(toRecover, \ + rootAddr, SecurePassphrase, Progress, returnError) + if SecurePassphrase: RecoveredWallet.kdfKey = \ + RecoveredWallet.kdf.DeriveKey(SecurePassphrase) + rootAddr.lock() + + if not isinstance(RecoveredWallet, PyBtcWallet): + if SecurePassphrase: SecurePassphrase.destroy() + if toRecover.kdfKey: toRecover.kdfKey.destroy() + return RecoveredWallet + + #build address pool + for i in range(1, self.naddress): + UIupdate = '- Building address chain: %d/%d
    ' % \ + (i+1, self.naddress) + if Progress(self.UIreport + UIupdate) == 0: + if SecurePassphrase: SecurePassphrase.destroy() + if toRecover.kdfKey: toRecover.kdfKey.destroy() + if RecoveredWallet.kdfKey: RecoveredWallet.kdfKey.destroy() + return 0 + + #TODO: check this builds the proper address chain, + #and saves encrypted private keys + RecoveredWallet.computeNextAddress(None, False, True) + + if Progress and self.naddress > 0: + self.UIreport = self.UIreport + UIupdate + + #save imported addresses + if rootAddr.isLocked: + rootAddr.unlock(toRecover.kdfKey) + invQ = self.getInvModOfHMAC(rootAddr.binPrivKey32_Plain.toBinStr()) + regQ = self.getValidKeyHMAC(rootAddr.binPrivKey32_Plain.toBinStr()) + rootAddr.lock() + + for i in range(0, self.nImports): + UIupdate = '- Saving imported addresses: %d/%d
    ' \ + % (i+1, self.nImports) + if Progress(self.UIreport + UIupdate) == 0: + if SecurePassphrase: SecurePassphrase.destroy() + if toRecover.kdfKey: toRecover.kdfKey.destroy() + if RecoveredWallet.kdfKey: RecoveredWallet.kdfKey.destroy() + return 0 + + entrylist = [] + entrylist = list(importedDict[i]) + newAddr = entrylist[0] + + if newAddr.isLocked: + newAddr.unlock(toRecover.kdfKey) + + if newAddr.chainIndex < -2: + privMultiplier = CryptoECDSA().ECMultiplyScalars( \ + newAddr.binPrivKey32_Plain.toBinStr(), + invQ.toBinStr()) + self.privKeyMultipliers.append(binary_to_hex(privMultiplier)) + + # Sanity check that the multipliers are correct + recov = CryptoECDSA().ECMultiplyScalars(privMultiplier, + regQ.toBinStr()) + if not recov==newAddr.binPrivKey32_Plain.toBinStr(): + # Unfortunately I'm not sure what to do here if it doesn't match + # We know no ther way to handle it... + LOGERROR('Logging a multiplier that does not match!?') + + if newAddr.isLocked: + newAddr.keyChanged = 1 + newAddr.lock(RecoveredWallet.kdfKey) + + RecoveredWallet.walletFileSafeUpdate([[WLT_UPDATE_ADD, \ + WLT_DATATYPE_KEYDATA, newAddr.addrStr20, newAddr]]) + + if Progress and self.nImports > 0: self.UIreport = \ + self.UIreport + UIupdate + + invQ,regQ = None,None + + #save comments + if rmode == RECOVERMODE.Full: + for i in range(0, self.ncomments): + UIupdate = '- Saving comment entries: %d/%d
    ' \ + % (i+1, self.ncomments) + if Progress.UpdateText(self.UIreport + UIupdate) == 0: + if SecurePassphrase: SecurePassphrase.destroy() + if toRecover.kdfKey: toRecover.kdfKey.destroy() + if RecoveredWallet.kdfKey: + RecoveredWallet.kdfKey.destroy() + return 0 + + entrylist = [] + entrylist = list(commentDict[i]) + RecoveredWallet.walletFileSafeUpdate([[WLT_UPDATE_ADD, \ + entrylist[2], entrylist[1], entrylist[0]]]) + + if Progress and self.ncomments > 0: self.UIreport = \ + self.UIreport + UIupdate + + if isinstance(rootAddr.binPrivKey32_Plain, SecureBinaryData): + rootAddr.lock() + + #TODO: nothing to process anymore at this point. if the recovery mode + #is 4 (meta), just return the comments dict + if isinstance(toRecover.kdfKey, SecureBinaryData): + toRecover.kdfKey.destroy() + if RecoveredWallet is not None: + if isinstance(RecoveredWallet.kdfKey, SecureBinaryData): + RecoveredWallet.kdfKey.destroy() + + if SecurePassphrase: SecurePassphrase.destroy() + + if rmode != RECOVERMODE.Meta: + if nerrors == 0: + return self.BuildLogFile(0, Progress, returnError, nerrors) + else: + return self.BuildLogFile(1, Progress, returnError, nerrors) + else: + return commentDict + + ############################################################################ + def createRecoveredWallet(self, toRecover, rootAddr, SecurePassphrase, + Progress, returnError): + self.newwalletPath = os.path.join(os.path.dirname(toRecover.walletPath), + 'armory_%s_RECOVERED%s.wallet' % \ + (toRecover.uniqueIDB58, '.watchonly' \ + if self.WO else '')) + + if os.path.exists(self.newwalletPath): + try: + os.remove(self.newwalletPath) + except: + LOGEXCEPT('') + return self.BuildLogFile(-2, Progress, returnError) + + try: + if not self.WO: + RecoveredWallet = PyBtcWallet() + RecoveredWallet.createNewWallet( \ + newWalletFilePath=self.newwalletPath, \ + securePassphrase=SecurePassphrase, \ + plainRootKey=rootAddr.binPrivKey32_Plain, \ + chaincode=rootAddr.chaincode, \ + #not registering with the BDM, + #so no addresses are computed + doRegisterWithBDM=False, \ + shortLabel=toRecover.labelName, \ + longLabel=toRecover.labelDescr) + else: + RecoveredWallet = self.createNewWO(toRecover, \ + self.newwalletPath, rootAddr) + except: + LOGEXCEPT('') + #failed to create new file + return self.BuildLogFile(-2, Progress, returnError) + + return RecoveredWallet + + def LookForFurtherEntry(self, rawdata, loc): + """ + Attempts to find valid data entries in wallet file by skipping known byte + widths. + + The process: + 1) Assume an address entry with invalid data type key and/or the hash160. + Read ahead and try to unserialize a valid PyBtcAddress + 2) Assume a corrupt address entry. Move 1+20+237 bytes ahead, try to + unpack the next entry + + At this point all entries are of random length. The most accurate way to + define them as valid is to try and unpack the next entry, or check end of + file has been hit gracefully + + 3) Try for address comment + 4) Try for transaction comment + 5) Try for deleted entry + + 6) At this point, can still try for random byte search. Essentially, push + an incremental amount of bytes until a valid entry or the end of the file + is hit. Simplest way around it is to recursively call this member with an + incremented loc + + + + About address entries: currently, the code tries to fully unserialize + tentative address entries. It will most likely raise at the slightest + error. However, that doesn't mean the entry is entirely bogus, or not an + address entry at all. Individual data packets should be checked against + their checksum for validity in a full implementation of the raw data + recovery layer of this tool. Other entries do not carry checksums and + thus appear opaque to this recovery layer. + + TODO: + 1) verify each checksum data block in address entries + 2) same with the file header + """ + + #check loc against data end. + if loc >= rawdata.getSize(): + return None, None, None, [0] + + #reset to last known good offset + rawdata.resetPosition(loc) + + #try for address entry: push 1 byte for the key, 20 for the public key + #hash, try to unpack the next 237 bytes as an address entry + try: + rawdata.advance(1) + hash160 = rawdata.get(BINARY_CHUNK, 20) + chunk = rawdata.get(BINARY_CHUNK, self.pybtcaddrSize) + + newAddr, chksumError = self.addrEntry_unserialize_recover(chunk) + #if we got this far, no exception was raised, return the valid entry + #and hash, but invalid key + + if chksumError != 0: + #had some checksum errors, pass the data on + return 0, hash160, chunk, [0, loc, newAddr, chksumError] + + return 0, hash160, chunk, [1, loc] + except: + LOGEXCEPT('') + #unserialize error, move on + rawdata.resetPosition(loc) + + #try for next entry + try: + rawdata.advance(1+20+237) + dtype, hash, chunk = PyBtcWallet().unpackNextEntry(rawdata) + if dtype>-1 and dtype<5: + return dtype, hash, chunk, [1, loc +1+20+237] + else: + rawdata.resetPosition(loc) + except: + LOGEXCEPT('') + rawdata.resetPosition(loc) + + #try for addr comment: push 1 byte for the key, 20 for the hash160, + #2 for the N and N for the comment + try: + rawdata.advance(1) + hash160 = rawdata.get(BINARY_CHUNK, 20) + chunk_length = rawdata.get(UINT16) + chunk = rawdata.get(BINARY_CHUNK, chunk_length) + + #test the next entry + dtype, hash, chunk2 = PyBtcWallet().unpackNextEntry(rawdata) + if dtype>-1 and dtype<5: + #good entry, return it + return 1, hash160, chunk, [1, loc] + else: + rawdata.resetPosition(loc) + except: + LOGEXCEPT('') + rawdata.resetPosition(loc) + + #try for txn comment: push 1 byte for the key, 32 for the txnhash, + #2 for N, and N for the comment + try: + rawdata.advance(1) + hash256 = rawdata.get(BINARY_CHUNK, 32) + chunk_length = rawdata.get(UINT16) + chunk = rawdata.get(BINARY_CHUNK, chunk_length) + + #test the next entry + dtype, hash, chunk2 = PyBtcWallet().unpackNextEntry(rawdata) + if dtype>-1 and dtype<5: + #good entry, return it + return 2, hash256, chunk, [1, loc] + else: + rawdata.resetPosition(loc) + except: + LOGEXCEPT('') + rawdata.resetPosition(loc) + + #try for deleted entry: 1 byte for the key, 2 bytes for N, N bytes + #worth of 0s + try: + rawdata.advance(1) + chunk_length = rawdata.get(UINT16) + chunk = rawdata.get(BINARY_CHUNK, chunk_length) + + #test the next entry + dtype, hash, chunk2 = PyBtcWallet().unpackNextEntry(rawdata) + if dtype>-1 and dtype<5: + baddata = 0 + for i in len(chunk): + if i != 0: + baddata = 1 + break + + if baddata != 0: + return 4, None, chunk, [1, loc] + + rawdata.resetPosition(loc) + except: + LOGEXCEPT('') + rawdata.resetPosition(loc) + + #couldn't find any valid entries, push loc by 1 and try again + loc = loc +1 + return self.LookForFurtherEntry(rawdata, loc) + + ############################################################################ + def addrEntry_unserialize_recover(self, toUnpack): + """ + Unserialze a raw address entry, test all checksum carrying members + + On errors, flags chksumError bits as follows: + + bit 0: addrStr20 error + + bit 1: private key error + bit 2: contains a valid private key even though containsPrivKey is 0 + + bit 3: iv error + bit 4: contains a valid iv even though useEncryption is 0 + + bit 5: pubkey error + bit 6: contains a valid pubkey even though containsPubKey is 0 + + bit 7: chaincode error + """ + + if isinstance(toUnpack, BinaryUnpacker): + serializedData = toUnpack + else: + serializedData = BinaryUnpacker( toUnpack ) + + + def chkzero(a): + """ + Due to fixed-width fields, we will get lots of zero-bytes + even when the binary data container was empty + """ + if a.count('\x00')==len(a): + return '' + else: + return a + + chksumError = 0 + + # Start with a fresh new address + retAddr = PyBtcAddress() + + retAddr.addrStr20 = serializedData.get(BINARY_CHUNK, 20) + chkAddr20 = serializedData.get(BINARY_CHUNK, 4) + + addrVerInt = serializedData.get(UINT32) + flags = serializedData.get(UINT64) + retAddr.addrStr20 = verifyChecksum(self.addrStr20, chkAddr20) + flags = int_to_bitset(flags, widthBytes=8) + + # Interpret the flags + containsPrivKey = (flags[0]=='1') + containsPubKey = (flags[1]=='1') + retAddr.useEncryption = (flags[2]=='1') + retAddr.createPrivKeyNextUnlock = (flags[3]=='1') + + if len(self.addrStr20)==0: + chksumError |= 1 + + + + # Write out address-chaining parameters (for deterministic wallets) + retAddr.chaincode = chkzero(serializedData.get(BINARY_CHUNK, 32)) + chkChaincode = serializedData.get(BINARY_CHUNK, 4) + retAddr.chainIndex = serializedData.get(INT64) + depth = serializedData.get(INT64) + retAddr.createPrivKeyNextUnlock_ChainDepth = depth + + # Correct errors, convert to secure container + retAddr.chaincode = SecureBinaryData(verifyChecksum(retAddr.chaincode, \ + chkChaincode)) + if retAddr.chaincode.getSize == 0: + chksumError |= 128 + + + # Write out whatever is appropriate for private-key data + # Binary-unpacker will write all 0x00 bytes if empty values are given + iv = chkzero(serializedData.get(BINARY_CHUNK, 16)) + chkIv = serializedData.get(BINARY_CHUNK, 4) + privKey = chkzero(serializedData.get(BINARY_CHUNK, 32)) + chkPriv = serializedData.get(BINARY_CHUNK, 4) + iv = SecureBinaryData(verifyChecksum(iv, chkIv)) + privKey = SecureBinaryData(verifyChecksum(privKey, chkPriv)) + + + # If this is SUPPOSED to contain a private key... + if containsPrivKey: + if privKey.getSize()==0: + chksumError |= 2 + containsPrivKey = 0 + else: + if privKey.getSize()==32: + chksumError |= 4 + containsPrivKey = 1 + + if retAddr.useEncryption: + if iv.getSize()==0: + chksumError |= 8 + retAddr.useEncryption = 0 + else: + if iv.getSize()==16: + chksumError |= 16 + retAddr.useEncryption = 1 + + if retAddr.useEncryption: + if retAddr.createPrivKeyNextUnlock: + retAddr.createPrivKeyNextUnlock_IVandKey[0] = iv.copy() + retAddr.createPrivKeyNextUnlock_IVandKey[1] = privKey.copy() + else: + retAddr.binInitVect16 = iv.copy() + retAddr.binPrivKey32_Encr = privKey.copy() + else: + retAddr.binInitVect16 = iv.copy() + retAddr.binPrivKey32_Plain = privKey.copy() + + pubKey = chkzero(serializedData.get(BINARY_CHUNK, 65)) + chkPub = serializedData.get(BINARY_CHUNK, 4) + pubKey = SecureBinaryData(verifyChecksum(pubKey, chkPub)) + + if containsPubKey: + if not pubKey.getSize()==65: + chksumError |= 32 + if retAddr.binPrivKey32_Plain.getSize()==32: + pubKey = CryptoECDSA().ComputePublicKey( + retAddr.binPrivKey32_Plain) + else: + if pubKey.getSize()==65: + chksumError |= 64 + + retAddr.binPublicKey65 = pubKey + + retAddr.timeRange[0] = serializedData.get(UINT64) + retAddr.timeRange[1] = serializedData.get(UINT64) + retAddr.blkRange[0] = serializedData.get(UINT32) + retAddr.blkRange[1] = serializedData.get(UINT32) + + retAddr.isInitialized = True + + if (chksumError and 171) == 171: + raise InvalidEntry + + if chksumError != 0: + #write out errors to the list + self.rawError.append(' Encountered checksum errors in follolwing \ + address entry members:') + + if chksumError and 1: + self.rawError.append(' - addrStr20') + if chksumError and 2: + self.rawError.append(' - private key') + if chksumError and 4: + self.rawError.append(' - hasPrivatKey flag') + if chksumError and 8: + self.rawError.append(' - Encryption IV') + if chksumError and 16: + self.rawError.append(' - useEncryption flag') + if chksumError and 32: + self.rawError.append(' - public key') + if chksumError and 64: + self.rawError.append(' - hasPublicKey flag') + if chksumError and 128: + self.rawError.append(' - chaincode') + + return retAddr, chksumError + + ############################################################################ + def createNewWO(self, toRecover, newPath, rootAddr): + newWO = PyBtcWallet() + + newWO.version = toRecover.version + newWO.magicBytes = toRecover.magicBytes + newWO.wltCreateDate = toRecover.wltCreateDate + newWO.uniqueIDBin = toRecover.uniqueIDBin + newWO.useEncryption = False + newWO.watchingOnly = True + newWO.walletPath = newPath + + if toRecover.labelName: + newWO.labelName = toRecover.labelName[:32] + if toRecover.labelDescr: + newWO.labelDescr = toRecover.labelDescr[:256] + + + newAddr = rootAddr.copy() + newAddr.binPrivKey32_Encr = SecureBinaryData() + newAddr.binPrivKey32_Plain = SecureBinaryData() + newAddr.useEncryption = False + newAddr.createPrivKeyNextUnlock = False + + newWO.addrMap['ROOT'] = newAddr + firstAddr = newAddr.extendAddressChain() + newWO.addrMap[firstAddr.getAddr160()] = firstAddr + + newWO.lastComputedChainAddr160 = firstAddr.getAddr160() + newWO.lastComputedChainIndex = firstAddr.chainIndex + newWO.highestUsedChainIndex = toRecover.highestUsedChainIndex + newWO.cppWallet = BtcWallet() + + newWO.writeFreshWalletFile(newPath) + + return newWO + + ############################################################################ + def getValidKeyHMAC(self, Q): + nonce = 0 + while True: + hmacQ = HMAC256(Q, 'LogMult%d' % nonce) + if binary_to_int(hmacQ, BIGENDIAN) >= SECP256K1_ORDER: + nonce += 1 + continue + + return SecureBinaryData(hmacQ) + + ############################################################################ + def getInvModOfHMAC(self, Q): + hmacQ = self.getValidKeyHMAC(Q) + return CryptoECDSA().InvMod(SecureBinaryData(hmacQ)) + + +############################################################################### +def WalletConsistencyCheck(wallet, prgAt=None): + """ + Checks consistency of non encrypted wallet data + Returns 0 if no error was found, otherwise a + string list of the scan full log + """ + + return PyBtcWalletRecovery().ProcessWallet(None, wallet, None, + RECOVERMODE.Check, prgAt, True) + +############################################################################# +# We don't have access to the qtdefines:tr function, but we still want +# the capability to print multi-line strings within the code. This simply +# strips each line and then concatenates them together +def tr_(s): + return ' '.join([line.strip() for line in s.split('\n')]) + +############################################################################# +@AllowAsync +def FixWallet(wltPath, wlt, mode=RECOVERMODE.Full, DoNotMove=False, + Passphrase=None, Progress=emptyFunc): + + ''' + return code: + 0 - no wallet errors found, nothing to fix + 1 - errors found, wallet fixed + str - errors found, couldnt fix wallet, returning the error as a str + ''' + fixer = PyBtcWalletRecovery() + frt = fixer.ProcessWallet(wltPath, wlt, Passphrase, mode, Progress=Progress) + + # Shorten a bunch of statements + datestr = RightNowStr('%Y-%m-%d-%H%M') + if wlt: + homedir = os.path.dirname(wlt.walletPath) + wltID = wlt.uniqueIDB58 + else: + homedir = os.path.dirname(wltPath) + wltID = fixer.UID + + + if frt == 0: + Progress(fixer.UIreport + fixer.EndLog) + return 0, 0, fixer + + elif frt == 1 or (isinstance(frt, dict) and frt['nErrors'] != 0): + Progress(fixer.UIreport) + + if DoNotMove: + Progress(fixer.UIreport + fixer.EndLog) + return 1, 0, fixer + else: + #move the old wallets and log files to another folder + corruptFolder = os.path.join(homedir, wltID, datestr) + if not os.path.exists(corruptFolder): + os.makedirs(corruptFolder) + + logsToCopy = ['armorylog.txt', 'armorycpplog.txt', 'multipliers.txt'] + wltCopyName = 'armory_%s_ORIGINAL_%s.wallet' % (wltID, '.watchonly') + wltLogName = 'armory_%s_LOGFILE_%s.log' % \ + (wltID, '.watchonly' if fixer.WO else '') + + corruptWltPath = os.path.join(corruptFolder, wltCopyName) + recoverLogPath = os.path.join(corruptFolder, wltLogName) + + try: + + if not fixer.WO: + #wallet has private keys, make a WO version and delete it + wlt.forkOnlineWallet(corruptWltPath, wlt.labelName, + wlt.labelDescr) + os.remove(wlt.walletPath) + else: + os.rename(wlt.walletPath, corruptWltPath) + + + if os.path.exists(fixer.LogPath): + os.rename(fixer.LogPath, os.path.join(corruptFolder, + wltLogName)) + + if os.path.exists(fixer.newwalletPath): + os.rename(fixer.newwalletPath, wlt.walletPath) + + #remove backups + origBackup = getSuffixedPath(wlt.walletPath, 'backup') + if os.path.exists(origBackup): + os.remove(origBackup) + + newBackup = getSuffixedPath(fixer.newwalletPath, 'backup') + if os.path.exists(newBackup): + os.remove(newBackup) + + # Copy all the relevant log files + for fn in logsToCopy: + fullpath = os.path.join(homedir, fn) + if os.path.exists(fullpath): + shutil.copy(fullpath, corruptFolder) + else: + LOGERROR('Expected log file was not copied: %s', fn) + + + fixer.EndLog = (""" +
    Wallet analysis and restoration complete.
    + The inconsistent wallet and log files were moved to: +
    %s/

    """) % corruptFolder + + Progress(fixer.UIreport + fixer.EndLog) + return 1, corruptFolder, fixer + + except Exception as e: + #failed to move files around, most likely a credential error + LOGEXCEPT(str(e)) + errStr = '
    An error occurred moving wallet files: %s' % e + Progress(fixer.UIreport + errStr) + + return -1, fixer.UIreport + errSt, fixer + else: + Progress(fixer.UIreport + fixer.EndLog) + return -1, fixer.UIreport + fixer.EndLog, fixer + +############################################################################### +@AllowAsync +def FixWalletList(wallets, dlg, Progress=emptyFunc): + + #It's the caller's responsibility to unload the wallets from his app + + #fix the wallets + fixedWlt = [] + wlterror = [] + goodWallets = [] + fixerObjs = [] + logsSaved = [] + + for wlt in wallets: + if dlg: + status = [0] + dlg.sigSetNewProgress(status) + while not status[0]: + sleep(0.01) + + wltStatus, extraData, recovObj = FixWallet( \ + '', wlt, Passphrase=dlg.AskUnlock, Progress=Progress) + + + fixerObjs.append(recovObj) + + if wltStatus == 0: + goodWallets.append(wlt.uniqueIDB58) + fixedWlt.append(wlt.walletPath) + + elif wltStatus == 1: + fixedWlt.append(wlt.walletPath) + logsSaved.append([wlt.uniqueIDB58, extraData]) + elif wltStatus == -1: + wlterror.append([wlt.uniqueIDB58, extraData]) + + if dlg: + dlg.setRecoveryDone(wlterror, goodWallets, fixedWlt, fixerObjs) + + #load the new wallets + dlg.loadFixedWallets(fixedWlt) + + else: + return wlterror + +############################################################################### +''' +Stand alone, one wallet a time, all purpose recovery call. +Used with unloaded wallets or modes other than Full, and for armoryd +If dlg is set, it will report to it (UI) +If not, it will log the wallet status with LOGERROR and LOGINFO, and return the +status code to the caller +''' +@AllowAsync +def ParseWallet(wltPath, wlt, mode, dlg, Progress=emptyFunc): + fixedWlt = [] + wlterror = [] + goodWallets = [] + + wltStatus, extraData, recovObj = FixWallet(wltPath, wlt, mode, True, + Passphrase=dlg.AskUnlock, + Progress=Progress) + if wltStatus == 0: + goodWallets.append(1) + fixedWlt.append(1) + + if dlg is None: + if wlt: LOGINFO('Wallet %s is consistent' % (wlt.uniqueIDB58)) + elif wltPath: LOGINFO('Wallet %s is consistent' % (wltPath)) + + elif wltStatus == 1: + fixedWlt.append(1) + + if dlg is None: + if wlt: LOGERROR('Wallet %s is inconsistent!!!' % (wlt.uniqueIDB58)) + elif wltPath: LOGERROR('Wallet %s is inconsistent!!!' % (wltPath)) + + elif wltStatus == -1: + wlterror.append(extraData) + + if dlg is None: + if wlt: + LOGERROR('Failed to perform consistency check on wallet %s!!!'\ + % (wlt.uniqueIDB58)) + elif wltPath: + LOGERROR('Failed to perform consistency check on wallet %s!!!'\ + % (wltPath)) + + if dlg: + dlg.setRecoveryDone(wlterror, goodWallets, fixedWlt, [recovObj]) + else: + return wltStatus + +############################################################################### + +""" +TODO: setup an array of tests: +2) broken header +3) oversized comment entries +4) comments for non existant addr or txn entries + +possible wallet corruption vectors: +1) PyBtcAddress.unlock verifies consistency between private and public key, \ + unless SkipCheck is forced to false and private key is already computed. + Look for this scenario +""" diff --git a/armoryengine/Script.py b/armoryengine/Script.py new file mode 100644 index 000000000..bf9fdda2c --- /dev/null +++ b/armoryengine/Script.py @@ -0,0 +1,669 @@ +################################################################################ +# # +# Copyright (C) 2011-2014, Armory Technologies, Inc. # +# Distributed under the GNU Affero General Public License (AGPL v3) # +# See LICENSE or http://www.gnu.org/licenses/agpl.html # +# # +################################################################################ +################################################################################ +# +# SCRIPTING! +# +################################################################################ +from armoryengine.ArmoryUtils import * +from armoryengine.BinaryPacker import UINT8, BINARY_CHUNK, UINT16, UINT32 +from armoryengine.BinaryUnpacker import BinaryUnpacker +from armoryengine.Timer import TimeThisFunction +from armoryengine.Transaction import * + + +def convertScriptToOpStrings(binScript): + opList = [] + + i = 0; + sz = len(binScript) + error = False; + while i < sz: + nextOp = ord(binScript[i]); + if nextOp == 0: + opList.append("0") + i+=1 + elif nextOp < 76: + opList.append('PUSHDATA(%s)' % str(nextOp)) + binObj = binScript[i+1:i+1+nextOp] + opList.append('['+binary_to_hex(binObj)+']') + i += nextOp+1 + elif nextOp == 76: + nb = binary_to_int(binScript[i+1:i+2]) + if i+1+1+nb > sz: + error = True; + break + binObj = binScript[i+2:i+2+nb] + opList.append('OP_PUSHDATA1(%s)' % str(nb)) + opList.append('['+binary_to_hex(binObj)+']') + i += nb+2 + elif nextOp == 77: + nb = binary_to_int(binScript[i+1:i+3]); + if i+1+2+nb > sz: + error = True; + break + nbprint = min(nb,256) + binObj = binScript[i+3:i+3+nbprint] + opList.append('OP_PUSHDATA2(%s)' % str(nb)) + opList.append('['+binary_to_hex(binObj)[:512] + '...]') + i += nb+3 + elif nextOp == 78: + nb = binScript[i+1:i+5]; + if i+1+4+nb > sz: + error = True; + break + nbprint = min(nb,256) + binObj = binScript[i+5,i+5+nbprint] + opList.append('[OP_PUSHDATA4(%s)]' % str(nb)) + opList.append('['+binary_to_hex(binObj)[:512] + '...]') + i += nb+5 + else: + opList.append(opnames[nextOp]); + i += 1 + + if error: + opList.append("ERROR PROCESSING SCRIPT"); + + return opList; + + +def pprintScript(binScript, nIndent=0): + indstr = indent*nIndent + print indstr + 'Script:' + opList = convertScriptToOpStrings(binScript) + for op in opList: + print indstr + indent + op + + +def serializeBytesWithPushData(binObj): + sz = len(binObj) + if sz <= 76: + lenByte = int_to_binary(sz, widthBytes=1) + return lenByte+binObj + elif sz <= 256: + lenByte = int_to_binary(sz, widthBytes=1) + return '\x4c' + lenByte + binObj + elif sz <= 65536: + lenBytes = int_to_binary(sz, widthBytes=2) + return '\x4d' + lenBytes + binObj + else: + InvalidScriptError('Cannot use PUSHDATA for len(obj)>65536') + + +TX_INVALID = 0 +OP_NOT_IMPLEMENTED = 1 +OP_DISABLED = 2 +SCRIPT_STACK_SIZE_ERROR = 3 +SCRIPT_ERROR = 4 +SCRIPT_NO_ERROR = 5 + + +class PyScriptProcessor(object): + """ + Use this class to evaluate a script. This method is more complicated + than some might expect, due to the fact that any OP_CHECKSIG or + OP_CHECKMULTISIG code requires the full transaction of the TxIn script + and also needs the TxOut script being spent. Since nearly every useful + script will have one of these operations, this class/method assumes + that all that data will be supplied. + + To simply execute a script not requiring any crypto operations: + + scriptIsValid = PyScriptProcessor().executeScript(binScript) + """ + + def __init__(self, txOldData=None, txNew=None, txInIndex=None): + self.stack = [] + self.txNew = None + self.script1 = None + self.script2 = None + if txOldData and txNew and not txInIndex==None: + self.setTxObjects(txOldData, txNew, txInIndex) + + + def setTxObjects(self, txOldData, txNew, txInIndex): + """ + The minimal amount of data necessary to evaluate a script that + has an signature check is the TxOut script that is being spent + and the entire Tx of the TxIn that is spending it. Thus, we + must supply at least the txOldScript, and a txNew with its + TxIn index (so we know which TxIn is spending that TxOut). + It is acceptable to pass in the full TxOut or the tx of the + TxOut instead of just the script itself. + """ + self.txNew = PyTx().unserialize(txNew.serialize()) + self.script1 = str(txNew.inputs[txInIndex].binScript) # copy + self.txInIndex = txInIndex + self.txOutIndex = txNew.inputs[txInIndex].outpoint.txOutIndex + self.txHash = txNew.inputs[txInIndex].outpoint.txHash + + if isinstance(txOldData, PyTx): + if not self.txHash == hash256(txOldData.serialize()): + LOGERROR('*** Supplied incorrect pair of transactions!') + self.script2 = str(txOldData.outputs[self.txOutIndex].binScript) + elif isinstance(txOldData, PyTxOut): + self.script2 = str(txOldData.binScript) + elif isinstance(txOldData, str): + self.script2 = str(txOldData) + + @TimeThisFunction + def verifyTransactionValid(self, txOldData=None, txNew=None, txInIndex=-1): + if txOldData and txNew and txInIndex != -1: + self.setTxObjects(txOldData, txNew, txInIndex) + else: + txOldData = self.script2 + txNew = self.txNew + txInIndex = self.txInIndex + + if self.script1==None or self.txNew==None: + raise VerifyScriptError, 'Cannot verify transactions, without setTxObjects call first!' + + # Execute TxIn script first + self.stack = [] + exitCode1 = self.executeScript(self.script1, self.stack) + + if not exitCode1 == SCRIPT_NO_ERROR: + raise VerifyScriptError, ('First script failed! Exit Code: ' + str(exitCode1)) + + exitCode2 = self.executeScript(self.script2, self.stack) + + if not exitCode2 == SCRIPT_NO_ERROR: + raise VerifyScriptError, ('Second script failed! Exit Code: ' + str(exitCode2)) + + return self.stack[-1]==1 + + + def executeScript(self, binaryScript, stack=[]): + self.stack = stack + self.stackAlt = [] + scriptData = BinaryUnpacker(binaryScript) + self.lastOpCodeSepPos = None + + while scriptData.getRemainingSize() > 0: + opcode = scriptData.get(UINT8) + exitCode = self.executeOpCode(opcode, scriptData, self.stack, self.stackAlt) + if not exitCode == SCRIPT_NO_ERROR: + if exitCode==OP_NOT_IMPLEMENTED: + LOGERROR('***ERROR: OpCodes OP_IF, OP_NOTIF, OP_ELSE, OP_ENDIF,') + LOGERROR(' have not been implemented, yet. This script') + LOGERROR(' could not be evaluated.') + if exitCode==OP_DISABLED: + LOGERROR('***ERROR: This script included an op code that has been') + LOGERROR(' disabled for security reasons. Script eval') + LOGERROR(' failed.') + return exitCode + + return SCRIPT_NO_ERROR + + + # Implementing this method exactly as in the client because it looks like + # there could be some subtleties with how it determines "true" + def castToBool(self, binData): + if isinstance(binData, int): + binData = int_to_binary(binData) + + for i,byte in enumerate(binData): + if not ord(byte) == 0: + # This looks like it's assuming LE encoding (?) + if (i == len(binData)-1) and (byte==0x80): + return False + return True + return False + + + def checkSig(self,binSig, binPubKey, txOutScript, txInTx, txInIndex, lastOpCodeSep=None): + """ + Generic method for checking Bitcoin tx signatures. This needs to be used for both + OP_CHECKSIG and OP_CHECKMULTISIG. Step 1 is to pop signature and public key off + the stack, which must be done outside this method and passed in through the argument + list. The remaining steps do not require access to the stack. + """ + + # 2. Subscript is from latest OP_CODESEPARATOR until end... if DNE, use whole script + subscript = txOutScript + if lastOpCodeSep: + subscript = subscript[lastOpCodeSep:] + + # 3. Signature is deleted from subscript + # I'm not sure why this line is necessary - maybe for non-standard scripts? + lengthInBinary = int_to_binary(len(binSig)) + subscript = subscript.replace( lengthInBinary + binSig, "") + + # 4. Hashtype is popped and stored + hashtype = binary_to_int(binSig[-1]) + justSig = binSig[:-1] + + if not hashtype == 1: + LOGERROR('Non-unity hashtypes not implemented yet! (hashtype = %d)', hashtype) + assert(False) + + # 5. Make a copy of the transaction -- we will be hashing a modified version + txCopy = PyTx().unserialize( txInTx.serialize() ) + + # 6. Remove all OP_CODESEPARATORs + subscript.replace( int_to_binary(OP_CODESEPARATOR), '') + + # 7. All the TxIn scripts in the copy are blanked (set to empty string) + for txin in txCopy.inputs: + txin.binScript = '' + + # 8. Script for the current input in the copy is set to subscript + txCopy.inputs[txInIndex].binScript = subscript + + # 9. Prepare the signature and public key + senderAddr = PyBtcAddress().createFromPublicKey(binPubKey) + binHashCode = int_to_binary(hashtype, widthBytes=4) + toHash = txCopy.serialize() + binHashCode + + # Hashes are computed as part of CppBlockUtils::CryptoECDSA methods + ##hashToVerify = hash256(toHash) + ##hashToVerify = binary_switchEndian(hashToVerify) + + # 10. Apply ECDSA signature verification + if senderAddr.verifyDERSignature(toHash, justSig): + return True + else: + return False + + + + + def executeOpCode(self, opcode, scriptUnpacker, stack, stackAlt): + """ + Executes the next OP_CODE given the current state of the stack(s) + """ + + # TODO: Gavin clarified the effects of OP_0, and OP_1-OP_16. + # OP_0 puts an empty string onto the stack, which evaluateses to + # false and is plugged into HASH160 as '' + # OP_X puts a single byte onto the stack, 0x01 to 0x10 + # + # I haven't implemented it this way yet, because I'm still missing + # some details. Since this "works" for available scripts, I'm going + # to leave it alone for now. + + ########################################################################## + ########################################################################## + ### This block produces very nice debugging output for script eval! + #def pr(s): + #if isinstance(s,int): + #return str(s) + #elif isinstance(s,str): + #if len(s)>8: + #return binary_to_hex(s)[:8] + #else: + #return binary_to_hex(s) + + #print ' '.join([pr(i) for i in stack]) + #print opnames[opcode][:12].ljust(12,' ') + ':', + ########################################################################## + ########################################################################## + + + stackSizeAtLeast = lambda n: (len(self.stack) >= n) + + if opcode == OP_FALSE: + stack.append(0) + elif 0 < opcode < 76: + stack.append(scriptUnpacker.get(BINARY_CHUNK, opcode)) + elif opcode == OP_PUSHDATA1: + nBytes = scriptUnpacker.get(UINT8) + stack.append(scriptUnpacker.get(BINARY_CHUNK, nBytes)) + elif opcode == OP_PUSHDATA2: + nBytes = scriptUnpacker.get(UINT16) + stack.append(scriptUnpacker.get(BINARY_CHUNK, nBytes)) + elif opcode == OP_PUSHDATA4: + nBytes = scriptUnpacker.get(UINT32) + stack.append(scriptUnpacker.get(BINARY_CHUNK, nBytes)) + elif opcode == OP_1NEGATE: + stack.append(-1) + elif opcode == OP_TRUE: + stack.append(1) + elif 81 < opcode < 97: + stack.append(opcode-80) + elif opcode == OP_NOP: + pass + + # TODO: figure out the conditional op codes... + elif opcode == OP_IF: + return OP_NOT_IMPLEMENTED + elif opcode == OP_NOTIF: + return OP_NOT_IMPLEMENTED + elif opcode == OP_ELSE: + return OP_NOT_IMPLEMENTED + elif opcode == OP_ENDIF: + return OP_NOT_IMPLEMENTED + + elif opcode == OP_VERIFY: + if not self.castToBool(stack.pop()): + stack.append(0) + return TX_INVALID + elif opcode == OP_RETURN: + return TX_INVALID + elif opcode == OP_TOALTSTACK: + stackAlt.append( stack.pop() ) + elif opcode == OP_FROMALTSTACK: + stack.append( stackAlt.pop() ) + + elif opcode == OP_IFDUP: + # Looks like this method duplicates the top item if it's not zero + if not stackSizeAtLeast(1): return SCRIPT_STACK_SIZE_ERROR + if self.castToBool(stack[-1]): + stack.append(stack[-1]); + + elif opcode == OP_DEPTH: + stack.append( len(stack) ) + elif opcode == OP_DROP: + stack.pop() + elif opcode == OP_DUP: + stack.append( stack[-1] ) + elif opcode == OP_NIP: + if not stackSizeAtLeast(2): return SCRIPT_STACK_SIZE_ERROR + del stack[-2] + elif opcode == OP_OVER: + if not stackSizeAtLeast(2): return SCRIPT_STACK_SIZE_ERROR + stack.append(stack[-2]) + elif opcode == OP_PICK: + n = stack.pop() + if not stackSizeAtLeast(n): return SCRIPT_STACK_SIZE_ERROR + stack.append(stack[-n]) + elif opcode == OP_ROLL: + n = stack.pop() + if not stackSizeAtLeast(n): return SCRIPT_STACK_SIZE_ERROR + stack.append(stack[-(n+1)]) + del stack[-(n+2)] + elif opcode == OP_ROT: + if not stackSizeAtLeast(3): return SCRIPT_STACK_SIZE_ERROR + stack.append( stack[-3] ) + del stack[-4] + elif opcode == OP_SWAP: + if not stackSizeAtLeast(2): return SCRIPT_STACK_SIZE_ERROR + x2 = stack.pop() + x1 = stack.pop() + stack.extend([x2, x1]) + elif opcode == OP_TUCK: + if not stackSizeAtLeast(2): return SCRIPT_STACK_SIZE_ERROR + x2 = stack.pop() + x1 = stack.pop() + stack.extend([x2, x1, x2]) + elif opcode == OP_2DROP: + if not stackSizeAtLeast(2): return SCRIPT_STACK_SIZE_ERROR + stack.pop() + stack.pop() + elif opcode == OP_2DUP: + if not stackSizeAtLeast(2): return SCRIPT_STACK_SIZE_ERROR + stack.append( stack[-2] ) + stack.append( stack[-2] ) + elif opcode == OP_3DUP: + if not stackSizeAtLeast(3): return SCRIPT_STACK_SIZE_ERROR + stack.append( stack[-3] ) + stack.append( stack[-3] ) + stack.append( stack[-3] ) + elif opcode == OP_2OVER: + if not stackSizeAtLeast(4): return SCRIPT_STACK_SIZE_ERROR + stack.append( stack[-4] ) + stack.append( stack[-4] ) + elif opcode == OP_2ROT: + if not stackSizeAtLeast(6): return SCRIPT_STACK_SIZE_ERROR + stack.append( stack[-6] ) + stack.append( stack[-6] ) + elif opcode == OP_2SWAP: + if not stackSizeAtLeast(4): return SCRIPT_STACK_SIZE_ERROR + x4 = stack.pop() + x3 = stack.pop() + x2 = stack.pop() + x1 = stack.pop() + stack.extend( [x3, x4, x1, x2] ) + elif opcode == OP_CAT: + return OP_DISABLED + elif opcode == OP_SUBSTR: + return OP_DISABLED + elif opcode == OP_LEFT: + return OP_DISABLED + elif opcode == OP_RIGHT: + return OP_DISABLED + elif opcode == OP_SIZE: + if isinstance(stack[-1], int): + stack.append(0) + else: + stack.append( len(stack[-1]) ) + elif opcode == OP_INVERT: + return OP_DISABLED + elif opcode == OP_AND: + return OP_DISABLED + elif opcode == OP_OR: + return OP_DISABLED + elif opcode == OP_XOR: + return OP_DISABLED + elif opcode == OP_EQUAL: + x1 = stack.pop() + x2 = stack.pop() + stack.append( 1 if x1==x2 else 0 ) + elif opcode == OP_EQUALVERIFY: + x1 = stack.pop() + x2 = stack.pop() + if not x1==x2: + stack.append(0) + return TX_INVALID + + + elif opcode == OP_1ADD: + stack[-1] += 1 + elif opcode == OP_1SUB: + stack[-1] -= 1 + elif opcode == OP_2MUL: + stack[-1] *= 2 + return OP_DISABLED + elif opcode == OP_2DIV: + stack[-1] /= 2 + return OP_DISABLED + elif opcode == OP_NEGATE: + stack[-1] *= -1 + elif opcode == OP_ABS: + stack[-1] = abs(stack[-1]) + elif opcode == OP_NOT: + top = stack.pop() + if top==0: + stack.append(1) + else: + stack.append(0) + elif opcode == OP_0NOTEQUAL: + top = stack.pop() + if top==0: + stack.append(0) + else: + stack.append(1) + top = stack.pop() + if top==0: + stack.append(1) + else: + stack.append(0) + elif opcode == OP_ADD: + b = stack.pop() + a = stack.pop() + stack.append(a+b) + elif opcode == OP_SUB: + b = stack.pop() + a = stack.pop() + stack.append(a-b) + elif opcode == OP_MUL: + return OP_DISABLED + elif opcode == OP_DIV: + return OP_DISABLED + elif opcode == OP_MOD: + return OP_DISABLED + elif opcode == OP_LSHIFT: + return OP_DISABLED + elif opcode == OP_RSHIFT: + return OP_DISABLED + elif opcode == OP_BOOLAND: + b = stack.pop() + a = stack.pop() + if (not a==0) and (not b==0): + stack.append(1) + else: + stack.append(0) + elif opcode == OP_BOOLOR: + b = stack.pop() + a = stack.pop() + stack.append( 1 if (self.castToBool(a) or self.castToBool(b)) else 0 ) + elif opcode == OP_NUMEQUAL: + b = stack.pop() + a = stack.pop() + stack.append( 1 if a==b else 0 ) + elif opcode == OP_NUMEQUALVERIFY: + b = stack.pop() + a = stack.pop() + if not a==b: + stack.append(0) + return TX_INVALID + elif opcode == OP_NUMNOTEQUAL: + b = stack.pop() + a = stack.pop() + stack.append( 1 if not a==b else 0 ) + elif opcode == OP_LESSTHAN: + b = stack.pop() + a = stack.pop() + stack.append( 1 if ab else 0) + elif opcode == OP_LESSTHANOREQUAL: + b = stack.pop() + a = stack.pop() + stack.append( 1 if a<=b else 0) + elif opcode == OP_GREATERTHANOREQUAL: + b = stack.pop() + a = stack.pop() + stack.append( 1 if a>=b else 0) + elif opcode == OP_MIN: + b = stack.pop() + a = stack.pop() + stack.append( min(a,b) ) + elif opcode == OP_MAX: + b = stack.pop() + a = stack.pop() + stack.append( max(a,b) ) + elif opcode == OP_WITHIN: + xmax = stack.pop() + xmin = stack.pop() + x = stack.pop() + stack.append( 1 if (xmin <= x < xmax) else 0 ) + + elif opcode == OP_RIPEMD160: + bits = stack.pop() + stack.append( ripemd160(bits) ) + elif opcode == OP_SHA1: + bits = stack.pop() + stack.append( sha1(bits) ) + elif opcode == OP_SHA256: + bits = stack.pop() + stack.append( sha256(bits) ) + elif opcode == OP_HASH160: + bits = stack.pop() + if isinstance(bits, int): + bits = '' + stack.append( hash160(bits) ) + elif opcode == OP_HASH256: + bits = stack.pop() + if isinstance(bits, int): + bits = '' + stack.append( sha256(sha256(bits) ) ) + elif opcode == OP_CODESEPARATOR: + self.lastOpCodeSepPos = scriptUnpacker.getPosition() + elif opcode == OP_CHECKSIG or opcode == OP_CHECKSIGVERIFY: + + # 1. Pop key and sig from the stack + binPubKey = stack.pop() + binSig = stack.pop() + + # 2-10. encapsulated in sep method so CheckMultiSig can use it too + txIsValid = self.checkSig( binSig, \ + binPubKey, \ + scriptUnpacker.getBinaryString(), \ + self.txNew, \ + self.txInIndex, \ + self.lastOpCodeSepPos) + stack.append(1 if txIsValid else 0) + if opcode==OP_CHECKSIGVERIFY: + verifyCode = self.executeOpCode(OP_VERIFY) + if verifyCode == TX_INVALID: + return TX_INVALID + + elif opcode == OP_CHECKMULTISIG or opcode == OP_CHECKMULTISIGVERIFY: + # OP_CHECKMULTISIG procedure ported directly from Satoshi client code + # Location: bitcoin-0.4.0-linux/src/src/script.cpp:775 + i=1 + if len(stack) < i: + return TX_INVALID + + nKeys = int(stack[-i]) + if nKeys < 0 or nKeys > 20: + return TX_INVALID + + i += 1 + iKey = i + i += nKeys + if len(stack) < i: + return TX_INVALID + + nSigs = int(stack[-i]) + if nSigs < 0 or nSigs > nKeys: + return TX_INVALID + + iSig = i + i += 1 + i += nSigs + if len(stack) < i: + return TX_INVALID + + stack.pop() + + # Apply the ECDSA verification to each of the supplied Sig-Key-pairs + enoughSigsMatch = True + while enoughSigsMatch and nSigs > 0: + binSig = stack[-iSig] + binKey = stack[-iKey] + + if( self.checkSig(binSig, \ + binKey, \ + scriptUnpacker.getBinaryString(), \ + self.txNew, \ + self.txInIndex, \ + self.lastOpCodeSepPos) ): + iSig += 1 + nSigs -= 1 + + iKey +=1 + nKeys -=1 + + if(nSigs > nKeys): + enoughSigsMatch = False + + # Now pop the things off the stack, we only accessed in-place before + while i > 1: + i -= 1 + stack.pop() + + + stack.append(1 if enoughSigsMatch else 0) + + if opcode==OP_CHECKMULTISIGVERIFY: + verifyCode = self.executeOpCode(OP_VERIFY) + if verifyCode == TX_INVALID: + return TX_INVALID + + else: + return SCRIPT_ERROR + + return SCRIPT_NO_ERROR + + +# Putting this at the end because of the circular dependency +from armoryengine.PyBtcAddress import PyBtcAddress diff --git a/armoryengine/Timer.py b/armoryengine/Timer.py new file mode 100644 index 000000000..b3843115b --- /dev/null +++ b/armoryengine/Timer.py @@ -0,0 +1,102 @@ +################################################################################ +# +# Copyright (C) 2011-2014, Armory Technologies, Inc. +# Distributed under the GNU Affero General Public License (AGPL v3) +# See LICENSE or http://www.gnu.org/licenses/agpl.html +# +################################################################################ +# +# Project: Armory +# Author: Alan Reiner +# Website: www.bitcoinarmory.com +# Orig Date: 20 November, 2011 +# +################################################################################ +from armoryengine.ArmoryUtils import LOGWARN, RightNow, LOGERROR + +class Timer(object): + + ################################################################################ + # + # Keep track of lots of different timers: + # + # Key: timerName + # Value: [cumulTime, numStart, lastStart, isRunning] + # + timerMap = {} + + def startTimer(self, timerName): + if not self.timerMap.has_key(timerName): + self.timerMap[timerName] = [0, 0, 0, False] + timerEntry = self.timerMap[timerName] + timerEntry[1] += 1 + timerEntry[2] = RightNow() + timerEntry[3] = True + + def stopTimer(self, timerName): + if not self.timerMap.has_key(timerName): + LOGWARN('Requested stop timer that does not exist! (%s)' % timerName) + return + if not self.timerMap[timerName][3]: + LOGWARN('Requested stop timer that is not running! (%s)' % timerName) + return + timerEntry = self.timerMap[timerName] + timerEntry[0] += RightNow() - timerEntry[2] + timerEntry[2] = 0 + timerEntry[3] = False + + def resetTimer(self, timerName): + if not self.timerMap.has_key(timerName): + LOGERROR('Requested reset timer that does not exist! (%s)' % timerName) + # Even if it didn't exist, it will be created now + self.timerMap[timerName] = [0, 0, 0, False] + + def readTimer(self, timerName): + if not self.timerMap.has_key(timerName): + LOGERROR('Requested read timer that does not exist! (%s)' % timerName) + return + timerEntry = self.timerMap[timerName] + return timerEntry[0] + (RightNow() - timerEntry[2]) + + def printTimings(self): + print 'Timings: '.ljust(30), + print 'nCall'.rjust(13), + print 'cumulTime'.rjust(13), + print 'avgTime'.rjust(13) + print '-'*70 + for tname,quad in self.timerMap.iteritems(): + print ('%s' % tname).ljust(30), + print ('%d' % quad[1]).rjust(13), + print ('%0.6f' % quad[0]).rjust(13), + avg = quad[0]/quad[1] + print ('%0.6f' % avg).rjust(13) + print '-'*70 + + def saveTimingsCSV(self, fname): + f = open(fname, 'w') + f.write( 'TimerName,') + f.write( 'nCall,') + f.write( 'cumulTime,') + f.write( 'avgTime\n\n') + for tname,quad in self.timerMap.iteritems(): + f.write('%s,' % tname) + f.write('%d,' % quad[1]) + f.write('%0.6f,' % quad[0]) + avg = quad[0]/quad[1] + f.write('%0.6f\n' % avg) + f.write('\n\nNote: timings may be incorrect if errors ' + 'were triggered in the timed functions') + print 'Saved timings to file: %s' % fname + + def __init__(selfparams): # @NoSelf + pass + + +def TimeThisFunction(func): + timer = Timer() + def inner(*args, **kwargs): + timer.startTimer(func.__name__) + ret = func(*args, **kwargs) + timer.stopTimer(func.__name__) + return ret + return inner diff --git a/armoryengine/Transaction.py b/armoryengine/Transaction.py new file mode 100644 index 000000000..8b22d5bf0 --- /dev/null +++ b/armoryengine/Transaction.py @@ -0,0 +1,1661 @@ +################################################################################ +# # +# Copyright (C) 2011-2014, Armory Technologies, Inc. # +# Distributed under the GNU Affero General Public License (AGPL v3) # +# See LICENSE or http://www.gnu.org/licenses/agpl.html # +# # +################################################################################ +import logging +import os + +import CppBlockUtils as Cpp +from armoryengine.ArmoryUtils import * +from armoryengine.BinaryPacker import * +from armoryengine.BinaryUnpacker import * + +################################################################################ +# Identify all the codes/strings that are needed for dealing with scripts +################################################################################ +# Start list of OP codes +OP_0 = 0 +OP_FALSE = 0 +OP_PUSHDATA1 = 76 +OP_PUSHDATA2 = 77 +OP_PUSHDATA4 = 78 +OP_1NEGATE = 79 +OP_1 = 81 +OP_TRUE = 81 +OP_2 = 82 +OP_3 = 83 +OP_4 = 84 +OP_5 = 85 +OP_6 = 86 +OP_7 = 87 +OP_8 = 88 +OP_9 = 89 +OP_10 = 90 +OP_11 = 91 +OP_12 = 92 +OP_13 = 93 +OP_14 = 94 +OP_15 = 95 +OP_16 = 96 +OP_NOP = 97 +OP_IF = 99 +OP_NOTIF = 100 +OP_ELSE = 103 +OP_ENDIF = 104 +OP_VERIFY = 105 +OP_RETURN = 106 +OP_TOALTSTACK = 107 +OP_FROMALTSTACK = 108 +OP_IFDUP = 115 +OP_DEPTH = 116 +OP_DROP = 117 +OP_DUP = 118 +OP_NIP = 119 +OP_OVER = 120 +OP_PICK = 121 +OP_ROLL = 122 +OP_ROT = 123 +OP_SWAP = 124 +OP_TUCK = 125 +OP_2DROP = 109 +OP_2DUP = 110 +OP_3DUP = 111 +OP_2OVER = 112 +OP_2ROT = 113 +OP_2SWAP = 114 +OP_CAT = 126 +OP_SUBSTR = 127 +OP_LEFT = 128 +OP_RIGHT = 129 +OP_SIZE = 130 +OP_INVERT = 131 +OP_AND = 132 +OP_OR = 133 +OP_XOR = 134 +OP_EQUAL = 135 +OP_EQUALVERIFY = 136 +OP_1ADD = 139 +OP_1SUB = 140 +OP_2MUL = 141 +OP_2DIV = 142 +OP_NEGATE = 143 +OP_ABS = 144 +OP_NOT = 145 +OP_0NOTEQUAL = 146 +OP_ADD = 147 +OP_SUB = 148 +OP_MUL = 149 +OP_DIV = 150 +OP_MOD = 151 +OP_LSHIFT = 152 +OP_RSHIFT = 153 +OP_BOOLAND = 154 +OP_BOOLOR = 155 +OP_NUMEQUAL = 156 +OP_NUMEQUALVERIFY = 157 +OP_NUMNOTEQUAL = 158 +OP_LESSTHAN = 159 +OP_GREATERTHAN = 160 +OP_LESSTHANOREQUAL = 161 +OP_GREATERTHANOREQUAL = 162 +OP_MIN = 163 +OP_MAX = 164 +OP_WITHIN = 165 +OP_RIPEMD160 = 166 +OP_SHA1 = 167 +OP_SHA256 = 168 +OP_HASH160 = 169 +OP_HASH256 = 170 +OP_CODESEPARATOR = 171 +OP_CHECKSIG = 172 +OP_CHECKSIGVERIFY = 173 +OP_CHECKMULTISIG = 174 +OP_CHECKMULTISIGVERIFY = 175 + +opnames = ['']*256 +opnames[0] = 'OP_0' +for i in range(1,76): + opnames[i] ='OP_PUSHDATA' +opnames[76] = 'OP_PUSHDATA1' +opnames[77] = 'OP_PUSHDATA2' +opnames[78] = 'OP_PUSHDATA4' +opnames[79] = 'OP_1NEGATE' +opnames[81] = 'OP_1' +opnames[81] = 'OP_TRUE' +for i in range(1,17): + opnames[80+i] = 'OP_' + str(i) +opnames[97] = 'OP_NOP' +opnames[99] = 'OP_IF' +opnames[100] = 'OP_NOTIF' +opnames[103] = 'OP_ELSE' +opnames[104] = 'OP_ENDIF' +opnames[105] = 'OP_VERIFY' +opnames[106] = 'OP_RETURN' +opnames[107] = 'OP_TOALTSTACK' +opnames[108] = 'OP_FROMALTSTACK' +opnames[115] = 'OP_IFDUP' +opnames[116] = 'OP_DEPTH' +opnames[117] = 'OP_DROP' +opnames[118] = 'OP_DUP' +opnames[119] = 'OP_NIP' +opnames[120] = 'OP_OVER' +opnames[121] = 'OP_PICK' +opnames[122] = 'OP_ROLL' +opnames[123] = 'OP_ROT' +opnames[124] = 'OP_SWAP' +opnames[125] = 'OP_TUCK' +opnames[109] = 'OP_2DROP' +opnames[110] = 'OP_2DUP' +opnames[111] = 'OP_3DUP' +opnames[112] = 'OP_2OVER' +opnames[113] = 'OP_2ROT' +opnames[114] = 'OP_2SWAP' +opnames[126] = 'OP_CAT' +opnames[127] = 'OP_SUBSTR' +opnames[128] = 'OP_LEFT' +opnames[129] = 'OP_RIGHT' +opnames[130] = 'OP_SIZE' +opnames[131] = 'OP_INVERT' +opnames[132] = 'OP_AND' +opnames[133] = 'OP_OR' +opnames[134] = 'OP_XOR' +opnames[135] = 'OP_EQUAL' +opnames[136] = 'OP_EQUALVERIFY' +opnames[139] = 'OP_1ADD' +opnames[140] = 'OP_1SUB' +opnames[141] = 'OP_2MUL' +opnames[142] = 'OP_2DIV' +opnames[143] = 'OP_NEGATE' +opnames[144] = 'OP_ABS' +opnames[145] = 'OP_NOT' +opnames[146] = 'OP_0NOTEQUAL' +opnames[147] = 'OP_ADD' +opnames[148] = 'OP_SUB' +opnames[149] = 'OP_MUL' +opnames[150] = 'OP_DIV' +opnames[151] = 'OP_MOD' +opnames[152] = 'OP_LSHIFT' +opnames[153] = 'OP_RSHIFT' +opnames[154] = 'OP_BOOLAND' +opnames[155] = 'OP_BOOLOR' +opnames[156] = 'OP_NUMEQUAL' +opnames[157] = 'OP_NUMEQUALVERIFY' +opnames[158] = 'OP_NUMNOTEQUAL' +opnames[159] = 'OP_LESSTHAN' +opnames[160] = 'OP_GREATERTHAN' +opnames[161] = 'OP_LESSTHANOREQUAL' +opnames[162] = 'OP_GREATERTHANOREQUAL' +opnames[163] = 'OP_MIN' +opnames[164] = 'OP_MAX' +opnames[165] = 'OP_WITHIN' +opnames[166] = 'OP_RIPEMD160' +opnames[167] = 'OP_SHA1' +opnames[168] = 'OP_SHA256' +opnames[169] = 'OP_HASH160' +opnames[170] = 'OP_HASH256' +opnames[171] = 'OP_CODESEPARATOR' +opnames[172] = 'OP_CHECKSIG' +opnames[173] = 'OP_CHECKSIGVERIFY' +opnames[174] = 'OP_CHECKMULTISIG' +opnames[175] = 'OP_CHECKMULTISIGVERIFY' + + +opCodeLookup = {} +opCodeLookup['OP_FALSE'] = 0 +opCodeLookup['OP_PUSHDATA1'] = 76 +opCodeLookup['OP_PUSHDATA2'] = 77 +opCodeLookup['OP_PUSHDATA4'] = 78 +opCodeLookup['OP_1NEGATE'] = 79 +opCodeLookup['OP_1'] = 81 +for i in range(1,17): + opCodeLookup['OP_'+str(i)] = 80+i +opCodeLookup['OP_TRUE'] = 81 +opCodeLookup['OP_NOP'] = 97 +opCodeLookup['OP_IF'] = 99 +opCodeLookup['OP_NOTIF'] = 100 +opCodeLookup['OP_ELSE'] = 103 +opCodeLookup['OP_ENDIF'] = 104 +opCodeLookup['OP_VERIFY'] = 105 +opCodeLookup['OP_RETURN'] = 106 +opCodeLookup['OP_TOALTSTACK'] = 107 +opCodeLookup['OP_FROMALTSTACK'] = 108 +opCodeLookup['OP_IFDUP'] = 115 +opCodeLookup['OP_DEPTH'] = 116 +opCodeLookup['OP_DROP'] = 117 +opCodeLookup['OP_DUP'] = 118 +opCodeLookup['OP_NIP'] = 119 +opCodeLookup['OP_OVER'] = 120 +opCodeLookup['OP_PICK'] = 121 +opCodeLookup['OP_ROLL'] = 122 +opCodeLookup['OP_ROT'] = 123 +opCodeLookup['OP_SWAP'] = 124 +opCodeLookup['OP_TUCK'] = 125 +opCodeLookup['OP_2DROP'] = 109 +opCodeLookup['OP_2DUP'] = 110 +opCodeLookup['OP_3DUP'] = 111 +opCodeLookup['OP_2OVER'] = 112 +opCodeLookup['OP_2ROT'] = 113 +opCodeLookup['OP_2SWAP'] = 114 +opCodeLookup['OP_CAT'] = 126 +opCodeLookup['OP_SUBSTR'] = 127 +opCodeLookup['OP_LEFT'] = 128 +opCodeLookup['OP_RIGHT'] = 129 +opCodeLookup['OP_SIZE'] = 130 +opCodeLookup['OP_INVERT'] = 131 +opCodeLookup['OP_AND'] = 132 +opCodeLookup['OP_OR'] = 133 +opCodeLookup['OP_XOR'] = 134 +opCodeLookup['OP_EQUAL'] = 135 +opCodeLookup['OP_EQUALVERIFY'] = 136 +opCodeLookup['OP_1ADD'] = 139 +opCodeLookup['OP_1SUB'] = 140 +opCodeLookup['OP_2MUL'] = 141 +opCodeLookup['OP_2DIV'] = 142 +opCodeLookup['OP_NEGATE'] = 143 +opCodeLookup['OP_ABS'] = 144 +opCodeLookup['OP_NOT'] = 145 +opCodeLookup['OP_0NOTEQUAL'] = 146 +opCodeLookup['OP_ADD'] = 147 +opCodeLookup['OP_SUB'] = 148 +opCodeLookup['OP_MUL'] = 149 +opCodeLookup['OP_DIV'] = 150 +opCodeLookup['OP_MOD'] = 151 +opCodeLookup['OP_LSHIFT'] = 152 +opCodeLookup['OP_RSHIFT'] = 153 +opCodeLookup['OP_BOOLAND'] = 154 +opCodeLookup['OP_BOOLOR'] = 155 +opCodeLookup['OP_NUMEQUAL'] = 156 +opCodeLookup['OP_NUMEQUALVERIFY'] = 157 +opCodeLookup['OP_NUMNOTEQUAL'] = 158 +opCodeLookup['OP_LESSTHAN'] = 159 +opCodeLookup['OP_GREATERTHAN'] = 160 +opCodeLookup['OP_LESSTHANOREQUAL'] = 161 +opCodeLookup['OP_GREATERTHANOREQUAL'] = 162 +opCodeLookup['OP_MIN'] = 163 +opCodeLookup['OP_MAX'] = 164 +opCodeLookup['OP_WITHIN'] = 165 +opCodeLookup['OP_RIPEMD160'] = 166 +opCodeLookup['OP_SHA1'] = 167 +opCodeLookup['OP_SHA256'] = 168 +opCodeLookup['OP_HASH160'] = 169 +opCodeLookup['OP_HASH256'] = 170 +opCodeLookup['OP_CODESEPARATOR'] = 171 +opCodeLookup['OP_CHECKSIG'] = 172 +opCodeLookup['OP_CHECKSIGVERIFY'] = 173 +opCodeLookup['OP_CHECKMULTISIG'] = 174 +opCodeLookup['OP_CHECKMULTISIGVERIFY'] = 175 +#Word Opcode Description +#OP_PUBKEYHASH = 253 Represents a public key hashed with OP_HASH160. +#OP_PUBKEY = 254 Represents a public key compatible with OP_CHECKSIG. +#OP_INVALIDOPCODE = 255 Matches any opcode that is not yet assigned. +#[edit] Reserved words +#Any opcode not assigned is also reserved. Using an unassigned opcode makes the transaction invalid. +#Word Opcode When used... +#OP_RESERVED = 80 Transaction is invalid +#OP_VER = 98 Transaction is invalid +#OP_VERIF = 101 Transaction is invalid +#OP_VERNOTIF = 102 Transaction is invalid +#OP_RESERVED1 = 137 Transaction is invalid +#OP_RESERVED2 = 138 Transaction is invalid +#OP_NOP1 = OP_NOP10 176-185 The word is ignored. + + +################################################################################ +def getOpCode(name): + return int_to_binary(opCodeLookup[name], widthBytes=1) + + +################################################################################ +def getMultisigScriptInfo(rawScript): + """ + Gets the Multi-Sig tx type, as well as all the address-160 strings of + the keys that are needed to satisfy this transaction. This currently + only identifies M-of-N transaction types, returning unknown otherwise. + + However, the address list it returns should be valid regardless of + whether the type was unknown: we assume all 20-byte chunks of data + are public key hashes, and 65-byte chunks are public keys. + + M==0 (output[0]==0) indicates this isn't a multisig script + """ + + scrAddr = '' + addr160List = [] + pubKeyList = [] + + M,N = 0,0 + + pubKeyStr = Cpp.BtcUtils().getMultisigPubKeyInfoStr(rawScript) + + bu = BinaryUnpacker(pubKeyStr) + M = bu.get(UINT8) + N = bu.get(UINT8) + + if M==0: + return [0, 0, None, None] + + for i in range(N): + pkstr = bu.get(BINARY_CHUNK, 33) + if pkstr[0] == '\x04': + pkstr += bu.get(BINARY_CHUNK, 32) + pubKeyList.append(pkstr) + addr160List.append(hash160(pkstr)) + + return M, N, addr160List, pubKeyList + + +################################################################################ +def getHash160ListFromMultisigScrAddr(scrAddr): + mslen = len(scrAddr) - 3 + if not (mslen%20==0 and scrAddr[0]==SCRADDR_MULTISIG_BYTE): + raise BadAddressError('Supplied ScrAddr is not multisig!') + + catList = scrAddr[3:] + return [catList[20*i:20*(i+1)] for i in range(len(catList)/20)] + + +################################################################################ +# These two methods are just easier-to-type wrappers around the C++ methods +def getTxOutScriptType(script): + return Cpp.BtcUtils().getTxOutScriptTypeInt(script) + + +################################################################################ +def getTxInScriptType(txinObj): + """ + NOTE: this method takes a TXIN object, not just the script itself. This + is because this method needs to see the OutPoint to distinguish an + UNKNOWN TxIn from a coinbase-TxIn + """ + script = txinObj.binScript + prevTx = txinObj.outpoint.txHash + return Cpp.BtcUtils().getTxInScriptTypeInt(script, prevTx) + +################################################################################ +def getTxInP2SHScriptType(txinObj): + """ + If this TxIn is identified as SPEND-P2SH, then it contains a subscript that + is really a TxOut script (which must hash to the value included on the + actual TxOut script). + + Use this to get the TxOut script type of the Spend-P2SH subscript + """ + scrType = getTxInScriptType(txinObj) + if not scrType==CPP_TXIN_SPENDP2SH: + return None + + lastPush = Cpp.BtcUtils().getLastPushDataInScript(txinObj.binScript) + + return getTxOutScriptType(lastPush) + + +################################################################################ +def TxInExtractAddrStrIfAvail(txinObj): + rawScript = txinObj.binScript + prevTxHash = txinObj.outpoint.txHash + scrType = Cpp.BtcUtils().getTxInScriptTypeInt(rawScript, prevTxHash) + lastPush = Cpp.BtcUtils().getLastPushDataInScript(rawScript) + + if scrType in [CPP_TXIN_STDUNCOMPR, CPP_TXIN_STDCOMPR]: + return hash160_to_addrStr( hash160(lastPush) ) + elif scrType == CPP_TXIN_SPENDP2SH: + return hash160_to_p2shStr( hash160(lastPush) ) + else: + return '' + + +################################################################################ +def TxInExtractPreImageIfAvail(txinObj): + rawScript = txinObj.binScript + prevTxHash = txinObj.outpoint.txHash + scrType = Cpp.BtcUtils().getTxInScriptTypeInt(rawScript, prevTxHash) + + if scrType == [CPP_TXIN_STDUNCOMPR, CPP_TXIN_STDCOMPR, CPP_TXIN_SPENDP2SH]: + return Cpp.BtcUtils().getLastPushDataInScript(rawScript) + else: + return '' + + + + +# Finally done with all the base conversion functions and ECDSA code +# Now define the classes for the objects that will use this + + +################################################################################ +# Transaction Classes +################################################################################ + + +##### +class BlockComponent(object): + + def copy(self): + return self.__class__().unserialize(self.serialize()) + + def serialize(self): + raise NotImplementedError + + def unserialize(self): + raise NotImplementedError + +################################################################################ +class PyOutPoint(BlockComponent): + #def __init__(self, txHash, txOutIndex): + #self.txHash = txHash + #self.txOutIndex = outIndex + + def unserialize(self, toUnpack): + if isinstance(toUnpack, BinaryUnpacker): + opData = toUnpack + else: + opData = BinaryUnpacker( toUnpack ) + + if opData.getRemainingSize() < 36: raise UnserializeError + self.txHash = opData.get(BINARY_CHUNK, 32) + self.txOutIndex = opData.get(UINT32) + return self + + def serialize(self): + binOut = BinaryPacker() + binOut.put(BINARY_CHUNK, self.txHash) + binOut.put(UINT32, self.txOutIndex) + return binOut.getBinaryString() + + def pprint(self, nIndent=0, endian=BIGENDIAN): + indstr = indent*nIndent + print indstr + 'OutPoint:' + print indstr + indent + 'PrevTxHash:', \ + binary_to_hex(self.txHash, endian), \ + '(BE)' if endian==BIGENDIAN else '(LE)' + print indstr + indent + 'TxOutIndex:', self.txOutIndex + + +##### +class PyTxIn(BlockComponent): + def __init__(self): + self.outpoint = UNINITIALIZED + self.binScript = UNINITIALIZED + self.intSeq = 2**32-1 + + def unserialize(self, toUnpack): + if isinstance(toUnpack, BinaryUnpacker): + txInData = toUnpack + else: + txInData = BinaryUnpacker( toUnpack ) + + self.outpoint = PyOutPoint().unserialize( txInData.get(BINARY_CHUNK, 36) ) + + scriptSize = txInData.get(VAR_INT) + if txInData.getRemainingSize() < scriptSize+4: raise UnserializeError + self.binScript = txInData.get(BINARY_CHUNK, scriptSize) + self.intSeq = txInData.get(UINT32) + return self + + def getScript(self): + return self.binScript + + def serialize(self): + binOut = BinaryPacker() + binOut.put(BINARY_CHUNK, self.outpoint.serialize() ) + binOut.put(VAR_INT, len(self.binScript)) + binOut.put(BINARY_CHUNK, self.binScript) + binOut.put(UINT32, self.intSeq) + return binOut.getBinaryString() + + def pprint(self, nIndent=0, endian=BIGENDIAN): + indstr = indent*nIndent + print indstr + 'PyTxIn:' + print indstr + indent + 'PrevTxHash:', \ + binary_to_hex(self.outpoint.txHash, endian), \ + '(BE)' if endian==BIGENDIAN else '(LE)' + print indstr + indent + 'TxOutIndex:', self.outpoint.txOutIndex + print indstr + indent + 'Script: ', \ + '('+binary_to_hex(self.binScript)[:64]+')' + addrStr = TxInExtractAddrStrIfAvail(self) + if len(addrStr)==0: + addrStr = '' + print indstr + indent + 'Sender: ', addrStr + print indstr + indent + 'Seq: ', self.intSeq + + def toString(self, nIndent=0, endian=BIGENDIAN): + indstr = indent*nIndent + indstr2 = indstr + indent + result = indstr + 'PyTxIn:' + result = ''.join([result, '\n', indstr2 + 'PrevTxHash:', \ + binary_to_hex(self.outpoint.txHash, endian), \ + '(BE)' if endian==BIGENDIAN else '(LE)']) + result = ''.join([result, '\n', indstr2 + 'TxOutIndex:', \ + str(self.outpoint.txOutIndex)]) + result = ''.join([result, '\n', indstr2 + 'Script: ', \ + '('+binary_to_hex(self.binScript)[:64]+')']) + addrStr = TxInExtractAddrStrIfAvail(self) + if len(addrStr)>0: + result = ''.join([result, '\n', indstr2 + 'Sender: ', addrStr]) + result = ''.join([result, '\n', indstr2 + 'Seq: ', str(self.intSeq)]) + return result + + # Before broadcasting a transaction make sure that the script is canonical + # This TX could have been signed by an older version of the software. + # Either on the offline Armory installation which may not have been upgraded + # or on a previous installation of Armory on this computer. + def minimizeDERSignaturePadding(self): + rsLen = binary_to_int(self.binScript[2:3]) + rLen = binary_to_int(self.binScript[4:5]) + rBin = self.binScript[5:5+rLen] + sLen = binary_to_int(self.binScript[6+rLen:7+rLen]) + sBin = self.binScript[7+rLen:7+rLen+sLen] + sigScript = createSigScriptFromRS(rBin, sBin) + newBinScript = int_to_binary(len(sigScript)+1) + sigScript + self.binScript[3+rsLen:] + paddingRemoved = newBinScript != self.binScript + newTxIn = self.copy() + newTxIn.binScript = newBinScript + return paddingRemoved, newTxIn + + +##### +class PyTxOut(BlockComponent): + def __init__(self): + self.value = UNINITIALIZED + self.binScript = UNINITIALIZED + + def unserialize(self, toUnpack): + if isinstance(toUnpack, BinaryUnpacker): + txOutData = toUnpack + else: + txOutData = BinaryUnpacker( toUnpack ) + + self.value = txOutData.get(UINT64) + scriptSize = txOutData.get(VAR_INT) + if txOutData.getRemainingSize() < scriptSize: raise UnserializeError + self.binScript = txOutData.get(BINARY_CHUNK, scriptSize) + return self + + def getValue(self): + return self.value + + def getScript(self): + return self.binScript + + def serialize(self): + binOut = BinaryPacker() + binOut.put(UINT64, self.value) + binOut.put(VAR_INT, len(self.binScript)) + binOut.put(BINARY_CHUNK, self.binScript) + return binOut.getBinaryString() + + def pprint(self, nIndent=0, endian=BIGENDIAN): + """ + indstr = indent*nIndent + indstr2 = indent*nIndent + indent + print indstr + 'TxOut:' + print indstr2 + 'Value: ', self.value, '(', float(self.value) / ONE_BTC, ')' + txoutType = getTxOutScriptType(self.binScript) + if txoutType in [CPP_TXOUT_STDPUBKEY33, CPP_TXOUT_STDPUBKEY65]: + print indstr2 + 'Script: PubKey(%s) OP_CHECKSIG' % \ + script_to_addrStr(self.binScript) + elif txoutType == CPP_TXOUT_STDHASH160: + print indstr2 + 'Script: OP_DUP OP_HASH160 (%s) OP_EQUALVERIFY OP_CHECKSIG' % \ + script_to_addrStr(self.binScript) + elif txoutType == CPP_TXOUT_P2SH: + print indstr2 + 'Script: OP_HASH160 (%s) OP_EQUAL' % \ + script_to_addrStr(self.binScript) + else: + opStrList = convertScriptToOpStrings(self.binScript) + print indstr + indent + 'Script: ', ' '.join(opStrList) + """ + print self.toString(nIndent, endian) + + def toString(self, nIndent=0, endian=BIGENDIAN): + indstr = indent*nIndent + indstr2 = indent*nIndent + indent + valStr, btcStr = str(self.value), str(float(self.value)/ONE_BTC) + result = indstr + 'TxOut:\n' + result += indstr2 + 'Value: %s (%s)\n' % (valStr, btcStr) + result += indstr2 + txoutType = getTxOutScriptType(self.binScript) + + if txoutType in [CPP_TXOUT_STDPUBKEY33, CPP_TXOUT_STDPUBKEY65]: + result += 'Script: PubKey(%s) OP_CHECKSIG \n' % \ + script_to_addrStr(self.binScript) + elif txoutType == CPP_TXOUT_STDHASH160: + result += 'Script: OP_DUP OP_HASH160 (%s) OP_EQUALVERIFY OP_CHECKSIG' % \ + script_to_addrStr(self.binScript) + elif txoutType == CPP_TXOUT_P2SH: + result += 'Script: OP_HASH160 (%s) OP_EQUAL' % \ + script_to_addrStr(self.binScript) + else: + opStrList = convertScriptToOpStrings(self.binScript) + result += 'Script: ' + ' '.join(opStrList) + + return result + +##### +class PyTx(BlockComponent): + def __init__(self): + self.version = UNINITIALIZED + self.inputs = UNINITIALIZED + self.outputs = UNINITIALIZED + self.lockTime = 0 + self.thisHash = UNINITIALIZED + + def serialize(self): + binOut = BinaryPacker() + binOut.put(UINT32, self.version) + binOut.put(VAR_INT, len(self.inputs)) + for txin in self.inputs: + binOut.put(BINARY_CHUNK, txin.serialize()) + binOut.put(VAR_INT, len(self.outputs)) + for txout in self.outputs: + binOut.put(BINARY_CHUNK, txout.serialize()) + binOut.put(UINT32, self.lockTime) + return binOut.getBinaryString() + + def unserialize(self, toUnpack): + if isinstance(toUnpack, BinaryUnpacker): + txData = toUnpack + else: + txData = BinaryUnpacker( toUnpack ) + + startPos = txData.getPosition() + self.inputs = [] + self.outputs = [] + self.version = txData.get(UINT32) + numInputs = txData.get(VAR_INT) + for i in xrange(numInputs): + self.inputs.append( PyTxIn().unserialize(txData) ) + numOutputs = txData.get(VAR_INT) + for i in xrange(numOutputs): + self.outputs.append( PyTxOut().unserialize(txData) ) + self.lockTime = txData.get(UINT32) + endPos = txData.getPosition() + self.nBytes = endPos - startPos + self.thisHash = hash256(self.serialize()) + return self + + # Before broadcasting a transaction make sure that the script is canonical + # This TX could have been signed by an older version of the software. + # Either on the offline Armory installation which may not have been upgraded + # or on a previous installation of Armory on this computer. + def minimizeDERSignaturePadding(self): + paddingRemoved = False + newTx = self.copy() + newTx.inputs = [] + for txIn in self.inputs: + paddingRemovedFromTxIn, newTxIn = txIn.minimizeDERSignaturePadding() + if paddingRemovedFromTxIn: + paddingRemoved = True + newTx.inputs.append(newTxIn) + else: + newTx.inputs.append(txIn) + + return paddingRemoved, newTx.copy() + + def getHash(self): + return hash256(self.serialize()) + + def getHashHex(self, endianness=LITTLEENDIAN): + return binary_to_hex(self.getHash(), endOut=endianness) + + def makeRecipientsList(self): + """ + Make a list of lists, each one containing information about + an output in this tx. Usually contains + [ScriptType, Value, Script] + May include more information if any of the scripts are multi-sig, + such as public keys and multi-sig type (M-of-N) + """ + recipInfoList = [] + for txout in self.outputs: + recipInfoList.append([]) + + scrType = getTxOutScriptType(txout.binScript) + recipInfoList[-1].append(scrType) + recipInfoList[-1].append(txout.value) + recipInfoList[-1].append(txout.binScript) + if scrType == CPP_TXOUT_MULTISIG: + recipInfoList[-1].append(getMultisigScriptInfo(txout.binScript)) + else: + recipInfoList[-1].append([]) + + return recipInfoList + + + def pprint(self, nIndent=0, endian=BIGENDIAN): + indstr = indent*nIndent + print indstr + 'Transaction:' + print indstr + indent + 'TxHash: ', self.getHashHex(endian), \ + '(BE)' if endian==BIGENDIAN else '(LE)' + print indstr + indent + 'Version: ', self.version + print indstr + indent + 'nInputs: ', len(self.inputs) + print indstr + indent + 'nOutputs: ', len(self.outputs) + print indstr + indent + 'LockTime: ', self.lockTime + print indstr + indent + 'Inputs: ' + for inp in self.inputs: + inp.pprint(nIndent+2, endian=endian) + print indstr + indent + 'Outputs: ' + for out in self.outputs: + out.pprint(nIndent+2, endian=endian) + + def toString(self, nIndent=0, endian=BIGENDIAN): + indstr = indent*nIndent + result = indstr + 'Transaction:' + result = ''.join([result, '\n', indstr + indent + 'TxHash: ', self.getHashHex(endian), \ + '(BE)' if endian==BIGENDIAN else '(LE)']) + result = ''.join([result, '\n', indstr + indent + 'Version: ', str(self.version)]) + result = ''.join([result, '\n', indstr + indent + 'nInputs: ', str(len(self.inputs))]) + result = ''.join([result, '\n', indstr + indent + 'nOutputs: ', str(len(self.outputs))]) + result = ''.join([result, '\n', indstr + indent + 'LockTime: ', str(self.lockTime)]) + result = ''.join([result, '\n', indstr + indent + 'Inputs: ']) + for inp in self.inputs: + result = ''.join([result, '\n', inp.toString(nIndent+2, endian=endian)]) + print indstr + indent + 'Outputs: ' + for out in self.outputs: + result = ''.join([result, '\n', out.toString(nIndent+2, endian=endian)]) + return result + + def fetchCpp(self): + """ Use the info in this PyTx to get the C++ version from TheBDM """ + return TheBDM.getTxByHash(self.getHash()) + + def pprintHex(self, nIndent=0): + bu = BinaryUnpacker(self.serialize()) + theSer = self.serialize() + print binary_to_hex(bu.get(BINARY_CHUNK, 4)) + nTxin = bu.get(VAR_INT) + print 'VAR_INT(%d)' % nTxin + for i in range(nTxin): + print binary_to_hex(bu.get(BINARY_CHUNK,32)) + print binary_to_hex(bu.get(BINARY_CHUNK,4)) + scriptSz = bu.get(VAR_INT) + print 'VAR_IN(%d)' % scriptSz + print binary_to_hex(bu.get(BINARY_CHUNK,scriptSz)) + print binary_to_hex(bu.get(BINARY_CHUNK,4)) + nTxout = bu.get(VAR_INT) + print 'VAR_INT(%d)' % nTxout + for i in range(nTxout): + print binary_to_hex(bu.get(BINARY_CHUNK,8)) + scriptSz = bu.get(VAR_INT) + print binary_to_hex(bu.get(BINARY_CHUNK,scriptSz)) + print binary_to_hex(bu.get(BINARY_CHUNK, 4)) + + + + + + +################################################################################ +################################################################################ +# This class can be used for both multi-signature tx collection, as well as +# offline wallet signing (you are collecting signatures for a 1-of-1 tx only +# involving yourself). +class PyTxDistProposal(object): + """ + PyTxDistProposal is created from a PyTx object, and represents + an unsigned transaction, that may require the signatures of + multiple parties before being accepted by the network. + + This technique (https://en.bitcoin.it/wiki/BIP_0010) is that + once TxDP is created, the system signing it only needs the + ECDSA private keys and nothing else. This enables the device + providing the signatures to be extremely lightweight, since it + doesn't have to store the blockchain. + + For a given TxDP, we will be storing the following structure + in memory. Use a 4-input tx as an example, with the first + being a 2-of-3 multi-sig transaction (unsigned), and the last + is a 2-o-2 P2SH input. + + self.scriptTypes = [ CPP_TXOUT_MULTISIG, + CPP_TXOUT_STDHASH160, + CPP_TXOUT_STDHASH160, + CPP_TXOUT_P2SH] + + self.inputValues = [ long(23.13 * ONE_BTC), + long( 4.00 * ONE_BTC), + long(10.00 * ONE_BTC), + long( 5.00 * ONE_BTC) ] + + self.signatures = [ ['', '', ''], + [''], + [''], + [''], ] + + self.inScrAddrList = [ fe0203, + HASH160PREFIX + a160_4, + HASH160PREFIX + a160_5, + P2SHPREFIX + p2sh160 ] + + self.p2shScripts = [ '', + '', + '', + ] + + # Usually only have public keys on multi-sig TxOuts + self.inPubKeyLists = [ [pubKey1, pubKey2, pubKey3], + [''], + [''], + [pubKey6, pubKey7] ] + + self.numSigsNeeded = [ 2, + 1, + 1, + 2 ] + + self.relevantTxMap = [ prevTx0Hash: prevTx0.serialize(), + prevTx1Hash: prevTx1.serialize(), + prevTx2Hash: prevTx2.serialize(), + prevTx3Hash: prevTx3.serialize() ] + + UPDATE Feb 2012: Before Jan 29, 2012, BIP 0010 used a different technique + for communicating blockchain information to the offline + device. This is no longer the case + + Gregory Maxwell identified a reasonable-enough security + risk with the fact that previous BIP 0010 cannot guarantee + validity of stated input values in a TxDP. This is solved + by adding the supporting transactions to the TxDP, so that + the signing device can get the input values from those + tx and verify the hash matches the OutPoint on the tx + being signed (which *is* part of what's being signed). + The concern was that someone could manipulate your online + computer to misrepresent the inputs, and cause you to + send you entire wallet to tx-fees. Not the most useful + attack (for someone trying to steal your coins), but it is + still a risk that can be avoided by adding some "bloat" to + the TxDP + + + + """ + ############################################################################# + def __init__(self, pytx=None, txMap={}): + self.pytxObj = UNINITIALIZED + self.uniqueB58 = '' + self.scriptTypes = [] + self.signatures = [] + self.txOutScripts = [] + self.inScrAddrList = [] + self.p2shScripts = [] + self.inPubKeyLists = [] + self.inputValues = [] + self.numSigsNeeded = [] + self.relevantTxMap = {} # needed to support input values of each TxIn + if pytx: + self.createFromPyTx(pytx, txMap) + + ############################################################################# + def createFromPyTx(self, pytx, txMap={}, p2shMap={}): + sz = len(pytx.inputs) + self.pytxObj = pytx.copy() + self.uniqueB58 = binary_to_base58(hash256(pytx.serialize()))[:8] + self.scriptTypes = [] + self.signatures = [] + self.txOutScripts = [] + self.inScrAddrList = [] + self.inPubKeyLists = [] + self.inputValues = [] + self.numSigsNeeded = [] + self.relevantTxMap = {} # needed to support input values of each TxIn + self.p2shScripts = [] + + if len(txMap)==0 and not TheBDM.getBDMState()=='BlockchainReady': + # TxDP includes the transactions that supply the inputs to this + # transaction, so the BDM needs to be available to fetch those. + raise BlockchainUnavailableError, ('Must input supporting transactions ' + 'or access to the blockchain, to ' + 'create the TxDP') + for i in range(sz): + # First, make sure that we have the previous Tx data available + # We can't continue without it, since BIP 0010 will now require + # the full tx of outputs being spent + outpt = self.pytxObj.inputs[i].outpoint + txhash = outpt.txHash + txidx = outpt.txOutIndex + pyPrevTx = None + if len(txMap)>0: + # If supplied a txMap, we expect it to have everything we need + if not txMap.has_key(txhash): + raise InvalidHashError, ('Could not find the referenced tx ' + 'in supplied txMap') + pyPrevTx = txMap[txhash].copy() + elif TheBDM.getBDMState()=='BlockchainReady': + cppPrevTx = TheBDM.getTxByHash(txhash) + if not cppPrevTx: + raise InvalidHashError, 'Could not find the referenced tx' + pyPrevTx = PyTx().unserialize(cppPrevTx.serialize()) + else: + raise InvalidScriptError, 'No previous-tx data available for TxDP' + + self.relevantTxMap[txhash] = pyPrevTx.copy() + + + # Now we have the previous transaction. We need to pull the + # script out of the specific TxOut so we know how it can be + # spent. + script = pyPrevTx.outputs[txidx].binScript + value = pyPrevTx.outputs[txidx].value + scrType = getTxOutScriptType(script) + + self.inputValues.append(value) + self.txOutScripts.append(str(script)) # copy it + self.scriptTypes.append(scrType) + + # Make sure we always add an element to each of these + self.numSigsNeeded.append(-1) + self.inScrAddrList.append('') + self.p2shScripts.append('') + self.inPubKeyLists.append([]) + self.signatures.append([]) + + # If this is a P2SH TxOut being spent, we store the information + # about the SUBSCRIPT, since that is ultimately what needs to be + # "solved" to spend the coins. The fact that self.p2shScripts[i] + # will be non-empty is how we know we need to add the serialized + # SUBSCRIPT to the end of the sigScript when signing. + if scrType==CPP_TXOUT_P2SH: + p2shScrAddr = script_to_scrAddr(script) + + if not p2shMap.has_key(p2shScrAddr): + raise InvalidScriptError, 'No P2SH script info avail for TxDP' + else: + scriptHash = hash160(p2shMap[p2shScrAddr]) + if not SCRADDR_P2SH_BYTE+scriptHash == p2shScrAddr: + raise InvalidScriptError, 'No P2SH script info avail for TxDP' + + script = p2shMap[p2shScrAddr] + self.p2shScripts[-1] = script + + scrType = getTxOutScriptType(script) + self.scriptTypes[-1] = scrType + + + # Fill some of the other fields with info needed to spend the script + if scrType==CPP_TXOUT_P2SH: + # Technically, this is just "OP_HASH160 OP_EQUAL" in the + # subscript which would be unusual and mostly useless. I'll assume + # here that it was an attempt at recursive P2SH, since they are + # both the same to our code: unspendable + raise InvalidScriptError('Cannot have recursive P2SH scripts!') + elif scrType in CPP_TXOUT_STDSINGLESIG: + self.numSigsNeeded[-1] = 1 + self.inScrAddrList[-1] = script_to_scrAddr(script) + self.signatures[-1] = [''] + elif scrType==CPP_TXOUT_MULTISIG: + M, N, a160s, pubs = getMultisigScriptInfo(script) + self.inScrAddrList[-1] = [SCRADDR_P2PKH_BYTE+a for a in a160s] + self.inPubKeyLists[-1] = pubs[:] + self.signatures[-1] = ['']*len(addrs) + self.numSigsNeeded[-1] = M + else: + LOGWARN("Non-standard script for TxIn %d" % i) + LOGWARN(binary_to_hex(script)) + pass + + return self + + + ############################################################################# + def createFromTxOutSelection(self, utxoSelection, scriptValuePairs, txMap={}): + """ + This creates a TxDP for a standard transaction from a list of inputs and + a list of recipient-value-pairs. + + NOTE: I have modified this so that if the "recip" is not a 20-byte binary + string, it is instead interpretted as a SCRIPT -- which could be + anything, including a multi-signature transaction + """ + + for scr,val in scriptValuePairs: + if len(scr)==20: + raise BadAddressError( tr(""" + createFromTxOutSelection() has changed to take (script, value) + pairs instead of (hash160, value) pairs. This is because we + need this function to be able to send to any arbitrary script, + not just pay2pubkeyhash scripts. Especially for P2SH support. + This method will check that it is either reg, P2SH or multisig + before continuing. Modify this function to allow more script + types to be handled.""")) + + + totalUtxoSum = sumTxOutList(utxoSelection) + totalOutputSum = sum([a[1] for a in scriptValuePairs]) + if not totalUtxoSum >= totalOutputSum: + raise TxdpError('More outputs than inputs!') + + thePyTx = PyTx() + thePyTx.version = 1 + thePyTx.lockTime = 0 + thePyTx.inputs = [] + thePyTx.outputs = [] + + # We can prepare the outputs, first + for script,value in scriptValuePairs: + txout = PyTxOut() + txout.value = long(value) + + # Assume recipObj is either a PBA or a string + if isinstance(script, PyBtcAddress): + LOGERROR("Didn't know any func was still using this conditional") + #scrAddr = addrStr_to_scrAddr(scrAddr.getAddrStr()) + #scrAddr = + + + intType = getTxOutScriptType(script) + if intType==CPP_TXOUT_NONSTANDARD: + LOGERROR('Only standard script types are valid for this call') + LOGERROR('Script: ' + binary_to_hex(script)) + raise BadAddressError('Invalid script for tx creation') + + txout.binScript = script[:] + thePyTx.outputs.append(txout) + + # Prepare the inputs based on the utxo objects + for iin,utxo in enumerate(utxoSelection): + # First, make sure that we have the previous Tx data available + # We can't continue without it, since BIP 0010 will now require + # the full tx of outputs being spent + txin = PyTxIn() + txin.outpoint = PyOutPoint() + txin.binScript = '' + txin.intSeq = 2**32-1 + + txhash = utxo.getTxHash() + txidx = utxo.getTxOutIndex() + txin.outpoint.txHash = str(txhash) + txin.outpoint.txOutIndex = txidx + thePyTx.inputs.append(txin) + + return self.createFromPyTx(thePyTx, txMap) + + + ############################################################################# + # Currently not used, but may be when we finally implement multi-sig (or coinjoin) + def appendSignature(self, binSig, txinIndex=None): + """ + Use this to add a signature to the TxDP object in memory. + """ + idx, pos, scrAddr = self.processSignature(binSig, txinIndex, checkAllInputs=True) + if scrAddr: + self.signatures[idx].append(binSig) + return True + + return False + + ############################################################################# + def processSignature(self, sigStr, txinIdx, checkAllInputs=False): + """ + For standard transaction types, the signature field is actually the raw + script to be plugged into the final transaction that allows it to eval + to true -- except for multi-sig transactions. We have to mess with the + data a little bit if we want to use the script-processor to verify the + signature. Instead, we will use the crypto ops directly. + + The return value is everything we need to know about this signature: + -- TxIn-index: if checkAllInputs=True, we need to know which one worked + -- Addr-position: for multi-sig tx, we need to know which addr it matches + -- Addr160: address to which this signature corresponds + """ + + if txinIdx==None or txinIdx<0 or txinIdx>=len(self.pytxObj.inputs): + pass + else: + scriptType = self.scriptTypes[txinIdx] + txCopy = self.pytxObj.copy() + + if scriptType in CPP_TXOUT_STDSINGLESIG: + # For standard Tx types, sigStr is the full script itself (copy it) + txCopy.inputs[txinIdx].binScript = str(sigStr) + prevOutScript = str(self.txOutScripts[txinIdx]) + psp = PyScriptProcessor(prevOutScript, txCopy, txinIdx) + if psp.verifyTransactionValid(): + return txinIdx, 0, script_to_scrAddr(prevOutScript) + elif scriptType == CPP_TXOUT_MULTISIG: + #STUB + pass + ''' + # For multi-sig, sigStr is the raw ECDSA sig ... we will have to + # manually construct a tx that the script processor can check, + # without the other signatures + for i in range(len(txCopy.inputs)): + if not i==idx: + txCopy.inputs[i].binScript = '' + else: + txCopy.inputs[i].binScript = self.txOutScripts[i] + + hashCode = binary_to_int(sigStr[-1]) + hashCode4 = int_to_binary(hashcode, widthBytes=4) + preHashMsg = txCopy.serialize() + hashCode4 + if not hashCode==1: + raise NotImplementedError, 'Non-standard hashcodes not supported!' + + # Now check all public keys in the multi-sig TxOut script + for i,pubkey in enumerate(self.inPubKeyLists): + tempAddr = PyBtcAddress().createFromPublicKeyData(pubkey) + if tempAddr.verifyDERSignature(preHashMsg, sigStr): + return txInIdx, i, hash160(pubkey) + ''' + + if checkAllInputs: + for i in range(len(self.pytxObj.inputs)): + idx, pos, scrAddr = self.processSignature(sigStr, i) + if idx>0: + return idx, pos, scrAddr + + return -1,-1,'' + + + ############################################################################# + def checkTxHasEnoughSignatures(self, alsoVerify=False): + """ + This method only counts signatures, unless verify==True + """ + for i in range(len(self.pytxObj.inputs)): + numSigsHave = sum( [(1 if sig else 0) for sig in self.signatures[i]] ) + if numSigsHave 0: + nextTx = PyTx().unserialize(binUnpacker) + self.relevantTxMap[nextTx.getHash()] = nextTx + + for txin in targetTx.inputs: + if not self.relevantTxMap.has_key(txin.outpoint.txHash): + raise TxdpError, 'Not all inputs can be verified for TxDP. Aborting!' + + self.createFromPyTx( targetTx, self.relevantTxMap ) + numIn = len(self.pytxObj.inputs) + + # Do some sanity checks + if not self.uniqueB58 == dpIdB58: + raise UnserializeError, 'TxDP: Actual DPID does not match listed ID' + if not MAGIC_BYTES==magic: + raise NetworkIDError, 'TxDP is for diff blockchain! (%s)' % \ + BLOCKCHAINS[magic] + + # At this point, we should have a TxDP constructed, now we need to + # simply scan the rest of the serialized structure looking for any + # signatures that may be included + while not 'END-TRANSACTION' in line: + [iin, val] = line.split('_')[2:] + iin = int(iin) + self.inputValues[iin] = str2coin(val) + + line = nextLine(L) + while '_SIG_' in line: + addrB58, sz, sigszHex = line.split('_')[2:] + sz = int(sz) + sigsz = hex_to_int(sigszHex, endIn=BIGENDIAN) + hexSig = '' + line = nextLine(L) + while (not '_SIG_' in line) and \ + (not 'TXINPUT' in line) and \ + (not 'END-TRANSACTION' in line): + hexSig += line + line = nextLine(L) + binSig = hex_to_binary(hexSig) + idx, sigOrder, scrAddr = self.processSignature(binSig, iin) + if idx == -1: + LOGWARN('Invalid sig: Input %d, addr=%s' % (iin, addrB58)) + elif not scrAddr_to_addrStr(scrAddr)== addrB58: + LOGERROR('Listed addr does not match computed addr') + raise BadAddressError + # If we got here, the signature is valid! + self.signatures[iin][sigOrder] = binSig + + return self + + + + ############################################################################# + def pprint(self, indent=' '): + tx = self.pytxObj + propID = hash256(tx.serialize()) + print indent+'Distribution Proposal : ', binary_to_base58(propID)[:8] + print indent+'Transaction Version : ', tx.version + print indent+'Transaction Lock Time : ', tx.lockTime + print indent+'Num Inputs : ', len(tx.inputs) + for i,txin in enumerate(tx.inputs): + prevHash = txin.outpoint.txHash + prevIndex = txin.outpoint.txOutIndex + #print ' PrevOut: (%s, index=%d)' % (binary_to_hex(prevHash[:8]),prevIndex), + print indent*2 + 'Value: %s' % self.inputValues[i] + print indent*2 + 'SrcScript: %s' % binary_to_hex(self.txOutScripts[i]) + for ns, sig in enumerate(self.signatures[i]): + print indent*2 + 'Sig%d = "%s"'%(ns, binary_to_hex(sig)) + print indent+'Num Outputs : ', len(tx.outputs) + for i,txout in enumerate(tx.outputs): + print ' Recipient: %s BTC' % coin2str(txout.value), + scrType = getTxOutScriptType(txout.binScript) + if scrType in CPP_TXOUT_HAS_ADDRSTR: + print script_to_addrStr(txout.binScript) + elif scrType == CPP_TXOUT_MULTISIG: + M, N, addrs, pubs = getMultisigScriptInfo(txout.binScript) + print 'MULTI-SIG-SCRIPT: %d-of-%d' % (M,N) + for addr in addrs: + print indent*2, hash160_to_addrStr(addr) + elif scrType == CPP_TXOUT_NONSTANDARD: + print 'Non-standard: ', binary_to_hex(txout.binScript) + + +################################################################################ +# NOTE: This method was actually used to create the Blockchain-reorg unit- +# test, and hence why coinbase transactions are supported. However, +# for normal transactions supported by PyBtcEngine, this support is +# unnecessary. +# +# Additionally, this method both creates and signs the tx: however +# PyBtcEngine employs TxDistProposals which require the construction +# and signing to be two separate steps. This method is not suited +# for most of the armoryengine CONOPS. +# +# On the other hand, this method DOES work, and there is no reason +# not to use it if you already have PyBtcAddress-w-PrivKeys avail +# and have a list of inputs and outputs as described below. +# +# This method will take an already-selected set of TxOuts, along with +# PyBtcAddress objects containing necessary the private keys +# +# Src TxOut ~ {PyBtcAddr, PrevTx, PrevTxOutIdx} --OR-- COINBASE = -1 +# Dst TxOut ~ {PyBtcAddr, value} +# +# Of course, we usually don't have the private keys of the dst addrs... +# +def PyCreateAndSignTx(srcTxOuts, dstAddrsVals): + newTx = PyTx() + newTx.version = 1 + newTx.lockTime = 0 + newTx.inputs = [] + newTx.outputs = [] + + + numInputs = len(srcTxOuts) + numOutputs = len(dstAddrsVals) + + coinbaseTx = False + if numInputs==1 and srcTxOuts[0] == -1: + coinbaseTx = True + + + ############################# + # Fill in TxOuts first + for i in range(numOutputs): + txout = PyTxOut() + txout.value = dstAddrsVals[i][1] + dst = dstAddrsVals[i][0] + if(coinbaseTx): + txout.binScript = pubkey_to_p2pk_script(dst.binPublicKey65.toBinStr()) + else: + txout.binScript = hash160_to_p2pkhash_script(dst.getAddr160()) + + newTx.outputs.append(txout) + + + ############################# + # Create temp TxIns with blank scripts + for i in range(numInputs): + txin = PyTxIn() + txin.outpoint = PyOutPoint() + if(coinbaseTx): + txin.outpoint.txHash = '\x00'*32 + txin.outpoint.txOutIndex = binary_to_int('\xff'*4) + else: + txin.outpoint.txHash = hash256(srcTxOuts[i][1].serialize()) + txin.outpoint.txOutIndex = srcTxOuts[i][2] + txin.binScript = '' + txin.intSeq = 2**32-1 + newTx.inputs.append(txin) + + + ############################# + # Now we apply the ultra-complicated signature procedure + # We need a copy of the Tx with all the txin scripts blanked out + txCopySerialized = newTx.serialize() + for i in range(numInputs): + if coinbaseTx: + pass + else: + txCopy = PyTx().unserialize(txCopySerialized) + srcAddr = srcTxOuts[i][0] + txoutIdx = srcTxOuts[i][2] + prevTxOut = srcTxOuts[i][1].outputs[txoutIdx] + binToSign = '' + + assert(srcAddr.hasPrivKey()) + + # Only implemented one type of hashing: SIGHASH_ALL + hashType = 1 # SIGHASH_ALL + hashCode1 = int_to_binary(1, widthBytes=1) + hashCode4 = int_to_binary(1, widthBytes=4) + + # Copy the script of the TxOut we're spending, into the txIn script + txCopy.inputs[i].binScript = prevTxOut.binScript + preHashMsg = txCopy.serialize() + hashCode4 + + # CppBlockUtils::CryptoECDSA modules do the hashing for us + ##binToSign = hash256(preHashMsg) + ##binToSign = binary_switchEndian(binToSign) + + signature = srcAddr.generateDERSignature(preHashMsg) + + + # If we are spending a Coinbase-TxOut, only need sig, no pubkey + # Don't forget to tack on the one-byte hashcode and consider it part of sig + if len(prevTxOut.binScript) > 30: + sigLenInBinary = int_to_binary(len(signature) + 1) + newTx.inputs[i].binScript = sigLenInBinary + signature + hashCode1 + else: + pubkey = srcAddr.binPublicKey65.toBinStr() + sigLenInBinary = int_to_binary(len(signature) + 1) + pubkeyLenInBinary = int_to_binary(len(pubkey) ) + newTx.inputs[i].binScript = sigLenInBinary + signature + hashCode1 + \ + pubkeyLenInBinary + pubkey + + ############################# + # Finally, our tx is complete! + return newTx + +############################################################################# +def getFeeForTx(txHash): + if TheBDM.getBDMState()=='BlockchainReady': + if not TheBDM.hasTxWithHash(txHash): + LOGERROR('Attempted to get fee for tx we don\'t have...? %s', \ + binary_to_hex(txHash,BIGENDIAN)) + return 0 + txref = TheBDM.getTxByHash(txHash) + valIn, valOut = 0,0 + for i in range(txref.getNumTxIn()): + valIn += TheBDM.getSentValue(txref.getTxInCopy(i)) + for i in range(txref.getNumTxOut()): + valOut += txref.getTxOutCopy(i).getValue() + return valIn - valOut + + +############################################################################# +def determineSentToSelfAmt(le, wlt): + """ + NOTE: this method works ONLY because we always generate a new address + whenever creating a change-output, which means it must have a + higher chainIndex than all other addresses. If you did something + creative with this tx, this may not actually work. + """ + amt = 0 + if TheBDM.isInitialized() and le.isSentToSelf(): + txref = TheBDM.getTxByHash(le.getTxHash()) + if not txref.isInitialized(): + return (0, 0) + if txref.getNumTxOut()==1: + return (txref.getTxOutCopy(0).getValue(), -1) + maxChainIndex = -5 + txOutChangeVal = 0 + changeIndex = -1 + valSum = 0 + for i in range(txref.getNumTxOut()): + valSum += txref.getTxOutCopy(i).getValue() + addr160 = CheckHash160(txref.getTxOutCopy(i).getScrAddressStr()) + addr = wlt.getAddrByHash160(addr160) + if addr and addr.chainIndex > maxChainIndex: + maxChainIndex = addr.chainIndex + txOutChangeVal = txref.getTxOutCopy(i).getValue() + changeIndex = i + + amt = valSum - txOutChangeVal + return (amt, changeIndex) + + +################################################################################ +#def getUnspentTxOutsForAddrList(addr160List, utxoType='Sweep', startBlk=-1, \ +def getUnspentTxOutsForAddr160List(addr160List, utxoType='Sweep', startBlk=-1, \ + abortIfBDMBusy=False): + """ + + You have a list of addresses (or just one) and you want to get all the + unspent TxOuts for it. This can either be for computing its balance, or + for sweeping the address(es). + + This will return a list of pairs of [addr160, utxoObj] + This isn't the most efficient method for producing the pairs + + NOTE: At the moment, this only gets STANDARD TxOuts... non-std uses + a different BDM call + + This method will return null output if the BDM is currently in the + middle of a scan. You can use waitAsLongAsNecessary=True if you + want to wait for the previous scan AND the next scan. Otherwise, + you can check for bal==-1 and then try again later... + + Multi-threading update: + + This one-stop-shop method has to be blocking. Instead, you might want + to register the address and rescan asynchronously, skipping this method + entirely: + + cppWlt = Cpp.BtcWallet() + cppWlt.addScrAddress_1_(Hash160ToScrAddr(self.getAddr160())) + TheBDM.registerScrAddr(Hash160ToScrAddr(self.getAddr160())) + TheBDM.rescanBlockchain(wait=False) + + <... do some other stuff ...> + + if TheBDM.getBDMState()=='BlockchainReady': + TheBDM.updateWalletsAfterScan(wait=True) # fast after a rescan + bal = cppWlt.getBalance('Spendable') + utxoList = cppWlt.getUnspentTxOutList() + else: + <...come back later...> + """ + if TheBDM.getBDMState()=='BlockchainReady' or \ + (TheBDM.isScanning() and not abortIfBDMBusy): + if not isinstance(addr160List, (list,tuple)): + addr160List = [addr160List] + + cppWlt = Cpp.BtcWallet() + for addr in addr160List: + if isinstance(addr, PyBtcAddress): + cppWlt.addScrAddress_1_(Hash160ToScrAddr(addr.getAddr160())) + else: + cppWlt.addScrAddress_1_(Hash160ToScrAddr(addr)) + + TheBDM.registerWallet(cppWlt) + currBlk = TheBDM.getTopBlockHeight() + TheBDM.scanBlockchainForTx(cppWlt, currBlk+1 if startBlk==-1 else startBlk) + #TheBDM.scanRegisteredTxForWallet(cppWlt, currBlk+1 if startBlk==-1 else startBlk) + + if utxoType.lower() in ('sweep','unspent','full','all','ultimate'): + return cppWlt.getFullTxOutList(currBlk) + elif utxoType.lower() in ('spend','spendable','confirmed'): + return cppWlt.getSpendableTxOutList(currBlk, IGNOREZC) + else: + raise TypeError, 'Unknown utxoType!' + else: + return [] + +def pprintLedgerEntry(le, indent=''): + if len(le.getScrAddr())==21: + hash160 = CheckHash160(le.getScrAddr()) + addrStr = hash160_to_addrStr(hash160)[:12] + else: + addrStr = '' + + leVal = coin2str(le.getValue(), maxZeros=1) + txType = '' + if le.isSentToSelf(): + txType = 'ToSelf' + else: + txType = 'Recv' if le.getValue()>0 else 'Sent' + + blkStr = str(le.getBlockNum()) + print indent + 'LE %s %s %s %s' % \ + (addrStr.ljust(15), leVal, txType.ljust(8), blkStr.ljust(8)) + +# Putting this at the end because of the circular dependency +from armoryengine.BDM import TheBDM +from armoryengine.PyBtcAddress import PyBtcAddress +from armoryengine.CoinSelection import pprintUnspentTxOutList, sumTxOutList +from armoryengine.Script import * diff --git a/dialogs/__init__.py b/armoryengine/__init__.py similarity index 100% rename from dialogs/__init__.py rename to armoryengine/__init__.py diff --git a/armoryengine/parseAnnounce.py b/armoryengine/parseAnnounce.py new file mode 100644 index 000000000..5f01967c7 --- /dev/null +++ b/armoryengine/parseAnnounce.py @@ -0,0 +1,402 @@ +################################################################################ +# # +# Copyright (C) 2011-2014, Armory Technologies, Inc. # +# Distributed under the GNU Affero General Public License (AGPL v3) # +# See LICENSE or http://www.gnu.org/licenses/agpl.html # +# # +################################################################################ +from ArmoryUtils import * +import os +from jasvet import readSigBlock +from copy import deepcopy + + +################################################################################ +# NOTE: These methods DO NOT verify signatures. It is assumed that the +# signature verification already happened, and these methods were only +# called if the signatures were good. They can be called on blocks +# of text WITH OR WITHOUT the signature block data (either pass it +# the signed block, or just whats inside the block). + +SIGNED_BLOCK_HEAD = '-----BEGIN BITCOIN SIGNED MESSAGE-----' +SIGNED_BLOCK_TAIL = '-----BEGIN BITCOIN SIGNATURE-----' + +################################################################################ +################################################################################ +class changelogParser(object): + """ + Returns a list of list of lists representing all version & changelg to the + stop version (or all versions, if 0) + + + changeLog == + [ + [ '0.88.1', 'December 27, 2013', + [ + [ 'Auto-run Bitcoind', 'This version will now run ...'], + [ 'Mac/OSX Version', 'Binaries now available on ...'], + [ 'Signed installers', 'All binaries are now sign ...'] + ] + ] + [ '0.87', 'April 18, 2013', + [ + [ 'QR Codes', 'QR codes added everywhere ...'], + [ 'Export History', 'Export tx history to CSV ...'] + ] + ] + ... + ] + + """ + + + ############################################################################# + def __init__(self, filename='', filetext=''): + self.changelog = [] + if not filename and not filetext: + return + + if filename and os.path.exists(filename): + f = open(filename, 'r') + filetext = f.read() + f.close() + + self.parseChangelogText(filetext) + + + + ############################################################################# + def parseChangelogFile(self, filename): + if not os.path.exists(filename): + LOGERROR('File does not exist: %s', filename) + + f = open(filename,'r') + verdata = f.read() + f.close() + + return self.parseVersionsText(verdata) + + + + ############################################################################# + def parseChangelogText(self, fileText): + + self.changelog = [] + + if fileText is None: + return None + + + try: + if SIGNED_BLOCK_HEAD in fileText: + fileText = readSigBlock(fileText)[1] + + versionLines = [line.strip() for line in fileText.split('\n')][::-1] + + + if len(versionLines)==0: + return None + + # All lines have been stripped already + while len(versionLines) > 0: + line = versionLines.pop() + + if line.startswith('#') or len(line)==0: + continue + + + if line.startswith('VERSION') and len(line.split())==2: + self.changelog.append([line.split(' ')[-1], '', []]) + elif line.upper().startswith('RELEASED'): + self.changelog[-1][1] = line[8:].strip() + elif line.startswith('-'): + featureTitle = line[2:] + self.changelog[-1][2].append([featureTitle, '']) + else: + curr = self.changelog[-1][2][-1][-1] + self.changelog[-1][2][-1][-1] += ('' if len(curr)==0 else ' ') + line + + return self.getChangelog() + except: + LOGEXCEPT('Failed to parse changelog') + return None + + + ############################################################################# + def getChangelog(self, stopAtVersion=0, dontStartBefore=UINT32_MAX): + output = [] + for ver in self.changelog: + verInt = getVersionInt(readVersionString(ver[0])) + + if verInt > dontStartBefore: + continue + + if verInt <= stopAtVersion: + break + + output.append(ver[:]) + + return output + + + + + + +################################################################################ +################################################################################ +class downloadLinkParser(object): + """ + Parse files with the following format: + + -----BEGIN BITCOIN SIGNED MESSAGE----- + # Armory for Windows + Armory 0.91 Windows XP 32 http://url/armory_0.91_xp32.exe 3afb9881c32 + Armory 0.91 Windows XP 64 http://url/armory_0.91_xp64.exe 8993ab127cf + Armory 0.91 Windows Vista,7,8 32,64 http://url/armory_0.91.exe 7f3b9964aa3 + + # Offline Bundles + ArmoryOffline 0.88 Ubuntu 10.04 32 http://url/offbundle-32.tar.gz 641382c93b9 + ArmoryOffline 0.88 Ubuntu 12.10 32 http://url/offbundle-64.tar.gz 5541af39c84 + + # Windows 32-bit Satoshi (Bitcoin-Qt/bitcoind) + Satoshi 0.9.0 Windows XP,Vista,7,8 32,64 http://btc.org/win0.9.0.exe 118372a9ff3 + Satoshi 0.9.0 Ubuntu 10.04 http://btc.org/win0.9.0.deb 2aa3f763c3b + + -----BEGIN BITCOIN SIGNATURE----- + ac389861cff8a989ae57ae67af43cb3716ca189aa178cff893179531 + -----END BITCOIN SIGNATURE----- + + + This will return a heavily-nested dictionary that will be easy to look up + after we have reduced the current OS to the right set of keys (we will + create a function that takes the output of + platform.system(), + platform.mac_ver(), + platform.linux_distribution(), and + platform.win32_ver() + and returns a sequence of keys we can use to look up the correct version + + self.downloadMap['Armory']['0.91']['Windows']['Vista']['64'] --> + ['http://url/armory_0.91.exe', '7f3b9964aa3'] + + Actually use "getDownloadLink + + """ + + ############################################################################# + def __init__(self, filename='', filetext=''): + self.downloadMap = {} + if not filename and not filetext: + return + + if filename and os.path.exists(filename): + f = open(filename, 'r') + self.parseDownloadList(f.read()) + f.close() + elif filetext: + self.parseDownloadList(filetext) + + + + ############################################################################# + def parseDownloadList(self, fileText): + self.downloadMap = {} + + if fileText is None: + return {} + + def insertLink(mapObj, urlAndHash, keyList): + if len(keyList)>1: + if not keyList[0] in mapObj: + mapObj[keyList[0]] = {} + insertLink(mapObj[keyList[0]], urlAndHash, keyList[1:]) + else: + mapObj[keyList[0]] = urlAndHash + + + try: + if SIGNED_BLOCK_HEAD in fileText: + fileText = readSigBlock(fileText)[1] + + + dlLines = [line.strip() for line in fileText.split('\n')][::-1] + + while len(dlLines) > 0: + + line = dlLines.pop() + + if line.startswith('#') or len(line)==0: + continue + + lineLists = [pc.split(',') for pc in line.split()[:-2]] + urlAndHash = line.split()[-2:] + + APPLIST, VERLIST, OSLIST, SUBOSLIST, BITLIST = range(5) + + for app in lineLists[APPLIST]: + for ver in lineLists[VERLIST]: + for opsys in lineLists[OSLIST]: + for subOS in lineLists[SUBOSLIST]: + for nbit in lineLists[BITLIST]: + insertLink(self.downloadMap, + urlAndHash, + [app, ver, opsys, subOS, nbit]) + + + return self.getNestedDownloadMap() + except: + LOGEXCEPT('Failed to parse downloads') + return None + + + ############################################################################# + def printDownloadMap(self): + + def recursePrint(theObj, indent=0): + if not isinstance(theObj, dict): + print ' '*indent + str(theObj) + else: + for key,val in theObj.iteritems(): + print ' '*indent + key + ':' + recursePrint(theObj[key], indent+5) + + recursePrint(self.downloadMap) + + ############################################################################# + def getDownloadLink(self, *keyList): + + def recurseGet(theMap, keyList): + if len(keyList)==0: + return None + + if not isinstance(theMap, dict): + return None + + if len(keyList)>1: + if not keyList[0] in theMap: + return None + return recurseGet(theMap[keyList[0]], keyList[1:]) + else: + return theMap[keyList[0]] + + if len(keyList)==0: + return None + + return recurseGet(self.downloadMap, keyList) + + + ############################################################################# + def getNestedDownloadMap(self): + return deepcopy(self.downloadMap) + + + +################################################################################ +################################################################################ +class notificationParser(object): + """ + # PRIORITY VALUES: + # Test announce: 1024 + # General Announcment: 2048 + # Important non-critical: 3072 + # Critical/security sens: 4096 + # + # Unique ID must be first, and signals that this is a new notification + + UNIQUEID: 873fbc11 + VERSION: 0 + STARTTIME: 0 + EXPIRES: 1500111222 + CANCELID: [] + MINVERSION: 0.87.2 + MAXVERSION: 0.88.1 + PRIORITY: 4096 + NOTIFYSEND: False + NOTIFYRECV: True + SHORTDESCR: Until further notice, require 30 confirmations for incoming transactions. + LONGDESCR: + THIS IS A FAKE ALERT FOR TESTING PURPOSES: + + There is some turbulence on the network that may result in some transactions + being accidentally reversed up to 30 confirmations. A clever attacker may + be able to exploit this to scam you. For incoming transactions from + parties with no existing trust, please wait at least 30 confirmations before + considering the coins to be yours. + ***** + """ + + ############################################################################# + def __init__(self, filename='', filetext=''): + self.notifications = {} + if not filename and not filetext: + return + + if filename and os.path.exists(filename): + f = open(filename, 'r') + filetext = f.read() + f.close() + + self.parseNotificationText(filetext) + + + + + ############################################################################# + def parseNotificationText(self, fileText): + self.notifications = {} + + if fileText is None: + return None + + + try: + if SIGNED_BLOCK_HEAD in fileText: + fileText = readSigBlock(fileText)[1] + + notifyLines = [line.strip() for line in fileText.split('\n')][::-1] + + + currID = '' + readLongDescr = False + longDescrAccum = '' + + while len(notifyLines) > 0: + + line = notifyLines.pop() + + if not readLongDescr and (line.startswith('#') or len(line)==0): + continue + + if line.upper().startswith('UNIQUEID'): + currID = line.split(':')[-1].strip() + self.notifications[currID] = {} + elif line.upper().startswith('LONGDESCR'): + readLongDescr = True + elif line.startswith("*****"): + readLongDescr = False + self.notifications[currID]['LONGDESCR'] = longDescrAccum + longDescrAccum = '' + elif readLongDescr: + if len(line.strip())==0: + longDescrAccum += '

    ' + else: + longDescrAccum += line.strip() + ' ' + else: + key = line.split(':')[ 0].strip().upper() + val = line.split(':')[-1].strip() + self.notifications[currID][key] = val + + return self.getNotificationMap() + except: + LOGEXCEPT('Failed to parse notifications') + return None + + + ############################################################################# + def getNotificationMap(self): + return deepcopy(self.notifications) + + + +# kate: indent-width 3; replace-tabs on; diff --git a/armoryengine/torrentDL.py b/armoryengine/torrentDL.py new file mode 100644 index 000000000..2093f0c10 --- /dev/null +++ b/armoryengine/torrentDL.py @@ -0,0 +1,527 @@ +import sys +import os + +sys.path.append('..') + +from ArmoryUtils import ARMORY_HOME_DIR, BTC_HOME_DIR, LOGEXCEPT, \ + LOGERROR, LOGWARN, LOGINFO, MEGABYTE, \ + AllowAsync, RightNow, unixTimeToFormatStr, \ + secondsToHumanTime, MAGIC_BYTES,\ + bytesToHumanSize, secondsToHumanTime +from BitTornado.download_bt1 import BT1Download, defaults, get_response +from BitTornado.RawServer import RawServer, UPnP_ERROR +from random import seed +from socket import error as socketerror +from BitTornado.bencode import bencode +from BitTornado.natpunch import UPnP_test +from threading import Event +from os.path import abspath +from sys import argv, stdout +import sys +import shutil +from sha import sha +from time import strftime, sleep +import types +from BitTornado.clock import clock +from BitTornado import createPeerID, version +from BitTornado.ConfigDir import ConfigDir +from BitTornado.download_bt1 import defaults, download +from BitTornado.ConfigDir import ConfigDir + + +# Totally should've used a decorator for the custom funcs... + + +class TorrentDownloadManager(object): + + ############################################################################# + def __init__(self, torrentFile=None, savePath=None, doDisable=False): + self.torrent = torrentFile + self.torrentDNE = False + self.cacheDir = os.path.join(ARMORY_HOME_DIR, 'bittorrentcache') + self.doneObj = Event() + self.customCallbacks = {} + self.minSecondsBetweenUpdates = 1 + self.lastUpdate = 0 + self.disabled = doDisable + self.satoshiDir = BTC_HOME_DIR + + # These need to exist even if setup hasn't been called + self.lastStats = {} + self.startTime = None + self.finishTime = None + self.dlFailed = False + self.bt1dow = None + self.response = None + self.torrentSize = None + self.torrentName = None + self.savePath = None + self.savePath_temp = None + + + + + ############################################################################# + def setupTorrent(self, torrentFile, savePath=None): + + # Some things to reset on every setup operation + self.lastStats = {} + self.startTime = None + self.finishTime = None + self.dlFailed = False + self.bt1dow = None + self.response = None + self.torrentSize = None + self.torrentName = None + self.savePath = None + self.savePath_temp = None + + # Set torrent file, bail if it doesn't exist + self.torrent = torrentFile + self.torrentDNE = False + + if not self.torrent or not os.path.exists(self.torrent): + LOGERROR('Attempted to setup TDM with non-existent torrent:') + if self.torrent: + LOGERROR('Torrent path: %s', self.torrent) + self.torrentDNE = True + return + + self.lastUpdate = RightNow() + + # Get some info about the torrent + if not self.torrentDNE: + self.response = get_response(self.torrent, '', self.errorFunc) + self.torrentSize = self.response['info']['length'] + self.torrentName = self.response['info']['name'] + LOGINFO('Torrent name is: %s' % self.torrentName) + LOGINFO('Torrent size is: %0.2f MB' % (self.torrentSize/float(MEGABYTE))) + + + self.savePath = savePath + if self.savePath is None: + self.savePath = os.path.join(BTC_HOME_DIR, self.torrentName) + self.savePath_temp = self.savePath + '.partial' + + ############################################################################# + def setSatoshiDir(self, btcDir): + self.satoshiDir = btcDir + + ############################################################################# + def isInitialized(self): + return (self.torrent is not None) + + ############################################################################# + def torrentIsMissing(self): + return self.torrentDNE + + ############################################################################# + def fileProgress(self): + """ + Either the mainsize is the same as the torrent (because it finished and + was renamed, or the .partial file is the current state of the DL, and + we report its size + """ + + mainsize = 0 + if os.path.exists(self.savePath): + mainsize = os.path.getsize(self.savePath) + + tempsize = 0 + if os.path.exists(self.savePath_temp): + tempsize = os.path.getsize(self.savePath_temp) + + + if tempsize > 0: + return (tempsize, self.torrentSize) + elif mainsize > 0: + if not mainsize == self.torrentSize: + LOGERROR('Torrent %s is not the correct size...?', self.torrentName) + return (0,0) + else: + return (mainsize, mainsize) + + return (0, self.torrentSize) + + + + ############################################################################# + def hasCustomFunc(self, funcName): + if not funcName in self.customCallbacks: + return False + + return isinstance(self.customCallbacks[funcName], types.FunctionType) + + + ############################################################################# + def setCallback(self, name, func): + if func is None: + if name in self.customCallbacks: + del self.customCallbacks[name] + return + + self.customCallbacks[name] = func + + ############################################################################# + def setSecondsBetweenUpdates(self, newSec): + self.minSecondsBetweenUpdates = newSec + + ############################################################################# + def isDone(self): + return self.doneObj.isSet() + + + ############################################################################# + def displayFunc(self, dpflag=Event(), + fractionDone=None, + timeEst=None, + downRate=None, + upRate=None, + activity=None, + statistics=None, + **kws): + + # Use caller-set function if it exists + if self.hasCustomFunc('displayFunc'): + self.customCallbacks['displayFunc'](dpflag, fractionDone, timeEst, \ + downRate, upRate, activity, \ + statistics, **kws) + return + + + pr = '' + pr += ('Done: %0.1f%%' % (fractionDone*100)) if fractionDone else '' + pr += (' (%0.1f kB/s' % (downRate/1024.)) if downRate else ' (' + pr += (' from %d seeds' % statistics.numSeeds) if statistics else '' + pr += (' and %d peers' % statistics.numPeers) if statistics else '' + if timeEst: + pr += '; Approx %s remaining' % secondsToHumanTime(timeEst) + pr += ')' + LOGINFO(pr) + + + + ############################################################################# + def statusFunc(self, dpflag=Event(), + fractionDone=None, + timeEst=None, + downRate=None, + upRate=None, + activity=None, + statistics=None, + **kws): + + # Want to be able to query a few things between status calls + self.lastStats['fracDone'] = fractionDone + self.lastStats['timeEst'] = timeEst + self.lastStats['downRate'] = downRate + self.lastStats['upRate'] = upRate + self.lastStats['activity'] = activity + self.lastStats['numSeeds'] = statistics.numSeeds if statistics else None + self.lastStats['numPeers'] = statistics.numPeers if statistics else None + self.lastStats['downTotal']= statistics.downTotal if statistics else None + self.lastStats['upTotal'] = statistics.upTotal if statistics else None + + try: + if (RightNow() - self.lastUpdate) < self.minSecondsBetweenUpdates: + return + + self.lastUpdate = RightNow() + + self.displayFunc(dpflag, fractionDone, timeEst, downRate, upRate, + activity, statistics, **kws) + + finally: + # Set this flag to let the caller know it's ready for the next update + dpflag.set() + + + ############################################################################# + def getLastStats(self, name): + return self.lastStats.get(name) + + ############################################################################# + def isStarted(self): + return (self.startTime is not None) + + ############################################################################# + def isFailed(self): + return self.dlFailed + + ############################################################################# + def isFinished(self): + return (self.finishTime is not None) or self.dlFailed + + ############################################################################# + def isRunning(self): + return self.isStarted() and not self.isFinished() + + ############################################################################# + def finishedFunc(self): + """ + This function must rename the ".partial" function to the correct name + """ + self.finishTime = RightNow() + LOGINFO('Download finished!') + + LOGINFO("Moving file") + LOGINFO(" From: %s", self.savePath_temp) + LOGINFO(" To: %s", self.savePath) + shutil.move(self.savePath_temp, self.savePath) + + # Use caller-set function if it exists + if self.hasCustomFunc('finishedFunc'): + self.customCallbacks['finishedFunc']() + + if self.bt1dow: + self.bt1dow.shutdown() + + + + ############################################################################# + def failedFunc(self, msg=''): + self.dlFailed = True + LOGEXCEPT('Download failed! %s', msg) + + # Use caller-set function if it exists + if self.hasCustomFunc('failedFunc'): + self.customCallbacks['failedFunc'](msg) + return + + if self.bt1dow: + self.bt1dow.shutdown() + + + + ############################################################################# + def errorFunc(self, errMsg): + # Use caller-set function if it exists + if self.hasCustomFunc('errorFunc'): + self.customCallbacks['errorFunc'](errMsg) + return + + LOGEXCEPT(errMsg) + + ############################################################################# + def excFunc(self, errMsg): + # Use caller-set function if it exists + if self.hasCustomFunc('excFunc'): + self.customCallbacks['excFunc'](errMsg) + return + + LOGEXCEPT(errMsg) + + ############################################################################# + def chooseFileFunc(self, default, fsize, saveas, thedir): + # Use caller-set function if it exists + if self.hasCustomFunc('chooseFileFunc'): + self.customCallbacks['chooseFileFunc'](default, fsize, saveas, thedir) + return + + return (default if saveas is None else saveas) + + + ############################################################################# + def getTDMState(self): + if self.disabled: + return 'Disabled' + + if not self.isInitialized(): + return 'Uninitialized' + + if self.torrentDNE: + return 'TorrentDNE' + + if not self.isStarted(): + return 'ReadyToStart' + + if self.dlFailed: + return 'DownloadFailed' + + if self.isFinished(): + return 'DownloadFinished' + + return 'Downloading' + + ############################################################################# + def startDownload(self): + return self.doTheDownloadThing(async=True) + + ############################################################################# + @AllowAsync + def doTheDownloadThing(self): + """ + This was copied and modified directly from btdownloadheadless.py + """ + + if self.disabled: + LOGERROR('Attempted to start DL but DISABLE_TORRENT is True') + return + + while 1: + + # Use this var to identify if we've started downloading + self.startTime = RightNow() + + + configdir = ConfigDir(self.cacheDir) + defaultsToIgnore = ['responsefile', 'url', 'priority'] + configdir.setDefaults(defaults, defaultsToIgnore) + config = configdir.loadConfig() + config['responsefile'] = self.torrent + config['url'] = '' + config['priority'] = '' + config['saveas'] = self.savePath_temp + config['save_options'] = 0 + config['max_uploads'] = 0 + config['max_files_open'] = 25 + + configdir.deleteOldCacheData(config['expire_cache_data']) + + myid = createPeerID() + seed(myid) + + rawserver = RawServer( self.doneObj, + config['timeout_check_interval'], + config['timeout'], + ipv6_enable = config['ipv6_enabled'], + failfunc = self.failedFunc, + errorfunc = self.errorFunc) + + upnp_type = UPnP_test(config['upnp_nat_access']) + + while True: + try: + listen_port = rawserver.find_and_bind( \ + config['minport'], + config['maxport'], + config['bind'], + ipv6_socket_style = config['ipv6_binds_v4'], + upnp = upnp_type, + randomizer = config['random_port']) + break + except socketerror, e: + if upnp_type and e == UPnP_ERROR: + LOGWARN('WARNING: COULD NOT FORWARD VIA UPnP') + upnp_type = 0 + continue + LOGERROR("error: Couldn't listen - " + str(e)) + self.failedFunc() + return + + if not self.response: + break + + infohash = sha(bencode(self.response['info'])).digest() + + LOGINFO('Downloading: %s', self.torrentName) + curr,tot = [float(a)/MEGABYTE for a in self.fileProgress()] + if curr == 0: + LOGINFO('Starting new download') + elif curr==tot: + LOGINFO('Torrent already finished!') + return + else: + LOGINFO('Picking up where left off at %0.0f of %0.0f MB' % (curr,tot)) + + self.bt1dow = BT1Download( self.statusFunc, + self.finishedFunc, + self.errorFunc, + self.excFunc, + self.doneObj, + config, + self.response, + infohash, + myid, + rawserver, + listen_port, + configdir) + + if not self.bt1dow.saveAs(self.chooseFileFunc): + break + + if not self.bt1dow.initFiles(old_style = True): + break + + if not self.bt1dow.startEngine(): + self.bt1dow.shutdown() + break + + + self.bt1dow.startRerequester() + self.bt1dow.autoStats() + + if not self.bt1dow.am_I_finished(): + self.statusFunc(activity = 'Connecting to peers') + + rawserver.listen_forever(self.bt1dow.getPortHandler()) + self.statusFunc(activity = 'Shutting down') + self.bt1dow.shutdown() + break + + try: + rawserver.shutdown() + except: + pass + + if not self.isDone(): + self.failedFunc() + + +# Run this file to test with your target torrent. Also shows an example +# of overriding methods with other custom methods. Just about +# any of the methods of TorrentDownloadManager can be replaced like this +if __name__=="__main__": + tdm = TorrentDownloadManager() + tdm.setupTorrent(argv[1], argv[2]) + + # Replace full-featured LOGINFOs with simple print message + def simplePrint( dpflag=Event(), + fractionDone=None, + timeEst=None, + downRate=None, + upRate=None, + activity=None, + statistics=None, + **kws): + + if fractionDone: + print 'TorrentThread: %0.1f%% done;' % (fractionDone*100), + + if timeEst: + print ', about %s remaining' % secondsToHumanTime(timeEst), + + if activity: + print ' (%s)'%activity + else: + print '' + + sys.stdout.flush() + + # Finish funct will still move file.partial to file, this is everything else + def notifyFinished(): + print 'TorrentThread: Finished downloading at %s' % unixTimeToFormatStr(RightNow()) + sys.stdout.flush() + + + tdm.setCallback('displayFunc', simplePrint) + tdm.setCallback('finishedFunc', notifyFinished) + tdm.setSecondsBetweenUpdates(1) + + thr = tdm.startDownload(async=True) + + # The above call was asynchronous + while not thr.isFinished(): + print 'MainThread: Still downloading;', + if tdm.getLastStats('downRate'): + print ' Last dl speed: %0.1f kB/s' % (tdm.getLastStats('downRate')/1024.) + else: + print '' + sys.stdout.flush() + sleep(10) + + + print 'Finished downloading! Exiting...' + + + diff --git a/armorymodels.py b/armorymodels.py index 9e2cca8d1..df82a7d97 100644 --- a/armorymodels.py +++ b/armorymodels.py @@ -1,23 +1,26 @@ ################################################################################ # # -# Copyright (C) 2011-2013, Armory Technologies, Inc. # +# Copyright (C) 2011-2014, Armory Technologies, Inc. # # Distributed under the GNU Affero General Public License (AGPL v3) # # See LICENSE or http://www.gnu.org/licenses/agpl.html # # # ################################################################################ +from os import path import os import platform import sys -from os import path + from PyQt4.QtCore import * from PyQt4.QtGui import * -sys.path.append('..') -sys.path.append('../cppForSwig') -from armoryengine import * + from CppBlockUtils import * +from armoryengine.ALL import * from qtdefines import * -from armorycolors import Colors, htmlColor + + +sys.path.append('..') +sys.path.append('../cppForSwig') @@ -178,7 +181,7 @@ def data(self, index, role=Qt.DisplayRole): #if self.index(index.row(),COL.DoubleSpend).data().toBool(): if rowData[COL.DoubleSpend]: return QVariant(Colors.TextRed) - if nConf <= 2: + if nConf < 2: return QVariant(Colors.TextNoConfirm) elif nConf <= 4: return QVariant(Colors.TextSomeConfirm) @@ -420,6 +423,7 @@ def filterAddrList(self): self.addr160List = [a.getAddr160() for a in addrList] + @TimeThisFunction def reset(self): self.filterAddrList() super(WalletAddrDispModel, self).reset() @@ -592,18 +596,19 @@ def __init__(self, pytx, txinListFromBDM=None, main=None): scrType = getTxInScriptType(txin) if txinListFromBDM and len(txinListFromBDM[i][0])>0: # We had a BDM to help us get info on each input -- use it - recip160,val,blk,hsh,idx = txinListFromBDM[i] + scrAddr,val,blk,hsh,idx = txinListFromBDM[i] + addrStr = scrAddr_to_addrStr(scrAddr) if main: - wltID = self.main.getWalletForAddr160(recip160) + wltID = self.main.getWalletForAddr160(scrAddr[1:]) dispcoin = '' if not val else coin2str(val,maxZeros=1) self.dispTable[-1].append(wltID) - self.dispTable[-1].append(hash160_to_addrStr(recip160)) + self.dispTable[-1].append(addrStr) self.dispTable[-1].append(dispcoin) self.dispTable[-1].append(binary_to_hex(hsh)) self.dispTable[-1].append(idx) self.dispTable[-1].append(blk) if pytxdp==None: - self.dispTable[-1].append(TXIN_TYPE_NAMES[scrType]) + self.dispTable[-1].append(CPP_TXIN_SCRIPT_NAMES[scrType]) else: # TODO: Assume NO multi-sig... will be updated in future to use # PyTxDP::isSigValidForInput which will handle all cases @@ -615,17 +620,19 @@ def __init__(self, pytx, txinListFromBDM=None, main=None): # We don't have any info from the BDM, display whatever we can # (which usually isn't much) recipAddr = '' - if scrType in (TXIN_SCRIPT_STANDARD,): - recipAddr = TxInScriptExtractAddr160IfAvail(txin) - if main: - wltID = self.main.getWalletForAddr160(recip) + recipAddr = TxInExtractAddrStrIfAvail(txin) + atype, a160 = '','' + if len(recipAddr) > 0: + atype, a160 = addrStr_to_hash160(recipAddr) + wltID = self.main.getWalletForAddr160(a160) + self.dispTable[-1].append(wltID) - self.dispTable[-1].append(recipAddr) + self.dispTable[-1].append(a160) self.dispTable[-1].append('') self.dispTable[-1].append(binary_to_hex(txin.outpoint.txHash)) self.dispTable[-1].append(str(txin.outpoint.txOutIndex)) self.dispTable[-1].append('') - self.dispTable[-1].append(TXIN_TYPE_NAMES[scrType]) + self.dispTable[-1].append(CPP_TXIN_SCRIPT_NAMES[scrType]) self.dispTable[-1].append(int_to_hex(txin.intSeq, widthBytes=4)) self.dispTable[-1].append(binary_to_hex(txin.binScript)) @@ -702,9 +709,9 @@ def __init__(self, pytx, main=None, idxGray=[]): self.main = main self.txOutList = [] self.wltIDList = [] - self.idxGray = idxGray + self.idxGray = idxGray[:] for i,txout in enumerate(self.tx.outputs): - recip160 = TxOutScriptExtractAddr160(txout.binScript) + recip160 = script_to_scrAddr(txout.binScript)[1:] self.txOutList.append(txout) if main: self.wltIDList.append(main.getWalletForAddr160(recip160)) @@ -722,31 +729,24 @@ def data(self, index, role=Qt.DisplayRole): COLS = TXOUTCOLS row,col = index.row(), index.column() txout = self.txOutList[row] - stype = getTxOutScriptType(txout.binScript) - stypeStr = TXOUT_TYPE_NAMES[stype] + stype = BtcUtils().getTxOutScriptTypeInt(txout.binScript) + stypeStr = CPP_TXOUT_SCRIPT_NAMES[stype] wltID = self.wltIDList[row] - if stype==TXOUT_SCRIPT_MULTISIG: - mstype = getTxOutMultiSigInfo(txout.binScript)[0] - stypeStr = 'Multi-Signature (%d-of-%d)' % mstype + if stype==CPP_TXOUT_MULTISIG: + M,N = getMultisigScriptInfo(txout.binScript)[:2] + stypeStr = 'MultiSig(%d-of-%d)' % (M,N) if role==Qt.DisplayRole: if col==COLS.WltID: return QVariant(wltID) if col==COLS.ScrType: return QVariant(stypeStr) if col==COLS.Script: return QVariant(binary_to_hex(txout.binScript)) - if stype==TXOUT_SCRIPT_STANDARD: - if col==COLS.Recip: return QVariant(TxOutScriptExtractAddrStr(txout.binScript)) - if col==COLS.Btc: return QVariant(coin2str(txout.getValue(),maxZeros=2)) - if stype==TXOUT_SCRIPT_COINBASE: - if col==COLS.Recip: return QVariant(TxOutScriptExtractAddrStr(txout.binScript)) - if col==COLS.Btc: return QVariant(coin2str(txout.getValue(),maxZeros=2)) - if stype==TXOUT_SCRIPT_MULTISIG: - if col==COLS.Recip: return QVariant('[[Multiple]]') - if col==COLS.Btc: return QVariant(coin2str(txout.getValue(),maxZeros=2)) - if stype==TXOUT_SCRIPT_UNKNOWN: - if col==COLS.Recip: return QVariant('[[Non-Standard]]') - if col==COLS.Btc: return QVariant(coin2str(txout.getValue(),maxZeros=2)) - if stype==TXOUT_SCRIPT_OP_EVAL: - if col==COLS.Recip: return QVariant('[[OP-EVAL]]') - if col==COLS.Btc: return QVariant(coin2str(txout.getValue(),maxZeros=2)) + if col==COLS.Btc: return QVariant(coin2str(txout.getValue(),maxZeros=2)) + if col==COLS.Recip: + if stype in CPP_TXOUT_HAS_ADDRSTR: + return QVariant(script_to_addrStr(txout.binScript)) + elif stype==CPP_TXOUT_MULTISIG: + return QVariant('[[Multiple]]') + elif stype==CPP_TXOUT_NONSTANDARD: + return QVariant('[[Non-Standard]]') elif role==Qt.TextAlignmentRole: if col==COLS.Recip: return QVariant(int(Qt.AlignLeft | Qt.AlignVCenter)) if col==COLS.Btc: return QVariant(int(Qt.AlignRight | Qt.AlignVCenter)) @@ -806,7 +806,12 @@ def __init__(self, wltID, main): # the python code... :( for abe in TheBDM.getAddressBook(self.wlt.cppWallet): - addr160 = CheckHash160(abe.getScrAddr()) + scrAddr = abe.getScrAddr() + try: + addr160 = addrStr_to_hash160(scrAddr_to_addrStr(scrAddr))[1] + except Exception as e: + LOGERROR(str(e)) + addr160 = '' # Only grab addresses that are not in any of your Armory wallets if not self.main.getWalletForAddr160(addr160): @@ -815,9 +820,8 @@ def __init__(self, wltID, main): txhashlist = [] for i in range(ntx): txhashlist.append( abeList[i].getTxHash() ) - self.addrBook.append( [ addr160, txhashlist] ) + self.addrBook.append( [scrAddr, txhashlist] ) - print 'Done collecting addresses for addrbook' def rowCount(self, index=QModelIndex()): return len(self.addrBook) @@ -828,8 +832,13 @@ def columnCount(self, index=QModelIndex()): def data(self, index, role=Qt.DisplayRole): COL = ADDRBOOKCOLS row,col = index.row(), index.column() - addr160 = self.addrBook[row][0] - addrB58 = hash160_to_addrStr(addr160) + scrAddr = self.addrBook[row][0] + if scrAddr[0] in [SCRADDR_P2PKH_BYTE, SCRADDR_P2SH_BYTE]: + addrB58 = scrAddr_to_addrStr(scrAddr) + addr160 = scrAddr[1:] + else: + addrB58 = '' + addr160 = '' wltID = self.main.getWalletForAddr160(addr160) txList = self.addrBook[row][1] numSent = len(txList) diff --git a/build_installer.bat b/build_installer.bat index d983c0089..e00b7b91e 100644 --- a/build_installer.bat +++ b/build_installer.bat @@ -1,9 +1,9 @@ REM This should only be run from cppForSwig\BitcoinArmory_SwigDLL directory -copy ..\libs\Win32\BitcoinArmory_SwigDLL.dll ..\..\_CppBlockUtils.pyd +copy ..\libs\Win32\BitcoinArmory_SwigDLL.dll ..\..\_CppBlockUtils.pyd C:\Python27\Lib\site-packages\PyQt4\pyrcc4.exe -o ..\..\qrc_img_resources.py ..\..\imgList.xml python ..\..\setup.py py2exe --includes sip,hashlib,json,twisted -d ..\..\ArmoryStandalone copy ..\..\img\*.ico ..\..\ArmoryStandalone copy ..\..\img\armory_logo*.png ..\..\ArmoryStandalone -rtc /F:..\..\edit_icons.rts +copy ..\..\default_bootstrap.torrent ..\..\ArmoryStandalone python ..\..\writeNSISCompilerArgs.py makensis.exe ..\..\ArmorySetup.nsi diff --git a/cppForSwig/BinaryData.cpp b/cppForSwig/BinaryData.cpp index 00feb2c26..d62ec04e5 100644 --- a/cppForSwig/BinaryData.cpp +++ b/cppForSwig/BinaryData.cpp @@ -1,6 +1,6 @@ //////////////////////////////////////////////////////////////////////////////// // // -// Copyright(C) 2011-2013, Armory Technologies, Inc. // +// Copyright (C) 2011-2014, Armory Technologies, Inc. // // Distributed under the GNU Affero General Public License (AGPL v3) // // See LICENSE or http://www.gnu.org/licenses/agpl.html // // // @@ -203,7 +203,7 @@ uint64_t BinaryReader::get_var_int(uint8_t* nRead) uint64_t BinaryRefReader::get_var_int(uint8_t* nRead) { uint32_t nBytes; - uint64_t varInt = BtcUtils::readVarInt( bdRef_.getPtr() + pos_, &nBytes); + uint64_t varInt = BtcUtils::readVarInt( bdRef_.getPtr() + pos_, getSizeRemaining(), &nBytes); if(nRead != NULL) *nRead = nBytes; pos_ += nBytes; diff --git a/cppForSwig/BinaryData.h b/cppForSwig/BinaryData.h index 2d3d45bf8..73f92e970 100644 --- a/cppForSwig/BinaryData.h +++ b/cppForSwig/BinaryData.h @@ -1,6 +1,6 @@ //////////////////////////////////////////////////////////////////////////////// // // -// Copyright(C) 2011-2013, Armory Technologies, Inc. // +// Copyright (C) 2011-2014, Armory Technologies, Inc. // // Distributed under the GNU Affero General Public License (AGPL v3) // // See LICENSE or http://www.gnu.org/licenses/agpl.html // // // @@ -10,7 +10,9 @@ #include #if defined(_MSC_VER) || defined(__MINGW32__) - + #if _MSC_PLATFORM_TOOLSET!=110 + #include + #endif #else #include #include @@ -528,7 +530,7 @@ class BinaryData // Absorb a binary file's data into a new BinaryData object int32_t readBinaryFile(string filename) { - ifstream is(filename.c_str(), ios::in | ios::binary ); + ifstream is(OS_TranslatePath(filename.c_str()), ios::in | ios::binary ); if( !is.is_open() ) return -1; @@ -1546,7 +1548,7 @@ class BinaryStreamBuffer streamPtr_ = new ifstream; weOwnTheStream_ = true; ifstream* ifstreamPtr = static_cast(streamPtr_); - ifstreamPtr->open(filename.c_str(), ios::in | ios::binary); + ifstreamPtr->open(OS_TranslatePath(filename.c_str()), ios::in | ios::binary); if( !ifstreamPtr->is_open() ) { cerr << "Could not open file for reading! File: " << filename.c_str() << endl; diff --git a/cppForSwig/BitcoinArmory.sln b/cppForSwig/BitcoinArmory.sln index ef344dbde..ed287e0b9 100644 --- a/cppForSwig/BitcoinArmory.sln +++ b/cppForSwig/BitcoinArmory.sln @@ -5,20 +5,9 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "guardian", "guardian\guardi EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cryptopp", "cryptopp\cryptopp.vcxproj", "{B1055DA3-83CE-47BF-98E6-2850E3411D39}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "snappy", "leveldbwin\build\msvc10\snappy\snappy.vcxproj", "{72639F93-D2E6-4220-AA46-E24C502E470C}" -EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "BitcoinArmory_SwigDLL", "BitcoinArmory_SwigDLL\BitcoinArmory_SwigDLL.vcxproj", "{19329A6B-FE96-4917-B69A-2B0375C12017}" - ProjectSection(ProjectDependencies) = postProject - {D35F732D-55D7-4037-9C6D-E141F740F802} = {D35F732D-55D7-4037-9C6D-E141F740F802} - {01CD1176-66F7-44AB-9E7B-8AEECC9915E8} = {01CD1176-66F7-44AB-9E7B-8AEECC9915E8} - {B1055DA3-83CE-47BF-98E6-2850E3411D39} = {B1055DA3-83CE-47BF-98E6-2850E3411D39} - EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "BitcoinArmory_CppTests", "BitcoinArmory_CppTests\BitcoinArmory_CppTests.vcxproj", "{D9733F9E-BE30-466F-B100-B686DF663C4D}" - ProjectSection(ProjectDependencies) = postProject - {01CD1176-66F7-44AB-9E7B-8AEECC9915E8} = {01CD1176-66F7-44AB-9E7B-8AEECC9915E8} - {B1055DA3-83CE-47BF-98E6-2850E3411D39} = {B1055DA3-83CE-47BF-98E6-2850E3411D39} - EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "leveldb_msvc11_port", "leveldb_windows_port\leveldb_msvc11_port.vcxproj", "{01CD1176-66F7-44AB-9E7B-8AEECC9915E8}" EndProject @@ -26,107 +15,66 @@ Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Win32 = Debug|Win32 Debug|x64 = Debug|x64 - DebugDll|Win32 = DebugDll|Win32 - DebugDll|x64 = DebugDll|x64 Release|Win32 = Release|Win32 Release|x64 = Release|x64 - ReleaseDll|Win32 = ReleaseDll|Win32 - ReleaseDll|x64 = ReleaseDll|x64 + WinXP_32|Win32 = WinXP_32|Win32 + WinXP_32|x64 = WinXP_32|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {D35F732D-55D7-4037-9C6D-E141F740F802}.Debug|Win32.ActiveCfg = Release|Win32 {D35F732D-55D7-4037-9C6D-E141F740F802}.Debug|Win32.Build.0 = Release|Win32 {D35F732D-55D7-4037-9C6D-E141F740F802}.Debug|x64.ActiveCfg = Release|x64 {D35F732D-55D7-4037-9C6D-E141F740F802}.Debug|x64.Build.0 = Release|x64 - {D35F732D-55D7-4037-9C6D-E141F740F802}.DebugDll|Win32.ActiveCfg = Release|Win32 - {D35F732D-55D7-4037-9C6D-E141F740F802}.DebugDll|Win32.Build.0 = Release|Win32 - {D35F732D-55D7-4037-9C6D-E141F740F802}.DebugDll|x64.ActiveCfg = Release|x64 - {D35F732D-55D7-4037-9C6D-E141F740F802}.DebugDll|x64.Build.0 = Release|x64 {D35F732D-55D7-4037-9C6D-E141F740F802}.Release|Win32.ActiveCfg = Release|Win32 - {D35F732D-55D7-4037-9C6D-E141F740F802}.Release|Win32.Build.0 = Release|Win32 {D35F732D-55D7-4037-9C6D-E141F740F802}.Release|x64.ActiveCfg = Release|x64 {D35F732D-55D7-4037-9C6D-E141F740F802}.Release|x64.Build.0 = Release|x64 - {D35F732D-55D7-4037-9C6D-E141F740F802}.ReleaseDll|Win32.ActiveCfg = Release|Win32 - {D35F732D-55D7-4037-9C6D-E141F740F802}.ReleaseDll|Win32.Build.0 = Release|Win32 - {D35F732D-55D7-4037-9C6D-E141F740F802}.ReleaseDll|x64.ActiveCfg = Release|x64 - {D35F732D-55D7-4037-9C6D-E141F740F802}.ReleaseDll|x64.Build.0 = Release|x64 + {D35F732D-55D7-4037-9C6D-E141F740F802}.WinXP_32|Win32.ActiveCfg = WinXP_32|Win32 + {D35F732D-55D7-4037-9C6D-E141F740F802}.WinXP_32|Win32.Build.0 = WinXP_32|Win32 + {D35F732D-55D7-4037-9C6D-E141F740F802}.WinXP_32|x64.ActiveCfg = Release|x64 + {D35F732D-55D7-4037-9C6D-E141F740F802}.WinXP_32|x64.Build.0 = Release|x64 {B1055DA3-83CE-47BF-98E6-2850E3411D39}.Debug|Win32.ActiveCfg = Debug|Win32 {B1055DA3-83CE-47BF-98E6-2850E3411D39}.Debug|Win32.Build.0 = Debug|Win32 {B1055DA3-83CE-47BF-98E6-2850E3411D39}.Debug|x64.ActiveCfg = Debug|x64 {B1055DA3-83CE-47BF-98E6-2850E3411D39}.Debug|x64.Build.0 = Debug|x64 - {B1055DA3-83CE-47BF-98E6-2850E3411D39}.DebugDll|Win32.ActiveCfg = Debug|Win32 - {B1055DA3-83CE-47BF-98E6-2850E3411D39}.DebugDll|Win32.Build.0 = Debug|Win32 - {B1055DA3-83CE-47BF-98E6-2850E3411D39}.DebugDll|x64.ActiveCfg = Debug|x64 - {B1055DA3-83CE-47BF-98E6-2850E3411D39}.DebugDll|x64.Build.0 = Debug|x64 {B1055DA3-83CE-47BF-98E6-2850E3411D39}.Release|Win32.ActiveCfg = Release|Win32 - {B1055DA3-83CE-47BF-98E6-2850E3411D39}.Release|Win32.Build.0 = Release|Win32 {B1055DA3-83CE-47BF-98E6-2850E3411D39}.Release|x64.ActiveCfg = Release|x64 {B1055DA3-83CE-47BF-98E6-2850E3411D39}.Release|x64.Build.0 = Release|x64 - {B1055DA3-83CE-47BF-98E6-2850E3411D39}.ReleaseDll|Win32.ActiveCfg = Release|Win32 - {B1055DA3-83CE-47BF-98E6-2850E3411D39}.ReleaseDll|Win32.Build.0 = Release|Win32 - {B1055DA3-83CE-47BF-98E6-2850E3411D39}.ReleaseDll|x64.ActiveCfg = Release|x64 - {B1055DA3-83CE-47BF-98E6-2850E3411D39}.ReleaseDll|x64.Build.0 = Release|x64 - {72639F93-D2E6-4220-AA46-E24C502E470C}.Debug|Win32.ActiveCfg = Debug|Win32 - {72639F93-D2E6-4220-AA46-E24C502E470C}.Debug|x64.ActiveCfg = Debug|x64 - {72639F93-D2E6-4220-AA46-E24C502E470C}.Debug|x64.Build.0 = Debug|x64 - {72639F93-D2E6-4220-AA46-E24C502E470C}.DebugDll|Win32.ActiveCfg = Debug|Win32 - {72639F93-D2E6-4220-AA46-E24C502E470C}.DebugDll|Win32.Build.0 = Debug|Win32 - {72639F93-D2E6-4220-AA46-E24C502E470C}.DebugDll|x64.ActiveCfg = Debug|x64 - {72639F93-D2E6-4220-AA46-E24C502E470C}.DebugDll|x64.Build.0 = Debug|x64 - {72639F93-D2E6-4220-AA46-E24C502E470C}.Release|Win32.ActiveCfg = Release|Win32 - {72639F93-D2E6-4220-AA46-E24C502E470C}.Release|Win32.Build.0 = Release|Win32 - {72639F93-D2E6-4220-AA46-E24C502E470C}.Release|x64.ActiveCfg = Release|x64 - {72639F93-D2E6-4220-AA46-E24C502E470C}.ReleaseDll|Win32.ActiveCfg = Release|Win32 - {72639F93-D2E6-4220-AA46-E24C502E470C}.ReleaseDll|Win32.Build.0 = Release|Win32 - {72639F93-D2E6-4220-AA46-E24C502E470C}.ReleaseDll|x64.ActiveCfg = Release|x64 - {72639F93-D2E6-4220-AA46-E24C502E470C}.ReleaseDll|x64.Build.0 = Release|x64 + {B1055DA3-83CE-47BF-98E6-2850E3411D39}.WinXP_32|Win32.ActiveCfg = WinXP_32|Win32 + {B1055DA3-83CE-47BF-98E6-2850E3411D39}.WinXP_32|Win32.Build.0 = WinXP_32|Win32 + {B1055DA3-83CE-47BF-98E6-2850E3411D39}.WinXP_32|x64.ActiveCfg = Release|x64 + {B1055DA3-83CE-47BF-98E6-2850E3411D39}.WinXP_32|x64.Build.0 = Release|x64 {19329A6B-FE96-4917-B69A-2B0375C12017}.Debug|Win32.ActiveCfg = Debug|Win32 {19329A6B-FE96-4917-B69A-2B0375C12017}.Debug|Win32.Build.0 = Debug|Win32 {19329A6B-FE96-4917-B69A-2B0375C12017}.Debug|x64.ActiveCfg = Debug|x64 {19329A6B-FE96-4917-B69A-2B0375C12017}.Debug|x64.Build.0 = Debug|x64 - {19329A6B-FE96-4917-B69A-2B0375C12017}.DebugDll|Win32.ActiveCfg = Debug|Win32 - {19329A6B-FE96-4917-B69A-2B0375C12017}.DebugDll|Win32.Build.0 = Debug|Win32 - {19329A6B-FE96-4917-B69A-2B0375C12017}.DebugDll|x64.ActiveCfg = Debug|x64 - {19329A6B-FE96-4917-B69A-2B0375C12017}.DebugDll|x64.Build.0 = Debug|x64 {19329A6B-FE96-4917-B69A-2B0375C12017}.Release|Win32.ActiveCfg = Release|Win32 {19329A6B-FE96-4917-B69A-2B0375C12017}.Release|Win32.Build.0 = Release|Win32 {19329A6B-FE96-4917-B69A-2B0375C12017}.Release|x64.ActiveCfg = Release|x64 {19329A6B-FE96-4917-B69A-2B0375C12017}.Release|x64.Build.0 = Release|x64 - {19329A6B-FE96-4917-B69A-2B0375C12017}.ReleaseDll|Win32.ActiveCfg = Release|Win32 - {19329A6B-FE96-4917-B69A-2B0375C12017}.ReleaseDll|Win32.Build.0 = Release|Win32 - {19329A6B-FE96-4917-B69A-2B0375C12017}.ReleaseDll|x64.ActiveCfg = Release|x64 - {19329A6B-FE96-4917-B69A-2B0375C12017}.ReleaseDll|x64.Build.0 = Release|x64 + {19329A6B-FE96-4917-B69A-2B0375C12017}.WinXP_32|Win32.ActiveCfg = WinXP_32|Win32 + {19329A6B-FE96-4917-B69A-2B0375C12017}.WinXP_32|Win32.Build.0 = WinXP_32|Win32 + {19329A6B-FE96-4917-B69A-2B0375C12017}.WinXP_32|x64.ActiveCfg = Release|x64 + {19329A6B-FE96-4917-B69A-2B0375C12017}.WinXP_32|x64.Build.0 = Release|x64 {D9733F9E-BE30-466F-B100-B686DF663C4D}.Debug|Win32.ActiveCfg = Debug|Win32 {D9733F9E-BE30-466F-B100-B686DF663C4D}.Debug|Win32.Build.0 = Debug|Win32 {D9733F9E-BE30-466F-B100-B686DF663C4D}.Debug|x64.ActiveCfg = Debug|x64 {D9733F9E-BE30-466F-B100-B686DF663C4D}.Debug|x64.Build.0 = Debug|x64 - {D9733F9E-BE30-466F-B100-B686DF663C4D}.DebugDll|Win32.ActiveCfg = Debug|Win32 - {D9733F9E-BE30-466F-B100-B686DF663C4D}.DebugDll|Win32.Build.0 = Debug|Win32 - {D9733F9E-BE30-466F-B100-B686DF663C4D}.DebugDll|x64.ActiveCfg = Debug|x64 - {D9733F9E-BE30-466F-B100-B686DF663C4D}.DebugDll|x64.Build.0 = Debug|x64 {D9733F9E-BE30-466F-B100-B686DF663C4D}.Release|Win32.ActiveCfg = Release|Win32 - {D9733F9E-BE30-466F-B100-B686DF663C4D}.Release|Win32.Build.0 = Release|Win32 {D9733F9E-BE30-466F-B100-B686DF663C4D}.Release|x64.ActiveCfg = Release|x64 - {D9733F9E-BE30-466F-B100-B686DF663C4D}.ReleaseDll|Win32.ActiveCfg = Release|Win32 - {D9733F9E-BE30-466F-B100-B686DF663C4D}.ReleaseDll|Win32.Build.0 = Release|Win32 - {D9733F9E-BE30-466F-B100-B686DF663C4D}.ReleaseDll|x64.ActiveCfg = Release|x64 - {D9733F9E-BE30-466F-B100-B686DF663C4D}.ReleaseDll|x64.Build.0 = Release|x64 - {01CD1176-66F7-44AB-9E7B-8AEECC9915E8}.Debug|Win32.ActiveCfg = Release|Win32 - {01CD1176-66F7-44AB-9E7B-8AEECC9915E8}.Debug|Win32.Build.0 = Release|Win32 + {D9733F9E-BE30-466F-B100-B686DF663C4D}.WinXP_32|Win32.ActiveCfg = Release|Win32 + {D9733F9E-BE30-466F-B100-B686DF663C4D}.WinXP_32|Win32.Build.0 = Release|Win32 + {D9733F9E-BE30-466F-B100-B686DF663C4D}.WinXP_32|x64.ActiveCfg = Release|x64 + {01CD1176-66F7-44AB-9E7B-8AEECC9915E8}.Debug|Win32.ActiveCfg = Debug|Win32 + {01CD1176-66F7-44AB-9E7B-8AEECC9915E8}.Debug|Win32.Build.0 = Debug|Win32 {01CD1176-66F7-44AB-9E7B-8AEECC9915E8}.Debug|x64.ActiveCfg = Debug|x64 {01CD1176-66F7-44AB-9E7B-8AEECC9915E8}.Debug|x64.Build.0 = Debug|x64 - {01CD1176-66F7-44AB-9E7B-8AEECC9915E8}.DebugDll|Win32.ActiveCfg = Debug|Win32 - {01CD1176-66F7-44AB-9E7B-8AEECC9915E8}.DebugDll|Win32.Build.0 = Debug|Win32 - {01CD1176-66F7-44AB-9E7B-8AEECC9915E8}.DebugDll|x64.ActiveCfg = Debug|x64 - {01CD1176-66F7-44AB-9E7B-8AEECC9915E8}.DebugDll|x64.Build.0 = Debug|x64 {01CD1176-66F7-44AB-9E7B-8AEECC9915E8}.Release|Win32.ActiveCfg = Release|Win32 - {01CD1176-66F7-44AB-9E7B-8AEECC9915E8}.Release|Win32.Build.0 = Release|Win32 {01CD1176-66F7-44AB-9E7B-8AEECC9915E8}.Release|x64.ActiveCfg = Release|x64 {01CD1176-66F7-44AB-9E7B-8AEECC9915E8}.Release|x64.Build.0 = Release|x64 - {01CD1176-66F7-44AB-9E7B-8AEECC9915E8}.ReleaseDll|Win32.ActiveCfg = Release|Win32 - {01CD1176-66F7-44AB-9E7B-8AEECC9915E8}.ReleaseDll|Win32.Build.0 = Release|Win32 - {01CD1176-66F7-44AB-9E7B-8AEECC9915E8}.ReleaseDll|x64.ActiveCfg = Release|x64 - {01CD1176-66F7-44AB-9E7B-8AEECC9915E8}.ReleaseDll|x64.Build.0 = Release|x64 + {01CD1176-66F7-44AB-9E7B-8AEECC9915E8}.WinXP_32|Win32.ActiveCfg = WinXP_32|Win32 + {01CD1176-66F7-44AB-9E7B-8AEECC9915E8}.WinXP_32|Win32.Build.0 = WinXP_32|Win32 + {01CD1176-66F7-44AB-9E7B-8AEECC9915E8}.WinXP_32|x64.ActiveCfg = Release|x64 + {01CD1176-66F7-44AB-9E7B-8AEECC9915E8}.WinXP_32|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/cppForSwig/BitcoinArmory_CppTests/BitcoinArmory_CppTests.vcxproj b/cppForSwig/BitcoinArmory_CppTests/BitcoinArmory_CppTests.vcxproj index 1b53ba3be..e0e845916 100644 --- a/cppForSwig/BitcoinArmory_CppTests/BitcoinArmory_CppTests.vcxproj +++ b/cppForSwig/BitcoinArmory_CppTests/BitcoinArmory_CppTests.vcxproj @@ -96,7 +96,7 @@ Console true - cryptopp_d.lib;leveldb_msvc11_port_d.lib;snappy_d.lib;%(AdditionalDependencies) + cryptopp_d.lib;leveldb_msvc11_port_d.lib;%(AdditionalDependencies) $(SolutionDir)libs\$(Platform)\;%(AdditionalLibraryDirectories) @@ -167,6 +167,7 @@ + @@ -180,6 +181,7 @@ + diff --git a/cppForSwig/BitcoinArmory_CppTests/BitcoinArmory_CppTests.vcxproj.filters b/cppForSwig/BitcoinArmory_CppTests/BitcoinArmory_CppTests.vcxproj.filters index 20e1904c8..13101ca1b 100644 --- a/cppForSwig/BitcoinArmory_CppTests/BitcoinArmory_CppTests.vcxproj.filters +++ b/cppForSwig/BitcoinArmory_CppTests/BitcoinArmory_CppTests.vcxproj.filters @@ -45,6 +45,9 @@ Header Files + + Header Files + @@ -77,5 +80,8 @@ Source Files + + Source Files + \ No newline at end of file diff --git a/cppForSwig/BitcoinArmory_SwigDLL/BitcoinArmory_SwigDLL.vcxproj b/cppForSwig/BitcoinArmory_SwigDLL/BitcoinArmory_SwigDLL.vcxproj index 60beb61ad..7d9a8cfb9 100644 --- a/cppForSwig/BitcoinArmory_SwigDLL/BitcoinArmory_SwigDLL.vcxproj +++ b/cppForSwig/BitcoinArmory_SwigDLL/BitcoinArmory_SwigDLL.vcxproj @@ -17,14 +17,6 @@ Release x64 - - WinXP Release - Win32 - - - WinXP Release - x64 - WinXP_32 Win32 @@ -33,22 +25,6 @@ WinXP_32 x64 - - WinXP_release_x86 - Win32 - - - WinXP_release_x86 - x64 - - - Win_XP_32 - Win32 - - - Win_XP_32 - x64 - {19329A6B-FE96-4917-B69A-2B0375C12017} @@ -176,7 +152,7 @@ $(SolutionDir)libs\x64 - false + true $(SolutionDir)libs\$(Platform)\ $(SolutionDir)libs\IntermediateBuildFiles\$(Configuration).$(ProjectName)\ @@ -217,6 +193,9 @@ C:\Python27\Lib\site-packages\PyQt4\pyrcc4.exe -o ..\..\qrc_img_resources.py ..\..\imgList.xml + + ..\swigwin\swig.exe -c++ -python -threads -classic -outdir ..\..\ -v ..\CppBlockUtils.i + @@ -263,6 +242,9 @@ C:\Python27\Lib\site-packages\PyQt4\pyrcc4.exe -o ..\..\qrc_img_resources.py ..\ C:\Python27_64\Lib\site-packages\PyQt4\pyrcc4.exe -o ..\..\qrc_img_resources.py ..\..\imgList.xml + + ..\swigwin\swig.exe -c++ -python -threads -classic -outdir ..\..\ -v ..\CppBlockUtils.i + @@ -289,23 +271,23 @@ C:\Python27_64\Lib\site-packages\PyQt4\pyrcc4.exe -o ..\..\qrc_img_resources.py Level3 - - + NotUsing Full true - false - WIN32;NDEBUG;_CONSOLE;_CRT_SECURE_NO_WARNINGS;_MSC_PLATFORM_TOOLSET=$(PlatformToolset);%(PreprocessorDefinitions) - ..\leveldb;..\leveldb\include;..\cryptopp;C:\Python27\include;C:\Python26\include;%(AdditionalIncludeDirectories) + true + _CRT_SECURE_NO_WARNINGS;_MSC_PLATFORM_TOOLSET=$(PlatformToolset);%(PreprocessorDefinitions) + ..\leveldb;..\leveldb\include;..\cryptopp;C:\Python27\include;C:\Python26\include;..\leveldb_windows_port\win32_posix;%(AdditionalIncludeDirectories) MultiThreaded Speed true None + CompileAsCpp Console false - true - false + false + true $(SolutionDir)libs\$(Platform)\;C:\Python27\libs;C:\Python26\libs;%(AdditionalLibraryDirectories) cryptopp.lib;leveldb_msvc11_port.lib;%(AdditionalDependencies) @@ -315,7 +297,9 @@ C:\Python27_64\Lib\site-packages\PyQt4\pyrcc4.exe -o ..\..\qrc_img_resources.py ..\swigwin\swig.exe -c++ -python -threads -classic -outdir ..\..\ -v ..\CppBlockUtils.i - ..\..\build_installer.bat + build_installer.bat +copy ..\libs\Win32\BitcoinArmory_SwigDLL.dll ..\..\_CppBlockUtils.pyd + false @@ -343,8 +327,8 @@ C:\Python27_64\Lib\site-packages\PyQt4\pyrcc4.exe -o ..\..\qrc_img_resources.py false true true - ..\pthreads-w32-2-9-1-release\Pre-built.2\lib\x86;$(SolutionDir)libs\$(Platform)\Win_XP;C:\Python27\libs;%(AdditionalLibraryDirectories) - cryptopp.lib;leveldb_msvc11_port.lib;pthreadVC2.lib;%(AdditionalDependencies) + $(SolutionDir)libs\$(Platform)\Win_XP;C:\Python27\libs;%(AdditionalLibraryDirectories) + cryptopp.lib;leveldb_msvc11_port.lib;%(AdditionalDependencies) @@ -372,8 +356,8 @@ C:\Python27_64\Lib\site-packages\PyQt4\pyrcc4.exe -o ..\..\qrc_img_resources.py false true true - C:\Python27_64\libs;..\libs\x64;..\pthreads-w32-2-9-1-release\Pre-built.2\lib\x64;%(AdditionalLibraryDirectories) - cryptopp.lib;leveldb_msvc11_port.lib;snappy.lib;pthreadVC2.lib;python27.lib;%(AdditionalDependencies) + C:\Python27_64\libs;..\libs\x64;%(AdditionalLibraryDirectories) + cryptopp.lib;leveldb_msvc11_port.lib;%(AdditionalDependencies) @@ -447,6 +431,7 @@ C:\Python27_64\Lib\site-packages\PyQt4\pyrcc4.exe -o ..\..\qrc_img_resources.py + diff --git a/cppForSwig/BitcoinArmory_SwigDLL/BitcoinArmory_SwigDLL.vcxproj.filters b/cppForSwig/BitcoinArmory_SwigDLL/BitcoinArmory_SwigDLL.vcxproj.filters index d30e06ed3..f8cc2c8a2 100644 --- a/cppForSwig/BitcoinArmory_SwigDLL/BitcoinArmory_SwigDLL.vcxproj.filters +++ b/cppForSwig/BitcoinArmory_SwigDLL/BitcoinArmory_SwigDLL.vcxproj.filters @@ -28,6 +28,9 @@ Source Files + + Source Files + diff --git a/cppForSwig/BlockObj.cpp b/cppForSwig/BlockObj.cpp index 2bbb3c793..823495405 100644 --- a/cppForSwig/BlockObj.cpp +++ b/cppForSwig/BlockObj.cpp @@ -1,6 +1,6 @@ //////////////////////////////////////////////////////////////////////////////// // // -// Copyright(C) 2011-2013, Armory Technologies, Inc. // +// Copyright (C) 2011-2014, Armory Technologies, Inc. // // Distributed under the GNU Affero General Public License (AGPL v3) // // See LICENSE or http://www.gnu.org/licenses/agpl.html // // // @@ -21,8 +21,10 @@ //////////////////////////////////////////////////////////////////////////////// -void BlockHeader::unserialize(uint8_t const * ptr) +void BlockHeader::unserialize(uint8_t const * ptr, uint32_t size) { + if (size < HEADER_SIZE) + throw BlockDeserializingException(); dataCopy_.copyFrom(ptr, HEADER_SIZE); BtcUtils::getHash256(dataCopy_.getPtr(), HEADER_SIZE, thisHash_); difficultyDbl_ = BtcUtils::convertDiffBitsToDouble( @@ -40,7 +42,7 @@ void BlockHeader::unserialize(uint8_t const * ptr) //////////////////////////////////////////////////////////////////////////////// void BlockHeader::unserialize(BinaryDataRef const & str) { - unserialize(str.getPtr()); + unserialize(str.getPtr(), str.getSize()); } //////////////////////////////////////////////////////////////////////////////// @@ -155,18 +157,25 @@ BinaryData OutPoint::serialize(void) const return bw.getData(); } -void OutPoint::unserialize(uint8_t const * ptr) +void OutPoint::unserialize(uint8_t const * ptr, uint32_t size) { + if (size < 32) + throw BlockDeserializingException(); + txHash_.copyFrom(ptr, 32); txOutIndex_ = READ_UINT32_LE(ptr+32); } void OutPoint::unserialize(BinaryReader & br) { + if (br.getSizeRemaining() < 32) + throw BlockDeserializingException(); br.get_BinaryData(txHash_, 32); txOutIndex_ = br.get_uint32_t(); } void OutPoint::unserialize(BinaryRefReader & brr) { + if (brr.getSizeRemaining() < 32) + throw BlockDeserializingException(); brr.get_BinaryData(txHash_, 32); txOutIndex_ = brr.get_uint32_t(); } @@ -174,11 +183,11 @@ void OutPoint::unserialize(BinaryRefReader & brr) void OutPoint::unserialize(BinaryData const & bd) { - unserialize(bd.getPtr()); + unserialize(bd.getPtr(), bd.getSize()); } void OutPoint::unserialize(BinaryDataRef const & bdRef) { - unserialize(bdRef.getPtr()); + unserialize(bdRef.getPtr(), bdRef.getSize()); } @@ -194,7 +203,7 @@ void OutPoint::unserialize(BinaryDataRef const & bdRef) OutPoint TxIn::getOutPoint(void) const { OutPoint op; - op.unserialize(getPtr()); + op.unserialize(getPtr(), getSize()); return op; } @@ -215,20 +224,26 @@ BinaryDataRef TxIn::getScriptRef(void) const } - ///////////////////////////////////////////////////////////////////////////// -void TxIn::unserialize(uint8_t const * ptr, +void TxIn::unserialize_checked(uint8_t const * ptr, + uint32_t size, uint32_t nbytes, TxRef parent, uint32_t idx) { parentTx_ = parent; index_ = idx; - uint32_t numBytes = (nbytes==0 ? BtcUtils::TxInCalcLength(ptr) : nbytes); + uint32_t numBytes = (nbytes==0 ? BtcUtils::TxInCalcLength(ptr, size) : nbytes); + if (size < numBytes) + throw BlockDeserializingException(); dataCopy_.copyFrom(ptr, numBytes); + if (dataCopy_.getSize()-36 < 1) + throw BlockDeserializingException(); scriptOffset_ = 36 + BtcUtils::readVarIntLength(getPtr()+36); + if (dataCopy_.getSize() < 32) + throw BlockDeserializingException(); scriptType_ = BtcUtils::getTxInScriptType(getScriptRef(), BinaryDataRef(getPtr(),32)); @@ -245,7 +260,7 @@ void TxIn::unserialize(BinaryRefReader & brr, TxRef parent, uint32_t idx) { - unserialize(brr.getCurrPtr(), nbytes, parent, idx); + unserialize_checked(brr.getCurrPtr(), brr.getSizeRemaining(), nbytes, parent, idx); brr.advance(getSize()); } @@ -255,7 +270,7 @@ void TxIn::unserialize(BinaryData const & str, TxRef parent, uint32_t idx) { - unserialize(str.getPtr(), nbytes, parent, idx); + unserialize_checked(str.getPtr(), str.getSize(), nbytes, parent, idx); } ///////////////////////////////////////////////////////////////////////////// @@ -264,7 +279,7 @@ void TxIn::unserialize(BinaryDataRef str, TxRef parent, uint32_t idx) { - unserialize(str.getPtr(), nbytes, parent, idx); + unserialize_checked(str.getPtr(), str.getSize(), nbytes, parent, idx); } @@ -281,7 +296,14 @@ bool TxIn::getSenderScrAddrIfAvail(BinaryData & addrTarget) const return false; } - addrTarget = BtcUtils::getTxInAddrFromType(getScript(), scriptType_); + try + { + addrTarget = BtcUtils::getTxInAddrFromType(getScript(), scriptType_); + } + catch (BlockDeserializingException&) + { + return false; + } return true; } @@ -355,9 +377,9 @@ BinaryDataRef TxOut::getScriptRef(void) return BinaryDataRef( dataCopy_.getPtr()+scriptOffset_, getScriptSize() ); } - ///////////////////////////////////////////////////////////////////////////// -void TxOut::unserialize( uint8_t const * ptr, +void TxOut::unserialize_checked( uint8_t const * ptr, + uint32_t size, uint32_t nbytes, TxRef parent, uint32_t idx) @@ -365,9 +387,13 @@ void TxOut::unserialize( uint8_t const * ptr, parentTx_ = parent; index_ = idx; uint32_t numBytes = (nbytes==0 ? BtcUtils::TxOutCalcLength(ptr) : nbytes); + if (size < numBytes) + throw BlockDeserializingException(); dataCopy_.copyFrom(ptr, numBytes); scriptOffset_ = 8 + BtcUtils::readVarIntLength(getPtr()+8); + if (dataCopy_.getSize()-scriptOffset_-getScriptSize() > size) + throw BlockDeserializingException(); BinaryDataRef scriptRef(dataCopy_.getPtr()+scriptOffset_, getScriptSize()); scriptType_ = BtcUtils::getTxOutScriptType(scriptRef); uniqueScrAddr_ = BtcUtils::getTxOutScrAddr(scriptRef); @@ -385,7 +411,7 @@ void TxOut::unserialize( BinaryData const & str, TxRef parent, uint32_t idx) { - unserialize(str.getPtr(), nbytes, parent, idx); + unserialize_checked(str.getPtr(), str.getSize(), nbytes, parent, idx); } ///////////////////////////////////////////////////////////////////////////// @@ -394,7 +420,7 @@ void TxOut::unserialize( BinaryDataRef const & str, TxRef parent, uint32_t idx) { - unserialize(str.getPtr(), nbytes, parent, idx); + unserialize_checked(str.getPtr(), str.getSize(), nbytes, parent, idx); } ///////////////////////////////////////////////////////////////////////////// @@ -403,7 +429,7 @@ void TxOut::unserialize( BinaryRefReader & brr, TxRef parent, uint32_t idx) { - unserialize( brr.getCurrPtr(), nbytes, parent, idx ); + unserialize_checked( brr.getCurrPtr(), brr.getSizeRemaining(), nbytes, parent, idx ); brr.advance(getSize()); } @@ -464,14 +490,20 @@ Tx::Tx(TxRef txref) } ///////////////////////////////////////////////////////////////////////////// -void Tx::unserialize(uint8_t const * ptr) +void Tx::unserialize(uint8_t const * ptr, uint32_t size) { - uint32_t nBytes = BtcUtils::TxCalcLength(ptr, &offsetsTxIn_, &offsetsTxOut_); + uint32_t nBytes = BtcUtils::TxCalcLength(ptr, size, &offsetsTxIn_, &offsetsTxOut_); + if (nBytes > size) + throw BlockDeserializingException(); dataCopy_.copyFrom(ptr, nBytes); BtcUtils::getHash256(ptr, nBytes, thisHash_); + if (8 > size) + throw BlockDeserializingException(); uint32_t numTxOut = offsetsTxOut_.size()-1; version_ = READ_UINT32_LE(ptr); + if (4 > size - offsetsTxOut_[numTxOut]) + throw BlockDeserializingException(); lockTime_ = READ_UINT32_LE(ptr + offsetsTxOut_[numTxOut]); isInitialized_ = true; @@ -497,7 +529,7 @@ BinaryData Tx::getThisHash(void) const ///////////////////////////////////////////////////////////////////////////// void Tx::unserialize(BinaryRefReader & brr) { - unserialize(brr.getCurrPtr()); + unserialize(brr.getCurrPtr(), brr.getSizeRemaining()); brr.advance(getSize()); } @@ -529,7 +561,8 @@ TxIn Tx::getTxInCopy(int i) { assert(isInitialized()); uint32_t txinSize = offsetsTxIn_[i+1] - offsetsTxIn_[i]; - TxIn out(dataCopy_.getPtr()+offsetsTxIn_[i], txinSize, txRefObj_, i); + TxIn out; + out.unserialize_checked(dataCopy_.getPtr()+offsetsTxIn_[i], dataCopy_.getSize()-offsetsTxIn_[i], txinSize, txRefObj_, i); if(txRefObj_.isInitialized()) { @@ -547,13 +580,13 @@ TxOut Tx::getTxOutCopy(int i) { assert(isInitialized()); uint32_t txoutSize = offsetsTxOut_[i+1] - offsetsTxOut_[i]; - TxOut out(dataCopy_.getPtr()+offsetsTxOut_[i], txoutSize, txRefObj_, i); - + TxOut out; + out.unserialize_checked(dataCopy_.getPtr()+offsetsTxOut_[i], dataCopy_.getSize()-offsetsTxOut_[i], txoutSize, txRefObj_, i); + out.setParentHash(getThisHash()); + if(txRefObj_.isInitialized()) - { - out.setParentHash(getThisHash()); out.setParentHeight(txRefObj_.getBlockHeight()); - } + return out; } @@ -999,7 +1032,7 @@ bool TxIOPair::isUnspent(void) } ////////////////////////////////////////////////////////////////////////////// -bool TxIOPair::isSpendable(uint32_t currBlk) +bool TxIOPair::isSpendable(uint32_t currBlk, bool ignoreAllZeroConf) { // Spendable TxOuts are ones with at least 1 confirmation, or zero-conf // TxOuts that were sent-to-self. Obviously, they should be unspent, too @@ -1016,13 +1049,13 @@ bool TxIOPair::isSpendable(uint32_t currBlk) } if( hasTxOutZC() && isTxOutFromSelf() ) - return true; + return !ignoreAllZeroConf; return false; } ////////////////////////////////////////////////////////////////////////////// -bool TxIOPair::isMineButUnconfirmed(uint32_t currBlk) +bool TxIOPair::isMineButUnconfirmed(uint32_t currBlk, bool inclAllZC) { // All TxOuts that were from our own transactions are always confirmed if(isTxOutFromSelf()) @@ -1039,8 +1072,7 @@ bool TxIOPair::isMineButUnconfirmed(uint32_t currBlk) else return (nConfputValue(BLKDATA, sbh.getDBKey(), sbh.serializeDBValue(BLKDATA)); +} + + +//////////////////////////////////////////////////////////////////////////////// +static StoredTx* makeSureSTXInMap( + InterfaceToLDB* iface, + BinaryDataRef txHash, + map & stxMap, + uint64_t* additionalSize) +{ + // TODO: If we are pruning, we may have completely removed this tx from + // the DB, which means that it won't be in the map or the DB. + // But this method was written before pruning was ever implemented... + StoredTx * stxptr; + + // Get the existing STX or make a new one + map::iterator txIter = stxMap.find(txHash); + if(ITER_IN_MAP(txIter, stxMap)) + stxptr = &(txIter->second); + else + { + StoredTx stxTemp; + iface->getStoredTx(stxTemp, txHash); + stxMap[txHash] = stxTemp; + stxptr = &stxMap[txHash]; + if (additionalSize) + *additionalSize += stxptr->numBytes_; + } + + return stxptr; +} + +//////////////////////////////////////////////////////////////////////////////// +// This avoids having to do the double-lookup when fetching by hash. +// We still pass in the hash anyway, because the map is indexed by the hash, +// and we'd like to not have to do a lookup for the hash if only provided +// {hgt, dup, idx} +static StoredTx* makeSureSTXInMap( + InterfaceToLDB* iface, + uint32_t hgt, + uint8_t dup, + uint16_t txIdx, + BinaryDataRef txHash, + map & stxMap, + uint64_t* additionalSize) +{ + StoredTx * stxptr; + + // Get the existing STX or make a new one + map::iterator txIter = stxMap.find(txHash); + if(ITER_IN_MAP(txIter, stxMap)) + stxptr = &(txIter->second); + else + { + StoredTx &stxTemp = stxMap[txHash]; + iface->getStoredTx(stxTemp, hgt, dup, txIdx); + stxptr = &stxMap[txHash]; + if (additionalSize) + *additionalSize += stxptr->numBytes_; + } + + return stxptr; +} + +static StoredScriptHistory* makeSureSSHInMap( + InterfaceToLDB* iface, + BinaryDataRef uniqKey, + BinaryDataRef hgtX, + map & sshMap, + uint64_t* additionalSize, + bool createIfDNE=true) +{ + SCOPED_TIMER("makeSureSSHInMap"); + StoredScriptHistory * sshptr; + + // If already in Map + map::iterator iter = sshMap.find(uniqKey); + if(ITER_IN_MAP(iter, sshMap)) + { + SCOPED_TIMER("___SSH_AlreadyInMap"); + sshptr = &(iter->second); + } + else + { + StoredScriptHistory sshTemp; + + iface->getStoredScriptHistorySummary(sshTemp, uniqKey); + // sshTemp.alreadyScannedUpToBlk_ = getAppliedToHeightInDB(); TODO + if (additionalSize) + *additionalSize += UPDATE_BYTES_SSH; + if(sshTemp.isInitialized()) + { + SCOPED_TIMER("___SSH_AlreadyInDB"); + // We already have an SSH in DB -- pull it into the map + sshMap[uniqKey] = sshTemp; + sshptr = &sshMap[uniqKey]; + } + else + { + SCOPED_TIMER("___SSH_NeedCreate"); + if(!createIfDNE) + return NULL; + + sshMap[uniqKey] = StoredScriptHistory(); + sshptr = &sshMap[uniqKey]; + sshptr->uniqueKey_ = uniqKey; + } + } + + + // If sub-history for this block doesn't exist, add an empty one before + // returning the pointer to the SSH. Since we haven't actually inserted + // anything into the SubSSH, we don't need to adjust the totalTxioCount_ + uint32_t prevSize = sshptr->subHistMap_.size(); + iface->fetchStoredSubHistory(*sshptr, hgtX, true, false); + uint32_t newSize = sshptr->subHistMap_.size(); + + if (additionalSize) + *additionalSize += (newSize - prevSize) * UPDATE_BYTES_SUBSSH; + return sshptr; +} + + BlockDataManager_LevelDB* BlockDataManager_LevelDB::theOnlyBDM_ = NULL; @@ -112,34 +238,34 @@ ScrAddrObj::ScrAddrObj(HashString addr, //////////////////////////////////////////////////////////////////////////////// -uint64_t ScrAddrObj::getSpendableBalance(uint32_t currBlk) +uint64_t ScrAddrObj::getSpendableBalance(uint32_t currBlk, bool ignoreAllZC) { uint64_t balance = 0; for(uint32_t i=0; iisSpendable(currBlk)) + if(relevantTxIOPtrs_[i]->isSpendable(currBlk, ignoreAllZC)) balance += relevantTxIOPtrs_[i]->getValue(); } for(uint32_t i=0; iisSpendable(currBlk)) + if(relevantTxIOPtrsZC_[i]->isSpendable(currBlk, ignoreAllZC)) balance += relevantTxIOPtrsZC_[i]->getValue(); } return balance; } //////////////////////////////////////////////////////////////////////////////// -uint64_t ScrAddrObj::getUnconfirmedBalance(uint32_t currBlk) +uint64_t ScrAddrObj::getUnconfirmedBalance(uint32_t currBlk, bool inclAllZC) { uint64_t balance = 0; for(uint32_t i=0; iisMineButUnconfirmed(currBlk)) + if(relevantTxIOPtrs_[i]->isMineButUnconfirmed(currBlk, inclAllZC)) balance += relevantTxIOPtrs_[i]->getValue(); } for(uint32_t i=0; iisMineButUnconfirmed(currBlk)) + if(relevantTxIOPtrsZC_[i]->isMineButUnconfirmed(currBlk, inclAllZC)) balance += relevantTxIOPtrsZC_[i]->getValue(); } return balance; @@ -165,22 +291,24 @@ uint64_t ScrAddrObj::getFullBalance(void) } //////////////////////////////////////////////////////////////////////////////// -vector ScrAddrObj::getSpendableTxOutList(uint32_t blkNum) +vector ScrAddrObj::getSpendableTxOutList(uint32_t blkNum, + bool ignoreAllZC) { vector utxoList(0); for(uint32_t i=0; i BtcWallet::isMineBulkFilter(Tx & tx, - bool withMultiSig) + bool withMultiSig) const { return isMineBulkFilter(tx, txioMap_, withMultiSig); } @@ -396,22 +524,24 @@ pair BtcWallet::isMineBulkFilter(Tx & tx, ///////////////////////////////////////////////////////////////////////////// // Determine, as fast as possible, whether this tx is relevant to us // Return -pair BtcWallet::isMineBulkFilter(Tx & tx, - map & txiomap, - bool withMultiSig) +pair BtcWallet::isMineBulkFilter( + Tx & tx, + map const & txiomap, + bool withMultiSig) const { // Since 99.999%+ of all transactions are not ours, let's do the // fastest bulk filter possible, even though it will add // redundant computation to the tx that are ours. In fact, // we will skip the TxIn/TxOut convenience methods and follow the - // pointers directly the data we want + // pointers directly to the data we want uint8_t const * txStartPtr = tx.getPtr(); for(uint32_t iin=0; iin(true,true); } @@ -565,6 +695,16 @@ void BtcWallet::pprintAlot(uint32_t topBlk, bool withAddr) } } +void BtcWallet::reorgChangeBlkNum(uint32_t newBlkHgt) +{ + if(newBlkHgtsize()-1; @@ -674,7 +814,7 @@ void BlockDataManager_LevelDB::registeredScrAddrScan( { // We have the txin, now check if it contains one of our TxOuts static OutPoint op; - op.unserialize(txStartPtr + (*txInOffsets)[iin]); + op.unserialize(txStartPtr + (*txInOffsets)[iin], txSize - (*txInOffsets)[iin]); if(registeredOutPoints_.count(op) > 0) { insertRegisteredTxIfNew(BtcUtils::getHash256(txptr, txSize)); @@ -769,7 +909,7 @@ void BlockDataManager_LevelDB::registeredScrAddrScan_IterSafe( { txInOffsets = &localOffsIn; txOutOffsets = &localOffsOut; - BtcUtils::TxCalcLength(txStartPtr, txInOffsets, txOutOffsets); + BtcUtils::TxCalcLength(txStartPtr, tx.getSize(), txInOffsets, txOutOffsets); } uint32_t nTxIn = txInOffsets->size()-1; @@ -779,7 +919,7 @@ void BlockDataManager_LevelDB::registeredScrAddrScan_IterSafe( { // We have the txin, now check if it spends one of our TxOuts static OutPoint op; - op.unserialize(txStartPtr + (*txInOffsets)[iin]); + op.unserialize(txStartPtr + (*txInOffsets)[iin], tx.getSize()-(*txInOffsets)[iin]); if(registeredOutPoints_.count(op) > 0) { insertRegisteredTxIfNew(tx.getTxRef(), @@ -870,7 +1010,8 @@ void BlockDataManager_LevelDB::registeredScrAddrScan( Tx & theTx ) void BtcWallet::scanTx(Tx & tx, uint32_t txIndex, uint32_t txtime, - uint32_t blknum) + uint32_t blknum, + bool mainwallet) { int64_t totalLedgerAmt = 0; @@ -896,226 +1037,225 @@ void BtcWallet::scanTx(Tx & tx, map::iterator addrIter; ScrAddrObj* thisAddrPtr; HashString scraddr; - //for(uint32_t i=0; i no addr inputs + if(outpt.getTxHashRef() == BtcUtils::EmptyHash_) + { + isCoinbaseTx = true; + continue; + } + + // We have the txin, now check if it contains one of our TxOuts + map::iterator txioIter = txioMap_.find(outpt); + //bool txioWasInMapAlready = (txioIter != txioMap_.end()); + bool txioWasInMapAlready = ITER_IN_MAP(txioIter, txioMap_); + if(txioWasInMapAlready) { - TxIn txin = tx.getTxInCopy(iin); - OutPoint outpt = txin.getOutPoint(); - // Empty hash in Outpoint means it's a COINBASE tx --> no addr inputs - if(outpt.getTxHashRef() == BtcUtils::EmptyHash_) + // If we are here, we know that this input is spending an + // output owned by this wallet. + // We will get here for every address in the search, even + // though it is only relevant to one of the addresses. + TxIOPair & txio = txioIter->second; + TxOut txout = txio.getTxOutCopy(); + + // It's our TxIn, so address should be in this wallet + scraddr = txout.getScrAddressStr(); + addrIter = scrAddrMap_.find(scraddr); + //if( addrIter == scrAddrMap_.end()) + if(ITER_NOT_IN_MAP(addrIter, scrAddrMap_)) { - isCoinbaseTx = true; + // Have TxIO but address is not in the map...? + LOGERR << "ERROR: TxIn in TxIO map, but addr not in wallet...?"; continue; } + thisAddrPtr = &addrIter->second; - // We have the txin, now check if it contains one of our TxOuts - map::iterator txioIter = txioMap_.find(outpt); - //bool txioWasInMapAlready = (txioIter != txioMap_.end()); - bool txioWasInMapAlready = ITER_IN_MAP(txioIter, txioMap_); - if(txioWasInMapAlready) + // We need to make sure the ledger entry makes sense, and make + // sure we update TxIO objects appropriately + int64_t thisVal = (int64_t)txout.getValue(); + totalLedgerAmt -= thisVal; + + // Skip, if zero-conf-spend, but it's already got a zero-conf + if( isZeroConf && txio.hasTxInZC() ) + return; // this tx can't be valid, might as well bail now + + if( !txio.hasTxInInMain() && !(isZeroConf && txio.hasTxInZC()) ) { - // If we are here, we know that this input is spending an - // output owned by this wallet. - // We will get here for every address in the search, even - // though it is only relevant to one of the addresses. - TxIOPair & txio = txioIter->second; - TxOut txout = txio.getTxOutCopy(); - - // It's our TxIn, so address should be in this wallet - scraddr = txout.getScrAddressStr(); - addrIter = scrAddrMap_.find(scraddr); - //if( addrIter == scrAddrMap_.end()) - if(ITER_NOT_IN_MAP(addrIter, scrAddrMap_)) - { - // Have TxIO but address is not in the map...? - LOGERR << "ERROR: TxIn in TxIO map, but addr not in wallet...?"; + // isValidNew only identifies whether this set-call succeeded + // If it didn't, it's because this is from a zero-conf tx but this + // TxIn already exists in the blockchain spending the same output. + // (i.e. we have a ref to the prev output, but it's been spent!) + bool isValidNew; + if(isZeroConf) + isValidNew = txio.setTxInZC(&tx, iin); + else + isValidNew = txio.setTxIn(tx.getTxRef(), iin); + + if(!isValidNew) continue; - } - thisAddrPtr = &addrIter->second; - // We need to make sure the ledger entry makes sense, and make - // sure we update TxIO objects appropriately - int64_t thisVal = (int64_t)txout.getValue(); - totalLedgerAmt -= thisVal; + anyNewTxInIsOurs = true; - // Skip, if zero-conf-spend, but it's already got a zero-conf - if( isZeroConf && txio.hasTxInZC() ) - return; // this tx can't be valid, might as well bail now + LedgerEntry newEntry(scraddr, + -(int64_t)thisVal, + blknum, + tx.getThisHash(), + iin, + txtime, + isCoinbaseTx, + false, // SentToSelf is meaningless for addr ledger + false); // "isChangeBack" is meaningless for TxIn + thisAddrPtr->addLedgerEntry(newEntry, isZeroConf); - if( !txio.hasTxInInMain() && !(isZeroConf && txio.hasTxInZC()) ) - { - // isValidNew only identifies whether this set-call succeeded - // If it didn't, it's because this is from a zero-conf tx but this - // TxIn already exists in the blockchain spending the same output. - // (i.e. we have a ref to the prev output, but it's been spent!) - bool isValidNew; - if(isZeroConf) - isValidNew = txio.setTxInZC(&tx, iin); - else - isValidNew = txio.setTxIn(tx.getTxRef(), iin); - - if(!isValidNew) - continue; + txLedgerForComments_.push_back(newEntry); + savedAsTxIn = true; - anyNewTxInIsOurs = true; - - LedgerEntry newEntry(scraddr, - -(int64_t)thisVal, - blknum, - tx.getThisHash(), - iin, - txtime, - isCoinbaseTx, - false, // SentToSelf is meaningless for addr ledger - false); // "isChangeBack" is meaningless for TxIn - thisAddrPtr->addLedgerEntry(newEntry, isZeroConf); - - // Update last seen on the network - thisAddrPtr->setLastTimestamp(txtime); - thisAddrPtr->setLastBlockNum(blknum); - } + // Update last seen on the network + thisAddrPtr->setLastTimestamp(txtime); + thisAddrPtr->setLastBlockNum(blknum); } - else + } + else + { + // Lots of txins that we won't have, this is a normal conditional + // But we should check the non-std txio list since it may actually + // be there + //if(nonStdTxioMap_.find(outpt) != nonStdTxioMap_.end()) + if(KEY_IN_MAP(outpt, nonStdTxioMap_)) { - // Lots of txins that we won't have, this is a normal conditional - // But we should check the non-std txio list since it may actually - // be there - //if(nonStdTxioMap_.find(outpt) != nonStdTxioMap_.end()) - if(KEY_IN_MAP(outpt, nonStdTxioMap_)) - { - if(isZeroConf) - nonStdTxioMap_[outpt].setTxInZC(&tx, iin); - else - nonStdTxioMap_[outpt].setTxIn(tx.getTxRef(), iin); - nonStdUnspentOutPoints_.erase(outpt); - } + if(isZeroConf) + nonStdTxioMap_[outpt].setTxInZC(&tx, iin); + else + nonStdTxioMap_[outpt].setTxIn(tx.getTxRef(), iin); + nonStdUnspentOutPoints_.erase(outpt); } - } // loop over TxIns - //} + } + } // loop over TxIns - //for(uint32_t i=0; i -1) - //scanNonStdTx(blknum, txIndex, tx, iout, *thisAddrPtr); - continue; - } + //if(txout.getScriptRef().find(scraddr) > -1) + //scanNonStdTx(blknum, txIndex, tx, iout, *thisAddrPtr); + continue; + } - scraddr = txout.getScrAddressStr(); - addrIter = scrAddrMap_.find(scraddr); - //if( addrIter != scrAddrMap_.end()) - if(ITER_IN_MAP(addrIter, scrAddrMap_)) + scraddr = txout.getScrAddressStr(); + addrIter = scrAddrMap_.find(scraddr); + //if( addrIter != scrAddrMap_.end()) + if(ITER_IN_MAP(addrIter, scrAddrMap_)) + { + thisAddrPtr = &addrIter->second; + // If we got here, at least this TxOut is for this address. + // But we still need to find out if it's new and update + // ledgers/TXIOs appropriately + int64_t thisVal = (int64_t)(txout.getValue()); + totalLedgerAmt += thisVal; + + OutPoint outpt(tx.getThisHash(), iout); + map::iterator txioIter = txioMap_.find(outpt); + //bool txioWasInMapAlready = (txioIter != txioMap_.end()); + bool txioWasInMapAlready = ITER_IN_MAP(txioIter, txioMap_); + bool doAddLedgerEntry = false; + if(txioWasInMapAlready) { - thisAddrPtr = &addrIter->second; - // If we got here, at least this TxOut is for this address. - // But we still need to find out if it's new and update - // ledgers/TXIOs appropriately - int64_t thisVal = (int64_t)(txout.getValue()); - totalLedgerAmt += thisVal; - - OutPoint outpt(tx.getThisHash(), iout); - map::iterator txioIter = txioMap_.find(outpt); - //bool txioWasInMapAlready = (txioIter != txioMap_.end()); - bool txioWasInMapAlready = ITER_IN_MAP(txioIter, txioMap_); - bool doAddLedgerEntry = false; - if(txioWasInMapAlready) + if(isZeroConf) { - if(isZeroConf) - { - // This is a real txOut, in the blockchain - if(txioIter->second.hasTxOutZC() || txioIter->second.hasTxOutInMain()) - continue; - - // If we got here, somehow the Txio existed already, but - // there was no existing TxOut referenced by it. Probably, - // there was, but that TxOut was invalidated due to reorg - // and now being re-added - txioIter->second.setTxOutZC(&tx, iout); - txioIter->second.setValue((uint64_t)thisVal); - thisAddrPtr->addTxIO( txioIter->second, isZeroConf); - doAddLedgerEntry = true; - } - else - { - if(txioIter->second.hasTxOutInMain()) // ...but we already have one - continue; - - // If we got here, we have an in-blockchain TxOut that is - // replacing a zero-conf txOut. Reset the txio to have - // only this real TxOut, blank out the ZC TxOut. And the addr - // relevantTxIOPtrs_ does not have this yet so it needs - // to be added (it's already part of the relevantTxIOPtrsZC_ - // but that will be removed) - txioIter->second.setTxOut(tx.getTxRef(), iout); - txioIter->second.setValue((uint64_t)thisVal); - thisAddrPtr->addTxIO( txioIter->second, isZeroConf); - doAddLedgerEntry = true; - } + // This is a real txOut, in the blockchain + if(txioIter->second.hasTxOutZC() || txioIter->second.hasTxOutInMain()) + continue; + + // If we got here, somehow the Txio existed already, but + // there was no existing TxOut referenced by it. Probably, + // there was, but that TxOut was invalidated due to reorg + // and now being re-added + txioIter->second.setTxOutZC(&tx, iout); + txioIter->second.setValue((uint64_t)thisVal); + thisAddrPtr->addTxIO( txioIter->second, isZeroConf); + doAddLedgerEntry = true; } else { - // TxIO is not in the map yet -- create and add it - TxIOPair newTxio(thisVal); - if(isZeroConf) - newTxio.setTxOutZC(&tx, iout); - else - newTxio.setTxOut(tx.getTxRef(), iout); - - pair toBeInserted(outpt, newTxio); - txioIter = txioMap_.insert(toBeInserted).first; + if(txioIter->second.hasTxOutInMain()) // ...but we already have one + continue; + + // If we got here, we have an in-blockchain TxOut that is + // replacing a zero-conf txOut. Reset the txio to have + // only this real TxOut, blank out the ZC TxOut. And the addr + // relevantTxIOPtrs_ does not have this yet so it needs + // to be added (it's already part of the relevantTxIOPtrsZC_ + // but that will be removed) + txioIter->second.setTxOut(tx.getTxRef(), iout); + txioIter->second.setValue((uint64_t)thisVal); thisAddrPtr->addTxIO( txioIter->second, isZeroConf); doAddLedgerEntry = true; } + } + else + { + // TxIO is not in the map yet -- create and add it + TxIOPair newTxio(thisVal); + if(isZeroConf) + newTxio.setTxOutZC(&tx, iout); + else + newTxio.setTxOut(tx.getTxRef(), iout); - if(anyTxInIsOurs) - txioIter->second.setTxOutFromSelf(true); - - if(isCoinbaseTx) - txioIter->second.setFromCoinbase(true); + pair toBeInserted(outpt, newTxio); + txioIter = txioMap_.insert(toBeInserted).first; + thisAddrPtr->addTxIO( txioIter->second, isZeroConf); + doAddLedgerEntry = true; + } - anyNewTxOutIsOurs = true; - thisTxOutIsOurs[iout] = true; + if(anyTxInIsOurs) + txioIter->second.setTxOutFromSelf(true); + + if(isCoinbaseTx) + txioIter->second.setFromCoinbase(true); - if(doAddLedgerEntry) - { - LedgerEntry newLedger(scraddr, - thisVal, - blknum, - tx.getThisHash(), - iout, - txtime, - isCoinbaseTx, // input was coinbase/generation - false, // sentToSelf meaningless for addr ledger - false); // we don't actually know - thisAddrPtr->addLedgerEntry(newLedger, isZeroConf); - } - // Check if this is the first time we've seen this - if(thisAddrPtr->getFirstTimestamp() == 0) - { - thisAddrPtr->setFirstBlockNum( blknum ); - thisAddrPtr->setFirstTimestamp( txtime ); - } - // Update last seen on the network - thisAddrPtr->setLastTimestamp(txtime); - thisAddrPtr->setLastBlockNum(blknum); + anyNewTxOutIsOurs = true; + thisTxOutIsOurs[iout] = true; + + if(doAddLedgerEntry) + { + LedgerEntry newLedger(scraddr, + thisVal, + blknum, + tx.getThisHash(), + iout, + txtime, + isCoinbaseTx, // input was coinbase/generation + false, // sentToSelf meaningless for addr ledger + false); // we don't actually know + thisAddrPtr->addLedgerEntry(newLedger, isZeroConf); + + if(!savedAsTxIn) txLedgerForComments_.push_back(newLedger); } - } // loop over TxOuts + // Check if this is the first time we've seen this + if(thisAddrPtr->getFirstTimestamp() == 0) + { + thisAddrPtr->setFirstBlockNum( blknum ); + thisAddrPtr->setFirstTimestamp( txtime ); + } + // Update last seen on the network + thisAddrPtr->setLastTimestamp(txtime); + thisAddrPtr->setLastBlockNum(blknum); + } + } // loop over TxOuts - //} // loop over all wallet addresses bool allTxOutIsOurs = true; bool anyTxOutIsOurs = false; @@ -1130,7 +1270,7 @@ void BtcWallet::scanTx(Tx & tx, bool isSentToSelf = (anyTxInIsOurs && allTxOutIsOurs); bool isChangeBack = (anyTxInIsOurs && anyTxOutIsOurs && !isSentToSelf); - if(anyNewTxInIsOurs || anyNewTxOutIsOurs) + if((anyNewTxInIsOurs || anyNewTxOutIsOurs)) { LedgerEntry le( BinaryData(0), totalLedgerAmt, @@ -1170,7 +1310,7 @@ LedgerEntry BtcWallet::calcLedgerEntryForTx(Tx & tx) { // We have the txin, now check if it contains one of our TxOuts static OutPoint op; - op.unserialize(txStartPtr + tx.getTxInOffset(iin)); + op.unserialize(txStartPtr + tx.getTxInOffset(iin), tx.getSize()-tx.getTxInOffset(iin)); if(op.getTxHashRef() == BtcUtils::EmptyHash_) isCoinbaseTx = true; @@ -1325,7 +1465,7 @@ void BtcWallet::scanNonStdTx(uint32_t blknum, //uint64_t BtcWallet::getBalance(bool blockchainOnly) //////////////////////////////////////////////////////////////////////////////// -uint64_t BtcWallet::getSpendableBalance(uint32_t currBlk) +uint64_t BtcWallet::getSpendableBalance(uint32_t currBlk, bool ignoreAllZC) { uint64_t balance = 0; map::iterator iter; @@ -1333,14 +1473,14 @@ uint64_t BtcWallet::getSpendableBalance(uint32_t currBlk) iter != txioMap_.end(); iter++) { - if(iter->second.isSpendable(currBlk)) + if(iter->second.isSpendable(currBlk, ignoreAllZC)) balance += iter->second.getValue(); } return balance; } //////////////////////////////////////////////////////////////////////////////// -uint64_t BtcWallet::getUnconfirmedBalance(uint32_t currBlk) +uint64_t BtcWallet::getUnconfirmedBalance(uint32_t currBlk, bool inclAllZC) { uint64_t balance = 0; map::iterator iter; @@ -1348,7 +1488,7 @@ uint64_t BtcWallet::getUnconfirmedBalance(uint32_t currBlk) iter != txioMap_.end(); iter++) { - if(iter->second.isMineButUnconfirmed(currBlk)) + if(iter->second.isMineButUnconfirmed(currBlk, inclAllZC)) balance += iter->second.getValue(); } return balance; @@ -1370,7 +1510,8 @@ uint64_t BtcWallet::getFullBalance(void) } //////////////////////////////////////////////////////////////////////////////// -vector BtcWallet::getSpendableTxOutList(uint32_t blkNum) +vector BtcWallet::getSpendableTxOutList(uint32_t blkNum, + bool ignoreAllZC) { vector utxoList(0); map::iterator iter; @@ -1379,7 +1520,7 @@ vector BtcWallet::getSpendableTxOutList(uint32_t blkNum) iter++) { TxIOPair & txio = iter->second; - if(txio.isSpendable(blkNum)) + if(txio.isSpendable(blkNum, ignoreAllZC)) { TxOut txout = txio.getTxOutCopy(); utxoList.push_back(UnspentTxOut(txout, blkNum) ); @@ -1529,4676 +1670,4760 @@ vector BtcWallet::createAddressBook(void) //////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// +// AddRawBlockTODB // -// Start BlockDataManager_LevelDB methods +// Assumptions: +// -- We have already determined the correct height and dup for the header +// and we assume it's part of the sbh object +// -- It has definitely been added to the headers DB (bail if not) +// -- We don't know if it's been added to the blkdata DB yet +// +// Things to do when adding a block: +// +// -- PREPARATION: +// -- Create list of all OutPoints affected, and scripts touched +// -- If not supernode, then check above data against registeredSSHs_ +// -- Fetch all StoredTxOuts from DB about to be removed +// -- Get/create TXHINT entries for all tx in block +// -- Compute all script keys and get/create all StoredScriptHistory objs +// -- Check if any multisig scripts are affected, if so get those objs +// -- If pruning, create StoredUndoData from TxOuts about to be removed +// -- Modify any Tx/TxOuts in the SBH tree to accommodate any tx in this +// block that affect any other tx in this block +// +// +// -- Check if the block {hgt,dup} has already been written to BLKDATA DB +// -- Check if the header has already been added to HEADERS DB +// +// -- BATCH (HEADERS) +// -- Add header to HEADHASH list +// -- Add header to HEADHGT list +// -- Update validDupByHeight_ +// -- Update DBINFO top block data +// +// -- BATCH (BLKDATA) +// -- Modify StoredTxOut with spentness info (or prep a delete operation +// if pruning). +// -- Modify StoredScriptHistory objs same as above. +// -- Modify StoredScriptHistory multisig objects as well. +// -- Update SSH objects alreadyScannedUpToBlk_, if necessary +// -- Write all new TXDATA entries for {hgt,dup} +// -- If pruning, write StoredUndoData objs to DB +// -- Update DBINFO top block data +// +// IMPORTANT: we also need to make sure this method does nothing if the +// block has already been added properly (though, it okay for +// it to take time to verify nothing needs to be done). We may +// end up replaying some blocks to force consistency of the DB, +// and this method needs to be robust to replaying already-added +// blocks, as well as fixing data if the replayed block appears +// to have been added already but is different. // //////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -BlockDataManager_LevelDB::BlockDataManager_LevelDB(void) -{ - Reset(); -} - -///////////////////////////////////////////////////////////////////////////// -BlockDataManager_LevelDB::~BlockDataManager_LevelDB(void) +BlockWriteBatcher::BlockWriteBatcher(InterfaceToLDB* iface) + : iface_(iface), dbUpdateSize_(0), mostRecentBlockApplied_(0) { - set::iterator iter; - for(iter = registeredWallets_.begin(); - iter != registeredWallets_.end(); - iter++) - { - delete *iter; - } - Reset(); } -///////////////////////////////////////////////////////////////////////////// -// We must set the network-specific data for this blockchain -// -// bdm.SetBtcNetworkParams( READHEX(MAINNET_GENESIS_HASH_HEX), -// READHEX(MAINNET_GENESIS_TX_HASH_HEX), -// READHEX(MAINNET_MAGIC_BYTES)); -// -// The above call will work -void BlockDataManager_LevelDB::SetBtcNetworkParams( - BinaryData const & GenHash, - BinaryData const & GenTxHash, - BinaryData const & MagicBytes) +BlockWriteBatcher::~BlockWriteBatcher() { - LOGINFO << "SetBtcNetworkParams"; - GenesisHash_.copyFrom(GenHash); - GenesisTxHash_.copyFrom(GenTxHash); - MagicBytes_.copyFrom(MagicBytes); + commit(); } - - -///////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::SetHomeDirLocation(string homeDir) +void BlockWriteBatcher::applyBlockToDB(StoredHeader &sbh) { - // This will eventually be used to store blocks/DB - LOGINFO << "Set home directory: " << armoryHomeDir_.c_str(); - armoryHomeDir_ = homeDir; - blkProgressFile_ = homeDir + string("/blkfiles.txt"); - abortLoadFile_ = homeDir + string("/abortload.txt"); -} + if(iface_->getValidDupIDForHeight(sbh.blockHeight_) != sbh.duplicateID_) + { + LOGERR << "Dup requested is not the main branch for the given height!"; + return; + } + else + sbh.isMainBranch_ = true; + + mostRecentBlockApplied_= sbh.blockHeight_; -///////////////////////////////////////////////////////////////////////////// -// Bitcoin-Qt/bitcoind 0.8+ changed the location and naming convention for -// the blkXXXX.dat files. The first block file use to be: -// -// ~/.bitcoin/blocks/blk00000.dat -// -// UPDATE: Compatibility with pre-0.8 nodes removed after 6+ months and -// a hard-fork that makes it tougher to use old versions. -// -bool BlockDataManager_LevelDB::SetBlkFileLocation(string blkdir) -{ - blkFileDir_ = blkdir; - isBlkParamsSet_ = true; + // We will accumulate undoData as we apply the tx + StoredUndoData sud; + sud.blockHash_ = sbh.thisHash_; + sud.blockHeight_ = sbh.blockHeight_; + sud.duplicateID_ = sbh.duplicateID_; + + // Apply all the tx to the update data + for(map::iterator iter = sbh.stxMap_.begin(); + iter != sbh.stxMap_.end(); iter++) + { + // This will fetch all the affected [Stored]Tx and modify the maps in + // RAM. It will check the maps first to see if it's already been pulled, + // and then it will modify either the pulled StoredTx or pre-existing + // one. This means that if a single Tx is affected by multiple TxIns + // or TxOuts, earlier changes will not be overwritten by newer changes. + applyTxToBatchWriteData(iter->second, &sud); + } - detectAllBlkFiles(); + // At this point we should have a list of STX and SSH with all the correct + // modifications (or creations) to represent this block. Let's apply it. + sbh.blockAppliedToDB_ = true; + updateBlkDataHeader(iface_, sbh); + //iface_->putStoredHeader(sbh, false); - LOGINFO << "Set blkfile dir: " << blkFileDir_.c_str(); + // we want to commit the undo data at the same time as actual changes + iface_->startBatch(BLKDATA); + + // Now actually write all the changes to the DB all at once + // if we've gotten to that threshold + if (dbUpdateSize_ > UPDATE_BYTES_THRESH) + commit(); - return (numBlkFiles_!=UINT16_MAX); + // Only if pruning, we need to store + // TODO: this is going to get run every block, probably should batch it + // like we do with the other data...when we actually implement pruning + if(DBUtils.getDbPruneType() == DB_PRUNE_ALL) + iface_->putStoredUndoData(sud); + + + iface_->commitBatch(BLKDATA); } -///////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::SetLevelDBLocation(string ldbdir) -{ - leveldbDir_ = ldbdir; - isLevelDBSet_ = true; - LOGINFO << "Set leveldb dir: " << leveldbDir_.c_str(); -} -///////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::SelectNetwork(string netName) +//////////////////////////////////////////////////////////////////////////////// +void BlockWriteBatcher::undoBlockFromDB(StoredUndoData & sud) { - if(netName.compare("Main") == 0) + SCOPED_TIMER("undoBlockFromDB"); + + StoredHeader sbh; + iface_->getStoredHeader(sbh, sud.blockHeight_, sud.duplicateID_); + if(!sbh.blockAppliedToDB_) { - SetBtcNetworkParams( READHEX(MAINNET_GENESIS_HASH_HEX), - READHEX(MAINNET_GENESIS_TX_HASH_HEX), - READHEX(MAINNET_MAGIC_BYTES) ); + LOGERR << "This block was never applied to the DB...can't undo!"; + return /*false*/; } - else if(netName.compare("Test") == 0) + + mostRecentBlockApplied_ = sud.blockHeight_; + + // In the future we will accommodate more user modes + if(DBUtils.getArmoryDbType() != ARMORY_DB_SUPER) { - SetBtcNetworkParams( READHEX(TESTNET_GENESIS_HASH_HEX), - READHEX(TESTNET_GENESIS_TX_HASH_HEX), - READHEX(TESTNET_MAGIC_BYTES) ); + LOGERR << "Don't know what to do this in non-supernode mode!"; } - else - LOGERR << "ERROR: Unrecognized network name"; + + ///// Put the STXOs back into the DB which were removed by this block + // Process the stxOutsRemovedByBlock_ in reverse order + // Use int32_t index so that -1 != UINT32_MAX and we go into inf loop + for(int32_t i=sud.stxOutsRemovedByBlock_.size()-1; i>=0; i--) + { + StoredTxOut & sudStxo = sud.stxOutsRemovedByBlock_[i]; + StoredTx * stxptr = makeSureSTXInMap( + iface_, + sudStxo.blockHeight_, + sudStxo.duplicateID_, + sudStxo.txIndex_, + sudStxo.parentHash_, + stxToModify_, + &dbUpdateSize_); - isNetParamsSet_ = true; -} + + const uint16_t stxoIdx = sudStxo.txOutIndex_; + + if(DBUtils.getDbPruneType() == DB_PRUNE_NONE) + { + // If full/super, we have the TxOut in DB, just need mark it unspent + map::iterator iter = stxptr->stxoMap_.find(stxoIdx); + //if(iter == stxptr->stxoMap_.end()) + if(ITER_NOT_IN_MAP(iter, stxptr->stxoMap_)) + { + LOGERR << "Expecting to find existing STXO, but DNE"; + continue; + } + + StoredTxOut & stxoReAdd = iter->second; + if(stxoReAdd.spentness_ == TXOUT_UNSPENT || + stxoReAdd.spentByTxInKey_.getSize() == 0 ) + { + LOGERR << "STXO needs to be re-added/marked-unspent but it"; + LOGERR << "was already declared unspent in the DB"; + } + + stxoReAdd.spentness_ = TXOUT_UNSPENT; + stxoReAdd.spentByTxInKey_ = BinaryData(0); + } + else + { + // If we're pruning, we should have the Tx in the DB, but without the + // TxOut because it had been pruned by this block on the forward op + map::iterator iter = stxptr->stxoMap_.find(stxoIdx); + //if(iter != stxptr->stxoMap_.end()) + if(ITER_IN_MAP(iter, stxptr->stxoMap_)) + LOGERR << "Somehow this TxOut had not been pruned!"; + else + iter->second = sudStxo; + iter->second.spentness_ = TXOUT_UNSPENT; + iter->second.spentByTxInKey_ = BinaryData(0); + } -///////////////////////////////////////////////////////////////////////////// -bool BlockDataManager_LevelDB::checkLdbStatus(leveldb::Status stat) -{ - if( stat.ok() ) - return true; + { + ////// Finished updating STX, now update the SSH in the DB + // Updating the SSH objects works the same regardless of pruning + map::iterator iter = stxptr->stxoMap_.find(stxoIdx); + //if(iter == stxptr->stxoMap_.end()) + if(ITER_NOT_IN_MAP(iter, stxptr->stxoMap_)) + { + LOGERR << "Somehow STXO DNE even though we should've just added it!"; + continue; + } - LOGERR << "***LevelDB Error: " << stat.ToString(); - return false; -} + StoredTxOut & stxoReAdd = iter->second; + BinaryData uniqKey = stxoReAdd.getScrAddress(); + BinaryData hgtX = stxoReAdd.getHgtX(); + StoredScriptHistory* sshptr = makeSureSSHInMap( + iface_, uniqKey, hgtX, sshToModify_, &dbUpdateSize_ + ); + if(sshptr==NULL) + { + LOGERR << "No SSH found for marking TxOut unspent on undo"; + continue; + } -////////////////////////////////////////////////////////////////////////// -// This method opens the databases, and figures out up to what block each -// of them is sync'd to. Then it figures out where that corresponds in -// the blk*.dat files, so that it can pick up where it left off. You can -// use the last argument to specify an approximate amount of blocks -// (specified in bytes) that you would like to replay: i.e. if 10 MB, -// startScanBlkFile_ and endOfLastBlockByte_ variables will be set to -// the first block that is approximately 10 MB behind your latest block. -// Then you can pick up from there and let the DB clean up any mess that -// was left from an unclean shutdown. -bool BlockDataManager_LevelDB::initializeDBInterface(ARMORY_DB_TYPE dbtype, - DB_PRUNE_TYPE prtype) -{ - SCOPED_TIMER("initializeDBInterface"); - if(!isBlkParamsSet_ || !isLevelDBSet_) - { - LOGERR << "Cannot sync DB until blkfile and LevelDB paths are set. "; - return false; + // Now get the TxIOPair in the StoredScriptHistory and mark unspent + sshptr->markTxOutUnspent(stxoReAdd.getDBKey(false), + stxoReAdd.getValue(), + stxoReAdd.isCoinbase_, + false); + + + // If multisig, we need to update the SSHs for individual addresses + if(uniqKey[0] == SCRIPT_PREFIX_MULTISIG) + { + vector addr160List; + BtcUtils::getMultisigAddrList(stxoReAdd.getScriptRef(), addr160List); + for(uint32_t a=0; amarkTxOutUnspent(stxoReAdd.getDBKey(false), + stxoReAdd.getValue(), + stxoReAdd.isCoinbase_, + true); + } + } + } } - if(iface_->databasesAreOpen()) + + // The OutPoint list is every new, unspent TxOut created by this block. + // When they were added, we updated all the StoredScriptHistory objects + // to include references to them. We need to remove them now. + // Use int32_t index so that -1 != UINT32_MAX and we go into inf loop + for(int16_t itx=sbh.numTx_-1; itx>=0; itx--) { - LOGERR << "Attempted to initialize a database that was already open"; - return false; - } + // Ironically, even though I'm using hgt & dup, I still need the hash + // in order to key the stxToModify map + BinaryData txHash = iface_->getHashForDBKey(sbh.blockHeight_, + sbh.duplicateID_, + itx); + StoredTx * stxptr = makeSureSTXInMap( + iface_, + sbh.blockHeight_, + sbh.duplicateID_, + itx, + txHash, + stxToModify_, + &dbUpdateSize_); - bool openWithErr = iface_->openDatabases(leveldbDir_, - GenesisHash_, - GenesisTxHash_, - MagicBytes_, - dbtype, - prtype); + for(int16_t txoIdx = stxptr->stxoMap_.size()-1; txoIdx >= 0; txoIdx--) + { - return openWithErr; + StoredTxOut & stxo = stxptr->stxoMap_[txoIdx]; + BinaryData stxoKey = stxo.getDBKey(false); + + + // Then fetch the StoredScriptHistory of the StoredTxOut scraddress + BinaryData uniqKey = stxo.getScrAddress(); + BinaryData hgtX = stxo.getHgtX(); + StoredScriptHistory * sshptr = makeSureSSHInMap( + iface_, uniqKey, + hgtX, + sshToModify_, + &dbUpdateSize_, + false); + + + // If we are tracking that SSH, remove the reference to this OutPoint + if(sshptr != NULL) + sshptr->eraseTxio(stxoKey); + + // Now remove any multisig entries that were added due to this TxOut + if(uniqKey[0] == SCRIPT_PREFIX_MULTISIG) + { + vector addr160List; + BtcUtils::getMultisigAddrList(stxo.getScriptRef(), addr160List); + for(uint32_t a=0; aeraseTxio(stxoKey); + } + } + } + } + + // Finally, mark this block as UNapplied. + sbh.blockAppliedToDB_ = false; + updateBlkDataHeader(iface_, sbh); + + if (dbUpdateSize_ > UPDATE_BYTES_THRESH) + commit(); } + //////////////////////////////////////////////////////////////////////////////// -bool BlockDataManager_LevelDB::detectCurrentSyncState( - bool forceRebuild, - bool initialLoad) +// Assume that stx.blockHeight_ and .duplicateID_ are set correctly. +// We created the maps and sets outside this function, because we need to keep +// a master list of updates induced by all tx in this block. +// TODO: Make sure that if Tx5 spends an input from Tx2 in the same +// block that it is handled correctly, etc. +bool BlockWriteBatcher::applyTxToBatchWriteData( + StoredTx & thisSTX, + StoredUndoData * sud) { - // Make sure we detected all the available blk files - detectAllBlkFiles(); - vector firstHashes = getFirstHashOfEachBlkFile(); - LOGINFO << "Total blk*.dat files: " << numBlkFiles_; + SCOPED_TIMER("applyTxToBatchWriteData"); - if(!iface_->databasesAreOpen()) - { - LOGERR << "Could not open databases!"; - return false; - } - - // We add 1 to each of these, since we always use exclusive upperbound - startHeaderHgt_ = getTopBlockHeightInDB(HEADERS) + 1; - startRawBlkHgt_ = getTopBlockHeightInDB(BLKDATA) + 1; - startApplyHgt_ = getAppliedToHeightInDB() + 1; - - // If the values were supposed to be zero, they'll get set to 1. Fix it - startHeaderHgt_ -= (startHeaderHgt_==1 ? 1 : 0); - startRawBlkHgt_ -= (startRawBlkHgt_==1 ? 1 : 0); - startApplyHgt_ -= (startApplyHgt_ ==1 ? 1 : 0); + Tx tx = thisSTX.getTxCopy(); - LOGINFO << "Current Top block in HEADERS DB: " << startHeaderHgt_; - LOGINFO << "Current Top block in BLKDATA DB: " << startRawBlkHgt_; - LOGINFO << "Current Applied blocks up to hgt: " << startApplyHgt_; + // We never expect thisSTX to already be in the map (other tx in the map + // may be affected/retrieved multiple times). + if(KEY_IN_MAP(tx.getThisHash(), stxToModify_)) + LOGERR << "How did we already add this tx?"; - if(startHeaderHgt_ == 0 || forceRebuild) - { - if(forceRebuild) - LOGINFO << "Ignore existing sync state, rebuilding databases"; + // I just noticed we never set TxOuts to TXOUT_UNSPENT. Might as well do + // it here -- by definition if we just added this Tx to the DB, it couldn't + // have been spent yet. + + for(map::iterator iter = thisSTX.stxoMap_.begin(); + iter != thisSTX.stxoMap_.end(); + iter++) + iter->second.spentness_ = TXOUT_UNSPENT; - startHeaderHgt_ = 0; - startHeaderBlkFile_ = 0; - startHeaderOffset_ = 0; - startRawBlkHgt_ = 0; - startRawBlkFile_ = 0; - startRawOffset_ = 0; - startApplyHgt_ = 0; - startApplyBlkFile_ = 0; - startApplyOffset_ = 0; - headerMap_.clear(); - topBlockPtr_ = NULL; - genBlockPtr_ = NULL; - lastTopBlock_ = UINT32_MAX;; - return true; - } + // This tx itself needs to be added to the map, which makes it accessible + // to future tx in the same block which spend outputs from this tx, without + // doing anything crazy in the code here + stxToModify_[tx.getThisHash()] = thisSTX; - // This fetches the header data from the DB - if(!initialLoad) + dbUpdateSize_ += thisSTX.numBytes_; + + // Go through and find all the previous TxOuts that are affected by this tx + for(uint32_t iin=0; iin sbhMap; - headerMap_.clear(); - iface_->readAllHeaders(headerMap_, sbhMap); - + TxIn txin = tx.getTxInCopy(iin); + if(txin.isCoinbase()) + continue; - // Organize them into the longest chain - organizeChain(true); // true ~ force rebuild + // Get the OutPoint data of TxOut being spent + const OutPoint op = txin.getOutPoint(); + const BinaryDataRef opTxHash = op.getTxHashRef(); + const uint32_t opTxoIdx = op.getTxOutIndex(); + // This will fetch the STX from DB and put it in the stxToModify + // map if it's not already there. Or it will do nothing if it's + // already part of the map. In both cases, it returns a pointer + // to the STX that will be written to DB that we can modify. + StoredTx * stxptr = makeSureSTXInMap(iface_, opTxHash, stxToModify_, &dbUpdateSize_); + StoredTxOut & stxo = stxptr->stxoMap_[opTxoIdx]; + BinaryData uniqKey = stxo.getScrAddress(); - // If the headers DB ended up corrupted (triggered by organizeChain), - // then nuke and rebuild the headers - if(corruptHeadersDB_) - { - LOGERR << "Corrupted headers DB!"; - startHeaderHgt_ = 0; - startHeaderBlkFile_ = 0; - startHeaderOffset_ = 0; - startRawBlkHgt_ = 0; - startRawBlkFile_ = 0; - startRawOffset_ = 0; - startApplyHgt_ = 0; - startApplyBlkFile_ = 0; - startApplyOffset_ = 0; - headerMap_.clear(); - headersByHeight_.clear(); - topBlockPtr_ = NULL; - prevTopBlockPtr_ = NULL; - corruptHeadersDB_ = false; - lastTopBlock_ = UINT32_MAX; - genBlockPtr_ = NULL; - return true; - } - else - { - // Now go through the linear list of main-chain headers, mark valid - for(uint32_t i=0; i::iterator iter = stxptr->stxoMap_.find(opTxoIdx); + + // Some sanity checks + //if(iter == stxptr->stxoMap_.end()) + if(ITER_NOT_IN_MAP(iter, stxptr->stxoMap_)) { - BinaryDataRef headHash = headersByHeight_[i]->getThisHashRef(); - StoredHeader & sbh = sbhMap[headHash]; - sbh.isMainBranch_ = true; - iface_->setValidDupIDForHeight(sbh.blockHeight_, sbh.duplicateID_); + LOGERR << "Needed to get OutPoint for a TxIn, but DNE"; + continue; } - // startHeaderBlkFile_/Offset_ is where we were before the last shutdown - for(startHeaderBlkFile_ = 0; - startHeaderBlkFile_ < firstHashes.size(); - startHeaderBlkFile_++) + // We're aliasing this because "iter->second" is not clear at all + StoredTxOut & stxoSpend = iter->second; + + if(stxoSpend.spentness_ == TXOUT_SPENT) { - // hasHeaderWithHash is probing the RAM block headers we just organized - if(!hasHeaderWithHash(firstHashes[startHeaderBlkFile_])) - break; + LOGERR << "Trying to mark TxOut spent, but it's already marked"; + continue; } - // If no new blkfiles since last load, the above loop ends w/o "break" - // If it's zero, then we don't have anything, start at zero - // If new blk file, then startHeaderBlkFile_ is at the first blk file - // with an unrecognized hash... we must've left off in the prev blkfile - if(startHeaderBlkFile_ > 0) - startHeaderBlkFile_--; - - startHeaderOffset_ = findOffsetFirstUnrecognized(startHeaderBlkFile_); - } + // Just about to {remove-if-pruning, mark-spent-if-not} STXO + // Record it in the StoredUndoData object + if(sud != NULL) + sud->stxOutsRemovedByBlock_.push_back(stxoSpend); - LOGINFO << "First unrecognized hash file: " << startHeaderBlkFile_; - LOGINFO << "Offset of first unrecog block: " << startHeaderOffset_; + // Need to modify existing UTXOs, so that we can delete or mark as spent + stxoSpend.spentness_ = TXOUT_SPENT; + stxoSpend.spentByTxInKey_ = thisSTX.getDBKeyOfChild(iin, false); + if(DBUtils.getArmoryDbType() != ARMORY_DB_SUPER) + { + LOGERR << "Don't know what to do this in non-supernode mode!"; + } - // Note that startRawBlkHgt_ is topBlk+1, so this return where we should - // actually start processing raw blocks, not the last one we processed - pair rawBlockLoc; - rawBlockLoc = findFileAndOffsetForHgt(startRawBlkHgt_, &firstHashes); - startRawBlkFile_ = rawBlockLoc.first; - startRawOffset_ = rawBlockLoc.second; - LOGINFO << "First blkfile not in DB: " << startRawBlkFile_; - LOGINFO << "Location of first block not in DB: " << startRawOffset_; + ////// Now update the SSH to show this TxIOPair was spent + // Same story as stxToModify above, except this will actually create a new + // SSH if it doesn't exist in the map or the DB + BinaryData hgtX = stxo.getHgtX(); + StoredScriptHistory* sshptr = makeSureSSHInMap( + iface_, + uniqKey, + hgtX, + sshToModify_, + &dbUpdateSize_ + ); - if(DBUtils.getArmoryDbType() != ARMORY_DB_BARE) - { - // TODO: finish this - findFirstUnappliedBlock(); - LOGINFO << "Blkfile of first unapplied block: " << startApplyBlkFile_; - LOGINFO << "Location of first unapplied block: " << startApplyOffset_; + // Assuming supernode, we don't need to worry about removing references + // to multisig scripts that reference this script. Simply find and + // update the correct SSH TXIO directly + sshptr->markTxOutSpent(stxoSpend.getDBKey(false), + thisSTX.getDBKeyOfChild(iin, false)); } - // If we're content here, just return - return true; - - /* - // If we want to replay some blocks, we need to adjust startScanBlkFile_ - // and startScanOffset_ to be approx "replayNBytes" behind where - // they are currently set. - int32_t targOffset = (int32_t)startScanOffset_ - (int32_t)replayNBytes; - if(targOffset > 0 || startScanBlkFile_==0) - { - targOffset = max(0, targOffset); - startScanOffset_ = findFirstBlkApproxOffset(startScanBlkFile_, targOffset); - } - else + // We don't need to update any TXDATA, since it is part of writing thisSTX + // to the DB ... but we do need to update the StoredScriptHistory objects + // with references to the new [unspent] TxOuts + for(uint32_t iout=0; ioutmarkTxOutUnspent(stxoToAdd.getDBKey(false), + stxoToAdd.getValue(), + stxoToAdd.isCoinbase_, + false); + + // If this was a multisig address, add a ref to each individual scraddr + if(uniqKey[0] == SCRIPT_PREFIX_MULTISIG) + { + vector addr160List; + BtcUtils::getMultisigAddrList(stxoToAdd.getScriptRef(), addr160List); + for(uint32_t a=0; amarkTxOutUnspent(stxoToAdd.getDBKey(false), + stxoToAdd.getValue(), + stxoToAdd.isCoinbase_, + true); + } + } } - LOGINFO << "Rewinding start block to enforce DB integrity"; - LOGINFO << "Start at blockfile: " << startScanBlkFile_; - LOGINFO << "Start location in above blkfile: " << startScanOffset_; return true; - */ } -//////////////////////////////////////////////////////////////////////////////// -vector BlockDataManager_LevelDB::getFirstHashOfEachBlkFile(void) const + +void BlockWriteBatcher::commit() { - if(!isBlkParamsSet_) + // Check for any SSH objects that are now completely empty. If they exist, + // they should be removed from the DB, instead of simply written as empty + // objects + const set keysToDelete = searchForSSHKeysToDelete(); + + iface_->startBatch(BLKDATA); + + for(map::iterator iter_stx = stxToModify_.begin(); + iter_stx != stxToModify_.end(); + iter_stx++) { - LOGERR << "Can't get blk files until blkfile params are set"; - return vector(0); + iface_->putStoredTx(iter_stx->second, true); + } + + for(map::iterator iter_ssh = sshToModify_.begin(); + iter_ssh != sshToModify_.end(); + iter_ssh++) + { + iface_->putStoredScriptHistory(iter_ssh->second); } - uint32_t nFile = (uint32_t)blkFileList_.size(); - BinaryData magic(4), szstr(4), rawHead(HEADER_SIZE); - vector headHashes(nFile); - for(uint32_t f=0; f::const_iterator iter_del = keysToDelete.begin(); + iter_del != keysToDelete.end(); + iter_del++) { - ifstream is(blkFileList_[f].c_str(), ios::in|ios::binary); - is.seekg(0, ios::end); - size_t filesize = (size_t)is.tellg(); - is.seekg(0, ios::beg); - if(filesize < 88) + iface_->deleteValue(BLKDATA, *iter_del); + } + + + if(mostRecentBlockApplied_ != 0) + { + StoredDBInfo sdbi; + iface_->getStoredDBInfo(BLKDATA, sdbi); + if(!sdbi.isInitialized()) + LOGERR << "How do we have invalid SDBI in applyMods?"; + else { - is.close(); - LOGERR << "File: " << blkFileList_[f] << " is less than 88 bytes!"; - continue; + sdbi.appliedToHgt_ = mostRecentBlockApplied_; + iface_->putStoredDBInfo(BLKDATA, sdbi); } + } - is.read((char*)magic.getPtr(), 4); - is.read((char*)szstr.getPtr(), 4); - if(magic != MagicBytes_) + iface_->commitBatch(BLKDATA); + + stxToModify_.clear(); + sshToModify_.clear(); + dbUpdateSize_ = 0; +} + +set BlockWriteBatcher::searchForSSHKeysToDelete() +{ + set keysToDelete; + vector fullSSHToDelete; + + for(map::iterator iterSSH = sshToModify_.begin(); + iterSSH != sshToModify_.end(); ) + { + // get our next one in case we delete the current + map::iterator nextSSHi = iterSSH; + ++nextSSHi; + + StoredScriptHistory & ssh = iterSSH->second; + + for(map::iterator iterSub = ssh.subHistMap_.begin(); + iterSub != ssh.subHistMap_.end(); + iterSub++) { - is.close(); - LOGERR << "Magic bytes mismatch. Block file is for another network!"; - return vector(0); + StoredSubHistory & subssh = iterSub->second; + if(subssh.txioSet_.size() == 0) + keysToDelete.insert(subssh.getDBKey(true)); + } + + // If the full SSH is empty (not just sub history), mark it to be removed + if(iterSSH->second.totalTxioCount_ == 0) + { + sshToModify_.erase(iterSSH); } - is.read((char*)rawHead.getPtr(), HEADER_SIZE); - headHashes[f] = BinaryData(32); - BtcUtils::getHash256(rawHead, headHashes[f]); - is.close(); + iterSSH = nextSSHi; } - return headHashes; + + return keysToDelete; } //////////////////////////////////////////////////////////////////////////////// -uint32_t BlockDataManager_LevelDB::findOffsetFirstUnrecognized(uint32_t fnum) +//////////////////////////////////////////////////////////////////////////////// +// +// Start BlockDataManager_LevelDB methods +// +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +BlockDataManager_LevelDB::BlockDataManager_LevelDB(void) { - uint32_t loc = 0; - BinaryData magic(4), szstr(4), rawHead(80), hashResult(32); + Reset(); +} - ifstream is(blkFileList_[fnum].c_str(), ios::in|ios::binary); - while(!is.eof()) +///////////////////////////////////////////////////////////////////////////// +BlockDataManager_LevelDB::~BlockDataManager_LevelDB(void) +{ + set::iterator iter; + for(iter = registeredWallets_.begin(); + iter != registeredWallets_.end(); + iter++) { - is.read((char*)magic.getPtr(), 4); - if(is.eof()) break; - - - // This is not an error, it just simply hit the padding - if(magic!=MagicBytes_) - break; + delete *iter; + } - is.read((char*)szstr.getPtr(), 4); - uint32_t blksize = READ_UINT32_LE(szstr.getPtr()); - if(is.eof()) break; + Reset(); +} - is.read((char*)rawHead.getPtr(), HEADER_SIZE); +///////////////////////////////////////////////////////////////////////////// +// We must set the network-specific data for this blockchain +// +// bdm.SetBtcNetworkParams( READHEX(MAINNET_GENESIS_HASH_HEX), +// READHEX(MAINNET_GENESIS_TX_HASH_HEX), +// READHEX(MAINNET_MAGIC_BYTES)); +// +// The above call will work +void BlockDataManager_LevelDB::SetBtcNetworkParams( + BinaryData const & GenHash, + BinaryData const & GenTxHash, + BinaryData const & MagicBytes) +{ + LOGINFO << "SetBtcNetworkParams"; + GenesisHash_.copyFrom(GenHash); + GenesisTxHash_.copyFrom(GenTxHash); + MagicBytes_.copyFrom(MagicBytes); +} - BtcUtils::getHash256_NoSafetyCheck(rawHead.getPtr(), HEADER_SIZE, hashResult); - if(getHeaderByHash(hashResult) == NULL) - break; // first hash in the file that isn't in our header map - loc += blksize + 8; - is.seekg(blksize - HEADER_SIZE, ios::cur); - } - - is.close(); - return loc; +///////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::SetHomeDirLocation(string homeDir) +{ + // This will eventually be used to store blocks/DB + LOGINFO << "Set home directory: " << armoryHomeDir_.c_str(); + armoryHomeDir_ = homeDir; + blkProgressFile_ = homeDir + string("/blkfiles.txt"); + abortLoadFile_ = homeDir + string("/abortload.txt"); } -//////////////////////////////////////////////////////////////////////////////// -uint32_t BlockDataManager_LevelDB::findFirstBlkApproxOffset(uint32_t fnum, - uint32_t offset) const +///////////////////////////////////////////////////////////////////////////// +// Bitcoin-Qt/bitcoind 0.8+ changed the location and naming convention for +// the blkXXXX.dat files. The first block file use to be: +// +// ~/.bitcoin/blocks/blk00000.dat +// +// UPDATE: Compatibility with pre-0.8 nodes removed after 6+ months and +// a hard-fork that makes it tougher to use old versions. +// +bool BlockDataManager_LevelDB::SetBlkFileLocation(string blkdir) { - if(fnum >= numBlkFiles_) - { - LOGERR << "Blkfile number out of range! (" << fnum << ")"; - return UINT32_MAX; - } - - uint32_t loc = 0; - BinaryData magic(4), szstr(4), rawHead(80), hashResult(32); - ifstream is(blkFileList_[fnum].c_str(), ios::in|ios::binary); - while(!is.eof() && loc <= offset) - { - is.read((char*)magic.getPtr(), 4); - if(is.eof()) break; - if(magic!=MagicBytes_) - return UINT32_MAX; + blkFileDir_ = blkdir; + isBlkParamsSet_ = true; - is.read((char*)szstr.getPtr(), 4); - uint32_t blksize = READ_UINT32_LE(szstr.getPtr()); - if(is.eof()) break; + detectAllBlkFiles(); - loc += blksize + 8; - is.seekg(blksize, ios::cur); - } + LOGINFO << "Set blkfile dir: " << blkFileDir_.c_str(); - is.close(); - return loc; + return (numBlkFiles_!=UINT16_MAX); } -//////////////////////////////////////////////////////////////////////////////// -pair BlockDataManager_LevelDB::findFileAndOffsetForHgt( - uint32_t hgt, - vector * firstHashes) +///////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::SetLevelDBLocation(string ldbdir) { - vector recomputedHashes; - if(firstHashes==NULL) - { - recomputedHashes = getFirstHashOfEachBlkFile(); - firstHashes = &recomputedHashes; - } + leveldbDir_ = ldbdir; + isLevelDBSet_ = true; + LOGINFO << "Set leveldb dir: " << leveldbDir_.c_str(); +} - pair outPair; - int32_t blkfile; - for(blkfile = 0; blkfile < (int32_t)firstHashes->size(); blkfile++) +///////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::SelectNetwork(string netName) +{ + if(netName.compare("Main") == 0) { - BlockHeader * bhptr = getHeaderByHash((*firstHashes)[blkfile]); - if(bhptr == NULL) - break; - - if(bhptr->getBlockHeight() > hgt) - break; + SetBtcNetworkParams( READHEX(MAINNET_GENESIS_HASH_HEX), + READHEX(MAINNET_GENESIS_TX_HASH_HEX), + READHEX(MAINNET_MAGIC_BYTES) ); } - - blkfile = max(blkfile-1, 0); - if(blkfile >= (int32_t)numBlkFiles_) + else if(netName.compare("Test") == 0) { - LOGERR << "Blkfile number out of range! (" << blkfile << ")"; - return outPair; + SetBtcNetworkParams( READHEX(TESTNET_GENESIS_HASH_HEX), + READHEX(TESTNET_GENESIS_TX_HASH_HEX), + READHEX(TESTNET_MAGIC_BYTES) ); } + else + LOGERR << "ERROR: Unrecognized network name"; - uint32_t loc = 0; - BinaryData magic(4), szstr(4), rawHead(HEADER_SIZE), hashResult(32); - ifstream is(blkFileList_[blkfile].c_str(), ios::in|ios::binary); - while(!is.eof()) - { - is.read((char*)magic.getPtr(), 4); - if(is.eof()) break; - if(magic!=MagicBytes_) - break; - - is.read((char*)szstr.getPtr(), 4); - uint32_t blksize = READ_UINT32_LE(szstr.getPtr()); - if(is.eof()) break; - - is.read((char*)rawHead.getPtr(), HEADER_SIZE); - BtcUtils::getHash256_NoSafetyCheck(rawHead.getPtr(), - HEADER_SIZE, - hashResult); - - BlockHeader * bhptr = getHeaderByHash(hashResult); - if(bhptr == NULL) - break; - - if(bhptr->getBlockHeight() >= hgt) - break; + isNetParamsSet_ = true; +} - loc += blksize + 8; - is.seekg(blksize - HEADER_SIZE, ios::cur); - } - is.close(); - outPair.first = blkfile; - outPair.second = loc; - - return outPair; - +///////////////////////////////////////////////////////////////////////////// +bool BlockDataManager_LevelDB::checkLdbStatus(leveldb::Status stat) +{ + if( stat.ok() ) + return true; + LOGERR << "***LevelDB Error: " << stat.ToString(); + return false; } - -//////////////////////////////////////////////////////////////////////////////// -// This behaves very much like the algorithm for finding the branch point -// in the header tree with a peer. -uint32_t BlockDataManager_LevelDB::findFirstUnappliedBlock(void) +////////////////////////////////////////////////////////////////////////// +// This method opens the databases, and figures out up to what block each +// of them is sync'd to. Then it figures out where that corresponds in +// the blk*.dat files, so that it can pick up where it left off. You can +// use the last argument to specify an approximate amount of blocks +// (specified in bytes) that you would like to replay: i.e. if 10 MB, +// startScanBlkFile_ and endOfLastBlockByte_ variables will be set to +// the first block that is approximately 10 MB behind your latest block. +// Then you can pick up from there and let the DB clean up any mess that +// was left from an unclean shutdown. +bool BlockDataManager_LevelDB::initializeDBInterface(ARMORY_DB_TYPE dbtype, + DB_PRUNE_TYPE prtype) { - SCOPED_TIMER("findFirstUnappliedBlock"); - - if(!iface_->databasesAreOpen()) + SCOPED_TIMER("initializeDBInterface"); + if(!isBlkParamsSet_ || !isLevelDBSet_) { - LOGERR << "Database is not open!"; - return UINT32_MAX; + LOGERR << "Cannot sync DB until blkfile and LevelDB paths are set. "; + return false; } - - int32_t blkCheck = (int32_t)getTopBlockHeightInDB(BLKDATA); - - StoredHeader sbh; - uint32_t toSub = 0; - uint32_t nIter = 0; - do - { - blkCheck -= toSub; - if(blkCheck < 0) - { - blkCheck = 0; - break; - } - - iface_->getStoredHeader(sbh, (uint32_t)blkCheck); - - if(nIter++ < 10) - toSub += 1; // we get some N^2 action here (for the first 10 iter) - else - toSub = (uint32_t)(1.5*toSub); // after that, increase exponentially - - } while(!sbh.blockAppliedToDB_); - // We likely overshot in the last loop, so walk forward until we get to it. - do + if(iface_->databasesAreOpen()) { - iface_->getStoredHeader(sbh, (uint32_t)blkCheck); - blkCheck += 1; - } while(sbh.blockAppliedToDB_); + LOGERR << "Attempted to initialize a database that was already open"; + return false; + } - return (uint32_t)blkCheck; -} -//////////////////////////////////////////////////////////////////////////////// -uint32_t BlockDataManager_LevelDB::getTopBlockHeightInDB(DB_SELECT db) -{ - StoredDBInfo sdbi; - iface_->getStoredDBInfo(db, sdbi, false); - return sdbi.topBlkHgt_; -} + bool openWithErr = iface_->openDatabases(leveldbDir_, + GenesisHash_, + GenesisTxHash_, + MagicBytes_, + dbtype, + prtype); -//////////////////////////////////////////////////////////////////////////////// -uint32_t BlockDataManager_LevelDB::getAppliedToHeightInDB(void) -{ - StoredDBInfo sdbi; - iface_->getStoredDBInfo(BLKDATA, sdbi, false); - return sdbi.appliedToHgt_; + return openWithErr; } //////////////////////////////////////////////////////////////////////////////// -// The name of this function reflects that we are going to implement headers- -// first "verification." Rather, we are going to organize the chain of headers -// before we add any blocks, and then only add blocks that are on the main -// chain. Return false if these headers induced a reorg. -bool BlockDataManager_LevelDB::addHeadersFirst(BinaryDataRef rawHeader) +bool BlockDataManager_LevelDB::detectCurrentSyncState( + bool forceRebuild, + bool initialLoad) { - vector toAdd(1); - toAdd[0].unserialize(rawHeader); - return addHeadersFirst(toAdd); -} + // Make sure we detected all the available blk files + detectAllBlkFiles(); + vector firstHashes = getFirstHashOfEachBlkFile(); + LOGINFO << "Total blk*.dat files: " << numBlkFiles_; -//////////////////////////////////////////////////////////////////////////////// -// Add the headers to the DB, which is required before putting raw blocks. -// Can only put raw blocks when we know their height and dupID. After we -// put the headers, then we put raw blocks. Then we go through the valid -// headers and applyToDB the raw blocks. -bool BlockDataManager_LevelDB::addHeadersFirst(vector const & headVect) -{ - vector headersToDB; - headersToDB.reserve(headVect.size()); - for(uint32_t h=0; hdatabasesAreOpen()) { - pair bhInputPair; - pair::iterator, bool> bhInsResult; + LOGERR << "Could not open databases!"; + return false; + } - // Actually insert it. Take note of whether it was already there. - bhInputPair.second.unserialize(headVect[h].dataCopy_); - bhInputPair.first = bhInputPair.second.getThisHash(); - bhInsResult = headerMap_.insert(bhInputPair); - if(!bhInsResult.second) - bhInsResult.first->second = bhInputPair.second; + // We add 1 to each of these, since we always use exclusive upperbound + startHeaderHgt_ = getTopBlockHeightInDB(HEADERS) + 1; + startRawBlkHgt_ = getTopBlockHeightInDB(BLKDATA) + 1; + startApplyHgt_ = getAppliedToHeightInDB() + 1; - //if(bhInsResult.second) // true means didn't exist before - headersToDB.push_back(&(bhInsResult.first->second)); - } + // If the values were supposed to be zero, they'll get set to 1. Fix it + startHeaderHgt_ -= (startHeaderHgt_==1 ? 1 : 0); + startRawBlkHgt_ -= (startRawBlkHgt_==1 ? 1 : 0); + startApplyHgt_ -= (startApplyHgt_ ==1 ? 1 : 0); - // Organize the chain with the new headers, note whether a reorg occurred - bool prevTopBlockStillValid = organizeChain(); + LOGINFO << "Current Top block in HEADERS DB: " << startHeaderHgt_; + LOGINFO << "Current Top block in BLKDATA DB: " << startRawBlkHgt_; + LOGINFO << "Current Applied blocks up to hgt: " << startApplyHgt_; - // Add the main-branch headers to the DB. We will handle reorg ops later. - // The batching is safe since we never write multiple blocks at the same hgt - iface_->startBatch(HEADERS); - for(uint32_t h=0; hisMainBranch()) - continue; + if(forceRebuild) + LOGINFO << "Ignore existing sync state, rebuilding databases"; - StoredHeader sbh; - sbh.createFromBlockHeader(*headersToDB[h]); - uint8_t dup = iface_->putBareHeader(sbh); - headersToDB[h]->setDuplicateID(dup); + startHeaderHgt_ = 0; + startHeaderBlkFile_ = 0; + startHeaderOffset_ = 0; + startRawBlkHgt_ = 0; + startRawBlkFile_ = 0; + startRawOffset_ = 0; + startApplyHgt_ = 0; + startApplyBlkFile_ = 0; + startApplyOffset_ = 0; + headerMap_.clear(); + topBlockPtr_ = NULL; + genBlockPtr_ = NULL; + lastTopBlock_ = UINT32_MAX;; + return true; } - iface_->commitBatch(HEADERS); - // We need to add the non-main-branch headers, too. - for(uint32_t h=0; hisMainBranch()) - continue; - - StoredHeader sbh; - sbh.createFromBlockHeader(*headersToDB[h]); - uint8_t dup = iface_->putBareHeader(sbh); - headersToDB[h]->setDuplicateID(dup); + // If this isn't the initial load, we assume everything is sync'd + startHeaderBlkFile_= numBlkFiles_ - 1; + startHeaderOffset_ = endOfLastBlockByte_; + startRawBlkHgt_ = startHeaderHgt_; + startRawBlkFile_ = numBlkFiles_ - 1; + startRawOffset_ = endOfLastBlockByte_; + startApplyHgt_ = startHeaderHgt_; + startApplyBlkFile_ = numBlkFiles_ - 1; + startApplyOffset_ = endOfLastBlockByte_; + return true; } - return prevTopBlockStillValid; -} - + map sbhMap; + headerMap_.clear(); + iface_->readAllHeaders(headerMap_, sbhMap); + // Organize them into the longest chain + organizeChain(true); // true ~ force rebuild -///////////////////////////////////////////////////////////////////////////// -// The only way to "create" a BDM is with this method, which creates it -// if one doesn't exist yet, or returns a reference to the only one -// that will ever exist -BlockDataManager_LevelDB & BlockDataManager_LevelDB::GetInstance(void) -{ - if( !bdmCreatedYet_ ) + // If the headers DB ended up corrupted (triggered by organizeChain), + // then nuke and rebuild the headers + if(corruptHeadersDB_) { - theOnlyBDM_ = new BlockDataManager_LevelDB; - bdmCreatedYet_ = true; - iface_ = LevelDBWrapper::GetInterfacePtr(); + LOGERR << "Corrupted headers DB!"; + startHeaderHgt_ = 0; + startHeaderBlkFile_ = 0; + startHeaderOffset_ = 0; + startRawBlkHgt_ = 0; + startRawBlkFile_ = 0; + startRawOffset_ = 0; + startApplyHgt_ = 0; + startApplyBlkFile_ = 0; + startApplyOffset_ = 0; + headerMap_.clear(); + headersByHeight_.clear(); + topBlockPtr_ = NULL; + prevTopBlockPtr_ = NULL; + corruptHeadersDB_ = false; + lastTopBlock_ = UINT32_MAX; + genBlockPtr_ = NULL; + return true; } - return (*theOnlyBDM_); -} + else + { + // Now go through the linear list of main-chain headers, mark valid + for(uint32_t i=0; igetThisHashRef(); + StoredHeader & sbh = sbhMap[headHash]; + sbh.isMainBranch_ = true; + iface_->setValidDupIDForHeight(sbh.blockHeight_, sbh.duplicateID_); + } + // startHeaderBlkFile_/Offset_ is where we were before the last shutdown + for(startHeaderBlkFile_ = 0; + startHeaderBlkFile_ < firstHashes.size(); + startHeaderBlkFile_++) + { + // hasHeaderWithHash is probing the RAM block headers we just organized + if(!hasHeaderWithHash(firstHashes[startHeaderBlkFile_])) + break; + } -///////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::DestroyInstance(void) -{ - theOnlyBDM_->Reset(); - iface_->closeDatabases(); - delete theOnlyBDM_; - bdmCreatedYet_ = false; - iface_ = NULL; -} + // If no new blkfiles since last load, the above loop ends w/o "break" + // If it's zero, then we don't have anything, start at zero + // If new blk file, then startHeaderBlkFile_ is at the first blk file + // with an unrecognized hash... we must've left off in the prev blkfile + if(startHeaderBlkFile_ > 0) + startHeaderBlkFile_--; -///////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::Reset(void) -{ - SCOPED_TIMER("BDM::Reset"); - - // Clear out all the "real" data in the blkfile - blkFileDir_ = ""; - headerMap_.clear(); - - zeroConfRawTxList_.clear(); - zeroConfMap_.clear(); - zcEnabled_ = false; - zcFilename_ = ""; - - isNetParamsSet_ = false; - isBlkParamsSet_ = false; - isLevelDBSet_ = false; - armoryHomeDir_ = string(""); - blkFileDir_ = string(""); - blkFileList_.clear(); - numBlkFiles_ = UINT32_MAX; - - dbUpdateSize_ = 0; - endOfLastBlockByte_ = 0; - - startHeaderHgt_ = 0; - startRawBlkHgt_ = 0; - startApplyHgt_ = 0; - startHeaderBlkFile_ = 0; - startHeaderOffset_ = 0; - startRawBlkFile_ = 0; - startRawOffset_ = 0; - startApplyBlkFile_ = 0; - startApplyOffset_ = 0; + startHeaderOffset_ = findOffsetFirstUnrecognized(startHeaderBlkFile_); + } + LOGINFO << "First unrecognized hash file: " << startHeaderBlkFile_; + LOGINFO << "Offset of first unrecog block: " << startHeaderOffset_; - // These should be set after the blockchain is organized - headersByHeight_.clear(); - topBlockPtr_ = NULL; - genBlockPtr_ = NULL; - lastTopBlock_ = UINT32_MAX;; - // Reorganization details - lastBlockWasReorg_ = false; - reorgBranchPoint_ = NULL; - txJustInvalidated_.clear(); - txJustAffected_.clear(); + // Note that startRawBlkHgt_ is topBlk+1, so this return where we should + // actually start processing raw blocks, not the last one we processed + pair rawBlockLoc; + rawBlockLoc = findFileAndOffsetForHgt(startRawBlkHgt_, &firstHashes); + startRawBlkFile_ = rawBlockLoc.first; + startRawOffset_ = rawBlockLoc.second; + LOGINFO << "First blkfile not in DB: " << startRawBlkFile_; + LOGINFO << "Location of first block not in DB: " << startRawOffset_; - // Reset orphan chains - previouslyValidBlockHeaderPtrs_.clear(); - orphanChainStartBlocks_.clear(); + if(DBUtils.getArmoryDbType() != ARMORY_DB_BARE) + { + // TODO: finish this + findFirstUnappliedBlock(); + LOGINFO << "Blkfile of first unapplied block: " << startApplyBlkFile_; + LOGINFO << "Location of first unapplied block: " << startApplyOffset_; + } - GenesisHash_.resize(0); - GenesisTxHash_.resize(0); - MagicBytes_.resize(0); - - totalBlockchainBytes_ = 0; - bytesReadSoFar_ = 0; - blocksReadSoFar_ = 0; - filesReadSoFar_ = 0; - isInitialized_ = false; - corruptHeadersDB_ = false; + // If we're content here, just return + return true; - // Clear out any of the registered tx data we have collected so far. - // Doesn't take any time to recollect if it we have to rescan, anyway. + /* - registeredWallets_.clear(); - registeredScrAddrMap_.clear(); - registeredTxSet_.clear(); - registeredTxList_.clear(); - registeredOutPoints_.clear(); - allScannedUpToBlk_ = 0; + // If we want to replay some blocks, we need to adjust startScanBlkFile_ + // and startScanOffset_ to be approx "replayNBytes" behind where + // they are currently set. + int32_t targOffset = (int32_t)startScanOffset_ - (int32_t)replayNBytes; + if(targOffset > 0 || startScanBlkFile_==0) + { + targOffset = max(0, targOffset); + startScanOffset_ = findFirstBlkApproxOffset(startScanBlkFile_, targOffset); + } + else + { + startScanBlkFile_--; + uint32_t prevFileSize = BtcUtils::GetFileSize(blkFileList_[startScanBlkFile_]); + targOffset = (int32_t)prevFileSize - (int32_t)replayNBytes; + targOffset = max(0, targOffset); + startScanOffset_ = findFirstBlkApproxOffset(startScanBlkFile_, targOffset); + } + LOGINFO << "Rewinding start block to enforce DB integrity"; + LOGINFO << "Start at blockfile: " << startScanBlkFile_; + LOGINFO << "Start location in above blkfile: " << startScanOffset_; + return true; + */ } - -///////////////////////////////////////////////////////////////////////////// -int32_t BlockDataManager_LevelDB::getNumConfirmations(HashString txHash) +//////////////////////////////////////////////////////////////////////////////// +vector BlockDataManager_LevelDB::getFirstHashOfEachBlkFile(void) const { - TxRef txrefobj = getTxRefByHash(txHash); - if(txrefobj.isNull()) - return TX_NOT_EXIST; - else + if(!isBlkParamsSet_) { - BlockHeader* bhptr = getHeaderPtrForTxRef(txrefobj); - if(bhptr == NULL) - return TX_0_UNCONFIRMED; - else - { - BlockHeader & txbh = *bhptr; - if(!txbh.isMainBranch()) - return TX_OFF_MAIN_BRANCH; + LOGERR << "Can't get blk files until blkfile params are set"; + return vector(0); + } - int32_t txBlockHeight = txbh.getBlockHeight(); - int32_t topBlockHeight = getTopBlockHeight(); - return topBlockHeight - txBlockHeight + 1; + uint32_t nFile = (uint32_t)blkFileList_.size(); + BinaryData magic(4), szstr(4), rawHead(HEADER_SIZE); + vector headHashes(nFile); + for(uint32_t f=0; f(0); } + + is.read((char*)rawHead.getPtr(), HEADER_SIZE); + headHashes[f] = BinaryData(32); + BtcUtils::getHash256(rawHead, headHashes[f]); + is.close(); } + return headHashes; } - -///////////////////////////////////////////////////////////////////////////// -BlockHeader & BlockDataManager_LevelDB::getTopBlockHeader(void) +//////////////////////////////////////////////////////////////////////////////// +uint32_t BlockDataManager_LevelDB::findOffsetFirstUnrecognized(uint32_t fnum) { - if(topBlockPtr_ == NULL) - topBlockPtr_ = &(getGenesisBlock()); - return *topBlockPtr_; -} + uint32_t loc = 0; + BinaryData magic(4), szstr(4), rawHead(80), hashResult(32); -///////////////////////////////////////////////////////////////////////////// -BlockHeader & BlockDataManager_LevelDB::getGenesisBlock(void) -{ - if(genBlockPtr_ == NULL) - genBlockPtr_ = &(headerMap_[GenesisHash_]); - return *genBlockPtr_; -} + ifstream is(blkFileList_[fnum].c_str(), ios::in|ios::binary); + while(!is.eof()) + { + is.read((char*)magic.getPtr(), 4); + if(is.eof()) break; -///////////////////////////////////////////////////////////////////////////// -// Get a blockheader based on its height on the main chain -BlockHeader * BlockDataManager_LevelDB::getHeaderByHeight(int index) -{ - if( index<0 || index>=(int)headersByHeight_.size()) - return NULL; - else - return headersByHeight_[index]; -} + + // This is not an error, it just simply hit the padding + if(magic!=MagicBytes_) + break; + is.read((char*)szstr.getPtr(), 4); + uint32_t blksize = READ_UINT32_LE(szstr.getPtr()); + if(is.eof()) break; -///////////////////////////////////////////////////////////////////////////// -// The most common access method is to get a block by its hash -BlockHeader * BlockDataManager_LevelDB::getHeaderByHash(HashString const & blkHash) -{ - map::iterator it = headerMap_.find(blkHash); - //if(it==headerMap_.end()) - if(ITER_NOT_IN_MAP(it, headerMap_)) - return NULL; - else - return &(it->second); -} + is.read((char*)rawHead.getPtr(), HEADER_SIZE); + BtcUtils::getHash256_NoSafetyCheck(rawHead.getPtr(), HEADER_SIZE, hashResult); + if(getHeaderByHash(hashResult) == NULL) + break; // first hash in the file that isn't in our header map + loc += blksize + 8; + is.seekg(blksize - HEADER_SIZE, ios::cur); -///////////////////////////////////////////////////////////////////////////// -TxRef BlockDataManager_LevelDB::getTxRefByHash(HashString const & txhash) -{ - return iface_->getTxRef(txhash); + } + + is.close(); + return loc; } - -///////////////////////////////////////////////////////////////////////////// -Tx BlockDataManager_LevelDB::getTxByHash(HashString const & txhash) +//////////////////////////////////////////////////////////////////////////////// +uint32_t BlockDataManager_LevelDB::findFirstBlkApproxOffset(uint32_t fnum, + uint32_t offset) const { - - TxRef txrefobj = getTxRefByHash(txhash); - - if(!txrefobj.isNull()) - return txrefobj.getTxCopy(); - else + if(fnum >= numBlkFiles_) { - // It's not in the blockchain, but maybe in the zero-conf tx list - map::const_iterator iter = zeroConfMap_.find(txhash); - //if(iter==zeroConfMap_.end()) - if(ITER_NOT_IN_MAP(iter, zeroConfMap_)) - return Tx(); - else - return iter->second.txobj_; + LOGERR << "Blkfile number out of range! (" << fnum << ")"; + return UINT32_MAX; } -} - -///////////////////////////////////////////////////////////////////////////// -TX_AVAILABILITY BlockDataManager_LevelDB::getTxHashAvail(BinaryDataRef txHash) -{ - if(getTxRefByHash(txHash).isNull()) + uint32_t loc = 0; + BinaryData magic(4), szstr(4), rawHead(80), hashResult(32); + ifstream is(blkFileList_[fnum].c_str(), ios::in|ios::binary); + while(!is.eof() && loc <= offset) { - //if(zeroConfMap_.find(txHash)==zeroConfMap_.end()) - if(KEY_NOT_IN_MAP(txHash, zeroConfMap_)) - return TX_DNE; // No tx at all - else - return TX_ZEROCONF; // Zero-conf tx - } - else - return TX_IN_BLOCKCHAIN; // In the blockchain already -} + is.read((char*)magic.getPtr(), 4); + if(is.eof()) break; + if(magic!=MagicBytes_) + return UINT32_MAX; -///////////////////////////////////////////////////////////////////////////// -bool BlockDataManager_LevelDB::hasTxWithHashInDB(BinaryData const & txHash) -{ - return iface_->getTxRef(txHash).isInitialized(); -} + is.read((char*)szstr.getPtr(), 4); + uint32_t blksize = READ_UINT32_LE(szstr.getPtr()); + if(is.eof()) break; -///////////////////////////////////////////////////////////////////////////// -bool BlockDataManager_LevelDB::hasTxWithHash(BinaryData const & txHash) -{ - if(iface_->getTxRef(txHash).isInitialized()) - return true; - else - return KEY_IN_MAP(txHash, zeroConfMap_); -} + loc += blksize + 8; + is.seekg(blksize, ios::cur); + } -///////////////////////////////////////////////////////////////////////////// -bool BlockDataManager_LevelDB::hasHeaderWithHash(BinaryData const & txHash) const -{ - //return (headerMap_.find(txHash) != headerMap_.end()); - return KEY_IN_MAP(txHash, headerMap_); + is.close(); + return loc; } -///////////////////////////////////////////////////////////////////////////// -/* -vector BlockDataManager_LevelDB::prefixSearchHeaders(BinaryData const & searchStr) +//////////////////////////////////////////////////////////////////////////////// +pair BlockDataManager_LevelDB::findFileAndOffsetForHgt( + uint32_t hgt, + vector * firstHashes) { - vector outList(0); - uint32_t lenSearch = searchStr.getSize(); - if(lenSearch < 2) - return outList; // don't search unless we have at least two bytes - - BinaryData searchLow(32); - BinaryData searchHigh(32); - for(uint32_t i=0; i recomputedHashes; + if(firstHashes==NULL) { - searchLow[i] = 0; - searchHigh[i] = 255; + recomputedHashes = getFirstHashOfEachBlkFile(); + firstHashes = &recomputedHashes; } - map::iterator iter; - for(iter = headerMap_.lower_bound(searchLow); - iter != headerMap_.upper_bound(searchHigh); - iter++) + pair outPair; + int32_t blkfile; + for(blkfile = 0; blkfile < (int32_t)firstHashes->size(); blkfile++) { - outList.push_back(&(iter->second)); - } - return outList; -} -*/ - -///////////////////////////////////////////////////////////////////////////// -/* -vector BlockDataManager_LevelDB::prefixSearchTx(BinaryData const & searchStr) -{ - vector outList(0); - uint32_t lenSearch = searchStr.getSize(); - if(lenSearch < 2) - return outList; // don't search unless we have at least two bytes + BlockHeader * bhptr = getHeaderByHash((*firstHashes)[blkfile]); + if(bhptr == NULL) + break; - BinaryData searchLow(32); - BinaryData searchHigh(32); - for(uint32_t i=0; igetBlockHeight() > hgt) + break; } - BinaryData searchLow4 = searchLow.getSliceCopy(0,4); - BinaryData searchHigh4 = searchHigh.getSliceCopy(0,4); - multimap::iterator iter; - for(iter = txHintMap_.lower_bound(searchLow4); - iter != txHintMap_.upper_bound(searchHigh4); - iter++) + blkfile = max(blkfile-1, 0); + if(blkfile >= (int32_t)numBlkFiles_) { - if(iter->second.getThisHash().startsWith(searchStr)) - outList.push_back(&(iter->second)); + LOGERR << "Blkfile number out of range! (" << blkfile << ")"; + return outPair; } - return outList; -} - -///////////////////////////////////////////////////////////////////////////// -// Since the cpp code doesn't have full addresses (only 20-byte hashes), -// that's all we can search for. -vector BlockDataManager_LevelDB::prefixSearchAddress(BinaryData const & searchStr) -{ - // Actually, we can't even search for this, because we don't have a list - // of addresses in the blockchain. We could construct one, but it would - // take up a lot of RAM (and time)... I will need to create a separate - // call to allow the caller to create a set of addresses - // before calling this method - return vector(0); -} -*/ - + uint32_t loc = 0; + BinaryData magic(4), szstr(4), rawHead(HEADER_SIZE), hashResult(32); + ifstream is(blkFileList_[blkfile].c_str(), ios::in|ios::binary); + while(!is.eof()) + { + is.read((char*)magic.getPtr(), 4); + if(is.eof()) break; + if(magic!=MagicBytes_) + break; + is.read((char*)szstr.getPtr(), 4); + uint32_t blksize = READ_UINT32_LE(szstr.getPtr()); + if(is.eof()) break; + is.read((char*)rawHead.getPtr(), HEADER_SIZE); + BtcUtils::getHash256_NoSafetyCheck(rawHead.getPtr(), + HEADER_SIZE, + hashResult); -///////////////////////////////////////////////////////////////////////////// -bool BlockDataManager_LevelDB::registerWallet(BtcWallet* wltPtr, bool wltIsNew) -{ - SCOPED_TIMER("registerWallet"); + BlockHeader * bhptr = getHeaderByHash(hashResult); + if(bhptr == NULL) + break; - // Check if the wallet is already registered - //if(registeredWallets_.find(wltPtr) != registeredWallets_.end()) - if(KEY_IN_MAP(wltPtr, registeredWallets_)) - return false; + if(bhptr->getBlockHeight() >= hgt) + break; - // Add it to the list of wallets to watch - registeredWallets_.insert(wltPtr); + loc += blksize + 8; + is.seekg(blksize - HEADER_SIZE, ios::cur); + } - // Now add all the individual addresses from the wallet - for(uint32_t i=0; igetNumScrAddr(); i++) - { - // If this is a new wallet, the value of getFirstBlockNum is irrelevant - ScrAddrObj & addr = wltPtr->getScrAddrObjByIndex(i); + is.close(); - if(wltIsNew) - registerNewScrAddr(addr.getScrAddr()); - else - registerImportedScrAddr(addr.getScrAddr(), addr.getFirstBlockNum()); - } + outPair.first = blkfile; + outPair.second = loc; + + return outPair; + - // We need to make sure the wallet can tell the BDM when an address is added - wltPtr->setBdmPtr(this); - return true; } -///////////////////////////////////////////////////////////////////////////// -bool BlockDataManager_LevelDB::registerScrAddr(HashString scraddr, - bool addrIsNew, - uint32_t firstBlk) +//////////////////////////////////////////////////////////////////////////////// +// This behaves very much like the algorithm for finding the branch point +// in the header tree with a peer. +uint32_t BlockDataManager_LevelDB::findFirstUnappliedBlock(void) { - SCOPED_TIMER("registerScrAddr"); - if(KEY_IN_MAP(scraddr, registeredScrAddrMap_)) + SCOPED_TIMER("findFirstUnappliedBlock"); + + if(!iface_->databasesAreOpen()) { - // Address is already registered. Don't think there's anything to do - return false; + LOGERR << "Database is not open!"; + return UINT32_MAX; } + + int32_t blkCheck = (int32_t)getTopBlockHeightInDB(BLKDATA); - if(addrIsNew) - firstBlk = getTopBlockHeight() + 1; + StoredHeader sbh; + uint32_t toSub = 0; + uint32_t nIter = 0; + do + { + blkCheck -= toSub; + if(blkCheck < 0) + { + blkCheck = 0; + break; + } - registeredScrAddrMap_[scraddr] = RegisteredScrAddr(scraddr, firstBlk); - allScannedUpToBlk_ = min(firstBlk, allScannedUpToBlk_); - return true; -} + iface_->getStoredHeader(sbh, (uint32_t)blkCheck); + if(nIter++ < 10) + toSub += 1; // we get some N^2 action here (for the first 10 iter) + else + toSub = (uint32_t)(1.5*toSub); // after that, increase exponentially -///////////////////////////////////////////////////////////////////////////// -bool BlockDataManager_LevelDB::registerNewScrAddr(HashString scraddr) -{ - SCOPED_TIMER("registerNewScrAddr"); - if(KEY_IN_MAP(scraddr, registeredScrAddrMap_)) - return false; + } while(!sbh.blockAppliedToDB_); - uint32_t currBlk = getTopBlockHeight(); - registeredScrAddrMap_[scraddr] = RegisteredScrAddr(scraddr, currBlk); + // We likely overshot in the last loop, so walk forward until we get to it. + do + { + iface_->getStoredHeader(sbh, (uint32_t)blkCheck); + blkCheck += 1; + } while(sbh.blockAppliedToDB_); - // New address cannot affect allScannedUpToBlk_, so don't bother - //allScannedUpToBlk_ = min(currBlk, allScannedUpToBlk_); - return true; + return (uint32_t)blkCheck; } -///////////////////////////////////////////////////////////////////////////// -bool BlockDataManager_LevelDB::registerImportedScrAddr(HashString scraddr, - uint32_t createBlk) +//////////////////////////////////////////////////////////////////////////////// +uint32_t BlockDataManager_LevelDB::getTopBlockHeightInDB(DB_SELECT db) { - SCOPED_TIMER("registerImportedScrAddr"); - if(KEY_IN_MAP(scraddr, registeredScrAddrMap_)) - return false; + StoredDBInfo sdbi; + iface_->getStoredDBInfo(db, sdbi, false); + return sdbi.topBlkHgt_; +} - // In some cases we may have used UINT32_MAX to specify "don't know" - if(createBlk==UINT32_MAX) - createBlk = 0; - - registeredScrAddrMap_[scraddr] = RegisteredScrAddr(scraddr, createBlk); - allScannedUpToBlk_ = min(createBlk, allScannedUpToBlk_); - return true; +//////////////////////////////////////////////////////////////////////////////// +uint32_t BlockDataManager_LevelDB::getAppliedToHeightInDB(void) +{ + StoredDBInfo sdbi; + iface_->getStoredDBInfo(BLKDATA, sdbi, false); + return sdbi.appliedToHgt_; } - -///////////////////////////////////////////////////////////////////////////// -bool BlockDataManager_LevelDB::unregisterScrAddr(HashString scraddr) +//////////////////////////////////////////////////////////////////////////////// +// The name of this function reflects that we are going to implement headers- +// first "verification." Rather, we are going to organize the chain of headers +// before we add any blocks, and then only add blocks that are on the main +// chain. Return false if these headers induced a reorg. +bool BlockDataManager_LevelDB::addHeadersFirst(BinaryDataRef rawHeader) { - SCOPED_TIMER("unregisterScrAddr"); - //if(registeredScrAddrMap_.find(scraddr) == registeredScrAddrMap_.end()) - if(KEY_IN_MAP(scraddr, registeredScrAddrMap_)) - return false; - - registeredScrAddrMap_.erase(scraddr); - allScannedUpToBlk_ = evalLowestBlockNextScan(); - return true; + vector toAdd(1); + toAdd[0].unserialize(rawHeader); + return addHeadersFirst(toAdd); } - -///////////////////////////////////////////////////////////////////////////// -BtcWallet::~BtcWallet(void) +//////////////////////////////////////////////////////////////////////////////// +// Add the headers to the DB, which is required before putting raw blocks. +// Can only put raw blocks when we know their height and dupID. After we +// put the headers, then we put raw blocks. Then we go through the valid +// headers and applyToDB the raw blocks. +bool BlockDataManager_LevelDB::addHeadersFirst(vector const & headVect) { - if(bdmPtr_!=NULL) - bdmPtr_->unregisterWallet(this); -} + vector headersToDB; + headersToDB.reserve(headVect.size()); + for(uint32_t h=0; h bhInputPair; + pair::iterator, bool> bhInsResult; + // Actually insert it. Take note of whether it was already there. + bhInputPair.second.unserialize(headVect[h].dataCopy_); + bhInputPair.first = bhInputPair.second.getThisHash(); + bhInsResult = headerMap_.insert(bhInputPair); + if(!bhInsResult.second) + bhInsResult.first->second = bhInputPair.second; -///////////////////////////////////////////////////////////////////////////// -uint32_t BlockDataManager_LevelDB::evalLowestBlockNextScan(void) -{ - SCOPED_TIMER("evalLowestBlockNextScan"); + //if(bhInsResult.second) // true means didn't exist before + headersToDB.push_back(&(bhInsResult.first->second)); + } - uint32_t lowestBlk = UINT32_MAX; - uint32_t i=0; - map::iterator rsaIter; - for(rsaIter = registeredScrAddrMap_.begin(); - rsaIter != registeredScrAddrMap_.end(); - rsaIter++) + // Organize the chain with the new headers, note whether a reorg occurred + bool prevTopBlockStillValid = organizeChain(); + + // Add the main-branch headers to the DB. We will handle reorg ops later. + // The batching is safe since we never write multiple blocks at the same hgt + iface_->startBatch(HEADERS); + for(uint32_t h=0; hsecond.alreadyScannedUpToBlk_); - } - return lowestBlk; -} + if(!headersToDB[h]->isMainBranch()) + continue; -///////////////////////////////////////////////////////////////////////////// -// This method isn't really used yet... -uint32_t BlockDataManager_LevelDB::evalLowestScrAddrCreationBlock(void) -{ - SCOPED_TIMER("evalLowestAddressCreationBlock"); + StoredHeader sbh; + sbh.createFromBlockHeader(*headersToDB[h]); + uint8_t dup = iface_->putBareHeader(sbh); + headersToDB[h]->setDuplicateID(dup); + } + iface_->commitBatch(HEADERS); - uint32_t lowestBlk = UINT32_MAX; - map::iterator rsaIter; - for(rsaIter = registeredScrAddrMap_.begin(); - rsaIter != registeredScrAddrMap_.end(); - rsaIter++) + // We need to add the non-main-branch headers, too. + for(uint32_t h=0; hsecond.blkCreated_); + if(headersToDB[h]->isMainBranch()) + continue; + + StoredHeader sbh; + sbh.createFromBlockHeader(*headersToDB[h]); + uint8_t dup = iface_->putBareHeader(sbh); + headersToDB[h]->setDuplicateID(dup); } - return lowestBlk; + return prevTopBlockStillValid; } -///////////////////////////////////////////////////////////////////////////// -bool BlockDataManager_LevelDB::evalRescanIsRequired(void) -{ - // This method tells us whether we have to scan ANY blocks from disk - // in order to produce accurate balances and TxOut lists. If this - // returns false, we can get away without any disk access at all, and - // just use the registeredTxList_ object to get our information. - allScannedUpToBlk_ = evalLowestBlockNextScan(); - return (allScannedUpToBlk_ < getTopBlockHeight()+1); -} -///////////////////////////////////////////////////////////////////////////// -// This method needs to be callable from another thread. Therefore, I don't -// seek an exact answer, instead just estimate it based on the last block, -// and the set of currently-registered addresses. The method called -// "evalRescanIsRequired()" answers a different question, and iterates -// through the list of registered addresses, which may be changing in -// another thread. -bool BlockDataManager_LevelDB::isDirty( - uint32_t numBlocksToBeConsideredDirty ) const -{ - if(!isInitialized_) - return false; - - uint32_t numBlocksBehind = lastTopBlock_-allScannedUpToBlk_; - return (numBlocksBehind > numBlocksToBeConsideredDirty); - -} + ///////////////////////////////////////////////////////////////////////////// -uint32_t BlockDataManager_LevelDB::numBlocksToRescan( BtcWallet & wlt, - uint32_t endBlk) +// The only way to "create" a BDM is with this method, which creates it +// if one doesn't exist yet, or returns a reference to the only one +// that will ever exist +BlockDataManager_LevelDB & BlockDataManager_LevelDB::GetInstance(void) { - SCOPED_TIMER("numBlocksToRescan"); - // This method tells us whether we have to scan ANY blocks from disk - // in order to produce accurate balances and TxOut lists. If this - // returns false, we can get away without any disk access at all, and - // just use the registeredTxList_ object to get our information. - uint32_t currNextBlk = getTopBlockHeight() + 1; - endBlk = min(endBlk, currNextBlk); - - // If wallet is registered and current, no rescan necessary - if(walletIsRegistered(wlt)) - return (endBlk - allScannedUpToBlk_); - - // The wallet isn't registered with the BDM, but there's a chance that - // each of its addresses are -- if any one is not, do rescan - uint32_t maxAddrBehind = 0; - for(uint32_t i=0; i::iterator rsaIter; - for(rsaIter = registeredScrAddrMap_.begin(); - rsaIter != registeredScrAddrMap_.end(); - rsaIter++) - { - rsaIter->second.alreadyScannedUpToBlk_ = newTopBlk; - } - + theOnlyBDM_->Reset(); + iface_->closeDatabases(); + delete theOnlyBDM_; + bdmCreatedYet_ = false; + iface_ = NULL; } -//////////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::resetRegisteredWallets(void) +///////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::Reset(void) { - SCOPED_TIMER("resetRegisteredWallets"); + SCOPED_TIMER("BDM::Reset"); - set::iterator wltPtrIter; - for(wltPtrIter = registeredWallets_.begin(); - wltPtrIter != registeredWallets_.end(); - wltPtrIter++) - { - // I'm not sure if there's anything else to do - // I think it's all encapsulated in this call! - (*wltPtrIter)->clearBlkData(); - } + // Clear out all the "real" data in the blkfile + blkFileDir_ = ""; + headerMap_.clear(); - // Reset all addresses to "new" - updateRegisteredScrAddrs(0); -} + zeroConfRawTxList_.clear(); + zeroConfMap_.clear(); + zcEnabled_ = false; + zcLiteMode_ = false; + zcFilename_ = ""; -///////////////////////////////////////////////////////////////////////////// -bool BlockDataManager_LevelDB::walletIsRegistered(BtcWallet & wlt) -{ - //return (registeredWallets_.find(&wlt)!=registeredWallets_.end()); - return KEY_IN_MAP(&wlt, registeredWallets_); -} + isNetParamsSet_ = false; + isBlkParamsSet_ = false; + isLevelDBSet_ = false; + armoryHomeDir_ = string(""); + blkFileDir_ = string(""); + blkFileList_.clear(); + numBlkFiles_ = UINT32_MAX; -///////////////////////////////////////////////////////////////////////////// -bool BlockDataManager_LevelDB::scrAddrIsRegistered(HashString scraddr) -{ - //return (registeredScrAddrMap_.find(scraddr)!=registeredScrAddrMap_.end()); - return KEY_IN_MAP(scraddr, registeredScrAddrMap_); -} + endOfLastBlockByte_ = 0; + startHeaderHgt_ = 0; + startRawBlkHgt_ = 0; + startApplyHgt_ = 0; + startHeaderBlkFile_ = 0; + startHeaderOffset_ = 0; + startRawBlkFile_ = 0; + startRawOffset_ = 0; + startApplyBlkFile_ = 0; + startApplyOffset_ = 0; -///////////////////////////////////////////////////////////////////////////// -// This method is now a hybrid of the original, Blockchain-in-RAM code, -// and the new mmap()-based blockchain operations. The initial blockchain -// scan will look for wallet-relevant transactions, and keep track of what -// blocks still need to be scanned given the registered wallets -// -// Therefore, when we scan, we will first scan the registered tx list, -// then any raw blocks that haven't been filtered yet, then all the -// zero-conf tx list. -// -// If the wallet contains any addresses that are not part of the prefiltered -// tx-hash-list, then the entire blockchain will have to be rescanned, anyway. -// It doesn't take any less time to search for one address than it does -// all of them. -// -// -// Some new notes on this method ... -// We will ONLY scan transactions from the registeredTxList_ -// -// Therefore, we need to make sure that registeredTxList_ is -// completely up-to-date FIRST. -// -// Then we can sort it and scanTx all of them with the wallet. -// -// We need to scan from blocks X-->Y. Assume X is 1000 and Y is 2000 -// If allScannedUpToBlk_==1500: -// -// registeredScrAddrScan from 1500-->2000 -// sort registered list -// scanTx all tx in registered list between 1000 and 2000 -void BlockDataManager_LevelDB::scanBlockchainForTx(BtcWallet & myWallet, - uint32_t startBlknum, - uint32_t endBlknum, - bool fetchFirst) -{ - SCOPED_TIMER("scanBlockchainForTx"); + // These should be set after the blockchain is organized + headersByHeight_.clear(); + topBlockPtr_ = NULL; + genBlockPtr_ = NULL; + lastTopBlock_ = UINT32_MAX;; - // TODO: We should implement selective fetching! (i.e. only fetch - // and register scraddr data that is between those two blocks). - // At the moment, it is - if(fetchFirst && DBUtils.getArmoryDbType()!=ARMORY_DB_BARE) - fetchAllRegisteredScrAddrData(myWallet); + // Reorganization details + lastBlockWasReorg_ = false; + reorgBranchPoint_ = NULL; + txJustInvalidated_.clear(); + txJustAffected_.clear(); - // The BDM knows the highest block to which ALL CURRENT REGISTERED ADDRESSES - // are up-to-date in the registeredTxList_ list. - // If this wallet is not registered, it needs to be, before we start - if(!walletIsRegistered(myWallet)) - registerWallet( &myWallet ); + // Reset orphan chains + previouslyValidBlockHeaderPtrs_.clear(); + orphanChainStartBlocks_.clear(); + GenesisHash_.resize(0); + GenesisTxHash_.resize(0); + MagicBytes_.resize(0); - // Check whether we can get everything we need from the registered tx list - endBlknum = min(endBlknum, getTopBlockHeight()+1); - uint32_t numRescan = numBlocksToRescan(myWallet, endBlknum); - - - // This is the part that might take a while... - //applyBlockRangeToDB(allScannedUpToBlk_, endBlknum); - scanDBForRegisteredTx(allScannedUpToBlk_, endBlknum); + totalBlockchainBytes_ = 0; + bytesReadSoFar_ = 0; + blocksReadSoFar_ = 0; + filesReadSoFar_ = 0; - allScannedUpToBlk_ = endBlknum; - updateRegisteredScrAddrs(endBlknum); + isInitialized_ = false; + corruptHeadersDB_ = false; + // Clear out any of the registered tx data we have collected so far. + // Doesn't take any time to recollect if it we have to rescan, anyway. - // *********************************************************************** // - // Finally, walk through all the registered tx - scanRegisteredTxForWallet(myWallet, startBlknum, endBlknum); + registeredWallets_.clear(); + registeredScrAddrMap_.clear(); + registeredTxSet_.clear(); + registeredTxList_.clear(); + registeredOutPoints_.clear(); + allScannedUpToBlk_ = 0; - // I think these lines of code where causing the serious peformance issues - // so they were commented out and don't appear to be needed - // if(zcEnabled_) - // rescanWalletZeroConf(myWallet); } + ///////////////////////////////////////////////////////////////////////////// -// This used to be "rescanBlocks", but now "scanning" has been replaced by -// "reapplying" the blockdata to the databases. Basically assumes that only -// raw blockdata is stored in the DB with no SSH objects. This goes through -// and processes every Tx, creating new SSHs if not there, and creating and -// marking-spent new TxOuts. -void BlockDataManager_LevelDB::applyBlockRangeToDB(uint32_t blk0, uint32_t blk1) +int32_t BlockDataManager_LevelDB::getNumConfirmations(HashString txHash) { - SCOPED_TIMER("applyBlockRangeToDB"); - - blk1 = min(blk1, getTopBlockHeight()+1); - - BinaryData startKey = DBUtils.getBlkDataKey(blk0, 0); - BinaryData endKey = DBUtils.getBlkDataKey(blk1, 0); - iface_->seekTo(BLKDATA, startKey); - - // Start scanning and timer - //bool doBatches = (blk1-blk0 > NUM_BLKS_BATCH_THRESH); - bool doBatches = true; - map stxToModify; - map sshToModify; - set keysToDelete; - - uint32_t hgt; - uint8_t dup; - do + TxRef txrefobj = getTxRefByHash(txHash); + if(txrefobj.isNull()) + return TX_NOT_EXIST; + else { - - StoredHeader sbh; - iface_->readStoredBlockAtIter(sbh); - hgt = sbh.blockHeight_; - dup = sbh.duplicateID_; - if(blk0 > hgt || hgt >= blk1) - break; - - - if(hgt%2500 == 2499) - LOGWARN << "Finished applying blocks up to " << (hgt+1); - - if(dup != iface_->getValidDupIDForHeight(hgt)) - continue; - - // Ugh! Design inefficiency: this loop and applyToBlockDB both use - // the same iterator, which means that applyBlockToDB will usually - // leave us with the iterator in a different place than we started. - // I'm not clear how inefficient it is to keep re-seeking (given that - // there's a lot of caching going on under-the-hood). It may be better - // to have each method create its own iterator... TODO: profile/test - // this idea. For now we will just save the current DB key, and - // re-seek to it afterwards. - BinaryData prevIterKey(0); - if(iface_->dbIterIsValid(BLKDATA)) - prevIterKey = iface_->getIterKeyCopy(); - - if(!doBatches) - applyBlockToDB(hgt, dup); + BlockHeader* bhptr = getHeaderPtrForTxRef(txrefobj); + if(bhptr == NULL) + return TX_0_UNCONFIRMED; else - { - bool commit = (dbUpdateSize_ > UPDATE_BYTES_THRESH); - if(commit) - LOGINFO << "Flushing DB cache after this block: " << hgt; - applyBlockToDB(hgt, dup, stxToModify, sshToModify, keysToDelete, commit); + { + BlockHeader & txbh = *bhptr; + if(!txbh.isMainBranch()) + return TX_OFF_MAIN_BRANCH; + + int32_t txBlockHeight = txbh.getBlockHeight(); + int32_t topBlockHeight = getTopBlockHeight(); + return topBlockHeight - txBlockHeight + 1; } + } +} - // If we had a valid iter position before applyBlockToDB, restore it - if(prevIterKey.getSize() > 0) - iface_->seekTo(BLKDATA, prevIterKey); +///////////////////////////////////////////////////////////////////////////// +BlockHeader & BlockDataManager_LevelDB::getTopBlockHeader(void) +{ + if(topBlockPtr_ == NULL) + topBlockPtr_ = &(getGenesisBlock()); + return *topBlockPtr_; +} - bytesReadSoFar_ += sbh.numBytes_; +///////////////////////////////////////////////////////////////////////////// +BlockHeader & BlockDataManager_LevelDB::getGenesisBlock(void) +{ + if(genBlockPtr_ == NULL) + genBlockPtr_ = &(headerMap_[GenesisHash_]); + return *genBlockPtr_; +} - // TODO: Check whether this is needed and if there is a performance - // improvement to removing it. For now, I'm including to be - // absolutely sure that the DB updates properly (not reading - // from an iterator that was created before the DB was last - // updated). But it may slow down the process considerably, - // since LevelDB has optimized the hell out of key-order - // traversal. - iface_->resetIterator(BLKDATA, true); +///////////////////////////////////////////////////////////////////////////// +// Get a blockheader based on its height on the main chain +BlockHeader * BlockDataManager_LevelDB::getHeaderByHeight(int index) +{ + if( index<0 || index>=(int)headersByHeight_.size()) + return NULL; + else + return headersByHeight_[index]; +} - // Will write out about once every 5 sec - writeProgressFile(DB_BUILD_APPLY, blkProgressFile_, "applyBlockRangeToDB"); - } while(iface_->advanceToNextBlock(false)); +///////////////////////////////////////////////////////////////////////////// +// The most common access method is to get a block by its hash +BlockHeader * BlockDataManager_LevelDB::getHeaderByHash(HashString const & blkHash) +{ + map::iterator it = headerMap_.find(blkHash); + //if(it==headerMap_.end()) + if(ITER_NOT_IN_MAP(it, headerMap_)) + return NULL; + else + return &(it->second); +} - // If we're batching, we probably haven't commited the last batch. Hgt - // and dup vars are still in scope. - if(doBatches) - applyModsToDB(stxToModify, sshToModify, keysToDelete); +///////////////////////////////////////////////////////////////////////////// +TxRef BlockDataManager_LevelDB::getTxRefByHash(HashString const & txhash) +{ + return iface_->getTxRef(txhash); } ///////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::writeProgressFile(DB_BUILD_PHASE phase, - string bfile, - string timerName) +Tx BlockDataManager_LevelDB::getTxByHash(HashString const & txhash) { - // Nothing to write if we don't even have a home dir - if(armoryHomeDir_.size() == 0 || bfile.size() == 0) - return; - - time_t currTime; - time(&currTime); - int32_t diffTime = (int32_t)currTime - (int32_t)progressTimer_; - - // Don't write out more than once every 5 sec - if(diffTime < 5) - return; - else - progressTimer_ = (uint32_t)currTime; - uint64_t offset; - uint32_t height, blkfile; + TxRef txrefobj = getTxRefByHash(txhash); - if(phase==DB_BUILD_ADD_RAW) - { - height = startRawBlkHgt_; - blkfile = startRawBlkFile_; - offset = startRawOffset_; - } - else if(phase==DB_BUILD_SCAN) - { - height = startScanHgt_; - blkfile = startScanBlkFile_; - offset = startScanOffset_; - } - else if(phase==DB_BUILD_APPLY) - { - height = startApplyHgt_; - blkfile = startApplyBlkFile_; - offset = startApplyOffset_; - } + if(!txrefobj.isNull()) + return txrefobj.getTxCopy(); else { - LOGERR << "What the heck build phase are we in: " << (uint32_t)phase; - return; + // It's not in the blockchain, but maybe in the zero-conf tx list + map::const_iterator iter = zeroConfMap_.find(txhash); + //if(iter==zeroConfMap_.end()) + if(ITER_NOT_IN_MAP(iter, zeroConfMap_)) + return Tx(); + else + return iter->second.txobj_; } - - uint64_t startAtByte = 0; - if(height!=0) - startAtByte = blkFileCumul_[blkfile] + offset; - - ofstream topblks(bfile.c_str(), ios::app); - double t = TIMER_READ_SEC(timerName); - topblks << (uint32_t)phase << " " - << startAtByte << " " - << bytesReadSoFar_ << " " - << totalBlockchainBytes_ << " " - << t << endl; } ///////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::pprintRegisteredWallets(void) +TX_AVAILABILITY BlockDataManager_LevelDB::getTxHashAvail(BinaryDataRef txHash) { - set::iterator iter; - for(iter = registeredWallets_.begin(); - iter != registeredWallets_.end(); - iter++) + if(getTxRefByHash(txHash).isNull()) { - cout << "Wallet:"; - cout << "\tBalance: " << (*iter)->getFullBalance(); - cout << "\tNAddr: " << (*iter)->getNumScrAddr(); - cout << "\tNTxio: " << (*iter)->getTxIOMap().size(); - cout << "\tNLedg: " << (*iter)->getTxLedger().size(); - cout << "\tNZC: " << (*iter)->getZeroConfLedger().size() << endl; + //if(zeroConfMap_.find(txHash)==zeroConfMap_.end()) + if(KEY_NOT_IN_MAP(txHash, zeroConfMap_)) + return TX_DNE; // No tx at all + else + return TX_ZEROCONF; // Zero-conf tx } + else + return TX_IN_BLOCKCHAIN; // In the blockchain already } ///////////////////////////////////////////////////////////////////////////// -BtcWallet* BlockDataManager_LevelDB::createNewWallet(void) +bool BlockDataManager_LevelDB::hasTxWithHashInDB(BinaryData const & txHash) { - BtcWallet* newWlt = new BtcWallet(this); - registeredWallets_.insert(newWlt); - return newWlt; + return iface_->getTxRef(txHash).isInitialized(); } +///////////////////////////////////////////////////////////////////////////// +bool BlockDataManager_LevelDB::hasTxWithHash(BinaryData const & txHash) +{ + if(iface_->getTxRef(txHash).isInitialized()) + return true; + else + return KEY_IN_MAP(txHash, zeroConfMap_); +} ///////////////////////////////////////////////////////////////////////////// -// This assumes that registeredTxList_ has already been populated from -// the initial blockchain scan. The blockchain contains millions of tx, -// but this list will at least 3 orders of magnitude smaller -void BlockDataManager_LevelDB::scanRegisteredTxForWallet( BtcWallet & wlt, - uint32_t blkStart, - uint32_t blkEnd) +bool BlockDataManager_LevelDB::hasHeaderWithHash(BinaryData const & txHash) const { - SCOPED_TIMER("scanRegisteredTxForWallet"); + //return (headerMap_.find(txHash) != headerMap_.end()); + return KEY_IN_MAP(txHash, headerMap_); +} - // Make sure RegisteredTx objects have correct data, then sort. - // TODO: Why did I not need this with the MMAP blockchain? Somehow - // I was able to sort correctly without this step, before...? - list::iterator txIter; - for(txIter = registeredTxList_.begin(); - txIter != registeredTxList_.end(); - txIter++) +///////////////////////////////////////////////////////////////////////////// +/* +vector BlockDataManager_LevelDB::prefixSearchHeaders(BinaryData const & searchStr) +{ + vector outList(0); + uint32_t lenSearch = searchStr.getSize(); + if(lenSearch < 2) + return outList; // don't search unless we have at least two bytes + + BinaryData searchLow(32); + BinaryData searchHigh(32); + for(uint32_t i=0; itxIndex_ > UINT32_MAX/2) - { - // The RegisteredTx was created before the chain was organized - txIter->blkNum_ = txIter->txRefObj_.getBlockHeight(); - txIter->txIndex_ = txIter->txRefObj_.getBlockTxIndex(); - } + searchLow[i] = searchStr[i]; + searchHigh[i] = searchStr[i]; } - registeredTxList_.sort(); - - ///// LOOP OVER ALL RELEVANT TX //// - for(txIter = registeredTxList_.begin(); - txIter != registeredTxList_.end(); - txIter++) + for(uint32_t i=lenSearch; i<32; i++) { - // Pull the tx from disk and check it for the supplied wallet - Tx theTx = txIter->getTxCopy(); - if( !theTx.isInitialized() ) - { - LOGWARN << "***WARNING: How did we get a NULL tx?"; - continue; - } - - BlockHeader* bhptr = getHeaderPtrForTx(theTx); - // This condition happens on invalid Tx (like invalid P2Pool coinbases) - if( bhptr==NULL ) - continue; - - if( !bhptr->isMainBranch() ) - continue; - - uint32_t thisBlk = bhptr->getBlockHeight(); - if(thisBlk < blkStart || thisBlk >= blkEnd) - continue; - - if( !isTxFinal(theTx) ) - continue; - - // If we made it here, we want to scan this tx! - wlt.scanTx(theTx, txIter->txIndex_, bhptr->getTimestamp(), thisBlk); + searchLow[i] = 0; + searchHigh[i] = 255; } - - wlt.sortLedger(); - - - // We should clean up any dangling TxIOs in the wallet then rescan - if(zcEnabled_) - rescanWalletZeroConf(wlt); + map::iterator iter; + for(iter = headerMap_.lower_bound(searchLow); + iter != headerMap_.upper_bound(searchHigh); + iter++) + { + outList.push_back(&(iter->second)); + } + return outList; } - +*/ ///////////////////////////////////////////////////////////////////////////// -uint64_t BlockDataManager_LevelDB::getDBBalanceForHash160( - BinaryDataRef addr160) +/* +vector BlockDataManager_LevelDB::prefixSearchTx(BinaryData const & searchStr) { - StoredScriptHistory ssh; + vector outList(0); + uint32_t lenSearch = searchStr.getSize(); + if(lenSearch < 2) + return outList; // don't search unless we have at least two bytes - iface_->getStoredScriptHistory(ssh, HASH160PREFIX + addr160); - if(!ssh.isInitialized()) - return 0; + BinaryData searchLow(32); + BinaryData searchHigh(32); + for(uint32_t i=0; i::iterator iter; + for(iter = txHintMap_.lower_bound(searchLow4); + iter != txHintMap_.upper_bound(searchHigh4); + iter++) + { + if(iter->second.getThisHash().startsWith(searchStr)) + outList.push_back(&(iter->second)); + } + return outList; } ///////////////////////////////////////////////////////////////////////////// -uint64_t BlockDataManager_LevelDB::getDBReceivedForHash160( - BinaryDataRef addr160) +// Since the cpp code doesn't have full addresses (only 20-byte hashes), +// that's all we can search for. +vector BlockDataManager_LevelDB::prefixSearchAddress(BinaryData const & searchStr) { - StoredScriptHistory ssh; + // Actually, we can't even search for this, because we don't have a list + // of addresses in the blockchain. We could construct one, but it would + // take up a lot of RAM (and time)... I will need to create a separate + // call to allow the caller to create a set of addresses + // before calling this method + return vector(0); +} +*/ + + - iface_->getStoredScriptHistory(ssh, HASH160PREFIX + addr160); - if(!ssh.isInitialized()) - return 0; - return ssh.getScriptReceived(); -} ///////////////////////////////////////////////////////////////////////////// -vector BlockDataManager_LevelDB::getUTXOVectForHash160( - BinaryDataRef addr160) +bool BlockDataManager_LevelDB::registerWallet(BtcWallet* wltPtr, bool wltIsNew) { - StoredScriptHistory ssh; - vector outVect(0); + SCOPED_TIMER("registerWallet"); - iface_->getStoredScriptHistory(ssh, HASH160PREFIX + addr160); - if(!ssh.isInitialized()) - return outVect; + // Check if the wallet is already registered + //if(registeredWallets_.find(wltPtr) != registeredWallets_.end()) + if(KEY_IN_MAP(wltPtr, registeredWallets_)) + return false; + // Add it to the list of wallets to watch + registeredWallets_.insert(wltPtr); - size_t numTxo = (size_t)ssh.totalTxioCount_; - outVect.reserve(numTxo); - map::iterator iterSubSSH; - map::iterator iterTxio; - for(iterSubSSH = ssh.subHistMap_.begin(); - iterSubSSH != ssh.subHistMap_.end(); - iterSubSSH++) + // Now add all the individual addresses from the wallet + for(uint32_t i=0; igetNumScrAddr(); i++) { - StoredSubHistory & subSSH = iterSubSSH->second; - for(iterTxio = subSSH.txioSet_.begin(); - iterTxio != subSSH.txioSet_.end(); - iterTxio++) - { - TxIOPair & txio = iterTxio->second; - StoredTx stx; - BinaryData txKey = txio.getTxRefOfOutput().getDBKey(); - uint16_t txoIdx = txio.getIndexOfOutput(); - iface_->getStoredTx(stx, txKey); + // If this is a new wallet, the value of getFirstBlockNum is irrelevant + ScrAddrObj & addr = wltPtr->getScrAddrObjByIndex(i); - StoredTxOut & stxo = stx.stxoMap_[txoIdx]; - if(stxo.isSpent()) - continue; - - UnspentTxOut utxo(stx.thisHash_, - txoIdx, - stx.blockHeight_, - txio.getValue(), - stx.stxoMap_[txoIdx].getScriptRef()); - - outVect.push_back(utxo); - } + if(wltIsNew) + registerNewScrAddr(addr.getScrAddr()); + else + registerImportedScrAddr(addr.getScrAddr(), addr.getFirstBlockNum()); } - return outVect; - + // We need to make sure the wallet can tell the BDM when an address is added + wltPtr->setBdmPtr(this); + return true; } + ///////////////////////////////////////////////////////////////////////////// -vector BlockDataManager_LevelDB::getHistoryForScrAddr( - BinaryDataRef uniqKey, - bool withMultisig) +bool BlockDataManager_LevelDB::registerScrAddr(HashString scraddr, + bool addrIsNew, + uint32_t firstBlk) { - StoredScriptHistory ssh; - iface_->getStoredScriptHistory(ssh, uniqKey); + SCOPED_TIMER("registerScrAddr"); + if(KEY_IN_MAP(scraddr, registeredScrAddrMap_)) + { + // Address is already registered. Don't think there's anything to do + return false; + } - map::iterator iter; - iter = registeredScrAddrMap_.find(uniqKey); - if(ITER_IN_MAP(iter, registeredScrAddrMap_)) - iter->second.alreadyScannedUpToBlk_ = ssh.alreadyScannedUpToBlk_; + if(addrIsNew) + firstBlk = getTopBlockHeight() + 1; - vector outVect(0); - if(!ssh.isInitialized()) - return outVect; - - outVect.reserve((size_t)ssh.totalTxioCount_); - map::iterator iterSubSSH; - map::iterator iterTxio; - for(iterSubSSH = ssh.subHistMap_.begin(); - iterSubSSH != ssh.subHistMap_.end(); - iterSubSSH++) - { - StoredSubHistory & subssh = iterSubSSH->second; - for(iterTxio = subssh.txioSet_.begin(); - iterTxio != subssh.txioSet_.end(); - iterTxio++) - { - TxIOPair & txio = iterTxio->second; - if(withMultisig || !txio.isMultisig()) - outVect.push_back(txio); - } - } - - return outVect; -} + registeredScrAddrMap_[scraddr] = RegisteredScrAddr(scraddr, firstBlk); + allScannedUpToBlk_ = min(firstBlk, allScannedUpToBlk_); + return true; +} ///////////////////////////////////////////////////////////////////////////// -/* This is not currently being used, and is actually likely to change - * a bit before it is needed, so I have just disabled it. -vector BlockDataManager_LevelDB::findAllNonStdTx(void) +bool BlockDataManager_LevelDB::registerNewScrAddr(HashString scraddr) { - PDEBUG("Finding all non-std tx"); - vector txVectOut(0); - uint32_t nHeaders = headersByHeight_.size(); - - ///// LOOP OVER ALL HEADERS //// - for(uint32_t h=0; h const & txlist = bhr.getTxRefPtrList(); + SCOPED_TIMER("registerNewScrAddr"); + if(KEY_IN_MAP(scraddr, registeredScrAddrMap_)) + return false; - ///// LOOP OVER ALL TX ///// - for(uint32_t itx=0; itxserialize().toHexStr() << endl; - cout << "pprint: " << endl; - BtcUtils::pprintScript(txin.getScript()); - cout << endl; - } - } + // New address cannot affect allScannedUpToBlk_, so don't bother + //allScannedUpToBlk_ = min(currBlk, allScannedUpToBlk_); + return true; +} - ///// LOOP OVER ALL TXOUT IN BLOCK ///// - for(uint32_t iout=0; ioutgetThisHash().toHexStr() - << ", " << txout.getIndex() << endl; - cout << "Raw Script: " << txout.getScript().toHexStr() << endl; - cout << "Raw Tx: " << txout.getParentTxPtr()->serialize().toHexStr() << endl; - cout << "pprint: " << endl; - BtcUtils::pprintScript(txout.getScript()); - cout << endl; - } +///////////////////////////////////////////////////////////////////////////// +bool BlockDataManager_LevelDB::registerImportedScrAddr(HashString scraddr, + uint32_t createBlk) +{ + SCOPED_TIMER("registerImportedScrAddr"); + if(KEY_IN_MAP(scraddr, registeredScrAddrMap_)) + return false; - } - } - } + // In some cases we may have used UINT32_MAX to specify "don't know" + if(createBlk==UINT32_MAX) + createBlk = 0; - PDEBUG("Done finding all non-std tx"); - return txVectOut; + registeredScrAddrMap_[scraddr] = RegisteredScrAddr(scraddr, createBlk); + allScannedUpToBlk_ = min(createBlk, allScannedUpToBlk_); + return true; } -*/ - ///////////////////////////////////////////////////////////////////////////// -// With the LevelDB database integration, we now index all blockchain data -// by block height and index (tx index in block, txout index in tx). The -// only way to actually do that is to process the headers first, so that -// when we do read the block data the first time, we know how to put it -// into the DB. -// -// For now, we have no problem holding all the headers in RAM and organizing -// them all in one shot. But RAM-limited devices (say, if this was going -// to be ported to Android), may not be able to do even that, and may have -// to read and process the headers in batches. -bool BlockDataManager_LevelDB::extractHeadersInBlkFile(uint32_t fnum, - uint64_t startOffset) +bool BlockDataManager_LevelDB::unregisterScrAddr(HashString scraddr) { - SCOPED_TIMER("extractHeadersInBlkFile"); - string filename = blkFileList_[fnum]; - uint64_t filesize = BtcUtils::GetFileSize(filename); - if(filesize == FILE_DOES_NOT_EXIST) - { - LOGERR << "File does not exist: " << filename.c_str(); + SCOPED_TIMER("unregisterScrAddr"); + //if(registeredScrAddrMap_.find(scraddr) == registeredScrAddrMap_.end()) + if(KEY_IN_MAP(scraddr, registeredScrAddrMap_)) return false; - } - - // This will trigger if this is the last blk file and no new blocks - if(filesize < startOffset) - return true; + registeredScrAddrMap_.erase(scraddr); + allScannedUpToBlk_ = evalLowestBlockNextScan(); + return true; +} - ifstream is(filename.c_str(), ios::in | ios::binary); - BinaryData fileMagic(4); - is.read((char*)(fileMagic.getPtr()), 4); - is.seekg(startOffset, ios::beg); - - if( !(fileMagic == MagicBytes_ ) ) - { - LOGERR << "Block file is the wrong network! MagicBytes: " - << fileMagic.toHexStr().c_str(); - return false; - } +///////////////////////////////////////////////////////////////////////////// +BtcWallet::~BtcWallet(void) +{ + if(bdmPtr_!=NULL) + bdmPtr_->unregisterWallet(this); +} - BinaryData rawHeader(HEADER_SIZE); - // Some objects to help insert header data efficiently - pair bhInputPair; - pair::iterator, bool> bhInsResult; - endOfLastBlockByte_ = startOffset; +///////////////////////////////////////////////////////////////////////////// +uint32_t BlockDataManager_LevelDB::evalLowestBlockNextScan(void) +{ + SCOPED_TIMER("evalLowestBlockNextScan"); - uint32_t const HEAD_AND_NTX_SZ = HEADER_SIZE + 10; // enough - BinaryData magic(4), szstr(4), rawHead(HEAD_AND_NTX_SZ); - while(!is.eof()) + uint32_t lowestBlk = UINT32_MAX; + uint32_t i=0; + map::iterator rsaIter; + for(rsaIter = registeredScrAddrMap_.begin(); + rsaIter != registeredScrAddrMap_.end(); + rsaIter++) { - is.read((char*)magic.getPtr(), 4); - if(magic!=MagicBytes_ || is.eof()) - break; - - is.read((char*)szstr.getPtr(), 4); - uint32_t nextBlkSize = READ_UINT32_LE(szstr.getPtr()); - if(is.eof()) break; - - is.read((char*)rawHead.getPtr(), HEAD_AND_NTX_SZ); // plus #tx var_int - if(is.eof()) break; - - // Create a reader for the entire block, grab header, skip rest - BinaryRefReader brr(rawHead); - bhInputPair.second.unserialize(brr); - uint32_t nTx = (uint32_t)brr.get_var_int(); - bhInputPair.first = bhInputPair.second.getThisHash(); - bhInsResult = headerMap_.insert(bhInputPair); - if(!bhInsResult.second) - { - // We exclude the genesis block which is always in the DB here - if(fnum!=0 || endOfLastBlockByte_!=0) - { - LOGWARN << "Somehow tried to add header that's already in map"; - LOGWARN << "Header Hash: " << bhInputPair.first.toHexStr().c_str(); - } - // But overwrite the header anyway - bhInsResult.first->second = bhInputPair.second; - } - - bhInsResult.first->second.setBlockFile(filename); - bhInsResult.first->second.setBlockFileNum(fnum); - bhInsResult.first->second.setBlockFileOffset(endOfLastBlockByte_); - bhInsResult.first->second.setNumTx(nTx); - bhInsResult.first->second.setBlockSize(nextBlkSize); - - endOfLastBlockByte_ += nextBlkSize+8; - is.seekg(nextBlkSize - HEAD_AND_NTX_SZ, ios::cur); + // If we happen to have any imported addresses, this will set the + // lowest block to 0, which will require a full rescan + lowestBlk = min(lowestBlk, rsaIter->second.alreadyScannedUpToBlk_); } - - is.close(); - return true; + return lowestBlk; } - ///////////////////////////////////////////////////////////////////////////// -uint32_t BlockDataManager_LevelDB::detectAllBlkFiles(void) +// This method isn't really used yet... +uint32_t BlockDataManager_LevelDB::evalLowestScrAddrCreationBlock(void) { - SCOPED_TIMER("detectAllBlkFiles"); - - // Next thing we need to do is find all the blk000X.dat files. - // BtcUtils::GetFileSize uses only ifstreams, and thus should be - // able to determine if a file exists in an OS-independent way. - numBlkFiles_=0; - totalBlockchainBytes_ = 0; - blkFileList_.clear(); - blkFileSizes_.clear(); - blkFileCumul_.clear(); - while(numBlkFiles_ < UINT16_MAX) - { - string path = BtcUtils::getBlkFilename(blkFileDir_, numBlkFiles_); - uint64_t filesize = BtcUtils::GetFileSize(path); - if(filesize == FILE_DOES_NOT_EXIST) - break; - - numBlkFiles_++; - blkFileList_.push_back(string(path)); - blkFileSizes_.push_back(filesize); - blkFileCumul_.push_back(totalBlockchainBytes_); - totalBlockchainBytes_ += filesize; - } + SCOPED_TIMER("evalLowestAddressCreationBlock"); - if(numBlkFiles_==UINT16_MAX) + uint32_t lowestBlk = UINT32_MAX; + map::iterator rsaIter; + for(rsaIter = registeredScrAddrMap_.begin(); + rsaIter != registeredScrAddrMap_.end(); + rsaIter++) { - LOGERR << "Error finding blockchain files (blkXXXX.dat)"; - return 0; + // If we happen to have any imported addresses, this will set the + // lowest block to 0, which will require a full rescan + lowestBlk = min(lowestBlk, rsaIter->second.blkCreated_); } - return numBlkFiles_; + return lowestBlk; } - ///////////////////////////////////////////////////////////////////////////// -bool BlockDataManager_LevelDB::processNewHeadersInBlkFiles(uint32_t fnumStart, - uint64_t startOffset) +bool BlockDataManager_LevelDB::evalRescanIsRequired(void) { - SCOPED_TIMER("processNewHeadersInBlkFiles"); - - detectAllBlkFiles(); - - // In first file, start at supplied offset; start at beginning for others - for(uint32_t fnum=fnumStart; fnum::iterator iter; - for(iter = headerMap_.begin(); iter != headerMap_.end(); iter++) - { - StoredHeader sbh; - sbh.createFromBlockHeader(iter->second); - uint8_t dup = iface_->putBareHeader(sbh); - iter->second.duplicateID_ = dup; // make sure headerMap_ and DB agree - } - - return prevTopBlkStillValid; + // This method tells us whether we have to scan ANY blocks from disk + // in order to produce accurate balances and TxOut lists. If this + // returns false, we can get away without any disk access at all, and + // just use the registeredTxList_ object to get our information. + allScannedUpToBlk_ = evalLowestBlockNextScan(); + return (allScannedUpToBlk_ < getTopBlockHeight()+1); } -//////////////////////////////////////////////////////////////////////////////// -// We assume that all the addresses we care about have been registered with -// the BDM. Before, the BDM we would rescan the blockchain and use the method -// isMineBulkFilter() to extract all "RegisteredTx" which are all tx relevant -// to the list of "RegisteredScrAddr" objects. Now, the DB defaults to super- -// node mode and tracks all that for us on disk. So when we start up, rather -// than having to search the blockchain, we just look the StoredScriptHistory -// list for each of our "RegisteredScrAddr" objects, and then pull all the -// relevant tx from the database. After that, the BDM operates 99% identically -// to before. We just didn't have to do a full scan to fill the RegTx list -// -// In the future, we will use the StoredScriptHistory objects to directly fill -// the TxIOPair map -- all the data is tracked by the DB and we could pull it -// directly. But that would require reorganizing a ton of BDM code, and may -// be difficult to guarantee that all the previous functionality was there and -// working. This way, all of our previously-tested code remains mostly -// untouched -void BlockDataManager_LevelDB::fetchAllRegisteredScrAddrData(void) -{ - fetchAllRegisteredScrAddrData(registeredScrAddrMap_); -} ///////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::fetchAllRegisteredScrAddrData( - BtcWallet & myWallet) -{ - SCOPED_TIMER("fetchAllRegisteredScrAddrData"); - - uint32_t numAddr = myWallet.getNumScrAddr(); - for(uint32_t s=0; s & addrMap) +// This method needs to be callable from another thread. Therefore, I don't +// seek an exact answer, instead just estimate it based on the last block, +// and the set of currently-registered addresses. The method called +// "evalRescanIsRequired()" answers a different question, and iterates +// through the list of registered addresses, which may be changing in +// another thread. +bool BlockDataManager_LevelDB::isDirty( + uint32_t numBlocksToBeConsideredDirty ) const { - SCOPED_TIMER("fetchAllRegisteredScrAddrData"); - - - map::iterator iter; - for(iter = addrMap.begin(); iter != addrMap.end(); iter++) - fetchAllRegisteredScrAddrData(iter->second.uniqueKey_); + if(!isInitialized_) + return false; + + uint32_t numBlocksBehind = lastTopBlock_-allScannedUpToBlk_; + return (numBlocksBehind > numBlocksToBeConsideredDirty); + } ///////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::fetchAllRegisteredScrAddrData( - BinaryData const & scrAddr) +uint32_t BlockDataManager_LevelDB::numBlocksToRescan( BtcWallet & wlt, + uint32_t endBlk) { - vector hist = getHistoryForScrAddr(scrAddr); + SCOPED_TIMER("numBlocksToRescan"); + // This method tells us whether we have to scan ANY blocks from disk + // in order to produce accurate balances and TxOut lists. If this + // returns false, we can get away without any disk access at all, and + // just use the registeredTxList_ object to get our information. + uint32_t currNextBlk = getTopBlockHeight() + 1; + endBlk = min(endBlk, currNextBlk); - BinaryData txKey; - StoredTx stx; - TxRef txref; - RegisteredTx regTx; - for(uint32_t i=0; igetStoredTx(stx, txref.getDBKey()); - regTx = RegisteredTx(txref, stx.thisHash_, stx.blockHeight_, stx.txIndex_); - insertRegisteredTxIfNew(regTx); - registeredOutPoints_.insert(hist[i].getOutPoint()); + ScrAddrObj & addr = wlt.getScrAddrObjByIndex(i); - txref = hist[i].getTxRefOfInput(); - if(txref.isNull()) - continue; + // If any address is not registered, will have to do a full scan + //if(registeredScrAddrMap_.find(addr.getScrAddr()) == registeredScrAddrMap_.end()) + if(KEY_NOT_IN_MAP(addr.getScrAddr(), registeredScrAddrMap_)) + return endBlk; // Gotta do a full rescan! - // If the coins were spent, also fetch the tx in which they were spent - iface_->getStoredTx(stx, txref.getDBKey()); - regTx = RegisteredTx(txref, stx.thisHash_, stx.blockHeight_, stx.txIndex_); - insertRegisteredTxIfNew(regTx); + RegisteredScrAddr & ra = registeredScrAddrMap_[addr.getScrAddr()]; + maxAddrBehind = max(maxAddrBehind, endBlk-ra.alreadyScannedUpToBlk_); } -} + // If we got here, then all addr are already registered and current + return maxAddrBehind; +} -///////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::destroyAndResetDatabases(void) +//////////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::updateRegisteredScrAddrs(uint32_t newTopBlk) { - if(iface_ != NULL) + map::iterator rsaIter; + for(rsaIter = registeredScrAddrMap_.begin(); + rsaIter != registeredScrAddrMap_.end(); + rsaIter++) { - LOGWARN << "Destroying databases; will need to be rebuilt"; - iface_->destroyAndResetDatabases(); - return; + rsaIter->second.alreadyScannedUpToBlk_ = newTopBlk; } - LOGERR << "Attempted to destroy databases, but no DB interface set"; } - -///////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::doRebuildDatabases(void) +//////////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::resetRegisteredWallets(void) { - LOGINFO << "Executing: doRebuildDatabases"; - buildAndScanDatabases(true, true, true, false); - // Rescan Rebuild !Fetch Initial -} + SCOPED_TIMER("resetRegisteredWallets"); -///////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::doFullRescanRegardlessOfSync(void) -{ - LOGINFO << "Executing: doFullRescanRegardlessOfSync"; - buildAndScanDatabases(true, false, true, false); - // Rescan Rebuild !Fetch Initial -} + set::iterator wltPtrIter; + for(wltPtrIter = registeredWallets_.begin(); + wltPtrIter != registeredWallets_.end(); + wltPtrIter++) + { + // I'm not sure if there's anything else to do + // I think it's all encapsulated in this call! + (*wltPtrIter)->clearBlkData(); + } -///////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::doSyncIfNeeded(void) -{ - LOGINFO << "Executing: doSyncIfNeeded"; - buildAndScanDatabases(false, false, true, false); - // Rescan Rebuild !Fetch Initial + // Reset all addresses to "new" + updateRegisteredScrAddrs(0); } ///////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::doInitialSyncOnLoad(void) +bool BlockDataManager_LevelDB::walletIsRegistered(BtcWallet & wlt) { - LOGINFO << "Executing: doInitialSyncOnLoad"; - buildAndScanDatabases(false, false, false, true); - // Rescan Rebuild !Fetch Initial + //return (registeredWallets_.find(&wlt)!=registeredWallets_.end()); + return KEY_IN_MAP(&wlt, registeredWallets_); } ///////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::doInitialSyncOnLoad_Rescan(void) +bool BlockDataManager_LevelDB::scrAddrIsRegistered(HashString scraddr) { - LOGINFO << "Executing: doInitialSyncOnLoad_Rescan"; - buildAndScanDatabases(true, false, false, true); - // Rescan Rebuild !Fetch Initial + //return (registeredScrAddrMap_.find(scraddr)!=registeredScrAddrMap_.end()); + return KEY_IN_MAP(scraddr, registeredScrAddrMap_); } -///////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::doInitialSyncOnLoad_Rebuild(void) -{ - LOGINFO << "Executing: doInitialSyncOnLoad_Rebuild"; - buildAndScanDatabases(false, true, true, true); - // Rescan Rebuild !Fetch Initial -} + ///////////////////////////////////////////////////////////////////////////// -// This used to be "parseEntireBlockchain()", but changed because it will -// only be used when rebuilding the DB from scratch (hopefully). +// first scans the blockchain and collects the registered tx (all tx relevant +// to your wallet), then does a heartier scan of that subset to actually +// collect balance information, utxo sets +// +// This method is now a hybrid of the original, Blockchain-in-RAM code, +// and the new mmap()-based blockchain operations. The initial blockchain +// scan will look for wallet-relevant transactions, and keep track of what +// blocks still need to be scanned given the registered wallets +// +// Therefore, when we scan, we will first scan the registered tx list, +// then any raw blocks that haven't been filtered yet, then all the +// zero-conf tx list. // -// The default behavior of this method is to do the minimal amount of work -// neceesary to get sync'd. It does this by assuming all database data is -// correct. We can choose to rebuild/recalculate. "forceRescan" and -// "skipFetch" are slightly different: forceRescan will guarantee that -// we always start scanning from block 0. skipFetch means we won't pull -// any data out of the database when this is called, but if all our -// wallets are already synchronized, we won't bother rescanning -void BlockDataManager_LevelDB::buildAndScanDatabases( - bool forceRescan, - bool forceRebuild, - bool skipFetch, - bool initialLoad) -{ - - SCOPED_TIMER("buildAndScanDatabases"); - LOGINFO << "Number of registered addr: " << registeredScrAddrMap_.size(); - - // Will use this updating the GUI with progress bar - time_t t; - time(&t); - progressTimer_ = (uint32_t)t; - - if(!iface_->databasesAreOpen()) - initializeDBInterface(DBUtils.getArmoryDbType(), DBUtils.getDbPruneType()); - - LOGDEBUG << "Called build&scan with (" - << (forceRescan ? 1 : 0) << "," - << (forceRebuild ? 1 : 0) << "," - << (skipFetch ? 1 : 0) << "," - << (initialLoad ? 1 : 0) << ")"; - - - // This will figure out where we should start reading headers, blocks, - // and where we should start applying or scanning - detectCurrentSyncState(forceRebuild, initialLoad); +// If the wallet contains any addresses that are not part of the prefiltered +// tx-hash-list, then the entire blockchain will have to be rescanned, anyway. +// It doesn't take any less time to search for one address than it does +// all of them. +// +// +// Some new notes on this method ... +// We will ONLY scan transactions from the registeredTxList_ +// +// Therefore, we need to make sure that registeredTxList_ is +// completely up-to-date FIRST. +// +// Then we can sort it and scanTx all of them with the wallet. +// +// We need to scan from blocks X-->Y. Assume X is 1000 and Y is 2000 +// If allScannedUpToBlk_==1500: +// +// registeredScrAddrScan from 1500-->2000 +// sort registered list +// scanTx all tx in registered list between 1000 and 2000 +void BlockDataManager_LevelDB::scanBlockchainForTx(BtcWallet & myWallet, + uint32_t startBlknum, + uint32_t endBlknum, + bool fetchFirst) +{ + SCOPED_TIMER("scanBlockchainForTx"); - // If we're going to rebuild, might as well destroy the DB for good measure - if(forceRebuild || (startHeaderHgt_==0 && startRawBlkHgt_==0)) - { - LOGINFO << "Clearing databases for clean build"; - forceRebuild = true; - forceRescan = true; - skipFetch = true; - destroyAndResetDatabases(); - } + // TODO: We should implement selective fetching! (i.e. only fetch + // and register scraddr data that is between those two blocks). + // At the moment, it is + if(fetchFirst && DBUtils.getArmoryDbType()!=ARMORY_DB_BARE) + fetchAllRegisteredScrAddrData(myWallet); - // If we're going to be rescanning, reset the wallets - if(forceRescan) - { - LOGINFO << "Resetting wallets for rescan"; - skipFetch = true; - resetRegisteredWallets(); - } + // The BDM knows the highest block to which ALL CURRENT REGISTERED ADDRESSES + // are up-to-date in the registeredTxList_ list. + // If this wallet is not registered, it needs to be, before we start + if(!walletIsRegistered(myWallet)) + registerWallet( &myWallet ); - // If no rescan is forced, grab the SSH entries from the DB - if(!skipFetch && initialLoad) - { - LOGINFO << "Fetching stored script histories from DB"; - fetchAllRegisteredScrAddrData(); - } + + // Check whether we can get everything we need from the registered tx list + endBlknum = min(endBlknum, getTopBlockHeight()+1); + uint32_t numRescan = numBlocksToRescan(myWallet, endBlknum); - if(initialLoad && DBUtils.getArmoryDbType() != ARMORY_DB_SUPER) - { - // We always delete the histories, regardless of whether we read them or - // not. We only save them on a clean shutdown, so we know they are - // consistent. Unclean shutdowns/kills will force a rescan simply by - // the fetch call (at the top) getting nothing out of the database - if(initialLoad) - deleteHistories(); - } + // This is the part that might take a while... + //applyBlockRangeToDB(allScannedUpToBlk_, endBlknum); + scanDBForRegisteredTx(allScannedUpToBlk_, endBlknum); + allScannedUpToBlk_ = endBlknum; + updateRegisteredScrAddrs(endBlknum); - // Remove this file - if(BtcUtils::GetFileSize(blkProgressFile_) != FILE_DOES_NOT_EXIST) - remove(blkProgressFile_.c_str()); - if(BtcUtils::GetFileSize(abortLoadFile_) != FILE_DOES_NOT_EXIST) - remove(abortLoadFile_.c_str()); - - if(!initialLoad) - detectAllBlkFiles(); // only need to spend time on this on the first call - if(numBlkFiles_==0) - { - LOGERR << "No blockfiles could be found! Aborting..."; - return; - } + // *********************************************************************** // + // Finally, walk through all the registered tx + scanRegisteredTxForWallet(myWallet, startBlknum, endBlknum); - if(GenesisHash_.getSize() == 0) - { - LOGERR << "***ERROR: Set net params before loading blockchain!"; - return; - } + // I think these lines of code where causing the serious peformance issues + // so they were commented out and don't appear to be needed + // if(zcEnabled_) + // rescanWalletZeroConf(myWallet); +} - ///////////////////////////////////////////////////////////////////////////// - // New with LevelDB: must read and organize headers before handling the - // full blockchain data. We need to figure out the longest chain and write - // the headers to the DB before actually processing any block data. - if(initialLoad || forceRebuild) - { - LOGINFO << "Reading all headers and building chain..."; - processNewHeadersInBlkFiles(startHeaderBlkFile_, startHeaderOffset_); - } - dbUpdateSize_ = 0; - LOGINFO << "Total number of blk*.dat files: " << numBlkFiles_; - LOGINFO << "Total number of blocks found: " << getTopBlockHeight() + 1; +///////////////////////////////////////////////////////////////////////////// +// This used to be "rescanBlocks", but now "scanning" has been replaced by +// "reapplying" the blockdata to the databases. Basically assumes that only +// raw blockdata is stored in the DB with no SSH objects. This goes through +// and processes every Tx, creating new SSHs if not there, and creating and +// marking-spent new TxOuts. +void BlockDataManager_LevelDB::applyBlockRangeToDB(uint32_t blk0, uint32_t blk1) +{ + SCOPED_TIMER("applyBlockRangeToDB"); - ///////////////////////////////////////////////////////////////////////////// - // Now we start the meat of this process... + blk1 = min(blk1, getTopBlockHeight()+1); - ///////////////////////////////////////////////////////////////////////////// - // Add the raw blocks from the blk*.dat files into the DB - blocksReadSoFar_ = 0; - bytesReadSoFar_ = 0; + BinaryData startKey = DBUtils.getBlkDataKey(blk0, 0); + BinaryData endKey = DBUtils.getBlkDataKey(blk1, 0); - if(initialLoad || forceRebuild) - { - LOGINFO << "Getting latest blocks from blk*.dat files"; - LOGINFO << "Total blockchain bytes: " - << BtcUtils::numToStrWCommas(totalBlockchainBytes_); - TIMER_START("dumpRawBlocksToDB"); - for(uint32_t fnum=startRawBlkFile_; fnumgetIterator(BLKDATA); + ldbIter.seekTo(startKey); - double timeElapsed = TIMER_READ_SEC("dumpRawBlocksToDB"); - LOGINFO << "Processed " << blocksReadSoFar_ << " raw blocks DB (" - << (int)timeElapsed << " seconds)"; + // Start scanning and timer + //bool doBatches = (blk1-blk0 > NUM_BLKS_BATCH_THRESH); + BlockWriteBatcher blockWrites(iface_); - // Now start scanning the raw blocks - if(registeredScrAddrMap_.size() == 0) - { - LOGWARN << "No addresses are registered with the BDM, so there's no"; - LOGWARN << "point in doing a blockchain scan yet."; - } - else if(DBUtils.getArmoryDbType() != ARMORY_DB_SUPER) + do { - // We don't do this in SUPER mode because there is no rescanning - // For progress bar purposes, let's find the blkfile location of scanStart - if(forceRescan) - { - startScanHgt_ = 0; - startScanBlkFile_ = 0; - startScanOffset_ = 0; - } - else - { - startScanHgt_ = evalLowestBlockNextScan(); - // Rewind 4 days, to rescan recent history in case problem last shutdown - startScanHgt_ = (startScanHgt_>576 ? startScanHgt_-576 : 0); - pair blkLoc = findFileAndOffsetForHgt(startScanHgt_); - startScanBlkFile_ = blkLoc.first; - startScanOffset_ = blkLoc.second; - } - - LOGINFO << "Starting scan from block height: " << startScanHgt_; - scanDBForRegisteredTx(startScanHgt_); - LOGINFO << "Finished blockchain scan in " - << TIMER_READ_SEC("ScanBlockchain") << " seconds"; - } - - // If bare mode, we don't do - if(DBUtils.getArmoryDbType() != ARMORY_DB_BARE) - { - // In any DB type other than bare, we will be walking through the blocks - // and updating the spentness fields and script histories - applyBlockRangeToDB(startApplyHgt_, getTopBlockHeight()+1); - } + StoredHeader sbh; + iface_->readStoredBlockAtIter(ldbIter, sbh); + const uint32_t hgt = sbh.blockHeight_; + const uint8_t dup = sbh.duplicateID_; + if(blk0 > hgt || hgt >= blk1) + break; - // We need to maintain the physical size of all blkXXXX.dat files together - totalBlockchainBytes_ = bytesReadSoFar_; + if(hgt%2500 == 2499) + LOGWARN << "Finished applying blocks up to " << (hgt+1); - // Update registered address list so we know what's already been scanned - lastTopBlock_ = getTopBlockHeight() + 1; - allScannedUpToBlk_ = lastTopBlock_; - updateRegisteredScrAddrs(lastTopBlock_); + if(dup != iface_->getValidDupIDForHeight(hgt)) + continue; + // IS THIS COMMENT STILL RELEVANT? ~CS + // Ugh! Design inefficiency: this loop and applyToBlockDB both use + // the same iterator, which means that applyBlockToDB will usually + // leave us with the iterator in a different place than we started. + // I'm not clear how inefficient it is to keep re-seeking (given that + // there's a lot of caching going on under-the-hood). It may be better + // to have each method create its own iterator... TODO: profile/test + // this idea. For now we will just save the current DB key, and + // re-seek to it afterwards. + blockWrites.applyBlockToDB(hgt, dup); - // Since loading takes so long, there's a good chance that new block data - // came in... let's get it. - readBlkFileUpdate(); - isInitialized_ = true; - purgeZeroConfPool(); + bytesReadSoFar_ += sbh.numBytes_; - #ifdef _DEBUG - UniversalTimer::instance().printCSV(string("timings.csv")); - #ifdef _DEBUG_FULL_VERBOSE - UniversalTimer::instance().printCSV(cout,true); - #endif - #endif + // Will write out about once every 5 sec + writeProgressFile(DB_BUILD_APPLY, blkProgressFile_, "applyBlockRangeToDB"); - /* - for(iter = registeredScrAddrMap_.begin(); - iter != registeredScrAddrMap_.end(); - iter ++) - LOGINFO << "ScrAddr: " << iter->second.uniqueKey_.toHexStr().c_str() - << " " << iter->second.alreadyScannedUpToBlk_; - */ + } while(iface_->advanceToNextBlock(ldbIter, false)); } -//////////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::readRawBlocksInFile(uint32_t fnum, uint32_t foffset) +///////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::writeProgressFile(DB_BUILD_PHASE phase, + string bfile, + string timerName) { + // Nothing to write if we don't even have a home dir + if(armoryHomeDir_.size() == 0 || bfile.size() == 0) + return; - string blkfile = blkFileList_[fnum]; - uint64_t filesize = BtcUtils::GetFileSize(blkfile); - string fsizestr = BtcUtils::numToStrWCommas(filesize); - LOGINFO << blkfile.c_str() << " is " << fsizestr.c_str() << " bytes"; + time_t currTime; + time(&currTime); + int32_t diffTime = (int32_t)currTime - (int32_t)progressTimer_; - // Open the file, and check the magic bytes on the first block - ifstream is(blkfile.c_str(), ios::in | ios::binary); - BinaryData fileMagic(4); - is.read((char*)(fileMagic.getPtr()), 4); - if( !(fileMagic == MagicBytes_ ) ) - { - LOGERR << "Block file is the wrong network! MagicBytes: " - << fileMagic.toHexStr().c_str(); - } - - // Seek to the supplied offset - is.seekg(foffset, ios::beg); - - BinaryStreamBuffer bsb; - bsb.attachAsStreamBuffer(is, (uint32_t)filesize-foffset); - - bool alreadyRead8B = false; - uint32_t nextBlkSize; - bool isEOF = false; - BinaryData firstFour(4); - - // We use these two vars to stop parsing if we exceed the last header - // that was processed (a new block was added since we processed headers) - bool breakbreak = false; - uint32_t locInBlkFile = foffset; + // Don't write out more than once every 5 sec + if(diffTime < 5) + return; + else + progressTimer_ = (uint32_t)currTime; - iface_->startBatch(BLKDATA); + uint64_t offset; + uint32_t height, blkfile; - // It turns out that this streambuffering is probably not helping, but - // it doesn't hurt either, so I'm leaving it alone - while(bsb.streamPull()) + if(phase==DB_BUILD_ADD_RAW) { - while(bsb.reader().getSizeRemaining() > 8) - { - - if(!alreadyRead8B) - { - bsb.reader().get_BinaryData(firstFour, 4); - if(firstFour!=MagicBytes_) - { - isEOF = true; - break; - } - nextBlkSize = bsb.reader().get_uint32_t(); - bytesReadSoFar_ += 8; - } - - if(bsb.reader().getSizeRemaining() < nextBlkSize) - { - alreadyRead8B = true; - break; - } - alreadyRead8B = false; - - BinaryRefReader brr(bsb.reader().getCurrPtr(), nextBlkSize); - - addRawBlockToDB(brr); - dbUpdateSize_ += nextBlkSize; - - if(dbUpdateSize_>UPDATE_BYTES_THRESH && iface_->isBatchOn(BLKDATA)) - { - dbUpdateSize_ = 0; - iface_->commitBatch(BLKDATA); - iface_->startBatch(BLKDATA); - } - - blocksReadSoFar_++; - bytesReadSoFar_ += nextBlkSize; - locInBlkFile += nextBlkSize + 8; - bsb.reader().advance(nextBlkSize); - - // This is a hack of hacks, but I can't seem to pass this data - // out through getLoadProgress* methods, because they don't - // update properly (from the main python thread) when the BDM - // is actively loading/scanning in a separate thread. - // We'll watch for this file from the python code. - writeProgressFile(DB_BUILD_ADD_RAW, blkProgressFile_, "dumpRawBlocksToDB"); - - // Don't read past the last header we processed (in case new - // blocks were added since we processed the headers - if(fnum == numBlkFiles_-1 && locInBlkFile >= endOfLastBlockByte_) - { - breakbreak = true; - break; - } - } - - - if(isEOF || breakbreak) - break; + height = startRawBlkHgt_; + blkfile = startRawBlkFile_; + offset = startRawOffset_; + } + else if(phase==DB_BUILD_SCAN) + { + height = startScanHgt_; + blkfile = startScanBlkFile_; + offset = startScanOffset_; + } + else if(phase==DB_BUILD_APPLY) + { + height = startApplyHgt_; + blkfile = startApplyBlkFile_; + offset = startApplyOffset_; + } + else + { + LOGERR << "What the heck build phase are we in: " << (uint32_t)phase; + return; } - - if(iface_->isBatchOn(BLKDATA)) - iface_->commitBatch(BLKDATA); + uint64_t startAtByte = 0; + if(height!=0) + startAtByte = blkFileCumul_[blkfile] + offset; + + ofstream topblks(OS_TranslatePath(bfile.c_str()), ios::app); + double t = TIMER_READ_SEC(timerName); + topblks << (uint32_t)phase << " " + << startAtByte << " " + << bytesReadSoFar_ << " " + << totalBlockchainBytes_ << " " + << t << endl; } - -//////////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::scanDBForRegisteredTx(uint32_t blk0, - uint32_t blk1) +///////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::pprintRegisteredWallets(void) { - SCOPED_TIMER("scanDBForRegisteredTx"); - bytesReadSoFar_ = 0; - - bool doScanProgressThing = (blk1-blk0 > NUM_BLKS_IS_DIRTY); - if(doScanProgressThing) + set::iterator iter; + for(iter = registeredWallets_.begin(); + iter != registeredWallets_.end(); + iter++) { - //if(BtcUtils::GetFileSize(bfile) != FILE_DOES_NOT_EXIST) - //remove(bfile.c_str()); + cout << "Wallet:"; + cout << "\tBalance: " << (*iter)->getFullBalance(); + cout << "\tNAddr: " << (*iter)->getNumScrAddr(); + cout << "\tNTxio: " << (*iter)->getTxIOMap().size(); + cout << "\tNLedg: " << (*iter)->getTxLedger().size(); + cout << "\tNZC: " << (*iter)->getZeroConfLedger().size() << endl; } +} - BinaryData firstKey = DBUtils.getBlkDataKey(blk0, 0); - iface_->seekTo(BLKDATA, firstKey); +///////////////////////////////////////////////////////////////////////////// +BtcWallet* BlockDataManager_LevelDB::createNewWallet(void) +{ + BtcWallet* newWlt = new BtcWallet(this); + registeredWallets_.insert(newWlt); + return newWlt; +} - TIMER_START("ScanBlockchain"); - while(iface_->dbIterIsValid(BLKDATA, DB_PREFIX_TXDATA)) - { - // Get the full block from the DB - StoredHeader sbh; - iface_->readStoredBlockAtIter(sbh); - bytesReadSoFar_ += sbh.numBytes_; +///////////////////////////////////////////////////////////////////////////// +// This assumes that registeredTxList_ has already been populated from +// the initial blockchain scan. The blockchain contains millions of tx, +// but this list will at least 3 orders of magnitude smaller +void BlockDataManager_LevelDB::scanRegisteredTxForWallet( BtcWallet & wlt, + uint32_t blkStart, + uint32_t blkEnd) +{ + SCOPED_TIMER("scanRegisteredTxForWallet"); - uint32_t hgt = sbh.blockHeight_; - uint8_t dup = sbh.duplicateID_; - uint8_t dupMain = iface_->getValidDupIDForHeight(hgt); - if(!sbh.isMainBranch_ || dup != dupMain) - continue; + if(!wlt.ignoreLastScanned_) + blkStart = wlt.lastScanned_; + else + wlt.ignoreLastScanned_ = false; - if(hgt >= blk1) - break; - - // If we're here, we need to check the tx for relevance to the - // global scrAddr list. Add to registered Tx map if so - map::iterator iter; - for(iter = sbh.stxMap_.begin(); - iter != sbh.stxMap_.end(); - iter++) + bool isMainWallet = true; + if(&wlt != (*registeredWallets_.begin())) isMainWallet = false; + + // Make sure RegisteredTx objects have correct data, then sort. + // TODO: Why did I not need this with the MMAP blockchain? Somehow + // I was able to sort correctly without this step, before...? + list::iterator txIter; + for(txIter = registeredTxList_.begin(); + txIter != registeredTxList_.end(); + txIter++) + { + if(txIter->txIndex_ > UINT32_MAX/2) { - StoredTx & stx = iter->second; - registeredScrAddrScan_IterSafe(stx); + // The RegisteredTx was created before the chain was organized + txIter->blkNum_ = txIter->txRefObj_.getBlockHeight(); + txIter->txIndex_ = txIter->txRefObj_.getBlockTxIndex(); } - - // This will write out about once every 5 sec - writeProgressFile(DB_BUILD_SCAN, blkProgressFile_, "ScanBlockchain"); } - TIMER_STOP("ScanBlockchain"); -} + registeredTxList_.sort(); -//////////////////////////////////////////////////////////////////////////////// -// Deletes all SSH entries in the database -void BlockDataManager_LevelDB::deleteHistories(void) -{ - SCOPED_TIMER("deleteHistories"); + ///// LOOP OVER ALL RELEVANT TX //// + for(txIter = registeredTxList_.begin(); + txIter != registeredTxList_.end(); + txIter++) + { + // Pull the tx from disk and check it for the supplied wallet + Tx theTx = txIter->getTxCopy(); + if( !theTx.isInitialized() ) + { + LOGWARN << "***WARNING: How did we get a NULL tx?"; + continue; + } - iface_->seekTo(BLKDATA, DB_PREFIX_SCRIPT, BinaryData(0)); + BlockHeader* bhptr = getHeaderPtrForTx(theTx); + // This condition happens on invalid Tx (like invalid P2Pool coinbases) + if( bhptr==NULL ) + continue; - if(!iface_->dbIterIsValid(BLKDATA, DB_PREFIX_SCRIPT)) - return; + if( !bhptr->isMainBranch() ) + continue; - ////////// - iface_->startBatch(BLKDATA); + uint32_t thisBlk = bhptr->getBlockHeight(); + if(thisBlk < blkStart || thisBlk >= blkEnd) + continue; - do - { - BinaryData key = iface_->getIterKeyCopy(); + if( !isTxFinal(theTx) ) + continue; - if(key.getSize() == 0) - break; + // If we made it here, we want to scan this tx! + wlt.scanTx(theTx, txIter->txIndex_, bhptr->getTimestamp(), thisBlk, isMainWallet); + } + + wlt.sortLedger(); - if(key[0] != (uint8_t)DB_PREFIX_SCRIPT) - break; - iface_->deleteValue(BLKDATA, key); - - } while(iface_->advanceIterAndRead(BLKDATA, DB_PREFIX_SCRIPT)); + // We should clean up any dangling TxIOs in the wallet then rescan + if(zcEnabled_) + rescanWalletZeroConf(wlt); - ////////// - iface_->commitBatch(BLKDATA); + uint32_t topBlk = getTopBlockHeight(); + if(blkEnd > topBlk) + wlt.lastScanned_ = topBlk; + else if(blkEnd!=0) + wlt.lastScanned_ = blkEnd; } -//////////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::shutdownSaveScrAddrHistories(void) +///////////////////////////////////////////////////////////////////////////// +uint64_t BlockDataManager_LevelDB::getDBBalanceForHash160( + BinaryDataRef addr160) { - LOGINFO << "Saving wallet history for next load"; + StoredScriptHistory ssh; - iface_->startBatch(BLKDATA); + iface_->getStoredScriptHistory(ssh, HASH160PREFIX + addr160); + if(!ssh.isInitialized()) + return 0; - uint32_t i=0; - set::iterator wltIter; - for(wltIter = registeredWallets_.begin(); - wltIter != registeredWallets_.end(); - wltIter++) - { - for(uint32_t a=0; a<(*wltIter)->getNumScrAddr(); a++) - { - ScrAddrObj & scrAddr = (*wltIter)->getScrAddrObjByIndex(a); - BinaryData uniqKey = scrAddr.getScrAddr(); - - if(KEY_NOT_IN_MAP(uniqKey, registeredScrAddrMap_)) - { - LOGERR << "How does the wallet have a non-registered ScrAddr?"; - continue; - } - - RegisteredScrAddr & rsa = registeredScrAddrMap_[uniqKey]; - vector & txioList = scrAddr.getTxIOList(); + return ssh.getScriptBalance(); +} - StoredScriptHistory ssh; - ssh.uniqueKey_ = scrAddr.getScrAddr(); - ssh.version_ = ARMORY_DB_VERSION; - ssh.alreadyScannedUpToBlk_ = rsa.alreadyScannedUpToBlk_; - for(uint32_t t=0; tputStoredScriptHistory(ssh); - - } - } + iface_->getStoredScriptHistory(ssh, HASH160PREFIX + addr160); + if(!ssh.isInitialized()) + return 0; - iface_->commitBatch(BLKDATA); + return ssh.getScriptReceived(); } - -//////////////////////////////////////////////////////////////////////////////// -// This method checks whether your blk0001.dat file is bigger than it was when -// we first read in the blockchain. If so, we read the new data and add it to -// the memory pool. Return value is how many blocks were added. -// -// NOTE: You might want to check lastBlockWasReorg_ variable to know whether -// to expect some previously valid headers/txs to still be valid -// -uint32_t BlockDataManager_LevelDB::readBlkFileUpdate(void) +///////////////////////////////////////////////////////////////////////////// +vector BlockDataManager_LevelDB::getUTXOVectForHash160( + BinaryDataRef addr160) { - SCOPED_TIMER("readBlkFileUpdate"); + StoredScriptHistory ssh; + vector outVect(0); - // Make sure the file exists and is readable - string filename = blkFileList_[blkFileList_.size()-1]; + iface_->getStoredScriptHistory(ssh, HASH160PREFIX + addr160); + if(!ssh.isInitialized()) + return outVect; - uint64_t filesize = FILE_DOES_NOT_EXIST; - ifstream is(filename.c_str(), ios::in|ios::binary); - if(is.is_open()) - { - is.seekg(0, ios::end); - filesize = (size_t)is.tellg(); - } - - uint32_t prevTopBlk = getTopBlockHeight()+1; - uint64_t currBlkBytesToRead; - if( filesize == FILE_DOES_NOT_EXIST ) - { - LOGERR << "***ERROR: Cannot open " << filename.c_str(); - return 0; - } - else if((int64_t)filesize-(int64_t)endOfLastBlockByte_ < 8) - { - // This condition triggers if we hit the end of the file -- will - // usually only be triggered by Bitcoin-Qt/bitcoind pre-0.8 - currBlkBytesToRead = 0; - } - else + size_t numTxo = (size_t)ssh.totalTxioCount_; + outVect.reserve(numTxo); + map::iterator iterSubSSH; + map::iterator iterTxio; + for(iterSubSSH = ssh.subHistMap_.begin(); + iterSubSSH != ssh.subHistMap_.end(); + iterSubSSH++) { - // For post-0.8, the filesize will almost always be larger (padded). - // Keep checking where we expect to see magic bytes, we know we're - // at the end if we see zero-bytes instead. - uint64_t endOfNewLastBlock = endOfLastBlockByte_; - BinaryData fourBytes(4); - while((int64_t)filesize - (int64_t)endOfNewLastBlock >= 8) + StoredSubHistory & subSSH = iterSubSSH->second; + for(iterTxio = subSSH.txioSet_.begin(); + iterTxio != subSSH.txioSet_.end(); + iterTxio++) { - is.seekg(endOfNewLastBlock, ios::beg); - is.read((char*)fourBytes.getPtr(), 4); + TxIOPair & txio = iterTxio->second; + StoredTx stx; + BinaryData txKey = txio.getTxRefOfOutput().getDBKey(); + uint16_t txoIdx = txio.getIndexOfOutput(); + iface_->getStoredTx(stx, txKey); - if(fourBytes != MagicBytes_) - break; - else - { - is.read((char*)fourBytes.getPtr(), 4); - endOfNewLastBlock += READ_UINT32_LE((fourBytes.getPtr())) + 8; - } + StoredTxOut & stxo = stx.stxoMap_[txoIdx]; + if(stxo.isSpent()) + continue; + + UnspentTxOut utxo(stx.thisHash_, + txoIdx, + stx.blockHeight_, + txio.getValue(), + stx.stxoMap_[txoIdx].getScriptRef()); + + outVect.push_back(utxo); } - - currBlkBytesToRead = endOfNewLastBlock - endOfLastBlockByte_; } - - - // Check to see if there was a blkfile split, and we have to switch - // to tracking the new file.. this condition triggers about once a week - string nextFilename = BtcUtils::getBlkFilename(blkFileDir_, numBlkFiles_); - uint64_t nextBlkBytesToRead = BtcUtils::GetFileSize(nextFilename); - if(nextBlkBytesToRead == FILE_DOES_NOT_EXIST) - nextBlkBytesToRead = 0; - else - LOGINFO << "New block file split! " << nextFilename.c_str(); + return outVect; - // If there is no new data, no need to continue - if(currBlkBytesToRead==0 && nextBlkBytesToRead==0) - return 0; - - // Observe if everything was up to date when we started, because we're - // going to add new blockchain data and don't want to trigger a rescan - // if this is just a normal update. - uint32_t nextBlk = getTopBlockHeight() + 1; - bool prevRegisteredUpToDate = (allScannedUpToBlk_==nextBlk); - - // Pull in the remaining data in old/curr blkfile, and beginning of new - BinaryData newBlockDataRaw((size_t)(currBlkBytesToRead+nextBlkBytesToRead)); +} - // Seek to the beginning of the new data and read it - if(currBlkBytesToRead>0) - { - ifstream is(filename.c_str(), ios::in | ios::binary); - is.seekg(endOfLastBlockByte_, ios::beg); - is.read((char*)newBlockDataRaw.getPtr(), currBlkBytesToRead); - is.close(); - } +///////////////////////////////////////////////////////////////////////////// +vector BlockDataManager_LevelDB::getHistoryForScrAddr( + BinaryDataRef uniqKey, + bool withMultisig) +{ + StoredScriptHistory ssh; + iface_->getStoredScriptHistory(ssh, uniqKey); - // If a new block file exists, read that one too - // nextBlkBytesToRead will include up to 16 MB of padding if our gateway - // is a bitcoind/qt 0.8+ node. Either way, it will be easy to detect when - // we've reached the end of the real data, as long as there is no gap - // between the end of currBlk data and the start of newBlk data (there isn't) - if(nextBlkBytesToRead>0) + map::iterator iter; + iter = registeredScrAddrMap_.find(uniqKey); + if(ITER_IN_MAP(iter, registeredScrAddrMap_)) { - uint8_t* ptrNextData = newBlockDataRaw.getPtr() + currBlkBytesToRead; - ifstream is(nextFilename.c_str(), ios::in | ios::binary); - is.read((char*)ptrNextData, nextBlkBytesToRead); - is.close(); + iter->second.alreadyScannedUpToBlk_ = ssh.alreadyScannedUpToBlk_; } + + vector outVect(0); + if(!ssh.isInitialized()) + return outVect; - - // Walk through each of the new blocks, adding each one to RAM and DB - // Do a full update of everything after each block, for simplicity - // (which means we may be adding a couple blocks, the first of which - // may appear valid but orphaned by later blocks -- that's okay as - // we'll just reverse it when we add the later block -- this is simpler) - BinaryRefReader brr(newBlockDataRaw); - BinaryData fourBytes(4); - uint32_t nBlkRead = 0; - vector blockAddResults; - bool keepGoing = true; - while(keepGoing) + outVect.reserve((size_t)ssh.totalTxioCount_); + map::iterator iterSubSSH; + map::iterator iterTxio; + for(iterSubSSH = ssh.subHistMap_.begin(); + iterSubSSH != ssh.subHistMap_.end(); + iterSubSSH++) { - // We concatenated all data together, even if across two files - // Check which file data belongs to and set FileDataPtr appropriately - uint32_t useFileIndex0Idx = numBlkFiles_-1; - uint32_t bhOffset = (uint32_t)(endOfLastBlockByte_ + 8); - if(brr.getPosition() >= currBlkBytesToRead) + StoredSubHistory & subssh = iterSubSSH->second; + for(iterTxio = subssh.txioSet_.begin(); + iterTxio != subssh.txioSet_.end(); + iterTxio++) { - useFileIndex0Idx = numBlkFiles_; - bhOffset = (uint32_t)(brr.getPosition() - currBlkBytesToRead + 8); + TxIOPair & txio = iterTxio->second; + if(withMultisig || !txio.isMultisig()) + outVect.push_back(txio); } - - - //////////// - // The reader should be at the start of magic bytes of the new block - brr.get_BinaryData(fourBytes, 4); - if(fourBytes != MagicBytes_) - break; - - uint32_t nextBlockSize = brr.get_uint32_t(); + } + + return outVect; +} - blockAddResults = addNewBlockData(brr, - useFileIndex0Idx, - bhOffset, - nextBlockSize); - bool blockAddSucceeded = blockAddResults[ADD_BLOCK_SUCCEEDED ]; - bool blockIsNewTop = blockAddResults[ADD_BLOCK_NEW_TOP_BLOCK]; - bool blockchainReorg = blockAddResults[ADD_BLOCK_CAUSED_REORG ]; +///////////////////////////////////////////////////////////////////////////// +/* This is not currently being used, and is actually likely to change + * a bit before it is needed, so I have just disabled it. +vector BlockDataManager_LevelDB::findAllNonStdTx(void) +{ + PDEBUG("Finding all non-std tx"); + vector txVectOut(0); + uint32_t nHeaders = headersByHeight_.size(); - if(blockAddSucceeded) - nBlkRead++; + ///// LOOP OVER ALL HEADERS //// + for(uint32_t h=0; h const & txlist = bhr.getTxRefPtrList(); - if(blockchainReorg) + ///// LOOP OVER ALL TX ///// + for(uint32_t itx=0; itxserialize().toHexStr() << endl; + cout << "pprint: " << endl; + BtcUtils::pprintScript(txin.getScript()); + cout << endl; + } } - // Replaced this with the scanDBForRegisteredTx call outside the loop - //StoredHeader sbh; - //iface_->getStoredHeader(sbh, hgt, dup); - //map::iterator iter; - //for(iter = sbh.stxMap_.begin(); iter != sbh.stxMap_.end(); iter++) - //{ - //Tx regTx = iter->second.getTxCopy(); - //registeredScrAddrScan(regTx.getPtr(), regTx.getSize()); - //} + ///// LOOP OVER ALL TXOUT IN BLOCK ///// + for(uint32_t iout=0; ioutgetThisHash().toHexStr() + << ", " << txout.getIndex() << endl; + cout << "Raw Script: " << txout.getScript().toHexStr() << endl; + cout << "Raw Tx: " << txout.getParentTxPtr()->serialize().toHexStr() << endl; + cout << "pprint: " << endl; + BtcUtils::pprintScript(txout.getScript()); + cout << endl; + } + + } } - else + } + + PDEBUG("Done finding all non-std tx"); + return txVectOut; +} +*/ + +static bool scanFor(std::istream &in, const uint8_t * bytes, const unsigned len) +{ + unsigned matched=0; // how many bytes we've matched so far + std::vector ahead(len); // the bytes matched + + in.read((char*)&ahead.front(), len); + unsigned count = in.gcount(); + if (count < len) return false; + + unsigned offset=0; // the index mod len which we're in ahead + + do + { + bool found=true; + for (unsigned i=0; i < len; i++) { - LOGWARN << "Block data did not extend the main chain!"; - // New block was added -- didn't cause a reorg but it's not the - // new top block either (it's a fork block). We don't do anything - // at all until the reorg actually happens + if (ahead[(i+offset)%len] != bytes[i]) + { + found=false; + break; + } } + if (found) + return true; - if(brr.isEndOfStream() || brr.getSizeRemaining() < 8) - keepGoing = false; + ahead[offset++%len] = in.get(); + + } while (!in.eof()); + return false; +} + +///////////////////////////////////////////////////////////////////////////// +// With the LevelDB database integration, we now index all blockchain data +// by block height and index (tx index in block, txout index in tx). The +// only way to actually do that is to process the headers first, so that +// when we do read the block data the first time, we know how to put it +// into the DB. +// +// For now, we have no problem holding all the headers in RAM and organizing +// them all in one shot. But RAM-limited devices (say, if this was going +// to be ported to Android), may not be able to do even that, and may have +// to read and process the headers in batches. +bool BlockDataManager_LevelDB::extractHeadersInBlkFile(uint32_t fnum, + uint64_t startOffset) +{ + SCOPED_TIMER("extractHeadersInBlkFile"); + + missingBlockHeaderHashes_.clear(); + + string filename = blkFileList_[fnum]; + uint64_t filesize = BtcUtils::GetFileSize(filename); + if(filesize == FILE_DOES_NOT_EXIST) + { + LOGERR << "File does not exist: " << filename.c_str(); + return false; } - lastTopBlock_ = getTopBlockHeight()+1; + // This will trigger if this is the last blk file and no new blocks + if(filesize < startOffset) + return true; + - purgeZeroConfPool(); - scanDBForRegisteredTx(prevTopBlk, lastTopBlock_); + ifstream is(filename.c_str(), ios::in | ios::binary); + BinaryData fileMagic(4); + is.read((char*)(fileMagic.getPtr()), 4); + is.seekg(startOffset, ios::beg); - if(prevRegisteredUpToDate) + if( !(fileMagic == MagicBytes_ ) ) { - allScannedUpToBlk_ = getTopBlockHeight()+1; - updateRegisteredScrAddrs(allScannedUpToBlk_); + LOGERR << "Block file is the wrong network! MagicBytes: " + << fileMagic.toHexStr().c_str(); + return false; } - // If the blk file split, switch to tracking it - LOGINFO << "Added new blocks to memory pool: " << nBlkRead; - // If we pull non-zero amount of data from next block file...there - // was a blkfile split! - if(nextBlkBytesToRead>0) + BinaryData rawHeader(HEADER_SIZE); + + // Some objects to help insert header data efficiently + pair bhInputPair; + pair::iterator, bool> bhInsResult; + endOfLastBlockByte_ = startOffset; + + uint32_t const HEAD_AND_NTX_SZ = HEADER_SIZE + 10; // enough + BinaryData magic(4), szstr(4), rawHead(HEAD_AND_NTX_SZ); + while(!is.eof()) { - numBlkFiles_ += 1; - blkFileList_.push_back(nextFilename); - } + is.read((char*)magic.getPtr(), 4); + if (is.eof()) + break; + + if(magic!=MagicBytes_) + { + // I have to start scanning for MagicBytes + + BinaryData nulls( (const uint8_t*)"\0\0\0\0", 4); + + if (magic == nulls) + break; + + LOGERR << "Did not find block header in expected location, " + "possible corrupt data, searching for next block header."; + + if (!scanFor(is, MagicBytes_.getPtr(), MagicBytes_.getSize())) + { + LOGERR << "No more blocks found in file " << filename; + break; + } + + LOGERR << "Next block header found at offset " << uint64_t(is.tellg())-4; + } + + is.read((char*)szstr.getPtr(), 4); + uint32_t nextBlkSize = READ_UINT32_LE(szstr.getPtr()); + if(is.eof()) break; - #ifdef _DEBUG - UniversalTimer::instance().printCSV(string("timings.csv")); - #ifdef _DEBUG_FULL_VERBOSE - UniversalTimer::instance().printCSV(cout,true); - #endif - #endif + is.read((char*)rawHead.getPtr(), HEAD_AND_NTX_SZ); // plus #tx var_int + if(is.eof()) break; + // Create a reader for the entire block, grab header, skip rest + BinaryRefReader brr(rawHead); + bhInputPair.second.unserialize(brr); + uint32_t nTx = (uint32_t)brr.get_var_int(); + bhInputPair.first = bhInputPair.second.getThisHash(); + bhInsResult = headerMap_.insert(bhInputPair); + if(!bhInsResult.second) + { + // We exclude the genesis block which is always in the DB here + if(fnum!=0 || endOfLastBlockByte_!=0) + { + LOGWARN << "Somehow tried to add header that's already in map"; + LOGWARN << "Header Hash: " << bhInputPair.first.toHexStr().c_str(); + } + // But overwrite the header anyway + bhInsResult.first->second = bhInputPair.second; + } - return nBlkRead; + bhInsResult.first->second.setBlockFile(filename); + bhInsResult.first->second.setBlockFileNum(fnum); + bhInsResult.first->second.setBlockFileOffset(endOfLastBlockByte_); + bhInsResult.first->second.setNumTx(nTx); + bhInsResult.first->second.setBlockSize(nextBlkSize); + + endOfLastBlockByte_ += nextBlkSize+8; + is.seekg(nextBlkSize - HEAD_AND_NTX_SZ, ios::cur); + + // now check if the previous hash is in there + // (unless the previous hash is 0 + if (headerMap_.find(bhInputPair.second.getPrevHash()) == headerMap_.end() + && BtcUtils::EmptyHash_ != bhInputPair.second.getPrevHash()) + { + LOGWARN << "Block header " << bhInputPair.second.getThisHash().toHexStr() + << " refers to missing previous hash " + << bhInputPair.second.getPrevHash().toHexStr(); + + missingBlockHeaderHashes_.push_back(bhInputPair.second.getPrevHash()); + } + + } + is.close(); + return true; } -//////////////////////////////////////////////////////////////////////////////// -// BDM detects the reorg, but is wallet-agnostic so it can't update any wallets -// You have to call this yourself after you check whether the last organizeChain -// call indicated that a reorg happened -void BlockDataManager_LevelDB::updateWalletAfterReorg(BtcWallet & wlt) +///////////////////////////////////////////////////////////////////////////// +uint32_t BlockDataManager_LevelDB::detectAllBlkFiles(void) { - SCOPED_TIMER("updateWalletAfterReorg"); + SCOPED_TIMER("detectAllBlkFiles"); - // Fix the wallet's ledger - vector & ledg = wlt.getTxLedger(); - for(uint32_t i=0; i 0) - ledg[i].setValid(false); + string path = BtcUtils::getBlkFilename(blkFileDir_, numBlkFiles_); + uint64_t filesize = BtcUtils::GetFileSize(path); + if(filesize == FILE_DOES_NOT_EXIST) + break; - if(txJustAffected_.count(txHash) > 0) - ledg[i].changeBlkNum(getTxRefByHash(txHash).getBlockHeight()); + numBlkFiles_++; + blkFileList_.push_back(string(path)); + blkFileSizes_.push_back(filesize); + blkFileCumul_.push_back(totalBlockchainBytes_); + totalBlockchainBytes_ += filesize; } - // Now fix the individual address ledgers - for(uint32_t a=0; a & addrLedg = addr.getTxLedger(); - for(uint32_t i=0; i 0) - addrLedg[i].setValid(false); - - if(txJustAffected_.count(txHash) > 0) - addrLedg[i].changeBlkNum(getTxRefByHash(txHash).getBlockHeight()); - } + LOGERR << "Error finding blockchain files (blkXXXX.dat)"; + return 0; } + return numBlkFiles_; } ///////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::updateWalletsAfterReorg(vector wltvect) +bool BlockDataManager_LevelDB::processNewHeadersInBlkFiles(uint32_t fnumStart, + uint64_t startOffset) { - for(uint32_t i=0; i::iterator iter; + for(iter = headerMap_.begin(); iter != headerMap_.end(); iter++) + { + StoredHeader sbh; + sbh.createFromBlockHeader(iter->second); + uint8_t dup = iface_->putBareHeader(sbh); + iter->second.duplicateID_ = dup; // make sure headerMap_ and DB agree + } + + return prevTopBlkStillValid; } -///////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::updateWalletsAfterReorg(set wltset) +//////////////////////////////////////////////////////////////////////////////// +// We assume that all the addresses we care about have been registered with +// the BDM. Before, the BDM we would rescan the blockchain and use the method +// isMineBulkFilter() to extract all "RegisteredTx" which are all tx relevant +// to the list of "RegisteredScrAddr" objects. Now, the DB defaults to super- +// node mode and tracks all that for us on disk. So when we start up, rather +// than having to search the blockchain, we just look the StoredScriptHistory +// list for each of our "RegisteredScrAddr" objects, and then pull all the +// relevant tx from the database. After that, the BDM operates 99% identically +// to before. We just didn't have to do a full scan to fill the RegTx list +// +// In the future, we will use the StoredScriptHistory objects to directly fill +// the TxIOPair map -- all the data is tracked by the DB and we could pull it +// directly. But that would require reorganizing a ton of BDM code, and may +// be difficult to guarantee that all the previous functionality was there and +// working. This way, all of our previously-tested code remains mostly +// untouched +void BlockDataManager_LevelDB::fetchAllRegisteredScrAddrData(void) { - set::iterator iter; - for(iter = wltset.begin(); iter != wltset.end(); iter++) - updateWalletAfterReorg(**iter); + fetchAllRegisteredScrAddrData(registeredScrAddrMap_); } ///////////////////////////////////////////////////////////////////////////// -/* This was never actually used -bool BlockDataManager_LevelDB::verifyBlkFileIntegrity(void) +void BlockDataManager_LevelDB::fetchAllRegisteredScrAddrData( + BtcWallet & myWallet) { - SCOPED_TIMER("verifyBlkFileIntegrity"); - PDEBUG("Verifying blk0001.dat integrity"); + SCOPED_TIMER("fetchAllRegisteredScrAddrData"); - bool isGood = true; - map::iterator headIter; - for(headIter = headerMap_.begin(); - headIter != headerMap_.end(); - headIter++) + uint32_t numAddr = myWallet.getNumScrAddr(); + for(uint32_t s=0; ssecond; - bool thisHeaderIsGood = bhr.verifyIntegrity(); - if( !thisHeaderIsGood ) - { - cout << "Blockfile contains incorrect header or tx data:" << endl; - cout << " Block number: " << bhr.getBlockHeight() << endl; - cout << " Block hash (BE): " << endl; - cout << " " << bhr.getThisHash().copySwapEndian().toHexStr() << endl; - cout << " Num Tx : " << bhr.getNumTx() << endl; - //cout << " Tx Hash List: (compare to raw tx data on blockexplorer)" << endl; - //for(uint32_t t=0; tgetThisHash().copySwapEndian().toHexStr() << endl; - } - isGood = isGood && thisHeaderIsGood; + ScrAddrObj & scrAddrObj = myWallet.getScrAddrObjByIndex(s); + fetchAllRegisteredScrAddrData(scrAddrObj.getScrAddr()); } - return isGood; - PDEBUG("Done verifying blockfile integrity"); } -*/ +//////////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::fetchAllRegisteredScrAddrData( + map & addrMap) +{ + SCOPED_TIMER("fetchAllRegisteredScrAddrData"); + set::iterator wltIter; -///////////////////////////////////////////////////////////////////////////// -// Pass in a BRR that starts at the beginning of the serialized block, -// i.e. the first 80 bytes of this BRR is the blockheader -/* -bool BlockDataManager_LevelDB::parseNewBlock(BinaryRefReader & brr, - uint32_t fileIndex0Idx, - uint32_t thisHeaderOffset, - uint32_t blockSize) -{ - if(brr.getSizeRemaining() < blockSize || brr.isEndOfStream()) + for(wltIter = registeredWallets_.begin(); + wltIter != registeredWallets_.end(); + wltIter++) { - LOGERR << "***ERROR: parseNewBlock did not get enough data..."; - return false; + (*wltIter)->ignoreLastScanned_ = true; } - // Create the objects once that will be used for insertion - // (txInsResult always succeeds--because multimap--so only iterator returns) - static pair bhInputPair; - static pair::iterator, bool> bhInsResult; - - // Read the header and insert it into the map. - bhInputPair.second.unserialize(brr); - bhInputPair.first = bhInputPair.second.getThisHash(); - bhInsResult = headerMap_.insert(bhInputPair); - BlockHeader * bhptr = &(bhInsResult.first->second); - if(!bhInsResult.second) - *bhptr = bhInsResult.first->second; // overwrite it even if insert fails - - // Then put the bare header into the DB and get its duplicate ID. - StoredHeader sbh; - sbh.createFromBlockHeader(*bhptr); - uint8_t dup = iface_->putBareHeader(sbh); - bhptr->setDuplicateID(dup); - - // Regardless of whether this was a reorg, we have to add the raw block - // to the DB, but we don't apply it yet. - brr.rewind(HEADER_SIZE); - addRawBlockToDB(brr); - - // Note where we will start looking for the next block, later - endOfLastBlockByte_ = thisHeaderOffset + blockSize; - - // Read the #tx and fill in some header properties - uint8_t viSize; - uint32_t nTx = (uint32_t)brr.get_var_int(&viSize); + map::iterator iter; + for(iter = addrMap.begin(); iter != addrMap.end(); iter++) + fetchAllRegisteredScrAddrData(iter->second.uniqueKey_); +} - // The file offset of the first tx in this block is after the var_int - uint32_t txOffset = thisHeaderOffset + HEADER_SIZE + viSize; - // Read each of the Tx - //bhptr->txPtrList_.resize(nTx); - uint32_t txSize; - static vector offsetsIn; - static vector offsetsOut; - static BinaryData hashResult(32); +///////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::fetchAllRegisteredScrAddrData( + BinaryData const & scrAddr) +{ + vector hist = getHistoryForScrAddr(scrAddr); - for(uint32_t i=0; igetStoredTx(stx, txref.getDBKey()); + regTx = RegisteredTx(txref, stx.thisHash_, stx.blockHeight_, stx.txIndex_); + insertRegisteredTxIfNew(regTx); + registeredOutPoints_.insert(hist[i].getOutPoint()); - // Figure out, as quickly as possible, whether this tx has any relevance - // to any of the registered addresses. Again, using pointers... - registeredScrAddrScan(ptrToRawTx, txSize, &offsetsIn, &offsetsOut); + txref = hist[i].getTxRefOfInput(); + if(txref.isNull()) + continue; - // Prepare for the next tx. Manually advance brr since used ptr directly - txOffset += txSize; - brr.advance(txSize); + // If the coins were spent, also fetch the tx in which they were spent + iface_->getStoredTx(stx, txref.getDBKey()); + regTx = RegisteredTx(txref, stx.thisHash_, stx.blockHeight_, stx.txIndex_); + insertRegisteredTxIfNew(regTx); } - return true; } -*/ - -//////////////////////////////////////////////////////////////////////////////// -// This method returns three booleans: -// (1) Block data was added to memory pool successfully -// (2) New block added is at the top of the chain -// (3) Adding block data caused blockchain reorganization -vector BlockDataManager_LevelDB::addNewBlockData( - BinaryRefReader & brrRawBlock, - uint32_t fileIndex0Idx, - uint32_t thisHeaderOffset, - uint32_t blockSize) -{ - SCOPED_TIMER("addNewBlockData"); - uint8_t const * startPtr = brrRawBlock.getCurrPtr(); - HashString newHeadHash = BtcUtils::getHash256(startPtr, HEADER_SIZE); - - vector vb(3); - vb[ADD_BLOCK_SUCCEEDED] = false; // Added to memory pool - vb[ADD_BLOCK_NEW_TOP_BLOCK] = false; // New block is new top of chain - vb[ADD_BLOCK_CAUSED_REORG] = false; // Add caused reorganization - ///////////////////////////////////////////////////////////////////////////// - // This used to be in parseNewBlock(...) but relocated here because it's - // not duplicated anywhere, and during the upgrade to LevelDB I needed - // the code flow to be more linear in order to figure out how to put - // all the pieces together properly. I may refactor this code out into - // its own method again, later - if(brrRawBlock.getSizeRemaining() < blockSize || brrRawBlock.isEndOfStream()) +///////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::destroyAndResetDatabases(void) +{ + if(iface_ != NULL) { - LOGERR << "***ERROR: parseNewBlock did not get enough data..."; - return vb; + LOGWARN << "Destroying databases; will need to be rebuilt"; + iface_->destroyAndResetDatabases(); + return; } + LOGERR << "Attempted to destroy databases, but no DB interface set"; +} - // Create the objects once that will be used for insertion - // (txInsResult always succeeds--because multimap--so only iterator returns) - static pair bhInputPair; - static pair::iterator, bool> bhInsResult; - - // Read the header and insert it into the map. - bhInputPair.second.unserialize(brrRawBlock); - bhInputPair.first = bhInputPair.second.getThisHash(); - bhInsResult = headerMap_.insert(bhInputPair); - BlockHeader * bhptr = &(bhInsResult.first->second); - if(!bhInsResult.second) - *bhptr = bhInputPair.second; // overwrite it even if insert fails - - // Finally, let's re-assess the state of the blockchain with the new data - // Check the lastBlockWasReorg_ variable to see if there was a reorg - bool prevTopBlockStillValid = organizeChain(); - lastBlockWasReorg_ = !prevTopBlockStillValid; - // Then put the bare header into the DB and get its duplicate ID. - StoredHeader sbh; - sbh.createFromBlockHeader(*bhptr); - uint8_t dup = iface_->putBareHeader(sbh); - bhptr->setDuplicateID(dup); +///////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::doRebuildDatabases(void) +{ + LOGINFO << "Executing: doRebuildDatabases"; + buildAndScanDatabases(true, true, true, false); + // Rescan Rebuild !Fetch Initial +} - // Regardless of whether this was a reorg, we have to add the raw block - // to the DB, but we don't apply it yet. - brrRawBlock.rewind(HEADER_SIZE); - addRawBlockToDB(brrRawBlock); +///////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::doFullRescanRegardlessOfSync(void) +{ + LOGINFO << "Executing: doFullRescanRegardlessOfSync"; + buildAndScanDatabases(true, false, true, false); + // Rescan Rebuild !Fetch Initial +} - // Note where we will start looking for the next block, later - endOfLastBlockByte_ = thisHeaderOffset + blockSize; +///////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::doSyncIfNeeded(void) +{ + LOGINFO << "Executing: doSyncIfNeeded"; + buildAndScanDatabases(false, false, true, false); + // Rescan Rebuild !Fetch Initial +} - /* From parseNewBlock but not needed here in the new code - // Read the #tx and fill in some header properties - uint8_t viSize; - uint32_t nTx = (uint32_t)brrRawBlock.get_var_int(&viSize); +///////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::doInitialSyncOnLoad(void) +{ + LOGINFO << "Executing: doInitialSyncOnLoad"; + buildAndScanDatabases(false, false, false, true); + // Rescan Rebuild !Fetch Initial +} - // The file offset of the first tx in this block is after the var_int - uint32_t txOffset = thisHeaderOffset + HEADER_SIZE + viSize; +///////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::doInitialSyncOnLoad_Rescan(void) +{ + LOGINFO << "Executing: doInitialSyncOnLoad_Rescan"; + buildAndScanDatabases(true, false, false, true); + // Rescan Rebuild !Fetch Initial +} - // Read each of the Tx - //bhptr->txPtrList_.resize(nTx); - uint32_t txSize; - static vector offsetsIn; - static vector offsetsOut; - static BinaryData hashResult(32); +///////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::doInitialSyncOnLoad_Rebuild(void) +{ + LOGINFO << "Executing: doInitialSyncOnLoad_Rebuild"; + buildAndScanDatabases(false, true, true, true); + // Rescan Rebuild !Fetch Initial +} - for(uint32_t i=0; idatabasesAreOpen()) + initializeDBInterface(DBUtils.getArmoryDbType(), DBUtils.getDbPruneType()); - txSize = BtcUtils::TxCalcLength(ptrToRawTx, &offsetsIn, &offsetsOut); - BtcUtils::getHash256_NoSafetyCheck(ptrToRawTx, txSize, hashResult); + LOGDEBUG << "Called build&scan with (" + << (forceRescan ? 1 : 0) << "," + << (forceRebuild ? 1 : 0) << "," + << (skipFetch ? 1 : 0) << "," + << (initialLoad ? 1 : 0) << ")"; - // Figure out, as quickly as possible, whether this tx has any relevance - registeredScrAddrScan(ptrToRawTx, txSize, &offsetsIn, &offsetsOut); - // Prepare for the next tx. Manually advance brr since used ptr directly - txOffset += txSize; - brrRawBlock.advance(txSize); + // This will figure out where we should start reading headers, blocks, + // and where we should start applying or scanning + detectCurrentSyncState(forceRebuild, initialLoad); + + // If we're going to rebuild, might as well destroy the DB for good measure + if(forceRebuild || (startHeaderHgt_==0 && startRawBlkHgt_==0)) + { + LOGINFO << "Clearing databases for clean build"; + forceRebuild = true; + forceRescan = true; + skipFetch = true; + destroyAndResetDatabases(); } - return true; - */ + // If we're going to be rescanning, reset the wallets + if(forceRescan) + { + LOGINFO << "Resetting wallets for rescan"; + skipFetch = true; + deleteHistories(); + resetRegisteredWallets(); + } - // Since this method only adds one block, if it's not on the main branch, - // then it's not the new head - bool newBlockIsNewTop = getHeaderByHash(newHeadHash)->isMainBranch(); + // If no rescan is forced, grab the SSH entries from the DB + if(!skipFetch && initialLoad) + { + LOGINFO << "Fetching stored script histories from DB"; + fetchAllRegisteredScrAddrData(); + } - // This method passes out 3 booleans - vb[ADD_BLOCK_SUCCEEDED] = true; - vb[ADD_BLOCK_NEW_TOP_BLOCK] = newBlockIsNewTop; - vb[ADD_BLOCK_CAUSED_REORG] = !prevTopBlockStillValid; - // We actually accessed the pointer directly in this method, without - // advancing the BRR position. But the outer function expects to see - // the new location we would've been at if the BRR was used directly. - brrRawBlock.advance(blockSize); - return vb; -} + // Remove this file +#ifndef _MSC_VER + if(BtcUtils::GetFileSize(blkProgressFile_) != FILE_DOES_NOT_EXIST) + remove(blkProgressFile_.c_str()); + if(BtcUtils::GetFileSize(abortLoadFile_) != FILE_DOES_NOT_EXIST) + remove(abortLoadFile_.c_str()); +#else + if(BtcUtils::GetFileSize(blkProgressFile_) != FILE_DOES_NOT_EXIST) + _wunlink(OS_TranslatePath(blkProgressFile_).c_str()); + if(BtcUtils::GetFileSize(abortLoadFile_) != FILE_DOES_NOT_EXIST) + _wunlink(OS_TranslatePath(abortLoadFile_).c_str()); +#endif + + if(!initialLoad) + detectAllBlkFiles(); // only need to spend time on this on the first call -// This piece may be useful for adding new data, but I don't want to enforce it, -// yet -/* -#ifndef _DEBUG - // In the real client, we want to execute these checks. But we may want - // to pass in hand-made data when debugging, and don't want to require - // the hand-made blocks to have leading zeros. - if(! (headHash.getSliceCopy(28,4) == BtcUtils::EmptyHash_.getSliceCopy(28,4))) + if(numBlkFiles_==0) { - cout << "***ERROR: header hash does not have leading zeros" << endl; - cerr << "***ERROR: header hash does not have leading zeros" << endl; - return true; // no data added, so no reorg + LOGERR << "No blockfiles could be found! Aborting..."; + return; } - // Same story with merkle roots in debug mode - HashString merkleRoot = BtcUtils::calculateMerkleRoot(txHashes); - if(! (merkleRoot == BinaryDataRef(rawHeader.getPtr() + 36, 32))) + if(GenesisHash_.getSize() == 0) { - cout << "***ERROR: merkle root does not match header data" << endl; - cerr << "***ERROR: merkle root does not match header data" << endl; - return true; // no data added, so no reorg + LOGERR << "***ERROR: Set net params before loading blockchain!"; + return; } -#endif -*/ - + ///////////////////////////////////////////////////////////////////////////// + // New with LevelDB: must read and organize headers before handling the + // full blockchain data. We need to figure out the longest chain and write + // the headers to the DB before actually processing any block data. + if(initialLoad || forceRebuild) + { + LOGINFO << "Reading all headers and building chain..."; + processNewHeadersInBlkFiles(startHeaderBlkFile_, startHeaderOffset_); + } -//////////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::reassessAfterReorg( BlockHeader* oldTopPtr, - BlockHeader* newTopPtr, - BlockHeader* branchPtr) -{ - SCOPED_TIMER("reassessAfterReorg"); - LOGINFO << "Reassessing Tx validity after reorg"; + LOGINFO << "Total number of blk*.dat files: " << numBlkFiles_; + LOGINFO << "Total number of blocks found: " << getTopBlockHeight() + 1; - // Walk down invalidated chain first, until we get to the branch point - // Mark transactions as invalid - txJustInvalidated_.clear(); - txJustAffected_.clear(); - BlockHeader* thisHeaderPtr = oldTopPtr; - LOGINFO << "Invalidating old-chain transactions..."; - while(thisHeaderPtr != branchPtr) - { - uint32_t hgt = thisHeaderPtr->getBlockHeight(); - uint8_t dup = thisHeaderPtr->getDuplicateID(); + ///////////////////////////////////////////////////////////////////////////// + // Now we start the meat of this process... - if(DBUtils.getArmoryDbType() != ARMORY_DB_BARE) - { - // Added with leveldb... in addition to reversing blocks in RAM, - // we also need to undo the blocks in the DB - StoredUndoData sud; - createUndoDataFromBlock(hgt, dup, sud); - undoBlockFromDB(sud); - } - - StoredHeader sbh; - iface_->getStoredHeader(sbh, hgt, dup, true); + ///////////////////////////////////////////////////////////////////////////// + // Add the raw blocks from the blk*.dat files into the DB + blocksReadSoFar_ = 0; + bytesReadSoFar_ = 0; - // This is the original, tested, reorg code - previouslyValidBlockHeaderPtrs_.push_back(thisHeaderPtr); - for(uint32_t i=0; igetPrevHash()); + TIMER_STOP("dumpRawBlocksToDB"); } - // Walk down the newly-valid chain and mark transactions as valid. If - // a tx is in both chains, it will still be valid after this process - // UPDATE for LevelDB upgrade: - // This used to start from the new top block and walk down, but - // I need to apply the blocks in order, so I switched it to start - // from the branch point and walk up - thisHeaderPtr = branchPtr; // note branch block was not undone, skip it - LOGINFO << "Marking new-chain transactions valid..."; - while( thisHeaderPtr->getNextHash() != BtcUtils::EmptyHash_ && - thisHeaderPtr->getNextHash().getSize() > 0 ) - { - thisHeaderPtr = getHeaderByHash(thisHeaderPtr->getNextHash()); - uint32_t hgt = thisHeaderPtr->getBlockHeight(); - uint8_t dup = thisHeaderPtr->getDuplicateID(); - iface_->markBlockHeaderValid(hgt, dup); - StoredHeader sbh; - iface_->getStoredHeader(sbh, hgt, dup, true); - - if(DBUtils.getArmoryDbType() != ARMORY_DB_BARE) - applyBlockToDB(sbh); + double timeElapsed = TIMER_READ_SEC("dumpRawBlocksToDB"); + LOGINFO << "Processed " << blocksReadSoFar_ << " raw blocks DB (" + << (int)timeElapsed << " seconds)"; - for(uint32_t i=0; i576 ? startScanHgt_-576 : 0); + pair blkLoc = findFileAndOffsetForHgt(startScanHgt_); + startScanBlkFile_ = blkLoc.first; + startScanOffset_ = blkLoc.second; } - } - LOGWARN << "Done reassessing tx validity"; -} + LOGINFO << "Starting scan from block height: " << startScanHgt_; + scanDBForRegisteredTx(startScanHgt_); + LOGINFO << "Finished blockchain scan in " + << TIMER_READ_SEC("ScanBlockchain") << " seconds"; + } -//////////////////////////////////////////////////////////////////////////////// -vector BlockDataManager_LevelDB::getHeadersNotOnMainChain(void) -{ - SCOPED_TIMER("getHeadersNotOnMainChain"); - PDEBUG("Getting headers not on main chain"); - vector out(0); - map::iterator iter; - for(iter = headerMap_.begin(); - iter != headerMap_.end(); - iter++) - { - if( ! iter->second.isMainBranch() ) - out.push_back(&(iter->second)); + // If bare mode, we don't do + if(DBUtils.getArmoryDbType() != ARMORY_DB_BARE) + { + // In any DB type other than bare, we will be walking through the blocks + // and updating the spentness fields and script histories + applyBlockRangeToDB(startApplyHgt_, getTopBlockHeight()+1); } - PDEBUG("Getting headers not on main chain"); - return out; -} + // We need to maintain the physical size of all blkXXXX.dat files together + totalBlockchainBytes_ = bytesReadSoFar_; -//////////////////////////////////////////////////////////////////////////////// -// This returns false if our new main branch does not include the previous -// topBlock. If this returns false, that probably means that we have -// previously considered some blocks to be valid that no longer are valid. -// TODO: Figure out if there is an elegant way to deal with a forked -// blockchain containing two equal-length chains -bool BlockDataManager_LevelDB::organizeChain(bool forceRebuild) -{ - SCOPED_TIMER("organizeChain"); + // Update registered address list so we know what's already been scanned + lastTopBlock_ = getTopBlockHeight() + 1; + allScannedUpToBlk_ = lastTopBlock_; - // Why did this line not through an error? I left here to remind - // myself to go figure it out. - //LOGINFO << ("Organizing chain", (forceRebuild ? "w/ rebuild" : "")); - LOGDEBUG << "Organizing chain " << (forceRebuild ? "w/ rebuild" : ""); + LOGINFO << "Updating registered addresses"; + updateRegisteredScrAddrs(lastTopBlock_); - // If rebuild, we zero out any original organization data and do a - // rebuild of the chain from scratch. This will need to be done in - // the event that our first call to organizeChain returns false, which - // means part of blockchain that was previously valid, has become - // invalid. Rather than get fancy, just rebuild all which takes less - // than a second, anyway. - if(forceRebuild) - { - map::iterator iter; - for( iter = headerMap_.begin(); - iter != headerMap_.end(); - iter++) - { - iter->second.difficultySum_ = -1; - iter->second.blockHeight_ = 0; - iter->second.isFinishedCalc_ = false; - iter->second.nextHash_ = BtcUtils::EmptyHash_; - iter->second.isMainBranch_ = false; - } - topBlockPtr_ = NULL; - } + // Since loading takes so long, there's a good chance that new block data + // came in... let's get it. + readBlkFileUpdate(); + uint32_t nWallet = 0; - // Set genesis block - BlockHeader & genBlock = getGenesisBlock(); - genBlock.blockHeight_ = 0; - genBlock.difficultyDbl_ = 1.0; - genBlock.difficultySum_ = 1.0; - genBlock.isMainBranch_ = true; - genBlock.isOrphan_ = false; - genBlock.isFinishedCalc_ = true; - genBlock.isInitialized_ = true; + LOGINFO << "Scanning Wallets"; + set::iterator wltIter; + for(wltIter = registeredWallets_.begin(); + wltIter != registeredWallets_.end(); + wltIter++) + { + nWallet++; + BtcWallet* wlt = *wltIter; + if(forceRebuild || forceRescan || skipFetch) + wlt->ignoreLastScanned_ = true; - // If this is the first run, the topBlock is the genesis block - if(topBlockPtr_ == NULL) - topBlockPtr_ = &genBlock; + LOGINFO << "Scanning Wallet #" << nWallet << " from height " << (wlt->ignoreLastScanned_ ? 0 : wlt->lastScanned_); - // Store the old top block so we can later check whether it is included - // in the new chain organization - prevTopBlockPtr_ = topBlockPtr_; + scanRegisteredTxForWallet(*wlt, 0, lastTopBlock_); + } - // Iterate over all blocks, track the maximum difficulty-sum block - map::iterator iter; - double maxDiffSum = prevTopBlockPtr_->getDifficultySum(); - for( iter = headerMap_.begin(); iter != headerMap_.end(); iter ++) - { - // *** Walk down the chain following prevHash fields, until - // you find a "solved" block. Then walk back up and - // fill in the difficulty-sum values (do not set next- - // hash ptrs, as we don't know if this is the main branch) - // Method returns instantly if block is already "solved" - double thisDiffSum = traceChainDown(iter->second); + isInitialized_ = true; + purgeZeroConfPool(); - // If we hit orphans, we flag headers DB corruption - if(corruptHeadersDB_) - return false; + #ifdef _DEBUG + UniversalTimer::instance().printCSV(string("timings.csv")); + #ifdef _DEBUG_FULL_VERBOSE + UniversalTimer::instance().printCSV(cout,true); + #endif + #endif + + /* + for(iter = registeredScrAddrMap_.begin(); + iter != registeredScrAddrMap_.end(); + iter ++) + LOGINFO << "ScrAddr: " << iter->second.uniqueKey_.toHexStr().c_str() + << " " << iter->second.alreadyScannedUpToBlk_; + */ +} - - // Determine if this is the top block. If it's the same diffsum - // as the prev top block, don't do anything - if(thisDiffSum > maxDiffSum) +// search for the next byte in bsb that looks like it could be a block +bool BlockDataManager_LevelDB::scanForMagicBytes(BinaryStreamBuffer& bsb, uint32_t *bytesSkipped) const +{ + BinaryData firstFour(4); + if (bytesSkipped) *bytesSkipped=0; + + do + { + while (bsb.reader().getSizeRemaining() >= 4) { - maxDiffSum = thisDiffSum; - topBlockPtr_ = &(iter->second); + bsb.reader().get_BinaryData(firstFour, 4); + if(firstFour==MagicBytes_) + { + bsb.reader().rewind(4); + return true; + } + // try again at the very next byte + if (bytesSkipped) (*bytesSkipped)++; + bsb.reader().rewind(3); } - } + + } while (bsb.streamPull()); + + return false; +} - // Walk down the list one more time, set nextHash fields - // Also set headersByHeight_; - bool prevChainStillValid = (topBlockPtr_ == prevTopBlockPtr_); - topBlockPtr_->nextHash_ = BtcUtils::EmptyHash_; - BlockHeader* thisHeaderPtr = topBlockPtr_; - //headersByHeight_.reserve(topBlockPtr_->getBlockHeight()+32768); - headersByHeight_.resize(topBlockPtr_->getBlockHeight()+1); - while( !thisHeaderPtr->isFinishedCalc_ ) +//////////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::readRawBlocksInFile(uint32_t fnum, uint32_t foffset) +{ + string blkfile = blkFileList_[fnum]; + uint64_t filesize = BtcUtils::GetFileSize(blkfile); + string fsizestr = BtcUtils::numToStrWCommas(filesize); + LOGINFO << blkfile.c_str() << " is " << fsizestr.c_str() << " bytes"; + + // Open the file, and check the magic bytes on the first block + ifstream is(blkfile.c_str(), ios::in | ios::binary); + BinaryData fileMagic(4); + is.read((char*)(fileMagic.getPtr()), 4); + if( !(fileMagic == MagicBytes_ ) ) { - thisHeaderPtr->isFinishedCalc_ = true; - thisHeaderPtr->isMainBranch_ = true; - thisHeaderPtr->isOrphan_ = false; - headersByHeight_[thisHeaderPtr->getBlockHeight()] = thisHeaderPtr; + LOGERR << "Block file is the wrong network! MagicBytes: " + << fileMagic.toHexStr().c_str(); + } - // This loop not necessary anymore with the DB implementation - // We need to guarantee that the txs are pointing to the right block - // header, because they could've been linked to an invalidated block - //for(uint32_t i=0; igetTxRefPtrList().size(); i++) - //{ - //TxRef & tx = *(thisHeaderPtr->getTxRefPtrList()[i]); - //tx.setHeaderPtr(thisHeaderPtr); - //tx.setMainBranch(true); - //} + // Seek to the supplied offset + is.seekg(foffset, ios::beg); + + uint64_t dbUpdateSize=0; - HashString & childHash = thisHeaderPtr->thisHash_; - thisHeaderPtr = &(headerMap_[thisHeaderPtr->getPrevHash()]); - thisHeaderPtr->nextHash_ = childHash; + BinaryStreamBuffer bsb; + bsb.attachAsStreamBuffer(is, (uint32_t)filesize-foffset); - if(thisHeaderPtr == prevTopBlockPtr_) - prevChainStillValid = true; + bool alreadyRead8B = false; + uint32_t nextBlkSize; + bool isEOF = false; + BinaryData firstFour(4); - } - // Last header in the loop didn't get added (the genesis block on first run) - thisHeaderPtr->isMainBranch_ = true; - headersByHeight_[thisHeaderPtr->getBlockHeight()] = thisHeaderPtr; + // We use these two vars to stop parsing if we exceed the last header + // that was processed (a new block was added since we processed headers) + bool breakbreak = false; + uint32_t locInBlkFile = foffset; + iface_->startBatch(BLKDATA); - // Force a full rebuild to make sure everything is marked properly - // On a full rebuild, prevChainStillValid should ALWAYS be true - if( !prevChainStillValid ) + unsigned failedAttempts=0; + + // It turns out that this streambuffering is probably not helping, but + // it doesn't hurt either, so I'm leaving it alone + while(bsb.streamPull()) { - LOGWARN << "Reorg detected!"; - reorgBranchPoint_ = thisHeaderPtr; - - // There was a dangerous bug -- prevTopBlockPtr_ is set correctly - // RIGHT NOW, but won't be once I make the recursive call to organizeChain - // I need to save it now, and re-assign it after the organizeChain call. - // (I might consider finding a way to avoid this, but it's fine as-is) - BlockHeader* prevtopblk = prevTopBlockPtr_; - organizeChain(true); // force-rebuild blockchain (takes less than 1s) - prevTopBlockPtr_ = prevtopblk; - return false; + while(bsb.reader().getSizeRemaining() >= 8) + { + + if(!alreadyRead8B) + { + bsb.reader().get_BinaryData(firstFour, 4); + if(firstFour!=MagicBytes_) + { + isEOF = true; + break; + } + nextBlkSize = bsb.reader().get_uint32_t(); + bytesReadSoFar_ += 8; + locInBlkFile += 8; + } + + if(bsb.reader().getSizeRemaining() < nextBlkSize) + { + alreadyRead8B = true; + break; + } + alreadyRead8B = false; + + BinaryRefReader brr(bsb.reader().getCurrPtr(), nextBlkSize); + + try + { + addRawBlockToDB(brr); + } + catch (BlockDeserializingException &e) + { + LOGERR << e.what() << " (error encountered processing block at byte " + << locInBlkFile << " file " + << blkfile << ", blocksize " << nextBlkSize << ")"; + failedAttempts++; + + if (failedAttempts >= 4) + { + // It looks like this file is irredeemably corrupt + LOGERR << "Giving up searching " << blkfile + << " after having found 4 block headers with unparseable contents"; + breakbreak=true; + break; + } + + uint32_t bytesSkipped; + const bool next = scanForMagicBytes(bsb, &bytesSkipped); + if (!next) + { + LOGERR << "Could not find another block in the file"; + breakbreak=true; + break; + } + else + { + locInBlkFile += bytesSkipped; + LOGERR << "Found another block header at " << locInBlkFile; + } + + continue; + } + dbUpdateSize += nextBlkSize; + + if(dbUpdateSize>BlockWriteBatcher::UPDATE_BYTES_THRESH && iface_->isBatchOn(BLKDATA)) + { + dbUpdateSize = 0; + iface_->commitBatch(BLKDATA); + iface_->startBatch(BLKDATA); + } + + blocksReadSoFar_++; + bytesReadSoFar_ += nextBlkSize; + locInBlkFile += nextBlkSize; + bsb.reader().advance(nextBlkSize); + + // This is a hack of hacks, but I can't seem to pass this data + // out through getLoadProgress* methods, because they don't + // update properly (from the main python thread) when the BDM + // is actively loading/scanning in a separate thread. + // We'll watch for this file from the python code. + writeProgressFile(DB_BUILD_ADD_RAW, blkProgressFile_, "dumpRawBlocksToDB"); + + // Don't read past the last header we processed (in case new + // blocks were added since we processed the headers + if(fnum == numBlkFiles_-1 && locInBlkFile >= endOfLastBlockByte_) + { + breakbreak = true; + break; + } + } + + + if(isEOF || breakbreak) + break; } + - // Let the caller know that there was no reorg - LOGDEBUG << "Done organizing chain"; - return true; + if(iface_->isBatchOn(BLKDATA)) + iface_->commitBatch(BLKDATA); } -///////////////////////////////////////////////////////////////////////////// -// Start from a node, trace down to the highest solved block, accumulate -// difficulties and difficultySum values. Return the difficultySum of -// this block. -double BlockDataManager_LevelDB::traceChainDown(BlockHeader & bhpStart) +//////////////////////////////////////////////////////////////////////////////// +StoredHeader BlockDataManager_LevelDB::getBlockFromDB(uint32_t hgt, uint8_t dup) { - if(bhpStart.difficultySum_ > 0) - return bhpStart.difficultySum_; + StoredHeader nullSBH; + StoredHeader returnSBH; - // Prepare some data structures for walking down the chain - vector headerPtrStack(headerMap_.size()); - vector difficultyStack(headerMap_.size()); - uint32_t blkIdx = 0; - double thisDiff; + LDBIter ldbIter = iface_->getIterator(BLKDATA); + BinaryData firstKey = DBUtils.getBlkDataKey(hgt, dup); - // Walk down the chain of prevHash_ values, until we find a block - // that has a definitive difficultySum value (i.e. >0). - BlockHeader* thisPtr = &bhpStart; - map::iterator iter; - while( thisPtr->difficultySum_ < 0) + if(!ldbIter.seekToExact(firstKey)) + return nullSBH; + + // Get the full block from the DB + iface_->readStoredBlockAtIter(ldbIter, returnSBH); + + if(returnSBH.blockHeight_ != hgt || returnSBH.duplicateID_ != dup) + return nullSBH; + + return returnSBH; + +} + +//////////////////////////////////////////////////////////////////////////////// +uint8_t BlockDataManager_LevelDB::getMainDupFromDB(uint32_t hgt) +{ + return iface_->getValidDupIDForHeight(hgt); +} + +//////////////////////////////////////////////////////////////////////////////// +StoredHeader BlockDataManager_LevelDB::getMainBlockFromDB(uint32_t hgt) +{ + uint8_t dupMain = iface_->getValidDupIDForHeight(hgt); + return getBlockFromDB(hgt, dupMain); +} + + +//////////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::scanDBForRegisteredTx(uint32_t blk0, + uint32_t blk1) +{ + SCOPED_TIMER("scanDBForRegisteredTx"); + bytesReadSoFar_ = 0; + + bool doScanProgressThing = (blk1-blk0 > NUM_BLKS_IS_DIRTY); + if(doScanProgressThing) { - thisDiff = thisPtr->difficultyDbl_; - difficultyStack[blkIdx] = thisDiff; - headerPtrStack[blkIdx] = thisPtr; - blkIdx++; + //if(BtcUtils::GetFileSize(bfile) != FILE_DOES_NOT_EXIST) + //remove(bfile.c_str()); + } - iter = headerMap_.find(thisPtr->getPrevHash()); - if(ITER_IN_MAP(iter, headerMap_)) - thisPtr = &(iter->second); - else + LDBIter ldbIter = iface_->getIterator(BLKDATA, BULK_SCAN); + BinaryData firstKey = DBUtils.getBlkDataKey(blk0, 0); + ldbIter.seekTo(firstKey); + + TIMER_START("ScanBlockchain"); + while(ldbIter.isValid(DB_PREFIX_TXDATA)) + { + // Get the full block from the DB + StoredHeader sbh; + iface_->readStoredBlockAtIter(ldbIter, sbh); + bytesReadSoFar_ += sbh.numBytes_; + + uint32_t hgt = sbh.blockHeight_; + uint8_t dup = sbh.duplicateID_; + uint8_t dupMain = iface_->getValidDupIDForHeight(hgt); + if(!sbh.isMainBranch_ || dup != dupMain) + continue; + + if(hgt >= blk1) + break; + + // If we're here, we need to check the tx for relevance to the + // global scrAddr list. Add to registered Tx map if so + map::iterator iter; + for(iter = sbh.stxMap_.begin(); + iter != sbh.stxMap_.end(); + iter++) { - // Under some circumstances, the headers DB is not getting written - // properly and triggering this code due to missing headers. For - // now, we simply avoid this condition by flagging the headers DB - // to be rebuilt. The bug probably has to do with batching of - // header data. - corruptHeadersDB_ = true; - return 0.0; - - // We didn't hit a known block, but we don't have this block's - // ancestor in the memory pool, so this is an orphan chain... - // at least temporarily - markOrphanChain(bhpStart); - return 0.0; + StoredTx & stx = iter->second; + registeredScrAddrScan_IterSafe(stx); } + + // This will write out about once every 5 sec + writeProgressFile(DB_BUILD_SCAN, blkProgressFile_, "ScanBlockchain"); } + TIMER_STOP("ScanBlockchain"); +} +//////////////////////////////////////////////////////////////////////////////// +// Deletes all SSH entries in the database +void BlockDataManager_LevelDB::deleteHistories(void) +{ + SCOPED_TIMER("deleteHistories"); - // Now we have a stack of difficulties and pointers. Walk back up - // (by pointer) and accumulate the difficulty values - double seedDiffSum = thisPtr->difficultySum_; - uint32_t blkHeight = thisPtr->blockHeight_; - for(int32_t i=blkIdx-1; i>=0; i--) + LDBIter ldbIter = iface_->getIterator(BLKDATA); + + if(!ldbIter.seekToStartsWith(DB_PREFIX_SCRIPT, BinaryData(0))) + return; + + ////////// + iface_->startBatch(BLKDATA); + + do { - seedDiffSum += difficultyStack[i]; - blkHeight++; - thisPtr = headerPtrStack[i]; - thisPtr->difficultyDbl_ = difficultyStack[i]; - thisPtr->difficultySum_ = seedDiffSum; - thisPtr->blockHeight_ = blkHeight; + BinaryData key = ldbIter.getKey(); + + if(key.getSize() == 0) + break; + + if(key[0] != (uint8_t)DB_PREFIX_SCRIPT) + break; + + iface_->deleteValue(BLKDATA, key); + + } while(ldbIter.advanceAndRead(DB_PREFIX_SCRIPT)); + + ////////// + iface_->commitBatch(BLKDATA); +} + + +//////////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::saveScrAddrHistories(void) +{ + LOGINFO << "Saving wallet history to DB"; + + if(DBUtils.getArmoryDbType() != ARMORY_DB_BARE) + { + LOGERR << "Should only use saveScrAddrHistories in ARMORY_DB_BARE mode"; + LOGERR << "Aborting save operation."; + return; } - - // Finally, we have all the difficulty sums calculated, return this one - return bhpStart.difficultySum_; - + + iface_->startBatch(BLKDATA); + + uint32_t i=0; + set::iterator wltIter; + for(wltIter = registeredWallets_.begin(); + wltIter != registeredWallets_.end(); + wltIter++) + { + for(uint32_t a=0; a<(*wltIter)->getNumScrAddr(); a++) + { + ScrAddrObj & scrAddr = (*wltIter)->getScrAddrObjByIndex(a); + BinaryData uniqKey = scrAddr.getScrAddr(); + + if(KEY_NOT_IN_MAP(uniqKey, registeredScrAddrMap_)) + { + LOGERR << "How does the wallet have a non-registered ScrAddr?"; + LOGERR << uniqKey.toHexStr().c_str(); + continue; + } + + RegisteredScrAddr & rsa = registeredScrAddrMap_[uniqKey]; + vector & txioList = scrAddr.getTxIOList(); + + StoredScriptHistory ssh; + ssh.uniqueKey_ = scrAddr.getScrAddr(); + ssh.version_ = ARMORY_DB_VERSION; + ssh.alreadyScannedUpToBlk_ = rsa.alreadyScannedUpToBlk_; + for(uint32_t t=0; tputStoredScriptHistory(ssh); + + } + } + + iface_->commitBatch(BLKDATA); } -///////////////////////////////////////////////////////////////////////////// -// In practice, orphan chains shouldn't ever happen. It means that there's -// a block in our database that doesn't trace down to the genesis block. -// Currently, we get our blocks from Bitcoin-Qt/bitcoind which is incapable -// of passing such blocks to us (or putting them in the blk*.dat files), so -// if this function gets called, it's most likely in error. -void BlockDataManager_LevelDB::markOrphanChain(BlockHeader & bhpStart) +//////////////////////////////////////////////////////////////////////////////// +// This method checks whether your blk0001.dat file is bigger than it was when +// we first read in the blockchain. If so, we read the new data and add it to +// the memory pool. Return value is how many blocks were added. +// +// NOTE: You might want to check lastBlockWasReorg_ variable to know whether +// to expect some previously valid headers/txs to still be valid +// +uint32_t BlockDataManager_LevelDB::readBlkFileUpdate(void) { - // TODO: This method was written 18 months ago, and appeared to have - // a bug in it when I revisited it. Not sure the bug was real - // but I attempted to fix it. This note is to remind you/me - // to check the old version of this method if any problems - // crop up. - LOGWARN << "Marking orphan chain"; - map::iterator iter; - iter = headerMap_.find(bhpStart.getThisHash()); - HashStringRef lastHeadHash; - while( ITER_IN_MAP(iter, headerMap_) ) + SCOPED_TIMER("readBlkFileUpdate"); + + // Make sure the file exists and is readable + string filename = blkFileList_[blkFileList_.size()-1]; + + uint64_t filesize = FILE_DOES_NOT_EXIST; + ifstream is(OS_TranslatePath(filename).c_str(), ios::in|ios::binary); + if(is.is_open()) { - // I don't see how it's possible to have a header that used to be - // in the main branch, but is now an ORPHAN (meaning it has no - // parent). It will be good to detect this case, though - if(iter->second.isMainBranch() == true) + is.seekg(0, ios::end); + filesize = (size_t)is.tellg(); + } + + uint32_t prevTopBlk = getTopBlockHeight()+1; + uint64_t currBlkBytesToRead; + + if( filesize == FILE_DOES_NOT_EXIST ) + { + LOGERR << "***ERROR: Cannot open " << filename.c_str(); + return 0; + } + else if((int64_t)filesize-(int64_t)endOfLastBlockByte_ < 8) + { + // This condition triggers if we hit the end of the file -- will + // usually only be triggered by Bitcoin-Qt/bitcoind pre-0.8 + currBlkBytesToRead = 0; + } + else + { + // For post-0.8, the filesize will almost always be larger (padded). + // Keep checking where we expect to see magic bytes, we know we're + // at the end if we see zero-bytes instead. + uint64_t endOfNewLastBlock = endOfLastBlockByte_; + BinaryData fourBytes(4); + while((int64_t)filesize - (int64_t)endOfNewLastBlock >= 8) + { + is.seekg(endOfNewLastBlock, ios::beg); + is.read((char*)fourBytes.getPtr(), 4); + + if(fourBytes != MagicBytes_) + break; + else + { + is.read((char*)fourBytes.getPtr(), 4); + endOfNewLastBlock += READ_UINT32_LE((fourBytes.getPtr())) + 8; + } + } + + currBlkBytesToRead = endOfNewLastBlock - endOfLastBlockByte_; + } + + + // Check to see if there was a blkfile split, and we have to switch + // to tracking the new file.. this condition triggers about once a week + string nextFilename = BtcUtils::getBlkFilename(blkFileDir_, numBlkFiles_); + uint64_t nextBlkBytesToRead = BtcUtils::GetFileSize(nextFilename); + if(nextBlkBytesToRead == FILE_DOES_NOT_EXIST) + nextBlkBytesToRead = 0; + else + LOGINFO << "New block file split! " << nextFilename.c_str(); + + + // If there is no new data, no need to continue + if(currBlkBytesToRead==0 && nextBlkBytesToRead==0) + return 0; + + // Observe if everything was up to date when we started, because we're + // going to add new blockchain data and don't want to trigger a rescan + // if this is just a normal update. + const uint32_t nextBlk = getTopBlockHeight() + 1; + const bool prevRegisteredUpToDate = (allScannedUpToBlk_==nextBlk); + + // Pull in the remaining data in old/curr blkfile, and beginning of new + BinaryData newBlockDataRaw((size_t)(currBlkBytesToRead+nextBlkBytesToRead)); + + // Seek to the beginning of the new data and read it + if(currBlkBytesToRead>0) + { + ifstream is(filename.c_str(), ios::in | ios::binary); + is.seekg(endOfLastBlockByte_, ios::beg); + is.read((char*)newBlockDataRaw.getPtr(), currBlkBytesToRead); + is.close(); + } + + // If a new block file exists, read that one too + // nextBlkBytesToRead will include up to 16 MB of padding if our gateway + // is a bitcoind/qt 0.8+ node. Either way, it will be easy to detect when + // we've reached the end of the real data, as long as there is no gap + // between the end of currBlk data and the start of newBlk data (there isn't) + if(nextBlkBytesToRead>0) + { + uint8_t* ptrNextData = newBlockDataRaw.getPtr() + currBlkBytesToRead; + ifstream is(nextFilename.c_str(), ios::in | ios::binary); + is.read((char*)ptrNextData, nextBlkBytesToRead); + is.close(); + } + + + // Walk through each of the new blocks, adding each one to RAM and DB + // Do a full update of everything after each block, for simplicity + // (which means we may be adding a couple blocks, the first of which + // may appear valid but orphaned by later blocks -- that's okay as + // we'll just reverse it when we add the later block -- this is simpler) + BinaryRefReader brr(newBlockDataRaw); + BinaryData fourBytes(4); + uint32_t nBlkRead = 0; + vector blockAddResults; + bool keepGoing = true; + while(keepGoing) + { + // We concatenated all data together, even if across two files + // Check which file data belongs to and set FileDataPtr appropriately + uint32_t useFileIndex0Idx = numBlkFiles_-1; + uint32_t bhOffset = (uint32_t)(endOfLastBlockByte_ + 8); + if(brr.getPosition() >= currBlkBytesToRead) + { + useFileIndex0Idx = numBlkFiles_; + bhOffset = (uint32_t)(brr.getPosition() - currBlkBytesToRead + 8); + } + + + //////////// + // The reader should be at the start of magic bytes of the new block + brr.get_BinaryData(fourBytes, 4); + if(fourBytes != MagicBytes_) + break; + + uint32_t nextBlockSize = brr.get_uint32_t(); + + blockAddResults = addNewBlockData(brr, + useFileIndex0Idx, + bhOffset, + nextBlockSize); + + bool blockAddSucceeded = blockAddResults[ADD_BLOCK_SUCCEEDED ]; + bool blockIsNewTop = blockAddResults[ADD_BLOCK_NEW_TOP_BLOCK]; + bool blockchainReorg = blockAddResults[ADD_BLOCK_CAUSED_REORG ]; + + if(blockAddSucceeded) + nBlkRead++; + + if(blockchainReorg) + { + LOGWARN << "Blockchain Reorganization detected!"; + reassessAfterReorg(prevTopBlockPtr_, topBlockPtr_, reorgBranchPoint_); + purgeZeroConfPool(); + + // Update all the registered wallets... + updateWalletsAfterReorg(registeredWallets_); + } + else if(blockIsNewTop) + { + BlockHeader & bh = getTopBlockHeader(); + uint32_t hgt = bh.getBlockHeight(); + uint8_t dup = bh.getDuplicateID(); + + if(DBUtils.getArmoryDbType() != ARMORY_DB_BARE) + { + LOGINFO << "Applying block to DB!"; + BlockWriteBatcher batcher(iface_); + batcher.applyBlockToDB(hgt, dup); + } + + // Replaced this with the scanDBForRegisteredTx call outside the loop + //StoredHeader sbh; + //iface_->getStoredHeader(sbh, hgt, dup); + //map::iterator iter; + //for(iter = sbh.stxMap_.begin(); iter != sbh.stxMap_.end(); iter++) + //{ + //Tx regTx = iter->second.getTxCopy(); + //registeredScrAddrScan(regTx.getPtr(), regTx.getSize()); + //} + } + else { - // NOTE: this actually gets triggered when we scan the testnet - // blk0001.dat file on main net, etc - LOGERR << "Block previously main branch, now orphan!?"; - previouslyValidBlockHeaderPtrs_.push_back(&(iter->second)); + LOGWARN << "Block data did not extend the main chain!"; + // New block was added -- didn't cause a reorg but it's not the + // new top block either (it's a fork block). We don't do anything + // at all until the reorg actually happens } - iter->second.isOrphan_ = true; - iter->second.isMainBranch_ = false; - lastHeadHash = iter->second.thisHash_.getRef(); - iter = headerMap_.find(iter->second.getPrevHash()); + + if(brr.isEndOfStream() || brr.getSizeRemaining() < 8) + keepGoing = false; } - orphanChainStartBlocks_.push_back(&(headerMap_[lastHeadHash.copy()])); - LOGWARN << "Done marking orphan chain"; -} + lastTopBlock_ = getTopBlockHeight()+1; -//////////////////////////////////////////////////////////////////////////////// -// We're going to need the BDM's help to get the sender for a TxIn since it -// sometimes requires going and finding the TxOut from the distant past -//////////////////////////////////////////////////////////////////////////////// -TxOut BlockDataManager_LevelDB::getPrevTxOut(TxIn & txin) -{ - if(txin.isCoinbase()) - return TxOut(); + purgeZeroConfPool(); + scanDBForRegisteredTx(prevTopBlk, lastTopBlock_); - OutPoint op = txin.getOutPoint(); - Tx theTx = getTxByHash(op.getTxHash()); - uint32_t idx = op.getTxOutIndex(); - return theTx.getTxOutCopy(idx); -} + if(prevRegisteredUpToDate) + { + allScannedUpToBlk_ = getTopBlockHeight()+1; + updateRegisteredScrAddrs(allScannedUpToBlk_); + } -//////////////////////////////////////////////////////////////////////////////// -// We're going to need the BDM's help to get the sender for a TxIn since it -// sometimes requires going and finding the TxOut from the distant past -//////////////////////////////////////////////////////////////////////////////// -Tx BlockDataManager_LevelDB::getPrevTx(TxIn & txin) -{ - if(txin.isCoinbase()) - return Tx(); + // If the blk file split, switch to tracking it + LOGINFO << "Added new blocks to memory pool: " << nBlkRead; - OutPoint op = txin.getOutPoint(); - return getTxByHash(op.getTxHash()); -} + // If we pull non-zero amount of data from next block file...there + // was a blkfile split! + if(nextBlkBytesToRead>0) + { + numBlkFiles_ += 1; + blkFileList_.push_back(nextFilename); + } -//////////////////////////////////////////////////////////////////////////////// -HashString BlockDataManager_LevelDB::getSenderScrAddr(TxIn & txin) -{ - if(txin.isCoinbase()) - return HashString(0); + #ifdef _DEBUG + UniversalTimer::instance().printCSV(string("timings.csv")); + #ifdef _DEBUG_FULL_VERBOSE + UniversalTimer::instance().printCSV(cout,true); + #endif + #endif + + return nBlkRead; - return getPrevTxOut(txin).getScrAddressStr(); } //////////////////////////////////////////////////////////////////////////////// -int64_t BlockDataManager_LevelDB::getSentValue(TxIn & txin) +// BDM detects the reorg, but is wallet-agnostic so it can't update any wallets +// You have to call this yourself after you check whether the last organizeChain +// call indicated that a reorg happened +void BlockDataManager_LevelDB::updateWalletAfterReorg(BtcWallet & wlt) { - if(txin.isCoinbase()) - return -1; - - return getPrevTxOut(txin).getValue(); - -} + SCOPED_TIMER("updateWalletAfterReorg"); + uint32_t changeToBlkNum; + // Fix the wallet's ledger + vector & ledg = wlt.getTxLedger(); + for(uint32_t i=0; i 0) + ledg[i].setValid(false); -//////////////////////////////////////////////////////////////////////////////// -BlockHeader* BlockDataManager_LevelDB::getHeaderPtrForTxRef(TxRef txr) -{ - if(txr.isNull()) - return NULL; + if(txJustAffected_.count(txHash) > 0) + ledg[i].changeBlkNum(getTxRefByHash(txHash).getBlockHeight()); + } - uint32_t hgt = txr.getBlockHeight(); - uint8_t dup = txr.getDuplicateID(); - BlockHeader* bhptr = headersByHeight_[hgt]; - if(bhptr->getDuplicateID() != dup) + // Now fix the individual address ledgers + for(uint32_t a=0; a & addrLedg = addr.getTxLedger(); + for(uint32_t i=0; i 0) + addrLedg[i].setValid(false); + + if(txJustAffected_.count(txHash) > 0) + { + changeToBlkNum = getTxRefByHash(txHash).getBlockHeight(); + addrLedg[i].changeBlkNum(changeToBlkNum); + + wlt.reorgChangeBlkNum(changeToBlkNum); + } + } } - return bhptr; } -//////////////////////////////////////////////////////////////////////////////// -BlockHeader* BlockDataManager_LevelDB::getHeaderPtrForTx(Tx & txObj) + +///////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::updateWalletsAfterReorg(vector wltvect) { - if(txObj.getTxRef().isNull()) - { - LOGERR << "TxRef in Tx object is not set, cannot get header ptr"; - return NULL; - } - - return getHeaderPtrForTxRef(txObj.getTxRef()); + for(uint32_t i=0; i wltset) { - SCOPED_TIMER("enableZeroConf"); - zcEnabled_ = true; - zcFilename_ = zcFilename; - - readZeroConfFile(zcFilename_); // does nothing if DNE + set::iterator iter; + for(iter = wltset.begin(); iter != wltset.end(); iter++) + updateWalletAfterReorg(**iter); } -//////////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::readZeroConfFile(string zcFilename) +///////////////////////////////////////////////////////////////////////////// +/* This was never actually used +bool BlockDataManager_LevelDB::verifyBlkFileIntegrity(void) { - SCOPED_TIMER("readZeroConfFile"); - uint64_t filesize = BtcUtils::GetFileSize(zcFilename); - if(filesize<8 || filesize==FILE_DOES_NOT_EXIST) - return; - - ifstream zcFile(zcFilename_.c_str(), ios::in | ios::binary); - BinaryData zcData((size_t)filesize); - zcFile.read((char*)zcData.getPtr(), filesize); - zcFile.close(); + SCOPED_TIMER("verifyBlkFileIntegrity"); + PDEBUG("Verifying blk0001.dat integrity"); - // We succeeded opening the file... - BinaryRefReader brr(zcData); - while(brr.getSizeRemaining() > 8) + bool isGood = true; + map::iterator headIter; + for(headIter = headerMap_.begin(); + headIter != headerMap_.end(); + headIter++) { - uint64_t txTime = brr.get_uint64_t(); - uint32_t txSize = BtcUtils::TxCalcLength(brr.getCurrPtr()); - BinaryData rawtx(txSize); - brr.get_BinaryData(rawtx.getPtr(), txSize); - addNewZeroConfTx(rawtx, (uint32_t)txTime, false); + BlockHeader & bhr = headIter->second; + bool thisHeaderIsGood = bhr.verifyIntegrity(); + if( !thisHeaderIsGood ) + { + cout << "Blockfile contains incorrect header or tx data:" << endl; + cout << " Block number: " << bhr.getBlockHeight() << endl; + cout << " Block hash (BE): " << endl; + cout << " " << bhr.getThisHash().copySwapEndian().toHexStr() << endl; + cout << " Num Tx : " << bhr.getNumTx() << endl; + //cout << " Tx Hash List: (compare to raw tx data on blockexplorer)" << endl; + //for(uint32_t t=0; tgetThisHash().copySwapEndian().toHexStr() << endl; + } + isGood = isGood && thisHeaderIsGood; } - purgeZeroConfPool(); + return isGood; + PDEBUG("Done verifying blockfile integrity"); } +*/ -//////////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::disableZeroConf(string zcFilename) -{ - SCOPED_TIMER("disableZeroConf"); - zcEnabled_ = false; -} -//////////////////////////////////////////////////////////////////////////////// -bool BlockDataManager_LevelDB::addNewZeroConfTx(BinaryData const & rawTx, - uint32_t txtime, - bool writeToFile) +///////////////////////////////////////////////////////////////////////////// +// Pass in a BRR that starts at the beginning of the serialized block, +// i.e. the first 80 bytes of this BRR is the blockheader +/* +bool BlockDataManager_LevelDB::parseNewBlock(BinaryRefReader & brr, + uint32_t fileIndex0Idx, + uint32_t thisHeaderOffset, + uint32_t blockSize) { - SCOPED_TIMER("addNewZeroConfTx"); - // TODO: We should do some kind of verification check on this tx - // to make sure it's potentially valid. Right now, it doesn't - // matter, because the Satoshi client is sitting between - // us and the network and doing the checking for us. - - if(txtime==0) - txtime = (uint32_t)time(NULL); - - HashString txHash = BtcUtils::getHash256(rawTx); - - // If this is already in the zero-conf map or in the blockchain, ignore it - //if(KEY_IN_MAP(txHash, zeroConfMap_) || !getTxRefByHash(txHash).isNull()) - if(hasTxWithHash(txHash)) + if(brr.getSizeRemaining() < blockSize || brr.isEndOfStream()) + { + LOGERR << "***ERROR: parseNewBlock did not get enough data..."; return false; + } + + // Create the objects once that will be used for insertion + // (txInsResult always succeeds--because multimap--so only iterator returns) + static pair bhInputPair; + static pair::iterator, bool> bhInsResult; - - zeroConfMap_[txHash] = ZeroConfData(); - ZeroConfData & zc = zeroConfMap_[txHash]; - zc.iter_ = zeroConfRawTxList_.insert(zeroConfRawTxList_.end(), rawTx); - zc.txobj_.unserialize(*(zc.iter_)); - zc.txtime_ = txtime; + // Read the header and insert it into the map. + bhInputPair.second.unserialize(brr); + bhInputPair.first = bhInputPair.second.getThisHash(); + bhInsResult = headerMap_.insert(bhInputPair); + BlockHeader * bhptr = &(bhInsResult.first->second); + if(!bhInsResult.second) + *bhptr = bhInsResult.first->second; // overwrite it even if insert fails - // Record time. Write to file - if(writeToFile) - { - ofstream zcFile(zcFilename_.c_str(), ios::app | ios::binary); - zcFile.write( (char*)(&zc.txtime_), sizeof(uint64_t) ); - zcFile.write( (char*)zc.txobj_.getPtr(), zc.txobj_.getSize()); - zcFile.close(); - } - return true; -} + // Then put the bare header into the DB and get its duplicate ID. + StoredHeader sbh; + sbh.createFromBlockHeader(*bhptr); + uint8_t dup = iface_->putBareHeader(sbh); + bhptr->setDuplicateID(dup); + // Regardless of whether this was a reorg, we have to add the raw block + // to the DB, but we don't apply it yet. + brr.rewind(HEADER_SIZE); + addRawBlockToDB(brr); + // Note where we will start looking for the next block, later + endOfLastBlockByte_ = thisHeaderOffset + blockSize; -//////////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::purgeZeroConfPool(void) -{ - SCOPED_TIMER("purgeZeroConfPool"); - list< map::iterator > mapRmList; + // Read the #tx and fill in some header properties + uint8_t viSize; + uint32_t nTx = (uint32_t)brr.get_var_int(&viSize); - // Find all zero-conf transactions that made it into the blockchain - map::iterator iter; - for(iter = zeroConfMap_.begin(); - iter != zeroConfMap_.end(); - iter++) - { - if(!getTxRefByHash(iter->first).isNull()) - mapRmList.push_back(iter); - } + // The file offset of the first tx in this block is after the var_int + uint32_t txOffset = thisHeaderOffset + HEADER_SIZE + viSize; - // We've made a list of the zc tx to remove, now let's remove them - // I decided this was safer than erasing the data as we were iterating - // over it in the previous loop - list< map::iterator >::iterator rmIter; - for(rmIter = mapRmList.begin(); - rmIter != mapRmList.end(); - rmIter++) + // Read each of the Tx + //bhptr->txPtrList_.resize(nTx); + uint32_t txSize; + static vector offsetsIn; + static vector offsetsOut; + static BinaryData hashResult(32); + + for(uint32_t i=0; isecond.iter_ ); - zeroConfMap_.erase( *rmIter ); - } + // We get a little funky here because I need to avoid ALL unnecessary + // copying -- therefore everything is pointers...and confusing... + uint8_t const * ptrToRawTx = brr.getCurrPtr(); + + txSize = BtcUtils::TxCalcLength(ptrToRawTx, &offsetsIn, &offsetsOut); + BtcUtils::getHash256_NoSafetyCheck(ptrToRawTx, txSize, hashResult); - // Rewrite the zero-conf pool file - if(mapRmList.size() > 0) - rewriteZeroConfFile(); + // Figure out, as quickly as possible, whether this tx has any relevance + // to any of the registered addresses. Again, using pointers... + registeredScrAddrScan(ptrToRawTx, txSize, &offsetsIn, &offsetsOut); + // Prepare for the next tx. Manually advance brr since used ptr directly + txOffset += txSize; + brr.advance(txSize); + } + return true; } +*/ + //////////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::rewriteZeroConfFile(void) +// This method returns three booleans: +// (1) Block data was added to memory pool successfully +// (2) New block added is at the top of the chain +// (3) Adding block data caused blockchain reorganization +vector BlockDataManager_LevelDB::addNewBlockData( + BinaryRefReader & brrRawBlock, + uint32_t fileIndex0Idx, + uint32_t thisHeaderOffset, + uint32_t blockSize) { - SCOPED_TIMER("rewriteZeroConfFile"); - ofstream zcFile(zcFilename_.c_str(), ios::out | ios::binary); + SCOPED_TIMER("addNewBlockData"); + uint8_t const * startPtr = brrRawBlock.getCurrPtr(); + HashString newHeadHash = BtcUtils::getHash256(startPtr, HEADER_SIZE); - static HashString txHash(32); - list::iterator iter; - for(iter = zeroConfRawTxList_.begin(); - iter != zeroConfRawTxList_.end(); - iter++) + vector vb(3); + vb[ADD_BLOCK_SUCCEEDED] = false; // Added to memory pool + vb[ADD_BLOCK_NEW_TOP_BLOCK] = false; // New block is new top of chain + vb[ADD_BLOCK_CAUSED_REORG] = false; // Add caused reorganization + + ///////////////////////////////////////////////////////////////////////////// + // This used to be in parseNewBlock(...) but relocated here because it's + // not duplicated anywhere, and during the upgrade to LevelDB I needed + // the code flow to be more linear in order to figure out how to put + // all the pieces together properly. I may refactor this code out into + // its own method again, later + if(brrRawBlock.getSizeRemaining() < blockSize || brrRawBlock.isEndOfStream()) { - BtcUtils::getHash256(*iter, txHash); - ZeroConfData & zcd = zeroConfMap_[txHash]; - zcFile.write( (char*)(&zcd.txtime_), sizeof(uint64_t) ); - zcFile.write( (char*)(zcd.txobj_.getPtr()), zcd.txobj_.getSize()); + LOGERR << "***ERROR: parseNewBlock did not get enough data..."; + return vb; } - zcFile.close(); - -} + // Create the objects once that will be used for insertion + // (txInsResult always succeeds--because multimap--so only iterator returns) + static pair bhInputPair; + static pair::iterator, bool> bhInsResult; + + // Read the header and insert it into the map. + bhInputPair.second.unserialize(brrRawBlock); + bhInputPair.first = bhInputPair.second.getThisHash(); + bhInsResult = headerMap_.insert(bhInputPair); + BlockHeader * bhptr = &(bhInsResult.first->second); + if(!bhInsResult.second) + *bhptr = bhInputPair.second; // overwrite it even if insert fails + // Finally, let's re-assess the state of the blockchain with the new data + // Check the lastBlockWasReorg_ variable to see if there was a reorg + bool prevTopBlockStillValid = organizeChain(); + lastBlockWasReorg_ = !prevTopBlockStillValid; -//////////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::rescanWalletZeroConf(BtcWallet & wlt) -{ - SCOPED_TIMER("rescanWalletZeroConf"); - // Clear the whole list, rebuild - wlt.clearZeroConfPool(); + // Then put the bare header into the DB and get its duplicate ID. + StoredHeader sbh; + sbh.createFromBlockHeader(*bhptr); + uint8_t dup = iface_->putBareHeader(sbh); + bhptr->setDuplicateID(dup); - static HashString txHash(32); - list::iterator iter; - for(iter = zeroConfRawTxList_.begin(); - iter != zeroConfRawTxList_.end(); - iter++) - { + // Regardless of whether this was a reorg, we have to add the raw block + // to the DB, but we don't apply it yet. + brrRawBlock.rewind(HEADER_SIZE); + addRawBlockToDB(brrRawBlock); - if(iter->getSize() == 0) - continue; + // Note where we will start looking for the next block, later + endOfLastBlockByte_ = thisHeaderOffset + blockSize; - BtcUtils::getHash256(*iter, txHash); - ZeroConfData & zcd = zeroConfMap_[txHash]; + /* From parseNewBlock but not needed here in the new code + // Read the #tx and fill in some header properties + uint8_t viSize; + uint32_t nTx = (uint32_t)brrRawBlock.get_var_int(&viSize); - if( !isTxFinal(zcd.txobj_) ) - continue; + // The file offset of the first tx in this block is after the var_int + uint32_t txOffset = thisHeaderOffset + HEADER_SIZE + viSize; - wlt.scanTx(zcd.txobj_, 0, (uint32_t)zcd.txtime_, UINT32_MAX); - } -} + // Read each of the Tx + //bhptr->txPtrList_.resize(nTx); + uint32_t txSize; + static vector offsetsIn; + static vector offsetsOut; + static BinaryData hashResult(32); -//////////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::pprintSSHInfoAboutHash160(BinaryData const & a160) -{ - StoredScriptHistory ssh; - iface_->getStoredScriptHistory(ssh, HASH160PREFIX + a160); - if(!ssh.isInitialized()) + for(uint32_t i=0; i utxos = getUTXOVectForHash160(a160); - vector txios = getHistoryForScrAddr(a160); - uint64_t bal = getDBBalanceForHash160(a160); - uint64_t rcv = getDBReceivedForHash160(a160); + // Since this method only adds one block, if it's not on the main branch, + // then it's not the new head + bool newBlockIsNewTop = getHeaderByHash(newHeadHash)->isMainBranch(); - cout << "Information for hash160: " << a160.toHexStr().c_str() << endl; - cout << "Received: " << rcv << endl; - cout << "Balance: " << bal << endl; - cout << "NumUtxos: " << utxos.size() << endl; - cout << "NumTxios: " << txios.size() << endl; - for(uint32_t i=0; i::iterator iter; - for(iter = zeroConfRawTxList_.begin(); - iter != zeroConfRawTxList_.end(); - iter++) + + +// This piece may be useful for adding new data, but I don't want to enforce it, +// yet +/* +#ifndef _DEBUG + // In the real client, we want to execute these checks. But we may want + // to pass in hand-made data when debugging, and don't want to require + // the hand-made blocks to have leading zeros. + if(! (headHash.getSliceCopy(28,4) == BtcUtils::EmptyHash_.getSliceCopy(28,4))) { - BtcUtils::getHash256(*iter, txHash); - ZeroConfData & zcd = zeroConfMap_[txHash]; - Tx & tx = zcd.txobj_; - cout << tx.getThisHash().getSliceCopy(0,8).toHexStr().c_str() << " "; - for(uint32_t i=0; iclearZeroConfPool(); - + SCOPED_TIMER("reassessAfterReorg"); + LOGINFO << "Reassessing Tx validity after reorg"; - // Need to "unlock" the TxIOPairs that were locked with zero-conf txs - list< map::iterator > rmList; - map::iterator iter; - for(iter = txioMap_.begin(); - iter != txioMap_.end(); - iter++) + // Walk down invalidated chain first, until we get to the branch point + // Mark transactions as invalid + txJustInvalidated_.clear(); + txJustAffected_.clear(); + + BlockWriteBatcher blockWrites(iface_); + + BlockHeader* thisHeaderPtr = oldTopPtr; + LOGINFO << "Invalidating old-chain transactions..."; + + while(thisHeaderPtr != branchPtr) { - iter->second.clearZCFields(); - if(!iter->second.hasTxOut()) - rmList.push_back(iter); + uint32_t hgt = thisHeaderPtr->getBlockHeight(); + uint8_t dup = thisHeaderPtr->getDuplicateID(); + + if(DBUtils.getArmoryDbType() != ARMORY_DB_BARE) + { + // Added with leveldb... in addition to reversing blocks in RAM, + // we also need to undo the blocks in the DB + StoredUndoData sud; + createUndoDataFromBlock(hgt, dup, sud); + blockWrites.undoBlockFromDB(sud); + } + + StoredHeader sbh; + iface_->getStoredHeader(sbh, hgt, dup, true); + + // This is the original, tested, reorg code + previouslyValidBlockHeaderPtrs_.push_back(thisHeaderPtr); + for(uint32_t i=0; igetPrevHash()); } - // If a TxIOPair exists only because of the TxOutZC, then we should - // remove to ensure that it won't conflict with any logic that only - // checks for the *existence* of a TxIOPair, whereas the TxIOPair might - // actually be "empty" but would throw off some other logic. - list< map::iterator >::iterator rmIter; - for(rmIter = rmList.begin(); - rmIter != rmList.end(); - rmIter++) + // Walk down the newly-valid chain and mark transactions as valid. If + // a tx is in both chains, it will still be valid after this process + // UPDATE for LevelDB upgrade: + // This used to start from the new top block and walk down, but + // I need to apply the blocks in order, so I switched it to start + // from the branch point and walk up + thisHeaderPtr = branchPtr; // note branch block was not undone, skip it + LOGINFO << "Marking new-chain transactions valid..."; + while( thisHeaderPtr->getNextHash() != BtcUtils::EmptyHash_ && + thisHeaderPtr->getNextHash().getSize() > 0 ) { - txioMap_.erase(*rmIter); - } -} + thisHeaderPtr = getHeaderByHash(thisHeaderPtr->getNextHash()); + uint32_t hgt = thisHeaderPtr->getBlockHeight(); + uint8_t dup = thisHeaderPtr->getDuplicateID(); + iface_->markBlockHeaderValid(hgt, dup); + StoredHeader sbh; + iface_->getStoredHeader(sbh, hgt, dup, true); -//////////////////////////////////////////////////////////////////////////////// -vector & BtcWallet::getTxLedger(HashString const * scraddr) -{ - SCOPED_TIMER("BtcWallet::getTxLedger"); + if(DBUtils.getArmoryDbType() != ARMORY_DB_BARE) + blockWrites.applyBlockToDB(sbh); - // Make sure to rebuild the ZC ledgers before calling this method - if(scraddr==NULL) - return ledgerAllAddr_; - else - { - //if(scrAddrMap_.find(*scraddr) == scrAddrMap_.end()) - if(KEY_NOT_IN_MAP(*scraddr, scrAddrMap_)) - return getEmptyLedger(); - else - return scrAddrMap_[*scraddr].getTxLedger(); + for(uint32_t i=0; i & BtcWallet::getZeroConfLedger(HashString const * scraddr) +vector BlockDataManager_LevelDB::getHeadersNotOnMainChain(void) { - SCOPED_TIMER("BtcWallet::getZeroConfLedger"); - - // Make sure to rebuild the ZC ledgers before calling this method - if(scraddr==NULL) - return ledgerAllAddrZC_; - else + SCOPED_TIMER("getHeadersNotOnMainChain"); + PDEBUG("Getting headers not on main chain"); + vector out(0); + map::iterator iter; + for(iter = headerMap_.begin(); + iter != headerMap_.end(); + iter++) { - //if(scrAddrMap_.find(*scraddr) == scrAddrMap_.end()) - if(KEY_NOT_IN_MAP(*scraddr, scrAddrMap_)) - return getEmptyLedger(); - else - return scrAddrMap_[*scraddr].getZeroConfLedger(); + if( ! iter->second.isMainBranch() ) + out.push_back(&(iter->second)); } + PDEBUG("Getting headers not on main chain"); + return out; } -///////////////////////////////////////////////////////////////////////////// -bool BlockDataManager_LevelDB::isTxFinal(Tx & tx) -{ - // Anything that is replaceable (regular or through blockchain injection) - // will be considered isFinal==false. Users shouldn't even see the tx, - // because the concept may be confusing, and the CURRENT use of non-final - // tx is most likely for malicious purposes (as of this writing) - // - // This will change as multi-sig becomes integrated, and replacement will - // eventually be enabled (properly), in which case I will expand this - // to be more rigorous. - // - // For now I consider anything time-based locktimes (instead of block- - // based locktimes) to be final if this is more than one day after the - // locktime expires. This accommodates the most extreme case of silliness - // due to time-zones (this shouldn't be an issue, but I haven't spent the - // time to figure out how UTC and local time interact with time.h and - // block timestamps). In cases where locktime is legitimately used, it - // is likely to be many days in the future, and one day may not even - // matter. I'm erring on the side of safety, not convenience. - - if(tx.getLockTime() == 0) - return true; - - bool allSeqMax = true; - for(uint32_t i=0; itx.getLockTime()); - else - return (time(NULL)>tx.getLockTime()+86400); -} - - - - - - //////////////////////////////////////////////////////////////////////////////// -// Assume that stx.blockHeight_ and .duplicateID_ are set correctly. -// We created the maps and sets outside this function, because we need to keep -// a master list of updates induced by all tx in this block. -// TODO: Make sure that if Tx5 spends an input from Tx2 in the same -// block that it is handled correctly, etc. -bool BlockDataManager_LevelDB::applyTxToBatchWriteData( - StoredTx & thisSTX, - map & stxToModify, - map & sshToModify, - set & keysToDelete, - StoredUndoData * sud) +// This returns false if our new main branch does not include the previous +// topBlock. If this returns false, that probably means that we have +// previously considered some blocks to be valid that no longer are valid. +// TODO: Figure out if there is an elegant way to deal with a forked +// blockchain containing two equal-length chains +bool BlockDataManager_LevelDB::organizeChain(bool forceRebuild) { - SCOPED_TIMER("applyTxToBatchWriteData"); + SCOPED_TIMER("organizeChain"); - Tx tx = thisSTX.getTxCopy(); + // Why did this line not through an error? I left here to remind + // myself to go figure it out. + //LOGINFO << ("Organizing chain", (forceRebuild ? "w/ rebuild" : "")); + LOGDEBUG << "Organizing chain " << (forceRebuild ? "w/ rebuild" : ""); - // We never expect thisSTX to already be in the map (other tx in the map - // may be affected/retrieved multiple times). - if(KEY_IN_MAP(tx.getThisHash(), stxToModify)) - LOGERR << "How did we already add this tx?"; + // If rebuild, we zero out any original organization data and do a + // rebuild of the chain from scratch. This will need to be done in + // the event that our first call to organizeChain returns false, which + // means part of blockchain that was previously valid, has become + // invalid. Rather than get fancy, just rebuild all which takes less + // than a second, anyway. + if(forceRebuild) + { + map::iterator iter; + for( iter = headerMap_.begin(); + iter != headerMap_.end(); + iter++) + { + iter->second.difficultySum_ = -1; + iter->second.blockHeight_ = 0; + iter->second.isFinishedCalc_ = false; + iter->second.nextHash_ = BtcUtils::EmptyHash_; + iter->second.isMainBranch_ = false; + } + topBlockPtr_ = NULL; + } - // I just noticed we never set TxOuts to TXOUT_UNSPENT. Might as well do - // it here -- by definition if we just added this Tx to the DB, it couldn't - // have been spent yet. - map::iterator iter; - for(iter = thisSTX.stxoMap_.begin(); - iter != thisSTX.stxoMap_.end(); - iter++) - iter->second.spentness_ = TXOUT_UNSPENT; + // Set genesis block + BlockHeader & genBlock = getGenesisBlock(); + genBlock.blockHeight_ = 0; + genBlock.difficultyDbl_ = 1.0; + genBlock.difficultySum_ = 1.0; + genBlock.isMainBranch_ = true; + genBlock.isOrphan_ = false; + genBlock.isFinishedCalc_ = true; + genBlock.isInitialized_ = true; - // This tx itself needs to be added to the map, which makes it accessible - // to future tx in the same block which spend outputs from this tx, without - // doing anything crazy in the code here - stxToModify[tx.getThisHash()] = thisSTX; + // If this is the first run, the topBlock is the genesis block + if(topBlockPtr_ == NULL) + topBlockPtr_ = &genBlock; - dbUpdateSize_ += thisSTX.numBytes_; - - // Go through and find all the previous TxOuts that are affected by this tx - StoredTx stxTemp; - StoredScriptHistory sshTemp; - for(uint32_t iin=0; iin::iterator iter; + double maxDiffSum = prevTopBlockPtr_->getDifficultySum(); + for( iter = headerMap_.begin(); iter != headerMap_.end(); iter ++) { - TxIn txin = tx.getTxInCopy(iin); - if(txin.isCoinbase()) - continue; + // *** Walk down the chain following prevHash fields, until + // you find a "solved" block. Then walk back up and + // fill in the difficulty-sum values (do not set next- + // hash ptrs, as we don't know if this is the main branch) + // Method returns instantly if block is already "solved" + double thisDiffSum = traceChainDown(iter->second); - // Get the OutPoint data of TxOut being spent - OutPoint op = txin.getOutPoint(); - BinaryDataRef opTxHash = op.getTxHashRef(); - uint32_t opTxoIdx = op.getTxOutIndex(); + // If we hit orphans, we flag headers DB corruption + if(corruptHeadersDB_) + return false; - // This will fetch the STX from DB and put it in the stxToModify - // map if it's not already there. Or it will do nothing if it's - // already part of the map. In both cases, it returns a pointer - // to the STX that will be written to DB that we can modify. - StoredTx * stxptr = makeSureSTXInMap(opTxHash, stxToModify); - StoredTxOut & stxo = stxptr->stxoMap_[opTxoIdx]; - BinaryData uniqKey = stxo.getScrAddress(); - // Update the stxo by marking it spent by this Block:TxIndex:TxInIndex - map::iterator iter = stxptr->stxoMap_.find(opTxoIdx); - // Some sanity checks - //if(iter == stxptr->stxoMap_.end()) - if(ITER_NOT_IN_MAP(iter, stxptr->stxoMap_)) - { - LOGERR << "Needed to get OutPoint for a TxIn, but DNE"; - continue; - } - - // We're aliasing this because "iter->second" is not clear at all - StoredTxOut & stxoSpend = iter->second; - - if(stxoSpend.spentness_ == TXOUT_SPENT) + // Determine if this is the top block. If it's the same diffsum + // as the prev top block, don't do anything + if(thisDiffSum > maxDiffSum) { - LOGERR << "Trying to mark TxOut spent, but it's already marked"; - continue; + maxDiffSum = thisDiffSum; + topBlockPtr_ = &(iter->second); } + } - // Just about to {remove-if-pruning, mark-spent-if-not} STXO - // Record it in the StoredUndoData object - if(sud != NULL) - sud->stxOutsRemovedByBlock_.push_back(stxoSpend); + // Walk down the list one more time, set nextHash fields + // Also set headersByHeight_; + bool prevChainStillValid = (topBlockPtr_ == prevTopBlockPtr_); + topBlockPtr_->nextHash_ = BtcUtils::EmptyHash_; + BlockHeader* thisHeaderPtr = topBlockPtr_; + //headersByHeight_.reserve(topBlockPtr_->getBlockHeight()+32768); + headersByHeight_.resize(topBlockPtr_->getBlockHeight()+1); + while( !thisHeaderPtr->isFinishedCalc_ ) + { + thisHeaderPtr->isFinishedCalc_ = true; + thisHeaderPtr->isMainBranch_ = true; + thisHeaderPtr->isOrphan_ = false; + headersByHeight_[thisHeaderPtr->getBlockHeight()] = thisHeaderPtr; - // Need to modify existing UTXOs, so that we can delete or mark as spent - stxoSpend.spentness_ = TXOUT_SPENT; - stxoSpend.spentByTxInKey_ = thisSTX.getDBKeyOfChild(iin, false); + // This loop not necessary anymore with the DB implementation + // We need to guarantee that the txs are pointing to the right block + // header, because they could've been linked to an invalidated block + //for(uint32_t i=0; igetTxRefPtrList().size(); i++) + //{ + //TxRef & tx = *(thisHeaderPtr->getTxRefPtrList()[i]); + //tx.setHeaderPtr(thisHeaderPtr); + //tx.setMainBranch(true); + //} - if(DBUtils.getArmoryDbType() != ARMORY_DB_SUPER) - { - LOGERR << "Don't know what to do this in non-supernode mode!"; - } + HashString & childHash = thisHeaderPtr->thisHash_; + thisHeaderPtr = &(headerMap_[thisHeaderPtr->getPrevHash()]); + thisHeaderPtr->nextHash_ = childHash; - ////// Now update the SSH to show this TxIOPair was spent - // Same story as stxToModify above, except this will actually create a new - // SSH if it doesn't exist in the map or the DB - BinaryData hgtX = stxo.getHgtX(); - StoredScriptHistory* sshptr = makeSureSSHInMap(uniqKey, hgtX, sshToModify); + if(thisHeaderPtr == prevTopBlockPtr_) + prevChainStillValid = true; - // Assuming supernode, we don't need to worry about removing references - // to multisig scripts that reference this script. Simply find and - // update the correct SSH TXIO directly - sshptr->markTxOutSpent(stxoSpend.getDBKey(false), - thisSTX.getDBKeyOfChild(iin, false)); } + // Last header in the loop didn't get added (the genesis block on first run) + thisHeaderPtr->isMainBranch_ = true; + headersByHeight_[thisHeaderPtr->getBlockHeight()] = thisHeaderPtr; - - // We don't need to update any TXDATA, since it is part of writing thisSTX - // to the DB ... but we do need to update the StoredScriptHistory objects - // with references to the new [unspent] TxOuts - for(uint32_t iout=0; ioutmarkTxOutUnspent(stxoToAdd.getDBKey(false), - stxoToAdd.getValue(), - stxoToAdd.isCoinbase_, - false); - - // If this was a multisig address, add a ref to each individual scraddr - if(uniqKey[0] == SCRIPT_PREFIX_MULTISIG) - { - vector addr160List; - BtcUtils::getMultisigAddrList(stxoToAdd.getScriptRef(), addr160List); - for(uint32_t a=0; amarkTxOutUnspent(stxoToAdd.getDBKey(false), - stxoToAdd.getValue(), - stxoToAdd.isCoinbase_, - true); - } - } + // There was a dangerous bug -- prevTopBlockPtr_ is set correctly + // RIGHT NOW, but won't be once I make the recursive call to organizeChain + // I need to save it now, and re-assign it after the organizeChain call. + // (I might consider finding a way to avoid this, but it's fine as-is) + BlockHeader* prevtopblk = prevTopBlockPtr_; + organizeChain(true); // force-rebuild blockchain (takes less than 1s) + prevTopBlockPtr_ = prevtopblk; + return false; } + // Let the caller know that there was no reorg + LOGDEBUG << "Done organizing chain"; return true; } +///////////////////////////////////////////////////////////////////////////// +// Start from a node, trace down to the highest solved block, accumulate +// difficulties and difficultySum values. Return the difficultySum of +// this block. +double BlockDataManager_LevelDB::traceChainDown(BlockHeader & bhpStart) +{ + if(bhpStart.difficultySum_ > 0) + return bhpStart.difficultySum_; + // Prepare some data structures for walking down the chain + vector headerPtrStack(headerMap_.size()); + vector difficultyStack(headerMap_.size()); + uint32_t blkIdx = 0; + double thisDiff; -//////////////////////////////////////////////////////////////////////////////// -// We must have already added this to the header map and DB and have a dupID -bool BlockDataManager_LevelDB::addRawBlockToDB(BinaryRefReader & brr) -{ - SCOPED_TIMER("addRawBlockToDB"); - - //if(sbh.stxMap_.size() == 0) - //{ - //LOGERR << "Cannot add raw block to DB without any transactions"; - //return false; - //} + // Walk down the chain of prevHash_ values, until we find a block + // that has a definitive difficultySum value (i.e. >0). + BlockHeader* thisPtr = &bhpStart; + map::iterator iter; + while( thisPtr->difficultySum_ < 0) + { + thisDiff = thisPtr->difficultyDbl_; + difficultyStack[blkIdx] = thisDiff; + headerPtrStack[blkIdx] = thisPtr; + blkIdx++; - BinaryDataRef first4 = brr.get_BinaryDataRef(4); + iter = headerMap_.find(thisPtr->getPrevHash()); + if(ITER_IN_MAP(iter, headerMap_)) + thisPtr = &(iter->second); + else + { + // Under some circumstances, the headers DB is not getting written + // properly and triggering this code due to missing headers. For + // now, we simply avoid this condition by flagging the headers DB + // to be rebuilt. The bug probably has to do with batching of + // header data. + corruptHeadersDB_ = true; + return 0.0; + + // We didn't hit a known block, but we don't have this block's + // ancestor in the memory pool, so this is an orphan chain... + // at least temporarily + markOrphanChain(bhpStart); + return 0.0; + } + } + + + // Now we have a stack of difficulties and pointers. Walk back up + // (by pointer) and accumulate the difficulty values + double seedDiffSum = thisPtr->difficultySum_; + uint32_t blkHeight = thisPtr->blockHeight_; + for(int32_t i=blkIdx-1; i>=0; i--) + { + seedDiffSum += difficultyStack[i]; + blkHeight++; + thisPtr = headerPtrStack[i]; + thisPtr->difficultyDbl_ = difficultyStack[i]; + thisPtr->difficultySum_ = seedDiffSum; + thisPtr->blockHeight_ = blkHeight; + } - // Skip magic bytes and block sz if exist, put ptr at beginning of header - if(first4 == MagicBytes_) - brr.advance(4); - else - brr.rewind(4); + // Finally, we have all the difficulty sums calculated, return this one + return bhpStart.difficultySum_; + +} - // Again, we rely on the assumption that the header has already been - // added to the headerMap and the DB, and we have its correct height - // and dupID - StoredHeader sbh; - sbh.unserializeFullBlock(brr, true, false); - BlockHeader & bh = headerMap_[sbh.thisHash_]; - sbh.blockHeight_ = bh.getBlockHeight(); - sbh.duplicateID_ = bh.getDuplicateID(); - sbh.isMainBranch_ = bh.isMainBranch(); - sbh.blockAppliedToDB_ = false; - // Don't put it into the DB if it's not proper! - if(sbh.blockHeight_==UINT32_MAX || sbh.duplicateID_==UINT8_MAX) +///////////////////////////////////////////////////////////////////////////// +// In practice, orphan chains shouldn't ever happen. It means that there's +// a block in our database that doesn't trace down to the genesis block. +// Currently, we get our blocks from Bitcoin-Qt/bitcoind which is incapable +// of passing such blocks to us (or putting them in the blk*.dat files), so +// if this function gets called, it's most likely in error. +void BlockDataManager_LevelDB::markOrphanChain(BlockHeader & bhpStart) +{ + // TODO: This method was written 18 months ago, and appeared to have + // a bug in it when I revisited it. Not sure the bug was real + // but I attempted to fix it. This note is to remind you/me + // to check the old version of this method if any problems + // crop up. + LOGWARN << "Marking orphan chain"; + map::iterator iter; + iter = headerMap_.find(bhpStart.getThisHash()); + HashStringRef lastHeadHash; + while( ITER_IN_MAP(iter, headerMap_) ) { - LOGERR << "Cannot add raw block to DB without hgt & dup"; - return false; + // I don't see how it's possible to have a header that used to be + // in the main branch, but is now an ORPHAN (meaning it has no + // parent). It will be good to detect this case, though + if(iter->second.isMainBranch() == true) + { + // NOTE: this actually gets triggered when we scan the testnet + // blk0001.dat file on main net, etc + LOGERR << "Block previously main branch, now orphan!?"; + previouslyValidBlockHeaderPtrs_.push_back(&(iter->second)); + } + iter->second.isOrphan_ = true; + iter->second.isMainBranch_ = false; + lastHeadHash = iter->second.thisHash_.getRef(); + iter = headerMap_.find(iter->second.getPrevHash()); } - - iface_->putStoredHeader(sbh, true); - return true; + orphanChainStartBlocks_.push_back(&(headerMap_[lastHeadHash.copy()])); + LOGWARN << "Done marking orphan chain"; } //////////////////////////////////////////////////////////////////////////////// -// Not sure if this deserves its own method anymore, but it has it anyway. -// Used to update the blockAppliedToDB_ flag, and maybe numTx and numBytes -// if needed for some reason. -void BlockDataManager_LevelDB::updateBlkDataHeader(StoredHeader const & sbh) +// We're going to need the BDM's help to get the sender for a TxIn since it +// sometimes requires going and finding the TxOut from the distant past +//////////////////////////////////////////////////////////////////////////////// +TxOut BlockDataManager_LevelDB::getPrevTxOut(TxIn & txin) { - iface_->putValue(BLKDATA, sbh.getDBKey(), sbh.serializeDBValue(BLKDATA)); + if(txin.isCoinbase()) + return TxOut(); + + OutPoint op = txin.getOutPoint(); + Tx theTx = getTxByHash(op.getTxHash()); + uint32_t idx = op.getTxOutIndex(); + return theTx.getTxOutCopy(idx); } //////////////////////////////////////////////////////////////////////////////// -// AddRawBlockTODB -// -// Assumptions: -// -- We have already determined the correct height and dup for the header -// and we assume it's part of the sbh object -// -- It has definitely been added to the headers DB (bail if not) -// -- We don't know if it's been added to the blkdata DB yet -// -// Things to do when adding a block: -// -// -- PREPARATION: -// -- Create list of all OutPoints affected, and scripts touched -// -- If not supernode, then check above data against registeredSSHs_ -// -- Fetch all StoredTxOuts from DB about to be removed -// -- Get/create TXHINT entries for all tx in block -// -- Compute all script keys and get/create all StoredScriptHistory objs -// -- Check if any multisig scripts are affected, if so get those objs -// -- If pruning, create StoredUndoData from TxOuts about to be removed -// -- Modify any Tx/TxOuts in the SBH tree to accommodate any tx in this -// block that affect any other tx in this block -// -// -// -- Check if the block {hgt,dup} has already been written to BLKDATA DB -// -- Check if the header has already been added to HEADERS DB -// -// -- BATCH (HEADERS) -// -- Add header to HEADHASH list -// -- Add header to HEADHGT list -// -- Update validDupByHeight_ -// -- Update DBINFO top block data -// -// -- BATCH (BLKDATA) -// -- Modify StoredTxOut with spentness info (or prep a delete operation -// if pruning). -// -- Modify StoredScriptHistory objs same as above. -// -- Modify StoredScriptHistory multisig objects as well. -// -- Update SSH objects alreadyScannedUpToBlk_, if necessary -// -- Write all new TXDATA entries for {hgt,dup} -// -- If pruning, write StoredUndoData objs to DB -// -- Update DBINFO top block data -// -// IMPORTANT: we also need to make sure this method does nothing if the -// block has already been added properly (though, it okay for -// it to take time to verify nothing needs to be done). We may -// end up replaying some blocks to force consistency of the DB, -// and this method needs to be robust to replaying already-added -// blocks, as well as fixing data if the replayed block appears -// to have been added already but is different. -// +// We're going to need the BDM's help to get the sender for a TxIn since it +// sometimes requires going and finding the TxOut from the distant past //////////////////////////////////////////////////////////////////////////////// -bool BlockDataManager_LevelDB::applyBlockToDB(uint32_t hgt, uint8_t dup) +Tx BlockDataManager_LevelDB::getPrevTx(TxIn & txin) { - map stxToModify; - map sshToModify; - set keysToDelete; - return applyBlockToDB(hgt, dup, stxToModify, sshToModify, keysToDelete, true); + if(txin.isCoinbase()) + return Tx(); + + OutPoint op = txin.getOutPoint(); + return getTxByHash(op.getTxHash()); } //////////////////////////////////////////////////////////////////////////////// -bool BlockDataManager_LevelDB::applyBlockToDB(StoredHeader & sbh) +HashString BlockDataManager_LevelDB::getSenderScrAddr(TxIn & txin) { - map stxToModify; - map sshToModify; - set keysToDelete; + if(txin.isCoinbase()) + return HashString(0); - return applyBlockToDB(sbh, stxToModify, sshToModify, keysToDelete, true); + return getPrevTxOut(txin).getScrAddressStr(); } + //////////////////////////////////////////////////////////////////////////////// -bool BlockDataManager_LevelDB::applyBlockToDB( - uint32_t hgt, - uint8_t dup, - map & stxToModify, - map & sshToModify, - set & keysToDelete, - bool applyWhenDone) +int64_t BlockDataManager_LevelDB::getSentValue(TxIn & txin) { - StoredHeader sbh; - iface_->getStoredHeader(sbh, hgt, dup); - return applyBlockToDB(sbh, - stxToModify, - sshToModify, - keysToDelete, - applyWhenDone); + if(txin.isCoinbase()) + return -1; + + return getPrevTxOut(txin).getValue(); } + //////////////////////////////////////////////////////////////////////////////// -bool BlockDataManager_LevelDB::applyBlockToDB( - StoredHeader & sbh, - map & stxToModify, - map & sshToModify, - set & keysToDelete, - bool applyWhenDone) +BlockHeader* BlockDataManager_LevelDB::getHeaderPtrForTxRef(TxRef txr) { - SCOPED_TIMER("applyBlockToDB"); + if(txr.isNull()) + return NULL; - if(iface_->getValidDupIDForHeight(sbh.blockHeight_) != sbh.duplicateID_) + uint32_t hgt = txr.getBlockHeight(); + uint8_t dup = txr.getDuplicateID(); + BlockHeader* bhptr = headersByHeight_[hgt]; + if(bhptr->getDuplicateID() != dup) { - LOGERR << "Dup requested is not the main branch for the given height!"; - return false; + LOGERR << "Requested txref not on main chain (BH dupID is diff)"; + return NULL; } - else - sbh.isMainBranch_ = true; - - // We will accumulate undoData as we apply the tx - StoredUndoData sud; - sud.blockHash_ = sbh.thisHash_; - sud.blockHeight_ = sbh.blockHeight_; - sud.duplicateID_ = sbh.duplicateID_; + return bhptr; +} - // Apply all the tx to the update data - map::iterator iter; - for(iter = sbh.stxMap_.begin(); iter != sbh.stxMap_.end(); iter++) +//////////////////////////////////////////////////////////////////////////////// +BlockHeader* BlockDataManager_LevelDB::getHeaderPtrForTx(Tx & txObj) +{ + if(txObj.getTxRef().isNull()) { - // This will fetch all the affected [Stored]Tx and modify the maps in - // RAM. It will check the maps first to see if it's already been pulled, - // and then it will modify either the pulled StoredTx or pre-existing - // one. This means that if a single Tx is affected by multiple TxIns - // or TxOuts, earlier changes will not be overwritten by newer changes. - applyTxToBatchWriteData(iter->second, - stxToModify, - sshToModify, - keysToDelete, - &sud); + LOGERR << "TxRef in Tx object is not set, cannot get header ptr"; + return NULL; } + + return getHeaderPtrForTxRef(txObj.getTxRef()); +} + +//////////////////////////////////////////////////////////////////////////////// +// Methods for handling zero-confirmation transactions +//////////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::enableZeroConf(string zcFilename, bool zcLite) +{ + SCOPED_TIMER("enableZeroConf"); + LOGINFO << "Enabling zero-conf tracking " << (zcLite ? "(lite)" : ""); + zcFilename_ = zcFilename; + zcEnabled_ = true; + zcLiteMode_ = zcLite; - // If, at the end of this process, we have any empty SSH objects - // (should only happen if pruning), then remove them from the - // to-modify list, and add to the keysToDelete list. - //findSSHEntriesToDelete(sshToModify, keysToDelete); + readZeroConfFile(zcFilename_); // does nothing if DNE +} - // At this point we should have a list of STX and SSH with all the correct - // modifications (or creations) to represent this block. Let's apply it. - sbh.blockAppliedToDB_ = true; - updateBlkDataHeader(sbh); - //iface_->putStoredHeader(sbh, false); - // Now actually write all the changes to the DB all at once - if(applyWhenDone) - applyModsToDB(stxToModify, sshToModify, keysToDelete); +//////////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::readZeroConfFile(string zcFilename) +{ + SCOPED_TIMER("readZeroConfFile"); + uint64_t filesize = BtcUtils::GetFileSize(zcFilename); + if(filesize<8 || filesize==FILE_DOES_NOT_EXIST) + return; - // Only if pruning, we need to store - // TODO: this is going to get run every block, probably should batch it - // like we do with the other data...when we actually implement pruning - if(DBUtils.getDbPruneType() == DB_PRUNE_ALL) - iface_->putStoredUndoData(sud); + ifstream zcFile(zcFilename_.c_str(), ios::in | ios::binary); + BinaryData zcData((size_t)filesize); + zcFile.read((char*)zcData.getPtr(), filesize); + zcFile.close(); - return true; + // We succeeded opening the file... + BinaryRefReader brr(zcData); + while(brr.getSizeRemaining() > 8) + { + uint64_t txTime = brr.get_uint64_t(); + uint32_t txSize = BtcUtils::TxCalcLength(brr.getCurrPtr(), brr.getSizeRemaining()); + BinaryData rawtx(txSize); + brr.get_BinaryData(rawtx.getPtr(), txSize); + addNewZeroConfTx(rawtx, (uint32_t)txTime, false); + } + purgeZeroConfPool(); } +//////////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::disableZeroConf(void) +{ + SCOPED_TIMER("disableZeroConf"); + zcEnabled_ = false; +} //////////////////////////////////////////////////////////////////////////////// -// For now, we will call createUndoDataFromBlock(), and then pass that data to -// undoBlockFromDB(), even though it will result in accessing the DB data -// twice -- -// (1) LevelDB does an excellent job caching results, so the second lookup -// should be instantaneous -// (2) We prefer to integrate StoredUndoData objects now, which will be -// needed for pruning even though we don't strictly need it for no-prune -// now (and could save a lookup by skipping it). But I want unified -// code flow for both pruning and non-pruning. -bool BlockDataManager_LevelDB::createUndoDataFromBlock(uint32_t hgt, - uint8_t dup, - StoredUndoData & sud) +bool BlockDataManager_LevelDB::addNewZeroConfTx(BinaryData const & rawTx, + uint32_t txtime, + bool writeToFile) { - SCOPED_TIMER("createUndoDataFromBlock"); + SCOPED_TIMER("addNewZeroConfTx"); - StoredHeader sbh; + if(txtime==0) + txtime = (uint32_t)time(NULL); - // Fetch the full, stored block - iface_->getStoredHeader(sbh, hgt, dup, true); - if(!sbh.haveFullBlock()) - { - LOGERR << "Cannot get undo data for block because not full!"; + HashString txHash = BtcUtils::getHash256(rawTx); + + // If this is already in the zero-conf map or in the blockchain, ignore it + if(hasTxWithHash(txHash)) return false; - } - sud.blockHash_ = sbh.thisHash_; - sud.blockHeight_ = sbh.blockHeight_; - sud.duplicateID_ = sbh.duplicateID_; - // Go through tx list, fetch TxOuts that are spent, record OutPoints added - for(uint32_t itx=0; itxgetStoredTx(prevStx, prevHash); - //if(prevStx.stxoMap_.find(prevIndex) == prevStx.stxoMap_.end()) - if(KEY_NOT_IN_MAP(prevIndex, prevStx.stxoMap_)) - { - LOGERR << "StoredTx retrieved from DB, but TxOut not with it"; - return false; - } - - // - sud.stxOutsRemovedByBlock_.push_back(prevStx.stxoMap_[prevIndex]); - } - - // Use the stxoMap_ to iterate through TxOuts - for(uint32_t iout=0; iout::iterator wltIter; + for(wltIter = registeredWallets_.begin(); + wltIter != registeredWallets_.end(); + wltIter++) { - OutPoint op(stx.thisHash_, iout); - sud.outPointsAddedByBlock_.push_back(op); + // The bulk filter returns pair + isOurs = isOurs || (*wltIter)->isMineBulkFilter(txObj).first; } + + if(!isOurs) + return false; } + + + zeroConfMap_[txHash] = ZeroConfData(); + ZeroConfData & zc = zeroConfMap_[txHash]; + zc.iter_ = zeroConfRawTxList_.insert(zeroConfRawTxList_.end(), rawTx); + zc.txobj_.unserialize(*(zc.iter_)); + zc.txtime_ = txtime; + // Record time. Write to file + if(writeToFile) + { + ofstream zcFile(zcFilename_.c_str(), ios::app | ios::binary); + zcFile.write( (char*)(&zc.txtime_), sizeof(uint64_t) ); + zcFile.write( (char*)zc.txobj_.getPtr(), zc.txobj_.getSize()); + zcFile.close(); + } return true; } + //////////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::applyModsToDB( - map & stxToModify, - map & sshToModify, - set & keysToDelete) +void BlockDataManager_LevelDB::purgeZeroConfPool(void) { - // Before we apply, let's figure out if some DB keys need to be deleted - findSSHEntriesToDelete(sshToModify, keysToDelete); - - uint32_t newAppliedToHeight = 0; - - iface_->startBatch(BLKDATA); + SCOPED_TIMER("purgeZeroConfPool"); + list< map::iterator > mapRmList; - map::iterator iter_stx; - for(iter_stx = stxToModify.begin(); - iter_stx != stxToModify.end(); - iter_stx++) - { - iface_->putStoredTx(iter_stx->second, true); - - // This list always contains the latest block num - // We detect here instead of complicating the interfaces - uint32_t thisHgt = iter_stx->second.blockHeight_; - newAppliedToHeight = max(newAppliedToHeight, thisHgt); - } - - map::iterator iter_ssh; - for(iter_ssh = sshToModify.begin(); - iter_ssh != sshToModify.end(); - iter_ssh++) + // Find all zero-conf transactions that made it into the blockchain + map::iterator iter; + for(iter = zeroConfMap_.begin(); + iter != zeroConfMap_.end(); + iter++) { - iface_->putStoredScriptHistory(iter_ssh->second); - + if(!getTxRefByHash(iter->first).isNull()) + mapRmList.push_back(iter); } - set::iterator iter_del; - for(iter_del = keysToDelete.begin(); - iter_del != keysToDelete.end(); - iter_del++) + // We've made a list of the zc tx to remove, now let's remove them + // I decided this was safer than erasing the data as we were iterating + // over it in the previous loop + list< map::iterator >::iterator rmIter; + for(rmIter = mapRmList.begin(); + rmIter != mapRmList.end(); + rmIter++) { - iface_->deleteValue(BLKDATA, *iter_del); + zeroConfRawTxList_.erase( (*rmIter)->second.iter_ ); + zeroConfMap_.erase( *rmIter ); } + // Rewrite the zero-conf pool file + if(mapRmList.size() > 0) + rewriteZeroConfFile(); + +} + - if(newAppliedToHeight != 0) +//////////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::rewriteZeroConfFile(void) +{ + SCOPED_TIMER("rewriteZeroConfFile"); + ofstream zcFile(zcFilename_.c_str(), ios::out | ios::binary); + + static HashString txHash(32); + list::iterator iter; + for(iter = zeroConfRawTxList_.begin(); + iter != zeroConfRawTxList_.end(); + iter++) { - StoredDBInfo sdbi; - iface_->getStoredDBInfo(BLKDATA, sdbi); - if(!sdbi.isInitialized()) - LOGERR << "How do we have invalid SDBI in applyMods?"; - else - { - sdbi.appliedToHgt_ = newAppliedToHeight; - iface_->putStoredDBInfo(BLKDATA, sdbi); - } + BtcUtils::getHash256(*iter, txHash); + ZeroConfData & zcd = zeroConfMap_[txHash]; + zcFile.write( (char*)(&zcd.txtime_), sizeof(uint64_t) ); + zcFile.write( (char*)(zcd.txobj_.getPtr()), zcd.txobj_.getSize()); } - iface_->commitBatch(BLKDATA); + zcFile.close(); - stxToModify.clear(); - sshToModify.clear(); - keysToDelete.clear(); - dbUpdateSize_ = 0; } - + //////////////////////////////////////////////////////////////////////////////// -bool BlockDataManager_LevelDB::undoBlockFromDB(StoredUndoData & sud) +void BlockDataManager_LevelDB::rescanWalletZeroConf(BtcWallet & wlt) { - SCOPED_TIMER("undoBlockFromDB"); + SCOPED_TIMER("rescanWalletZeroConf"); + // Clear the whole list, rebuild + wlt.clearZeroConfPool(); - StoredHeader sbh; - iface_->getStoredHeader(sbh, sud.blockHeight_, sud.duplicateID_); - if(!sbh.blockAppliedToDB_) + static HashString txHash(32); + list::iterator iter; + for(iter = zeroConfRawTxList_.begin(); + iter != zeroConfRawTxList_.end(); + iter++) { - LOGERR << "This block was never applied to the DB...can't undo!"; - return false; + + if(iter->getSize() == 0) + continue; + + BtcUtils::getHash256(*iter, txHash); + ZeroConfData & zcd = zeroConfMap_[txHash]; + + if( !isTxFinal(zcd.txobj_) ) + continue; + + wlt.scanTx(zcd.txobj_, 0, (uint32_t)zcd.txtime_, UINT32_MAX); } +} - map stxToModify; - map sshToModify; - set keysToDelete; - - // In the future we will accommodate more user modes - if(DBUtils.getArmoryDbType() != ARMORY_DB_SUPER) +//////////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::pprintSSHInfoAboutHash160(BinaryData const & a160) +{ + StoredScriptHistory ssh; + iface_->getStoredScriptHistory(ssh, HASH160PREFIX + a160); + if(!ssh.isInitialized()) { - LOGERR << "Don't know what to do this in non-supernode mode!"; + LOGERR << "Address is not in DB: " << a160.toHexStr().c_str(); + return; } - - ///// Put the STXOs back into the DB which were removed by this block - // Process the stxOutsRemovedByBlock_ in reverse order - // Use int32_t index so that -1 != UINT32_MAX and we go into inf loop - for(int32_t i=sud.stxOutsRemovedByBlock_.size()-1; i>=0; i--) - { - StoredTxOut & sudStxo = sud.stxOutsRemovedByBlock_[i]; - StoredTx * stxptr = makeSureSTXInMap( sudStxo.blockHeight_, - sudStxo.duplicateID_, - sudStxo.txIndex_, - sudStxo.parentHash_, - stxToModify); + vector utxos = getUTXOVectForHash160(a160); + vector txios = getHistoryForScrAddr(a160); - - uint16_t stxoIdx = sudStxo.txOutIndex_; - map::iterator iter; + uint64_t bal = getDBBalanceForHash160(a160); + uint64_t rcv = getDBReceivedForHash160(a160); + cout << "Information for hash160: " << a160.toHexStr().c_str() << endl; + cout << "Received: " << rcv << endl; + cout << "Balance: " << bal << endl; + cout << "NumUtxos: " << utxos.size() << endl; + cout << "NumTxios: " << txios.size() << endl; + for(uint32_t i=0; istxoMap_.find(stxoIdx); - //if(iter == stxptr->stxoMap_.end()) - if(ITER_NOT_IN_MAP(iter, stxptr->stxoMap_)) - { - LOGERR << "Expecting to find existing STXO, but DNE"; - continue; - } + cout << "Full SSH info:" << endl; + ssh.pprintFullSSH(); +} - StoredTxOut & stxoReAdd = iter->second; - if(stxoReAdd.spentness_ == TXOUT_UNSPENT || - stxoReAdd.spentByTxInKey_.getSize() == 0 ) - { - LOGERR << "STXO needs to be re-added/marked-unspent but it"; - LOGERR << "was already declared unspent in the DB"; - } - - stxoReAdd.spentness_ = TXOUT_UNSPENT; - stxoReAdd.spentByTxInKey_ = BinaryData(0); - } - else - { - // If we're pruning, we should have the Tx in the DB, but without the - // TxOut because it had been pruned by this block on the forward op - iter = stxptr->stxoMap_.find(stxoIdx); - //if(iter != stxptr->stxoMap_.end()) - if(ITER_IN_MAP(iter, stxptr->stxoMap_)) - LOGERR << "Somehow this TxOut had not been pruned!"; - else - iter->second = sudStxo; +//////////////////////////////////////////////////////////////////////////////// +void BlockDataManager_LevelDB::pprintZeroConfPool(void) +{ + static HashString txHash(32); + list::iterator iter; + for(iter = zeroConfRawTxList_.begin(); + iter != zeroConfRawTxList_.end(); + iter++) + { + BtcUtils::getHash256(*iter, txHash); + ZeroConfData & zcd = zeroConfMap_[txHash]; + Tx & tx = zcd.txobj_; + cout << tx.getThisHash().getSliceCopy(0,8).toHexStr().c_str() << " "; + for(uint32_t i=0; isecond.spentness_ = TXOUT_UNSPENT; - iter->second.spentByTxInKey_ = BinaryData(0); - } +//////////////////////////////////////////////////////////////////////////////// +void ScrAddrObj::clearZeroConfPool(void) +{ + ledgerZC_.clear(); + relevantTxIOPtrsZC_.clear(); +} - ////// Finished updating STX, now update the SSH in the DB - // Updating the SSH objects works the same regardless of pruning - iter = stxptr->stxoMap_.find(stxoIdx); - //if(iter == stxptr->stxoMap_.end()) - if(ITER_NOT_IN_MAP(iter, stxptr->stxoMap_)) - { - LOGERR << "Somehow STXO DNE even though we should've just added it!"; - continue; - } - StoredTxOut & stxoReAdd = iter->second; - BinaryData uniqKey = stxoReAdd.getScrAddress(); - BinaryData hgtX = stxoReAdd.getHgtX(); - StoredScriptHistory* sshptr = makeSureSSHInMap(uniqKey, hgtX, sshToModify); - if(sshptr==NULL) - { - LOGERR << "No SSH found for marking TxOut unspent on undo"; - continue; - } +//////////////////////////////////////////////////////////////////////////////// +void BtcWallet::clearZeroConfPool(void) +{ + SCOPED_TIMER("clearZeroConfPool"); + ledgerAllAddrZC_.clear(); + for(uint32_t i=0; iclearZeroConfPool(); - // Now get the TxIOPair in the StoredScriptHistory and mark unspent - sshptr->markTxOutUnspent(stxoReAdd.getDBKey(false), - stxoReAdd.getValue(), - stxoReAdd.isCoinbase_, - false); - - // If multisig, we need to update the SSHs for individual addresses - if(uniqKey[0] == SCRIPT_PREFIX_MULTISIG) - { + // Need to "unlock" the TxIOPairs that were locked with zero-conf txs + list< map::iterator > rmList; + map::iterator iter; + for(iter = txioMap_.begin(); + iter != txioMap_.end(); + iter++) + { + iter->second.clearZCFields(); + if(!iter->second.hasTxOut()) + rmList.push_back(iter); + } - vector addr160List; - BtcUtils::getMultisigAddrList(stxoReAdd.getScriptRef(), addr160List); - for(uint32_t a=0; amarkTxOutUnspent(stxoReAdd.getDBKey(false), - stxoReAdd.getValue(), - stxoReAdd.isCoinbase_, - true); - } - } + // If a TxIOPair exists only because of the TxOutZC, then we should + // remove to ensure that it won't conflict with any logic that only + // checks for the *existence* of a TxIOPair, whereas the TxIOPair might + // actually be "empty" but would throw off some other logic. + list< map::iterator >::iterator rmIter; + for(rmIter = rmList.begin(); + rmIter != rmList.end(); + rmIter++) + { + txioMap_.erase(*rmIter); } +} +//////////////////////////////////////////////////////////////////////////////// +vector & BtcWallet::getTxLedger(HashString const * scraddr) +{ + SCOPED_TIMER("BtcWallet::getTxLedger"); - // The OutPoint list is every new, unspent TxOut created by this block. - // When they were added, we updated all the StoredScriptHistory objects - // to include references to them. We need to remove them now. - // Use int32_t index so that -1 != UINT32_MAX and we go into inf loop - for(int16_t itx=sbh.numTx_-1; itx>=0; itx--) + // Make sure to rebuild the ZC ledgers before calling this method + if(scraddr==NULL) + return ledgerAllAddr_; + else { - // Ironically, even though I'm using hgt & dup, I still need the hash - // in order to key the stxToModify map - BinaryData txHash = iface_->getHashForDBKey(sbh.blockHeight_, - sbh.duplicateID_, - itx); + //if(scrAddrMap_.find(*scraddr) == scrAddrMap_.end()) + if(KEY_NOT_IN_MAP(*scraddr, scrAddrMap_)) + return getEmptyLedger(); + else + return scrAddrMap_[*scraddr].getTxLedger(); + } +} - StoredTx * stxptr = makeSureSTXInMap(sbh.blockHeight_, - sbh.duplicateID_, - itx, - txHash, - stxToModify); +//////////////////////////////////////////////////////////////////////////////// +vector & BtcWallet::getZeroConfLedger(HashString const * scraddr) +{ + SCOPED_TIMER("BtcWallet::getZeroConfLedger"); - for(int16_t txoIdx = stxptr->stxoMap_.size()-1; txoIdx >= 0; txoIdx--) - { + // Make sure to rebuild the ZC ledgers before calling this method + if(scraddr==NULL) + return ledgerAllAddrZC_; + else + { + //if(scrAddrMap_.find(*scraddr) == scrAddrMap_.end()) + if(KEY_NOT_IN_MAP(*scraddr, scrAddrMap_)) + return getEmptyLedger(); + else + return scrAddrMap_[*scraddr].getZeroConfLedger(); + } +} - StoredTxOut & stxo = stxptr->stxoMap_[txoIdx]; - BinaryData stxoKey = stxo.getDBKey(false); +///////////////////////////////////////////////////////////////////////////// +bool BlockDataManager_LevelDB::isTxFinal(Tx & tx) +{ + // Anything that is replaceable (regular or through blockchain injection) + // will be considered isFinal==false. Users shouldn't even see the tx, + // because the concept may be confusing, and the CURRENT use of non-final + // tx is most likely for malicious purposes (as of this writing) + // + // This will change as multi-sig becomes integrated, and replacement will + // eventually be enabled (properly), in which case I will expand this + // to be more rigorous. + // + // For now I consider anything time-based locktimes (instead of block- + // based locktimes) to be final if this is more than one day after the + // locktime expires. This accommodates the most extreme case of silliness + // due to time-zones (this shouldn't be an issue, but I haven't spent the + // time to figure out how UTC and local time interact with time.h and + // block timestamps). In cases where locktime is legitimately used, it + // is likely to be many days in the future, and one day may not even + // matter. I'm erring on the side of safety, not convenience. - // Then fetch the StoredScriptHistory of the StoredTxOut scraddress - BinaryData uniqKey = stxo.getScrAddress(); - BinaryData hgtX = stxo.getHgtX(); - StoredScriptHistory * sshptr = makeSureSSHInMap(uniqKey, - hgtX, - sshToModify, - false); - - - // If we are tracking that SSH, remove the reference to this OutPoint - if(sshptr != NULL) - sshptr->eraseTxio(stxoKey); - - // Now remove any multisig entries that were added due to this TxOut - if(uniqKey[0] == SCRIPT_PREFIX_MULTISIG) - { - vector addr160List; - BtcUtils::getMultisigAddrList(stxo.getScriptRef(), addr160List); - for(uint32_t a=0; aeraseTxio(stxoKey); - } - } - } - } + if(tx.getLockTime() == 0) + return true; + bool allSeqMax = true; + for(uint32_t i=0; itx.getLockTime()); + else + return (time(NULL)>tx.getLockTime()+86400); +} - // Check for any SSH objects that are now completely empty. If they exist, - // they should be removed from the DB, instead of simply written as empty - // objects - findSSHEntriesToDelete(sshToModify, keysToDelete); - // Finally, mark this block as UNapplied. - sbh.blockAppliedToDB_ = false; - updateBlkDataHeader(sbh); - applyModsToDB(stxToModify, sshToModify, keysToDelete); - return true; -} //////////////////////////////////////////////////////////////////////////////// -StoredScriptHistory* BlockDataManager_LevelDB::makeSureSSHInMap( - BinaryDataRef uniqKey, - BinaryDataRef hgtX, - map & sshMap, - bool createIfDNE) +// We must have already added this to the header map and DB and have a dupID +void BlockDataManager_LevelDB::addRawBlockToDB(BinaryRefReader & brr) { - SCOPED_TIMER("makeSureSSHInMap"); - StoredScriptHistory * sshptr; - StoredScriptHistory sshTemp; + SCOPED_TIMER("addRawBlockToDB"); + + //if(sbh.stxMap_.size() == 0) + //{ + //LOGERR << "Cannot add raw block to DB without any transactions"; + //return false; + //} - // If already in Map - map::iterator iter = sshMap.find(uniqKey); - if(ITER_IN_MAP(iter, sshMap)) + BinaryDataRef first4 = brr.get_BinaryDataRef(4); + + // Skip magic bytes and block sz if exist, put ptr at beginning of header + if(first4 == MagicBytes_) + brr.advance(4); + else + brr.rewind(4); + + // Again, we rely on the assumption that the header has already been + // added to the headerMap and the DB, and we have its correct height + // and dupID + StoredHeader sbh; + try { - SCOPED_TIMER("___SSH_AlreadyInMap"); - sshptr = &(iter->second); + sbh.unserializeFullBlock(brr, true, false); } - else + catch (BlockDeserializingException &) { - iface_->getStoredScriptHistorySummary(sshTemp, uniqKey); - dbUpdateSize_ += UPDATE_BYTES_SSH; - if(sshTemp.isInitialized()) + if (sbh.hasBlockHeader_) { - SCOPED_TIMER("___SSH_AlreadyInDB"); - // We already have an SSH in DB -- pull it into the map - sshMap[uniqKey] = sshTemp; - sshptr = &sshMap[uniqKey]; + // we still add this block to the chain in this case, + // if we miss a few transactions it's better than + // missing the entire block + BlockHeader & bh = headerMap_[sbh.thisHash_]; + sbh.blockHeight_ = bh.getBlockHeight(); + sbh.duplicateID_ = bh.getDuplicateID(); + sbh.isMainBranch_ = bh.isMainBranch(); + sbh.blockAppliedToDB_ = false; + + // Don't put it into the DB if it's not proper! + if(sbh.blockHeight_==UINT32_MAX || sbh.duplicateID_==UINT8_MAX) + throw BlockDeserializingException("Error parsing block (corrupt?) - Cannot add raw block to DB without hgt & dup"); + + iface_->putStoredHeader(sbh, true); + missingBlockHashes_.push_back( sbh.thisHash_ ); + throw BlockDeserializingException("Error parsing block (corrupt?) - block header valid"); } else { - SCOPED_TIMER("___SSH_NeedCreate"); - if(!createIfDNE) - return NULL; - - sshMap[uniqKey] = StoredScriptHistory(); - sshptr = &sshMap[uniqKey]; - sshptr->uniqueKey_ = uniqKey; + throw BlockDeserializingException("Error parsing block (corrupt?) and block header invalid"); } + // throw a new exception with a useful "what" } + BlockHeader & bh = headerMap_[sbh.thisHash_]; + sbh.blockHeight_ = bh.getBlockHeight(); + sbh.duplicateID_ = bh.getDuplicateID(); + sbh.isMainBranch_ = bh.isMainBranch(); + sbh.blockAppliedToDB_ = false; - - // If sub-history for this block doesn't exist, add an empty one before - // returning the pointer to the SSH. Since we haven't actually inserted - // anything into the SubSSH, we don't need to adjust the totalTxioCount_ - uint32_t prevSize = sshptr->subHistMap_.size(); - iface_->fetchStoredSubHistory(*sshptr, hgtX, true, false); - uint32_t newSize = sshptr->subHistMap_.size(); - - dbUpdateSize_ += (newSize - prevSize) * UPDATE_BYTES_SUBSSH; - return sshptr; + // Don't put it into the DB if it's not proper! + if(sbh.blockHeight_==UINT32_MAX || sbh.duplicateID_==UINT8_MAX) + throw BlockDeserializingException("Cannot add raw block to DB without hgt & dup"); + iface_->putStoredHeader(sbh, true); } -//////////////////////////////////////////////////////////////////////////////// -StoredTx* BlockDataManager_LevelDB::makeSureSTXInMap( - BinaryDataRef txHash, - map & stxMap) -{ - // TODO: If we are pruning, we may have completely removed this tx from - // the DB, which means that it won't be in the map or the DB. - // But this method was written before pruning was ever implemented... - StoredTx * stxptr; - StoredTx stxTemp; - // Get the existing STX or make a new one - map::iterator txIter = stxMap.find(txHash); - if(ITER_IN_MAP(txIter, stxMap)) - stxptr = &(txIter->second); - else - { - iface_->getStoredTx(stxTemp, txHash); - stxMap[txHash] = stxTemp; - stxptr = &stxMap[txHash]; - dbUpdateSize_ += stxptr->numBytes_; - } - - return stxptr; -} + //////////////////////////////////////////////////////////////////////////////// -// This avoids having to do the double-lookup when fetching by hash. -// We still pass in the hash anyway, because the map is indexed by the hash, -// and we'd like to not have to do a lookup for the hash if only provided -// {hgt, dup, idx} -StoredTx* BlockDataManager_LevelDB::makeSureSTXInMap( - uint32_t hgt, - uint8_t dup, - uint16_t txIdx, - BinaryDataRef txHash, - map & stxMap) +// For now, we will call createUndoDataFromBlock(), and then pass that data to +// undoBlockFromDB(), even though it will result in accessing the DB data +// twice -- +// (1) LevelDB does an excellent job caching results, so the second lookup +// should be instantaneous +// (2) We prefer to integrate StoredUndoData objects now, which will be +// needed for pruning even though we don't strictly need it for no-prune +// now (and could save a lookup by skipping it). But I want unified +// code flow for both pruning and non-pruning. +bool BlockDataManager_LevelDB::createUndoDataFromBlock(uint32_t hgt, + uint8_t dup, + StoredUndoData & sud) { - StoredTx * stxptr; - StoredTx stxTemp; + SCOPED_TIMER("createUndoDataFromBlock"); - // Get the existing STX or make a new one - map::iterator txIter = stxMap.find(txHash); - if(ITER_IN_MAP(txIter, stxMap)) - stxptr = &(txIter->second); - else + StoredHeader sbh; + + // Fetch the full, stored block + iface_->getStoredHeader(sbh, hgt, dup, true); + if(!sbh.haveFullBlock()) { - iface_->getStoredTx(stxTemp, hgt, dup, txIdx); - stxMap[txHash] = stxTemp; - stxptr = &stxMap[txHash]; - dbUpdateSize_ += stxptr->numBytes_; + LOGERR << "Cannot get undo data for block because not full!"; + return false; } - - return stxptr; -} + sud.blockHash_ = sbh.thisHash_; + sud.blockHeight_ = sbh.blockHeight_; + sud.duplicateID_ = sbh.duplicateID_; -//////////////////////////////////////////////////////////////////////////////// -void BlockDataManager_LevelDB::findSSHEntriesToDelete( - map & sshMap, - set & keysToDelete) -{ - vector fullSSHToDelete(0); - map::iterator iterSSH; - for(iterSSH = sshMap.begin(); - iterSSH != sshMap.end(); - iterSSH++) + // Go through tx list, fetch TxOuts that are spent, record OutPoints added + for(uint32_t itx=0; itxsecond; - map::iterator iterSub; - for(iterSub = ssh.subHistMap_.begin(); - iterSub != ssh.subHistMap_.end(); - iterSub++) + StoredTx & stx = sbh.stxMap_[itx]; + + // Convert to a regular tx to make accessing TxIns easier + Tx regTx = stx.getTxCopy(); + for(uint32_t iin=0; iinsecond; - if(subssh.txioSet_.size() == 0) - keysToDelete.insert(subssh.getDBKey(true)); + TxIn txin = regTx.getTxInCopy(iin); + BinaryData prevHash = txin.getOutPoint().getTxHash(); + uint16_t prevIndex = txin.getOutPoint().getTxOutIndex(); + + // Skip if coinbase input + if(prevHash == BtcUtils::EmptyHash_) + continue; + + // Above we checked the block to be undone is full, but we + // still need to make sure the prevTx we just fetched has our data. + StoredTx prevStx; + iface_->getStoredTx(prevStx, prevHash); + //if(prevStx.stxoMap_.find(prevIndex) == prevStx.stxoMap_.end()) + if(KEY_NOT_IN_MAP(prevIndex, prevStx.stxoMap_)) + { + LOGERR << "StoredTx retrieved from DB, but TxOut not with it"; + return false; + } + + // + sud.stxOutsRemovedByBlock_.push_back(prevStx.stxoMap_[prevIndex]); } - - // If the full SSH is empty (not just sub history), mark it to be removed - if(iterSSH->second.totalTxioCount_ == 0) + + // Use the stxoMap_ to iterate through TxOuts + for(uint32_t iout=0; ioutfirst); - keysToDelete.insert(iterSSH->second.getDBKey(true)); + OutPoint op(stx.thisHash_, iout); + sud.outPointsAddedByBlock_.push_back(op); } } - // We have to delete in a separate loop, because we don't want to delete - // elements in the map we are iterating over, in the above loop. - for(uint32_t i=0; i getFullTxOutList(uint32_t currBlk=0); - vector getSpendableTxOutList(uint32_t currBlk=0); + vector getSpendableTxOutList(uint32_t currBlk=0, + bool ignoreAllZeroConf=false); void clearZeroConfPool(void); @@ -334,7 +337,7 @@ class ScrAddrObj class BtcWallet { public: - BtcWallet(void) : bdmPtr_(NULL) {} + BtcWallet(void) : bdmPtr_(NULL), lastScanned_(0), ignoreLastScanned_(true) {} explicit BtcWallet(BlockDataManager_LevelDB* bdm) : bdmPtr_(bdm) {} ~BtcWallet(void); @@ -373,22 +376,23 @@ class BtcWallet uint32_t lastTimestamp, uint32_t lastBlockNum); - bool hasScrAddress(BinaryData const & scrAddr); + bool hasScrAddress(BinaryData const & scrAddr) const; // Scan a Tx for our TxIns/TxOuts. Override default blk vals if you think // you will save time by not checking addresses that are much newer than // the block pair isMineBulkFilter( Tx & tx, - bool withMultiSig=false); + bool withMultiSig=false) const; pair isMineBulkFilter( Tx & tx, - map & txiomap, - bool withMultiSig=false); + map const & txiomap, + bool withMultiSig=false) const; - void scanTx(Tx & tx, + void scanTx(Tx & tx, uint32_t txIndex = UINT32_MAX, uint32_t blktime = UINT32_MAX, - uint32_t blknum = UINT32_MAX); + uint32_t blknum = UINT32_MAX, + bool mainwallet = true); void scanNonStdTx(uint32_t blknum, uint32_t txidx, @@ -406,10 +410,13 @@ class BtcWallet // the Utxos in the list. If you don't care (i.e. you only want to // know what TxOuts are available to spend, you can pass in 0 for currBlk uint64_t getFullBalance(void); - uint64_t getSpendableBalance(uint32_t currBlk=0); - uint64_t getUnconfirmedBalance(uint32_t currBlk); + uint64_t getSpendableBalance(uint32_t currBlk=0, + bool ignoreAllZeroConf=false); + uint64_t getUnconfirmedBalance(uint32_t currBlk, + bool includeAllZeroConf=false); vector getFullTxOutList(uint32_t currBlk=0); - vector getSpendableTxOutList(uint32_t currBlk=0); + vector getSpendableTxOutList(uint32_t currBlk=0, + bool ignoreAllZeroConf=false); void clearZeroConfPool(void); @@ -425,6 +432,10 @@ class BtcWallet map & getTxIOMap(void) {return txioMap_;} map & getNonStdTxIO(void) {return nonStdTxioMap_;} + + vector & getTxLedgerForComments(void) + { return txLedgerForComments_; } + bool isOutPointMine(BinaryData const & hsh, uint32_t idx); void pprintLedger(void); @@ -437,13 +448,21 @@ class BtcWallet vector & getEmptyLedger(void) { EmptyLedger_.clear(); return EmptyLedger_;} + void reorgChangeBlkNum(uint32_t newBlkHgt); + + uint32_t lastScanned_; + bool ignoreLastScanned_; + private: vector scrAddrPtrs_; map scrAddrMap_; map txioMap_; vector ledgerAllAddr_; - vector ledgerAllAddrZC_; + vector ledgerAllAddrZC_; + + // Work around for address comments populating until 1:1 wallets are adopted + vector txLedgerForComments_; // For non-std transactions map nonStdTxioMap_; @@ -452,7 +471,6 @@ class BtcWallet BlockDataManager_LevelDB* bdmPtr_; static vector EmptyLedger_; // just a null-reference object - }; @@ -466,16 +484,52 @@ struct ZeroConfData }; +/* + This class accumulates changes to write to the database, + and will do so when it gets to a certain threshold +*/ +class BlockWriteBatcher +{ +public: + static const uint64_t UPDATE_BYTES_THRESH = 96*1024*1024; + + BlockWriteBatcher(InterfaceToLDB* iface); + ~BlockWriteBatcher(); + + void applyBlockToDB(StoredHeader &sbh); + void applyBlockToDB(uint32_t hgt, uint8_t dup) + { + StoredHeader sbh; + iface_->getStoredHeader(sbh, hgt, dup); + applyBlockToDB(sbh); + } + void undoBlockFromDB(StoredUndoData &sud); + +private: + // We have accumulated enough data, actually write it to the db + void commit(); + + // search for entries in sshToModify_ that are empty and should + // be deleted, removing those empty ones from sshToModify + set searchForSSHKeysToDelete(); + + bool applyTxToBatchWriteData( + StoredTx & thisSTX, + StoredUndoData * sud); +private: + InterfaceToLDB* const iface_; + + // turn off batches by setting this to 0 + uint64_t dbUpdateSize_; + map stxToModify_; + map sshToModify_; + + // (theoretically) incremented for each + // applyBlockToDB and decremented for each + // undoBlockFromDB + uint32_t mostRecentBlockApplied_; +}; -//////////////////////////////////////////////////////////////////////////////// -// A somewhat convenient way to store and pass around block-update data but I -// never actually used it on the .cpp side. -//struct BlockWriteBatchData -//{ - //map stxToModify; - //map sshToModify; - //set keysToDelete; -//} @@ -516,6 +570,7 @@ class BlockDataManager_LevelDB list zeroConfRawTxList_; map zeroConfMap_; bool zcEnabled_; + bool zcLiteMode_; string zcFilename_; // This is for detecting external changes made to the blk0001.dat file @@ -554,7 +609,6 @@ class BlockDataManager_LevelDB uint64_t startApplyOffset_; // Used to estimate how much data is queued to be written to DB - uint64_t dbUpdateSize_; bool requestRescan_; // These should be set after the blockchain is organized @@ -579,6 +633,7 @@ class BlockDataManager_LevelDB static BlockDataManager_LevelDB* theOnlyBDM_; static bool bdmCreatedYet_; bool isInitialized_; + uint32_t lastScannedBlock_; // These will be set for the specific network we are testing @@ -611,6 +666,14 @@ class BlockDataManager_LevelDB set registeredOutPoints_; uint32_t allScannedUpToBlk_; // one past top + // list of block headers that appear to be missing + // when scanned by buildAndScanDatabases + vector missingBlockHeaderHashes_; + // list of blocks whose contents are invalid but we have + // their headers + vector missingBlockHashes_; + + // TODO: We eventually want to maintain some kind of master TxIO map, instead // of storing them in the individual wallets. With the new DB, it makes more // sense to do this, and it will become easier to compute total balance when @@ -773,6 +836,8 @@ class BlockDataManager_LevelDB bool forceRebuild=false, bool skipFetch=false, bool initialLoad=false); + bool scanForMagicBytes(BinaryStreamBuffer& bsb, uint32_t *bytesSkipped=0) const; + void readRawBlocksInFile(uint32_t blkFileNum, uint32_t offset); // These are wrappers around "buildAndScanDatabases" void doRebuildDatabases(void); @@ -782,34 +847,12 @@ class BlockDataManager_LevelDB void doInitialSyncOnLoad_Rescan(void); void doInitialSyncOnLoad_Rebuild(void); - bool addRawBlockToDB(BinaryRefReader & brr); - void updateBlkDataHeader(StoredHeader const & sbh); - - // On the first pass through the blockchain data, we only write the raw - // blocks to do the DB. We don't "apply" them (marking TxOuts spent and - // updating StoredScriptHistory objects). When we know the longest chain, - // we do apply them. - bool applyBlockToDB(uint32_t hgt, uint8_t dup); - bool applyBlockToDB(StoredHeader & sbh); - bool applyBlockToDB( - StoredHeader & sbh, - map & stxToModify, - map & sshToModify, - set & keysToDelete, - bool applyWhenDone=true); - bool applyBlockToDB( - uint32_t hgt, - uint8_t dup, - map & stxToModify, - map & sshToModify, - set & keysToDelete, - bool applyWhenDone=true); + void addRawBlockToDB(BinaryRefReader & brr); void applyBlockRangeToDB(uint32_t blk0=0, uint32_t blk1=UINT32_MAX); // When we reorg, we have to undo blocks that have been applied. bool createUndoDataFromBlock(uint32_t hgt, uint8_t dup, StoredUndoData & sud); - bool undoBlockFromDB(StoredUndoData & sud); // When we add new block data, we will need to store/copy it to its // permanent memory location before parsing it. @@ -825,7 +868,7 @@ class BlockDataManager_LevelDB void deleteHistories(void); - void shutdownSaveScrAddrHistories(void); + void saveScrAddrHistories(void); void fetchAllRegisteredScrAddrData(void); void fetchAllRegisteredScrAddrData(BtcWallet & myWlt); @@ -841,13 +884,12 @@ class BlockDataManager_LevelDB uint32_t getNumBlocks(void) const { return headerMap_.size(); } //uint32_t getNumTx(void) const { return txHintMap_.size(); } + StoredHeader getMainBlockFromDB(uint32_t hgt); + uint8_t getMainDupFromDB(uint32_t hgt); + StoredHeader getBlockFromDB(uint32_t hgt, uint8_t dup); vector getHeadersNotOnMainChain(void); - //vector prefixSearchHeaders(BinaryData const & searchStr); - //vector prefixSearchTx (BinaryData const & searchStr); - //vector prefixSearchAddress(BinaryData const & searchStr); - bool addHeadersFirst(BinaryDataRef rawHeader); bool addHeadersFirst(vector const & headVect); @@ -882,9 +924,9 @@ class BlockDataManager_LevelDB bool withMultiSig=false); // For zero-confirmation tx-handling - void enableZeroConf(string); - void disableZeroConf(string); - void readZeroConfFile(string); + void enableZeroConf(string filename, bool zcLite=true); + void disableZeroConf(void); + void readZeroConfFile(string filename); bool addNewZeroConfTx(BinaryData const & rawTx, uint32_t txtime, bool writeToFile); void purgeZeroConfPool(void); void pprintZeroConfPool(void); @@ -950,41 +992,10 @@ class BlockDataManager_LevelDB void markOrphanChain(BlockHeader & bhpStart); ///////////////////////////////////////////////////////////////////////////// - // Helper methods for updating the DB - bool applyTxToBatchWriteData( - StoredTx & thisSTX, - map & stxToModify, - map & sshToModify, - set & keysToDelete, - StoredUndoData * sud); - - void applyModsToDB( map & stxToModify, - map & sshToModify, - set & keysToDelete); - - - ///////////////////////////////////////////////////////////////////////////// - StoredScriptHistory* makeSureSSHInMap( - BinaryDataRef uniqKey, - BinaryDataRef hgtX, - map & sshMap, - bool createIfDNE=true); - - StoredTx* makeSureSTXInMap( BinaryDataRef txHash, - map & stxMap); - - StoredTx* makeSureSTXInMap( uint32_t height, - uint8_t dupID, - uint16_t txIndex, - BinaryDataRef txHash, - map & stxMap); - - void findSSHEntriesToDelete( map & sshMap, - set & keysToDelete); - - void setMaxOpenFiles(uint32_t n) {iface_->setMaxOpenFiles(n);} uint32_t getMaxOpenFiles(void) {return iface_->getMaxOpenFiles();} + void setLdbBlockSize(uint32_t sz){iface_->setLdbBlockSize(sz);} + uint32_t getLdbBlockSize(void) {return iface_->getLdbBlockSize();} // Simple wrapper around the logger so that they are easy to access from SWIG void StartCppLogging(string fname, int lvl) { STARTLOGGING(fname, (LogLevel)lvl); } @@ -1001,6 +1012,10 @@ class BlockDataManager_LevelDB //bool estimateDBUpdateSize( //map & stxToModify, //map & sshToModify); + + vector missingBlockHeaderHashes() const { return missingBlockHeaderHashes_; } + + vector missingBlockHashes() const { return missingBlockHashes_; } }; @@ -1023,5 +1038,6 @@ class BlockDataManager BlockDataManager_LevelDB* bdm_; }; +// kate: indent-width 3; replace-tabs on; #endif diff --git a/cppForSwig/BtcUtils.cpp b/cppForSwig/BtcUtils.cpp index 16961b622..0ba085d6b 100644 --- a/cppForSwig/BtcUtils.cpp +++ b/cppForSwig/BtcUtils.cpp @@ -1,6 +1,6 @@ //////////////////////////////////////////////////////////////////////////////// // // -// Copyright(C) 2011-2013, Armory Technologies, Inc. // +// Copyright (C) 2011-2014, Armory Technologies, Inc. // // Distributed under the GNU Affero General Public License (AGPL v3) // // See LICENSE or http://www.gnu.org/licenses/agpl.html // // // diff --git a/cppForSwig/BtcUtils.h b/cppForSwig/BtcUtils.h index 4920a8dae..6a44b07ee 100644 --- a/cppForSwig/BtcUtils.h +++ b/cppForSwig/BtcUtils.h @@ -1,6 +1,6 @@ //////////////////////////////////////////////////////////////////////////////// // // -// Copyright(C) 2011-2013, Armory Technologies, Inc. // +// Copyright (C) 2011-2014, Armory Technologies, Inc. // // Distributed under the GNU Affero General Public License (AGPL v3) // // See LICENSE or http://www.gnu.org/licenses/agpl.html // // // @@ -19,6 +19,8 @@ #include #include #include +#include +#include #include "BinaryData.h" #include "cryptlib.h" @@ -253,6 +255,13 @@ enum OPCODETYPE }; +class BlockDeserializingException : public runtime_error +{ +public: + BlockDeserializingException(const string &what="") + : runtime_error(what) + { } +}; // This class holds only static methods. @@ -315,6 +324,45 @@ class BtcUtils return READ_UINT64_LE(strmPtr+1); } } + ///////////////////////////////////////////////////////////////////////////// + static uint64_t readVarInt(uint8_t const * strmPtr, size_t remaining, uint32_t* lenOutPtr=NULL) + { + if (remaining < 1) + throw BlockDeserializingException(); + uint8_t firstByte = strmPtr[0]; + + if(firstByte < 0xfd) + { + if(lenOutPtr != NULL) + *lenOutPtr = 1; + return firstByte; + } + if(firstByte == 0xfd) + { + if (remaining < 3) + throw BlockDeserializingException(); + if(lenOutPtr != NULL) + *lenOutPtr = 3; + return READ_UINT16_LE(strmPtr+1); + + } + else if(firstByte == 0xfe) + { + if (remaining < 5) + throw BlockDeserializingException(); + if(lenOutPtr != NULL) + *lenOutPtr = 5; + return READ_UINT32_LE(strmPtr+1); + } + else //if(firstByte == 0xff) + { + if (remaining < 9) + throw BlockDeserializingException(); + if(lenOutPtr != NULL) + *lenOutPtr = 9; + return READ_UINT64_LE(strmPtr+1); + } + } ///////////////////////////////////////////////////////////////////////////// @@ -356,7 +404,7 @@ class BtcUtils ///////////////////////////////////////////////////////////////////////////// static uint64_t GetFileSize(string filename) { - ifstream is(filename.c_str(), ios::in|ios::binary); + ifstream is(OS_TranslatePath(filename.c_str()), ios::in|ios::binary); if(!is.is_open()) return FILE_DOES_NOT_EXIST; @@ -437,7 +485,7 @@ class BtcUtils uint32_t nBytes, BinaryData & hashOutput) { - static CryptoPP::SHA256 sha256_; + CryptoPP::SHA256 sha256_; if(hashOutput.getSize() != 32) hashOutput.resize(32); @@ -451,7 +499,7 @@ class BtcUtils uint32_t nBytes, BinaryData & hashOutput) { - static CryptoPP::SHA256 sha256_; + CryptoPP::SHA256 sha256_; sha256_.CalculateDigest(hashOutput.getPtr(), strToHash, nBytes); sha256_.CalculateDigest(hashOutput.getPtr(), hashOutput.getPtr(), 32); @@ -461,7 +509,7 @@ class BtcUtils static BinaryData getHash256(uint8_t const * strToHash, uint32_t nBytes) { - static CryptoPP::SHA256 sha256_; + CryptoPP::SHA256 sha256_; BinaryData hashOutput(32); sha256_.CalculateDigest(hashOutput.getPtr(), strToHash, nBytes); @@ -505,9 +553,9 @@ class BtcUtils uint32_t nBytes, BinaryData & hashOutput) { - static CryptoPP::SHA256 sha256_; - static CryptoPP::RIPEMD160 ripemd160_; - static BinaryData bd32(32); + CryptoPP::SHA256 sha256_; + CryptoPP::RIPEMD160 ripemd160_; + BinaryData bd32(32); if(hashOutput.getSize() != 20) hashOutput.resize(20); @@ -521,9 +569,9 @@ class BtcUtils uint32_t nBytes, BinaryData & hashOutput) { - static CryptoPP::SHA256 sha256_; - static CryptoPP::RIPEMD160 ripemd160_; - static BinaryData bd32(32); + CryptoPP::SHA256 sha256_; + CryptoPP::RIPEMD160 ripemd160_; + BinaryData bd32(32); sha256_.CalculateDigest(bd32.getPtr(), strToHash, nBytes); ripemd160_.CalculateDigest(hashOutput.getPtr(), bd32.getPtr(), 32); @@ -575,8 +623,8 @@ class BtcUtils // I need a non-static, non-overloaded method to be able to use this in SWIG BinaryData ripemd160_SWIG(BinaryData const & strToHash) { - static CryptoPP::RIPEMD160 ripemd160_; - static BinaryData bd20(20); + CryptoPP::RIPEMD160 ripemd160_; + BinaryData bd20(20); ripemd160_.CalculateDigest(bd20.getPtr(), strToHash.getPtr(), strToHash.getSize()); return bd20; @@ -597,7 +645,7 @@ class BtcUtils // and copy the result to the right size list afterwards uint32_t numTx = txhashlist.size(); vector merkleTree(3*numTx); - static CryptoPP::SHA256 sha256_; + CryptoPP::SHA256 sha256_; BinaryData hashInput(64); BinaryData hashOutput(32); @@ -651,6 +699,15 @@ class BtcUtils uint32_t scrLen = (uint32_t)readVarInt(ptr+36, &viLen); return (36 + viLen + scrLen + 4); } + + static uint32_t TxInCalcLength(uint8_t const * ptr, uint32_t size) + { + if (size < 37) + throw BlockDeserializingException(); + uint32_t viLen; + uint32_t scrLen = (uint32_t)readVarInt(ptr+36, size-36, &viLen); + return (36 + viLen + scrLen + 4); + } ///////////////////////////////////////////////////////////////////////////// static uint32_t TxOutCalcLength(uint8_t const * ptr) @@ -659,14 +716,27 @@ class BtcUtils uint32_t scrLen = (uint32_t)readVarInt(ptr+8, &viLen); return (8 + viLen + scrLen); } + + ///////////////////////////////////////////////////////////////////////////// + static uint32_t TxOutCalcLength(uint8_t const * ptr, uint32_t size) + { + if (size < 9) + throw BlockDeserializingException(); + uint32_t viLen; + uint32_t scrLen = (uint32_t)readVarInt(ptr+8, size-8, &viLen); + return (8 + viLen + scrLen); + } ///////////////////////////////////////////////////////////////////////////// static uint32_t TxCalcLength(uint8_t const * ptr, + uint32_t size, vector * offsetsIn=NULL, vector * offsetsOut=NULL) { - BinaryRefReader brr(ptr); + BinaryRefReader brr(ptr, size); + if (brr.getSizeRemaining() < 4) + throw BlockDeserializingException(); // Tx Version; brr.advance(4); @@ -678,7 +748,7 @@ class BtcUtils for(uint32_t i=0; i splitScr = splitPushOnlyScriptRefs(s); + vector splitScr = splitPushOnlyScriptRefs(script); if(splitScr.size() == 0) return TXIN_SCRIPT_NONSTANDARD; - if(isMultisigScript(splitScr[splitScr.size()-1])) - return TXIN_SCRIPT_SPENDP2SH; + // TODO: Maybe should identify whether the other pushed data + // in the script is a potential solution for the + // subscript... meh? + //BinaryDataRef lastObj = splitScr[splitScr.size() - 1]; - if(s[2]==0x30 && s[4]==0x02) + if(script[2]==0x30 && script[4]==0x02) return TXIN_SCRIPT_SPENDMULTI; } - if( !(s[1]==0x30 && s[3]==0x02) ) + if( !(script[1]==0x30 && script[3]==0x02) ) return TXIN_SCRIPT_NONSTANDARD; - uint32_t sigSize = s[2] + 4; + uint32_t sigSize = script[2] + 4; - if(s.getSize() == sigSize) + if(script.getSize() == sigSize) return TXIN_SCRIPT_SPENDPUBKEY; uint32_t keySizeFull = 66; // \x41 \x04 [X32] [Y32] uint32_t keySizeCompr= 34; // \x41 \x02 [X32] - if(s.getSize() == sigSize + keySizeFull) + if(script.getSize() == sigSize + keySizeFull) return TXIN_SCRIPT_STDUNCOMPR; - else if(s.getSize() == sigSize + keySizeCompr) + else if(script.getSize() == sigSize + keySizeCompr) return TXIN_SCRIPT_STDCOMPR; return TXIN_SCRIPT_NONSTANDARD; @@ -931,6 +1008,27 @@ class BtcUtils } } + ///////////////////////////////////////////////////////////////////////////// + // This is basically just for SWIG to access via python + static BinaryData getScrAddrForScript(BinaryData const & script) + { + return getTxOutScrAddr(script.getRef()); + } + + ///////////////////////////////////////////////////////////////////////////// + // This is basically just for SWIG to access via python + static uint32_t getTxOutScriptTypeInt(BinaryData const & script) + { + return (uint32_t)getTxOutScriptType(script.getRef()); + } + + ///////////////////////////////////////////////////////////////////////////// + // This is basically just for SWIG to access via python + static uint32_t getTxInScriptTypeInt(BinaryData const & script, + BinaryData const & prevHash) + { + return (uint32_t)getTxInScriptType(script.getRef(), prevHash); + } ///////////////////////////////////////////////////////////////////////////// static bool isMultisigScript(BinaryDataRef script) @@ -939,6 +1037,7 @@ class BtcUtils } ///////////////////////////////////////////////////////////////////////////// + // "UniqueKey"=="ScrAddr" - prefix // TODO: Interesting exercise: is there a non-standard script that could // look like the output of this function operating on a multisig // script (doesn't matter if it's valid or not)? In other words, is @@ -948,49 +1047,56 @@ class BtcUtils // output of this function? My guess is, no. And my guess is that // it's not a very useful even if it did. But it would be good to // rule it out. - static BinaryData getMultisigUniqueKey(BinaryDataRef script) + static BinaryData getMultisigUniqueKey(BinaryData const & script) { - static BinaryData nothing(0); - if( script[-1] != 0xae ) - return nothing; + vector a160List(0); - uint8_t M = script[0]; - uint8_t N = script[-2]; - - if(M<81 || M>96|| N<81 || N>96) - return nothing; + uint8_t M = getMultisigAddrList(script, a160List); + uint8_t N = a160List.size(); - M -= 80; - N -= 80; + if(M==0) + return BinaryData(0); BinaryWriter bw(2 + N*20); // reserve enough space for header + N addr - bw.put_uint8_t(M); bw.put_uint8_t(N); - BinaryRefReader brr(script); - brr.advance(1); - for(uint8_t i=0; i>; you can get N from list.size() - static uint8_t getMultisigAddrList( BinaryDataRef script, + // Returns M in M-of-N. Use addr160List.size() for N. Output is sorted. + static uint8_t getMultisigAddrList( BinaryData const & script, vector & addr160List) { - addr160List.clear(); + vector pkList; + uint32_t M = getMultisigPubKeyList(script, pkList); + uint32_t N = pkList.size(); + + if(M==0) + return 0; + + addr160List.resize(N); + for(uint32_t i=0; i & pkList) + { if( script[-1] != 0xae ) return 0; @@ -1006,20 +1112,57 @@ class BtcUtils BinaryRefReader brr(script); brr.advance(1); // Skip over M-value - vector accumList(N); + pkList.resize(N); for(uint8_t i=0; i outVect; + uint32_t M = getMultisigAddrList(script, outVect); + uint32_t N = outVect.size(); + + BinaryWriter bw(2 + N*20); // reserve enough space for header + N addr + bw.put_uint8_t(M); + bw.put_uint8_t(N); + for(uint32_t i=0; i outVect; + uint32_t M = getMultisigPubKeyList(script, outVect); + uint32_t N = outVect.size(); + + BinaryWriter bw(2 + N*20); // reserve enough space for header + N addr + bw.put_uint8_t(M); + bw.put_uint8_t(N); + for(uint32_t i=0; i pushVect = splitPushOnlyScriptRefs(script); + vector pushVect = splitPushOnlyScriptRefs(script); return getHash160(pushVect[pushVect.size()-1]); } case(TXIN_SCRIPT_COINBASE): @@ -1063,6 +1210,13 @@ class BtcUtils return BadAddress_; } } + + ///////////////////////////////////////////////////////////////////////////// + static BinaryData getTxInAddrFromTypeInt( BinaryData const & script, + uint32_t typeInt) + { + return getTxInAddrFromType(script.getRef(), (TXIN_SCRIPT_TYPE)typeInt); + } ///////////////////////////////////////////////////////////////////////////// static vector splitPushOnlyScriptRefs(BinaryDataRef script) @@ -1129,6 +1283,15 @@ class BtcUtils return out; } + ///////////////////////////////////////////////////////////////////////////// + static BinaryData getLastPushDataInScript(BinaryData const & script) + { + vector refs = splitPushOnlyScriptRefs(script); + if(refs.size()==0) + return BinaryData(0); + + return refs[refs.size() - 1]; + } ///////////////////////////////////////////////////////////////////////////// static double convertDiffBitsToDouble(BinaryData const & diffBitsBinary) diff --git a/cppForSwig/CppBlockUtils.i b/cppForSwig/CppBlockUtils.i index df710e19a..94f2fc0fb 100755 --- a/cppForSwig/CppBlockUtils.i +++ b/cppForSwig/CppBlockUtils.i @@ -2,7 +2,7 @@ /* //////////////////////////////////////////////////////////////////////////////// // // -// Copyright (C) 2011-2013, Armory Technologies, Inc. // +// Copyright (C) 2011-2014, Armory Technologies, Inc. // // support@bitcoinarmory.com // // Distributed under the GNU Affero General Public License (AGPL v3) // // See LICENSE or http://www.gnu.org/licenses/agpl.html // @@ -15,6 +15,7 @@ %{ #define SWIG_PYTHON_EXTRA_NATIVE_CONTAINERS #include "BlockObj.h" +#include "StoredBlockObj.h" #include "BlockUtils.h" #include "BtcUtils.h" #include "EncryptionUtils.h" @@ -99,6 +100,7 @@ namespace std /* With our typemaps, we can finally include our other objects */ %include "BlockObj.h" +%include "StoredBlockObj.h" %include "BlockUtils.h" %include "BtcUtils.h" %include "EncryptionUtils.h" diff --git a/cppForSwig/EncryptionUtils.cpp b/cppForSwig/EncryptionUtils.cpp index ca72ed851..6f9280d42 100644 --- a/cppForSwig/EncryptionUtils.cpp +++ b/cppForSwig/EncryptionUtils.cpp @@ -1,11 +1,12 @@ //////////////////////////////////////////////////////////////////////////////// // // -// Copyright(C) 2011-2013, Armory Technologies, Inc. // +// Copyright (C) 2011-2014, Armory Technologies, Inc. // // Distributed under the GNU Affero General Public License (AGPL v3) // // See LICENSE or http://www.gnu.org/licenses/agpl.html // // // //////////////////////////////////////////////////////////////////////////////// #include "EncryptionUtils.h" +#include "log.h" #include "integer.h" #include "oids.h" @@ -71,9 +72,17 @@ SecureBinaryData SecureBinaryData::copySwapEndian(size_t pos1, size_t pos2) cons } ///////////////////////////////////////////////////////////////////////////// -SecureBinaryData SecureBinaryData::GenerateRandom(uint32_t numBytes) +SecureBinaryData SecureBinaryData::GenerateRandom(uint32_t numBytes, + SecureBinaryData entropy) { - static BTC_PRNG prng; + BTC_PRNG prng; + + // Entropy here refers to *EXTRA* entropy. Crypto++ has it's own mechanism + // for generating entropy which is sufficient, but it doesn't hurt to add + // more if you have it. + if(entropy.getSize() > 0) + prng.IncorporateEntropy( (byte*)entropy.getPtr(), entropy.getSize()); + SecureBinaryData randData(numBytes); prng.GenerateBlock(randData.getPtr(), numBytes); return randData; @@ -202,7 +211,7 @@ void KdfRomix::printKdfParams(void) ///////////////////////////////////////////////////////////////////////////// SecureBinaryData KdfRomix::DeriveKey_OneIter(SecureBinaryData const & password) { - static CryptoPP::SHA512 sha512; + CryptoPP::SHA512 sha512; // Concatenate the salt/IV to the password SecureBinaryData saltedPassword = password + salt_; @@ -423,9 +432,9 @@ SecureBinaryData CryptoAES::DecryptCBC(SecureBinaryData & data, ///////////////////////////////////////////////////////////////////////////// -BTC_PRIVKEY CryptoECDSA::CreateNewPrivateKey(void) +BTC_PRIVKEY CryptoECDSA::CreateNewPrivateKey(SecureBinaryData entropy) { - return ParsePrivateKey(SecureBinaryData().GenerateRandom(32)); + return ParsePrivateKey(SecureBinaryData().GenerateRandom(32, entropy)); } ///////////////////////////////////////////////////////////////////////////// @@ -464,7 +473,7 @@ BTC_PUBKEY CryptoECDSA::ParsePublicKey(SecureBinaryData const & pubKeyX32B, cppPubKey.Initialize(CryptoPP::ASN1::secp256k1(), publicPoint); // Validate the public key -- not sure why this needs a PRNG - static BTC_PRNG prng; + BTC_PRNG prng; assert(cppPubKey.Validate(prng, 3)); return cppPubKey; @@ -509,7 +518,7 @@ BTC_PUBKEY CryptoECDSA::ComputePublicKey(BTC_PRIVKEY const & cppPrivKey) cppPrivKey.MakePublicKey(cppPubKey); // Validate the public key -- not sure why this needs a prng... - static BTC_PRNG prng; + BTC_PRNG prng; assert(cppPubKey.Validate(prng, 3)); assert(cppPubKey.Validate(prng, 3)); @@ -517,9 +526,9 @@ BTC_PUBKEY CryptoECDSA::ComputePublicKey(BTC_PRIVKEY const & cppPrivKey) } //////////////////////////////////////////////////////////////////////////////// -SecureBinaryData CryptoECDSA::GenerateNewPrivateKey(void) +SecureBinaryData CryptoECDSA::GenerateNewPrivateKey(SecureBinaryData entropy) { - return SecureBinaryData().GenerateRandom(32); + return SecureBinaryData().GenerateRandom(32, entropy); } @@ -573,7 +582,7 @@ bool CryptoECDSA::VerifyPublicKeyValid(SecureBinaryData const & pubKey65) cppPubKey.Initialize(CryptoPP::ASN1::secp256k1(), publicPoint); // Validate the public key -- not sure why this needs a PRNG - static BTC_PRNG prng; + BTC_PRNG prng; return cppPubKey.Validate(prng, 3); } @@ -599,8 +608,8 @@ SecureBinaryData CryptoECDSA::SignData(SecureBinaryData const & binToSign, // We trick the Crypto++ ECDSA module by passing it a single-hashed // message, it will do the second hash before it signs it. This is // exactly what we need. - static CryptoPP::SHA256 sha256; - static BTC_PRNG prng; + CryptoPP::SHA256 sha256; + BTC_PRNG prng; // Execute the first sha256 op -- the signer will do the other one SecureBinaryData hashVal(32); @@ -643,8 +652,8 @@ bool CryptoECDSA::VerifyData(SecureBinaryData const & binMessage, { - static CryptoPP::SHA256 sha256; - static BTC_PRNG prng; + CryptoPP::SHA256 sha256; + BTC_PRNG prng; assert(cppPubKey.Validate(prng, 3)); @@ -672,7 +681,8 @@ bool CryptoECDSA::VerifyData(SecureBinaryData const & binMessage, SecureBinaryData CryptoECDSA::ComputeChainedPrivateKey( SecureBinaryData const & binPrivKey, SecureBinaryData const & chainCode, - SecureBinaryData binPubKey) + SecureBinaryData binPubKey, + SecureBinaryData* multiplierOut) { if(CRYPTO_DEBUG) { @@ -688,12 +698,12 @@ SecureBinaryData CryptoECDSA::ComputeChainedPrivateKey( if( binPrivKey.getSize() != 32 || chainCode.getSize() != 32) { - cerr << "***ERROR: Invalid private key or chaincode (both must be 32B)"; - cerr << endl; - cerr << "BinPrivKey: " << binPrivKey.getSize() << endl; - cerr << "BinPrivKey: " << binPrivKey.toHexStr() << endl; - cerr << "BinChain : " << chainCode.getSize() << endl; - cerr << "BinChain : " << chainCode.toHexStr() << endl; + LOGERR << "***ERROR: Invalid private key or chaincode (both must be 32B)"; + LOGERR << "BinPrivKey: " << binPrivKey.getSize(); + LOGERR << "BinPrivKey: (not logged for security)"; + //LOGERR << "BinPrivKey: " << binPrivKey.toHexStr(); + LOGERR << "BinChain : " << chainCode.getSize(); + LOGERR << "BinChain : " << chainCode.toHexStr(); } // Adding extra entropy to chaincode by xor'ing with hash256 of pubkey @@ -709,13 +719,14 @@ SecureBinaryData CryptoECDSA::ComputeChainedPrivateKey( *(uint32_t*)(chainOrig.getPtr()+offset); } + // Hard-code the order of the group static SecureBinaryData SECP256K1_ORDER_BE = SecureBinaryData().CreateFromHex( "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"); - CryptoPP::Integer chaincode, origPrivExp, ecOrder; + CryptoPP::Integer mult, origPrivExp, ecOrder; // A - chaincode.Decode(chainXor.getPtr(), chainXor.getSize(), UNSIGNED); + mult.Decode(chainXor.getPtr(), chainXor.getSize(), UNSIGNED); // B origPrivExp.Decode(binPrivKey.getPtr(), binPrivKey.getSize(), UNSIGNED); // C @@ -723,11 +734,21 @@ SecureBinaryData CryptoECDSA::ComputeChainedPrivateKey( // A*B mod C will get us a new private key exponent CryptoPP::Integer newPrivExponent = - a_times_b_mod_c(chaincode, origPrivExp, ecOrder); + a_times_b_mod_c(mult, origPrivExp, ecOrder); // Convert new private exponent to big-endian binary string SecureBinaryData newPrivData(32); newPrivExponent.Encode(newPrivData.getPtr(), newPrivData.getSize(), UNSIGNED); + + if(multiplierOut != NULL) + (*multiplierOut) = SecureBinaryData(chainXor); + + //LOGINFO << "Computed new chained private key using:"; + //LOGINFO << " Public key: " << binPubKey.toHexStr().c_str(); + //LOGINFO << " PubKeyHash: " << chainMod.toHexStr().c_str(); + //LOGINFO << " Chaincode: " << chainOrig.toHexStr().c_str(); + //LOGINFO << " Multiplier: " << chainXor.toHexStr().c_str(); + return newPrivData; } @@ -735,7 +756,8 @@ SecureBinaryData CryptoECDSA::ComputeChainedPrivateKey( // Deterministically generate new public key using a chaincode SecureBinaryData CryptoECDSA::ComputeChainedPublicKey( SecureBinaryData const & binPubKey, - SecureBinaryData const & chainCode) + SecureBinaryData const & chainCode, + SecureBinaryData* multiplierOut) { if(CRYPTO_DEBUG) { @@ -760,18 +782,43 @@ SecureBinaryData CryptoECDSA::ComputeChainedPublicKey( } // Parse the chaincode as a big-endian integer - CryptoPP::Integer chaincode; - chaincode.Decode(chainXor.getPtr(), chainXor.getSize(), UNSIGNED); + CryptoPP::Integer mult; + mult.Decode(chainXor.getPtr(), chainXor.getSize(), UNSIGNED); // "new" init as "old", to make sure it's initialized on the correct curve BTC_PUBKEY oldPubKey = ParsePublicKey(binPubKey); BTC_PUBKEY newPubKey = ParsePublicKey(binPubKey); // Let Crypto++ do the EC math for us, serialize the new public key - newPubKey.SetPublicElement( oldPubKey.ExponentiatePublicElement(chaincode) ); + newPubKey.SetPublicElement( oldPubKey.ExponentiatePublicElement(mult) ); + + if(multiplierOut != NULL) + (*multiplierOut) = SecureBinaryData(chainXor); + + //LOGINFO << "Computed new chained public key using:"; + //LOGINFO << " Public key: " << binPubKey.toHexStr().c_str(); + //LOGINFO << " PubKeyHash: " << chainMod.toHexStr().c_str(); + //LOGINFO << " Chaincode: " << chainOrig.toHexStr().c_str(); + //LOGINFO << " Multiplier: " << chainXor.toHexStr().c_str(); + return CryptoECDSA::SerializePublicKey(newPubKey); } +//////////////////////////////////////////////////////////////////////////////// +SecureBinaryData CryptoECDSA::InvMod(const SecureBinaryData& m) +{ + static BinaryData N = BinaryData::CreateFromHex( + "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"); + CryptoPP::Integer cppM; + CryptoPP::Integer cppModulo; + cppM.Decode(m.getPtr(), m.getSize(), UNSIGNED); + cppModulo.Decode(N.getPtr(), N.getSize(), UNSIGNED); + CryptoPP::Integer cppResult = cppM.InverseMod(cppModulo); + SecureBinaryData result(32); + cppResult.Encode(result.getPtr(), result.getSize(), UNSIGNED); + return result; +} + //////////////////////////////////////////////////////////////////////////////// bool CryptoECDSA::ECVerifyPoint(BinaryData const & x, @@ -789,34 +836,40 @@ bool CryptoECDSA::ECVerifyPoint(BinaryData const & x, cppPubKey.Initialize(CryptoPP::ASN1::secp256k1(), publicPoint); // Validate the public key -- not sure why this needs a PRNG - static BTC_PRNG prng; + BTC_PRNG prng; return cppPubKey.Validate(prng, 3); } //////////////////////////////////////////////////////////////////////////////// -CryptoPP::ECP& CryptoECDSA::Get_secp256k1_ECP(void) +CryptoPP::ECP CryptoECDSA::Get_secp256k1_ECP(void) { static bool firstRun = true; - static CryptoPP::ECP theECP; - if(firstRun) + static CryptoPP::Integer intN; + static CryptoPP::Integer inta; + static CryptoPP::Integer intb; + + static BinaryData N; + static BinaryData a; + static BinaryData b; + + if(firstRun) { - BinaryData N = BinaryData::CreateFromHex( + firstRun = false; + N = BinaryData::CreateFromHex( "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"); - BinaryData a = BinaryData::CreateFromHex( + a = BinaryData::CreateFromHex( "0000000000000000000000000000000000000000000000000000000000000000"); - BinaryData b = BinaryData::CreateFromHex( - "0000000000000000000000000000000000000000000000000000000000000007"); - - CryptoPP::Integer intN, inta, intb; + b = BinaryData::CreateFromHex( + "0000000000000000000000000000000000000000000000000000000000000007"); intN.Decode( N.getPtr(), N.getSize(), UNSIGNED); inta.Decode( a.getPtr(), a.getSize(), UNSIGNED); intb.Decode( b.getPtr(), b.getSize(), UNSIGNED); - - theECP = CryptoPP::ECP(intN, inta, intb); } - return theECP; + + + return CryptoPP::ECP(intN, inta, intb); } @@ -896,7 +949,7 @@ BinaryData CryptoECDSA::ECInverse(BinaryData const & Ax, BinaryData const & Ay) { - CryptoPP::ECP & ecp = Get_secp256k1_ECP(); + CryptoPP::ECP ecp = Get_secp256k1_ECP(); CryptoPP::Integer intAx, intAy, intCx, intCy; intAx.Decode(Ax.getPtr(), Ax.getSize(), UNSIGNED); @@ -916,7 +969,7 @@ BinaryData CryptoECDSA::ECInverse(BinaryData const & Ax, //////////////////////////////////////////////////////////////////////////////// SecureBinaryData CryptoECDSA::CompressPoint(SecureBinaryData const & pubKey65) { - CryptoPP::ECP & ecp = Get_secp256k1_ECP(); + CryptoPP::ECP ecp = Get_secp256k1_ECP(); BTC_ECPOINT ptPub; ecp.DecodePoint(ptPub, (byte*)pubKey65.getPtr(), 65); SecureBinaryData ptCompressed(33); @@ -927,7 +980,7 @@ SecureBinaryData CryptoECDSA::CompressPoint(SecureBinaryData const & pubKey65) //////////////////////////////////////////////////////////////////////////////// SecureBinaryData CryptoECDSA::UncompressPoint(SecureBinaryData const & pubKey33) { - CryptoPP::ECP & ecp = Get_secp256k1_ECP(); + CryptoPP::ECP ecp = Get_secp256k1_ECP(); BTC_ECPOINT ptPub; ecp.DecodePoint(ptPub, (byte*)pubKey33.getPtr(), 33); SecureBinaryData ptUncompressed(65); @@ -966,12 +1019,3 @@ SecureBinaryData CryptoECDSA::UncompressPoint(SecureBinaryData const & pubKey33) EC_KEY_free(pubKey); return SecureBinaryData(sigSpace.getPtr(), sigSize); */ - - - - - - - - - diff --git a/cppForSwig/EncryptionUtils.h b/cppForSwig/EncryptionUtils.h index 89757747d..123e7c788 100644 --- a/cppForSwig/EncryptionUtils.h +++ b/cppForSwig/EncryptionUtils.h @@ -1,6 +1,6 @@ //////////////////////////////////////////////////////////////////////////////// // // -// Copyright(C) 2011-2013, Armory Technologies, Inc. // +// Copyright (C) 2011-2014, Armory Technologies, Inc. // // Distributed under the GNU Affero General Public License (AGPL v3) // // See LICENSE or http://www.gnu.org/licenses/agpl.html // // // @@ -186,7 +186,8 @@ class SecureBinaryData : public BinaryData // This would be a static method, as would be appropriate, except SWIG won't // play nice with static methods. Instead, we will just use // SecureBinaryData().GenerateRandom(32), etc - SecureBinaryData GenerateRandom(uint32_t numBytes); + SecureBinaryData GenerateRandom(uint32_t numBytes, + SecureBinaryData extraEntropy=SecureBinaryData()); void lockData(void) { @@ -314,7 +315,8 @@ class CryptoECDSA CryptoECDSA(void) {} ///////////////////////////////////////////////////////////////////////////// - static BTC_PRIVKEY CreateNewPrivateKey(void); + static BTC_PRIVKEY CreateNewPrivateKey( + SecureBinaryData extraEntropy=SecureBinaryData()); ///////////////////////////////////////////////////////////////////////////// static BTC_PRIVKEY ParsePrivateKey(SecureBinaryData const & privKeyData); @@ -355,7 +357,7 @@ class CryptoECDSA ///////////////////////////////////////////////////////////////////////////// // For doing direct raw ECPoint operations... need the ECP object - static CryptoPP::ECP & Get_secp256k1_ECP(void); + static CryptoPP::ECP Get_secp256k1_ECP(void); ///////////////////////////////////////////////////////////////////////////// @@ -364,7 +366,8 @@ class CryptoECDSA // SWIG to take BTC_PUBKEY and BTC_PRIVKEY ///////////////////////////////////////////////////////////////////////////// - SecureBinaryData GenerateNewPrivateKey(void); + SecureBinaryData GenerateNewPrivateKey( + SecureBinaryData extraEntropy=SecureBinaryData()); ///////////////////////////////////////////////////////////////////////////// SecureBinaryData ComputePublicKey(SecureBinaryData const & cppPrivKey); @@ -395,15 +398,21 @@ class CryptoECDSA // hurt to add some extra entropy/non-linearity to the chain // generation process) SecureBinaryData ComputeChainedPrivateKey( - SecureBinaryData const & binPrivKey, - SecureBinaryData const & chainCode, - SecureBinaryData binPubKey=SecureBinaryData()); + SecureBinaryData const & binPrivKey, + SecureBinaryData const & chainCode, + SecureBinaryData binPubKey=SecureBinaryData(), + SecureBinaryData* computedMultiplier=NULL); ///////////////////////////////////////////////////////////////////////////// // Deterministically generate new private key using a chaincode - SecureBinaryData ComputeChainedPublicKey(SecureBinaryData const & binPubKey, - SecureBinaryData const & chainCode); + SecureBinaryData ComputeChainedPublicKey( + SecureBinaryData const & binPubKey, + SecureBinaryData const & chainCode, + SecureBinaryData* multiplierOut=NULL); + ///////////////////////////////////////////////////////////////////////////// + // We need some direct access to Crypto++ math functions + SecureBinaryData InvMod(const SecureBinaryData& m); ///////////////////////////////////////////////////////////////////////////// // Some standard ECC operations @@ -434,17 +443,3 @@ class CryptoECDSA #endif - - - - - - - - - - - - - - diff --git a/cppForSwig/Makefile b/cppForSwig/Makefile index fbf6362ad..6ef1dace2 100644 --- a/cppForSwig/Makefile +++ b/cppForSwig/Makefile @@ -8,39 +8,38 @@ CFLAGS=-O2 -pipe -fPIC CXXFLAGS=-O2 -pipe -fPIC endif +platform=$(shell uname) + +ifeq ($(shell uname), Darwin) +MACOSX_DEPLOYMENT_TARGET=10.7 +export MACOSX_DEPLOYMENT_TARGET +LDFLAGS += -undefined dynamic_lookup -headerpad_max_install_names +endif + #************************************************************************** LINK = $(CXX) -OBJS = UniversalTimer.o BinaryData.o leveldb_wrapper.o StoredBlockObj.o BtcUtils.o BlockObj.o BlockUtils.o EncryptionUtils.o libcryptopp.a libleveldb.a - -# This is a script created by goatpig which detects where the python -# dependencies are and writes them to the pypaths.txt file. It defines : -# $(PYTHON_LIB) -# $(PYVER) -# $(PYTHON_INCLUDE) -# If this makefile doesn't work for you, you should manually modify -# pypaths.txt to define those three variables, and then comment out -# DO_EXEC_WHEREISPY line (to prevent the script from attempting to run -# and overwriting the manual values). -DO_EXEC_WHEREISPY := $(shell ./whereispy.sh) -include ./pypaths.txt +OBJS = UniversalTimer.o BinaryData.o leveldb_wrapper.o StoredBlockObj.o BtcUtils.o BlockObj.o BlockUtils.o EncryptionUtils.o libcryptopp.a libleveldb.a sighandler.o +#if python is specified, use it +ifndef PYVER +PYTHON_INCLUDES=$(shell python-config --includes ) +else +PYTHON_INCLUDES=$(shell $(PYVER)-config --includes ) +endif -CPPFLAGS += -Icryptopp -Ileveldb/include -DUSE_CRYPTOPP -D__STDC_LIMIT_MACROS -LDLIBS += -lpthread -Lleveldb -L$(PYTHON_LIB) -l$(PYVER) +CPPFLAGS += $(ARMORY_CPPFLAGS) -Icryptopp -Ileveldb/include -DUSE_CRYPTOPP -D__STDC_LIMIT_MACROS +LDLIBS += -lpthread -Lleveldb SWIG_OPTS += -c++ -python -classic -threads -SWIG_INC += -I"$(PYTHON_INCLUDE)" -STATICPYTHON += "$(PYTHON_LIB)" +SWIG_INC += $(PYTHON_INCLUDES) CXXCPP += $(CPPFLAGS) -#************************************************************************** -all: - $(MAKE) BlockUtilsTest.out +#************************************************************************** -swig: ../_CppBlockUtils.so ../qrc_img_resources.py +all: ../_CppBlockUtils.so ../qrc_img_resources.py ../_CppBlockUtils.so: $(OBJS) CppBlockUtils_wrap.o $(LINK) -shared -fPIC $(LDLIBS) $(LDFLAGS) $(CXXFLAGS) $(OBJS) $(STATICPYTHON) CppBlockUtils_wrap.o -o ../_CppBlockUtils.so @@ -48,9 +47,6 @@ swig: ../_CppBlockUtils.so ../qrc_img_resources.py ../qrc_img_resources.py: ../imgList.xml pyrcc4 -o ../qrc_img_resources.py ../imgList.xml -BlockUtilsTest.out : $(OBJS) BlockUtilsTest.cpp - $(LINK) $(LDLIBS) $(LDFLAGS) $(OBJS) -o BlockUtilsTest.out $(CXXCPP) $(CXXFLAGS) BlockUtilsTest.cpp - #************************************************************************** libcryptopp.a: Makefile @@ -85,10 +81,14 @@ CppBlockUtils_wrap.o: log.h BlockUtils.h BinaryData.h UniversalTimer.h CppBlock #************************************************************************ clean: touch CppBlockUtils.i - rm -f *.o *.out + rm -f *.o *.out *.a rm -f CppBlockUtils_wrap.cxx - rm pypaths.txt - $(MAKE) -C cryptopp clean - - + rm -f pypaths.txt +hardclean: + touch CppBlockUtils.i + rm -f *.o *.out *.a + rm -f CppBlockUtils_wrap.cxx + rm -f pypaths.txt + $(MAKE) -C cryptopp clean + $(MAKE) -C leveldb clean diff --git a/cppForSwig/OS_TranslatePath.h b/cppForSwig/OS_TranslatePath.h new file mode 100644 index 000000000..68c53eb47 --- /dev/null +++ b/cppForSwig/OS_TranslatePath.h @@ -0,0 +1,8 @@ +//Defines path translation for unicode support. Currently only active in Windows + +#ifndef _MSC_VER + #define OS_TranslatePath(X) X +#else + #include + #define OS_TranslatePath Win_TranslatePath +#endif diff --git a/cppForSwig/PartialMerkle.h b/cppForSwig/PartialMerkle.h index 86d62ac53..7bba1090e 100644 --- a/cppForSwig/PartialMerkle.h +++ b/cppForSwig/PartialMerkle.h @@ -1,6 +1,6 @@ //////////////////////////////////////////////////////////////////////////////// // // -// Copyright(C) 2011-2013, Armory Technologies, Inc. // +// Copyright (C) 2011-2014, Armory Technologies, Inc. // // Distributed under the GNU Affero General Public License (AGPL v3) // // See LICENSE or http://www.gnu.org/licenses/agpl.html // // // diff --git a/cppForSwig/StoredBlockObj.cpp b/cppForSwig/StoredBlockObj.cpp index 84f6d87f0..ac0cea301 100644 --- a/cppForSwig/StoredBlockObj.cpp +++ b/cppForSwig/StoredBlockObj.cpp @@ -1,6 +1,6 @@ //////////////////////////////////////////////////////////////////////////////// // // -// Copyright(C) 2011-2013, Armory Technologies, Inc. // +// Copyright (C) 2011-2014, Armory Technologies, Inc. // // Distributed under the GNU Affero General Public License (AGPL v3) // // See LICENSE or http://www.gnu.org/licenses/agpl.html // // // @@ -198,6 +198,25 @@ void StoredHeader::createFromBlockHeader(BlockHeader & bh) blockHeight_ = bh.getBlockHeight(); duplicateID_ = UINT8_MAX; isMainBranch_ = bh.isMainBranch(); + hasBlockHeader_ = true; +} + +//////////////////////////////////////////////////////////////////////////////// +Tx StoredHeader::getTxCopy(uint16_t i) +{ + if(KEY_IN_MAP(i, stxMap_)) + return stxMap_[i].getTxCopy(); + else + return Tx(); +} + +//////////////////////////////////////////////////////////////////////////////// +BinaryData StoredHeader::getSerializedTx(uint16_t i) +{ + if(KEY_IN_MAP(i, stxMap_)) + return stxMap_[i].getSerializedTx(); + else + return BinaryData(0); } ///////////////////////////////////////////////////////////////////////////// @@ -1173,7 +1192,9 @@ TxOut StoredTxOut::getTxOutCopy(void) const LOGERR << "Attempted to get TxOut copy but not initialized"; return TxOut(); } - return TxOut(dataCopy_.getPtr()); + TxOut o; + o.unserialize_checked(dataCopy_.getPtr(), dataCopy_.getSize()); + return o; } //////////////////////////////////////////////////////////////////////////////// @@ -2558,6 +2579,23 @@ BinaryData StoredHeadHgtList::getDBKey(bool withPrefix) const } +//////////////////////////////////////////////////////////////////////////////// +void StoredHeadHgtList::unserializeDBKey(BinaryDataRef key) +{ + BinaryRefReader brr(key); + if(key.getSize() == 5) + { + uint8_t prefix = brr.get_uint8_t(); + if(prefix != DB_PREFIX_HEADHGT) + { + LOGERR << "Unserialized HEADHGT key but wrong prefix"; + return; + } + } + + height_ = brr.get_uint32_t(BIGENDIAN); +} + //////////////////////////////////////////////////////////////////////////////// BLKDATA_TYPE GlobalDBUtilities::readBlkDataKey( BinaryRefReader & brr, @@ -2791,14 +2829,14 @@ BinaryData GlobalDBUtilities::getBlkDataKeyNoPrefix( uint32_t height, } ///////////////////////////////////////////////////////////////////////////// -uint32_t GlobalDBUtilities::hgtxToHeight(BinaryData hgtx) +uint32_t GlobalDBUtilities::hgtxToHeight(const BinaryData& hgtx) { return (READ_UINT32_BE(hgtx) >> 8); } ///////////////////////////////////////////////////////////////////////////// -uint8_t GlobalDBUtilities::hgtxToDupID(BinaryData hgtx) +uint8_t GlobalDBUtilities::hgtxToDupID(const BinaryData& hgtx) { return (READ_UINT32_BE(hgtx) & 0x7f); } @@ -2810,3 +2848,4 @@ BinaryData GlobalDBUtilities::heightAndDupToHgtx(uint32_t hgt, uint8_t dup) return WRITE_UINT32_BE(hgtxInt); } +// kate: indent-width 3; replace-tabs on; diff --git a/cppForSwig/StoredBlockObj.h b/cppForSwig/StoredBlockObj.h index 5b8364189..f857c6b3c 100644 --- a/cppForSwig/StoredBlockObj.h +++ b/cppForSwig/StoredBlockObj.h @@ -1,6 +1,6 @@ //////////////////////////////////////////////////////////////////////////////// // // -// Copyright(C) 2011-2013, Armory Technologies, Inc. // +// Copyright (C) 2011-2014, Armory Technologies, Inc. // // Distributed under the GNU Affero General Public License (AGPL v3) // // See LICENSE or http://www.gnu.org/licenses/agpl.html // // // @@ -126,8 +126,8 @@ class GlobalDBUtilities { public: - static uint32_t hgtxToHeight(BinaryData hgtx); - static uint8_t hgtxToDupID(BinaryData hgtx); + static uint32_t hgtxToHeight(const BinaryData& hgtx); + static uint8_t hgtxToDupID(const BinaryData& hgtx); static BinaryData heightAndDupToHgtx(uint32_t hgt, uint8_t dup); ///////////////////////////////////////////////////////////////////////////// @@ -255,6 +255,7 @@ class StoredDBInfo pruneType_(DBUtils.getDbPruneType()) {} bool isInitialized(void) const { return magic_.getSize() > 0; } + bool isNull(void) { return !isInitialized(); } static BinaryData getDBKey(void); @@ -290,16 +291,24 @@ class StoredHeader merkle_(0), isMainBranch_(false), blockAppliedToDB_(false), - merkleIsPartial_(false) {} + merkleIsPartial_(false), + hasBlockHeader_(false) {} bool isInitialized(void) const {return dataCopy_.getSize() > 0;} + bool isNull(void) const {return !isInitialized(); } bool haveFullBlock(void) const; BlockHeader getBlockHeaderCopy(void) const; BinaryData getSerializedBlock(void) const; BinaryData getSerializedBlockHeader(void) const; void createFromBlockHeader(BlockHeader & bh); + uint32_t getNumTx() { return (isNull() ? 0 : numTx_); } + + Tx getTxCopy(uint16_t i); + BinaryData getSerializedTx(uint16_t i); + + void addTxToMap(uint16_t txIdx, Tx & tx); void addStoredTxToMap(uint16_t txIdx, StoredTx & tx); @@ -363,6 +372,8 @@ class StoredHeader DB_PRUNE_TYPE unserPrType_; MERKLE_SER_TYPE unserMkType_; + bool hasBlockHeader_; + }; @@ -380,6 +391,7 @@ class StoredTx fragBytes_(UINT32_MAX) {} bool isInitialized(void) const {return dataCopy_.getSize() > 0;} + bool isNull(void) { return !isInitialized(); } bool haveAllTxOut(void) const; StoredTx& createFromTx(Tx & tx, @@ -455,6 +467,7 @@ class StoredTxOut spentByTxInKey_(0) {} bool isInitialized(void) const {return dataCopy_.getSize() > 0;} + bool isNull(void) { return !isInitialized(); } void unserialize(BinaryData const & data); void unserialize(BinaryDataRef data); void unserialize(BinaryRefReader & brr); @@ -531,6 +544,7 @@ class StoredScriptHistory bool isInitialized(void) const { return uniqueKey_.getSize() > 0; } + bool isNull(void) { return !isInitialized(); } void unserializeDBValue(BinaryRefReader & brr); void serializeDBValue(BinaryWriter & bw ) const; @@ -604,6 +618,7 @@ class StoredSubHistory bool isInitialized(void) { return uniqueKey_.getSize() > 0; } + bool isNull(void) { return !isInitialized(); } void unserializeDBValue(BinaryRefReader & brr); void serializeDBValue(BinaryWriter & bw ) const; @@ -616,8 +631,8 @@ class StoredSubHistory SCRIPT_PREFIX getScriptType(void) const; uint64_t getTxioCount(void) const {return (uint64_t)txioSet_.size();} - void pprintOneLine(uint32_t indent=3); - void pprintFullSSH(uint32_t indent=3); + //void pprintOneLine(uint32_t indent=3); + //void pprintFullSSH(uint32_t indent=3); TxIOPair* findTxio(BinaryData const & dbKey8B, bool includeMultisig=false); TxIOPair& insertTxio(TxIOPair const & txio, bool withOverwrite=true); @@ -658,6 +673,7 @@ class StoredUndoData StoredUndoData(void) {} bool isInitialized(void) { return (outPointsAddedByBlock_.size() > 0);} + bool isNull(void) { return !isInitialized(); } void unserializeDBValue(BinaryRefReader & brr); void serializeDBValue(BinaryWriter & bw ) const; @@ -683,6 +699,7 @@ class StoredTxHints StoredTxHints(void) : txHashPrefix_(0), dbKeyList_(0), preferredDBKey_(0) {} bool isInitialized(void) { return txHashPrefix_.getSize() > 0; } + bool isNull(void) { return !isInitialized(); } uint32_t getNumHints(void) const { return dbKeyList_.size(); } BinaryDataRef getHint(uint32_t i) const { return dbKeyList_[i].getRef(); } @@ -736,6 +753,7 @@ class StoredHeadHgtList BinaryData getDBKey(bool withPrefix=true) const; bool isInitialized(void) { return (height_ != UINT32_MAX);} + bool isNull(void) { return !isInitialized(); } void setPreferredDupID(uint8_t newDup) {preferredDup_ = newDup;} diff --git a/cppForSwig/UniversalTimer.cpp b/cppForSwig/UniversalTimer.cpp index be084eaca..442844c35 100644 --- a/cppForSwig/UniversalTimer.cpp +++ b/cppForSwig/UniversalTimer.cpp @@ -1,6 +1,6 @@ //////////////////////////////////////////////////////////////////////////////// // // -// Copyright(C) 2011-2013, Armory Technologies, Inc. // +// Copyright (C) 2011-2014, Armory Technologies, Inc. // // Distributed under the GNU Affero General Public License (AGPL v3) // // See LICENSE or http://www.gnu.org/licenses/agpl.html // // // @@ -152,7 +152,7 @@ double UniversalTimer::read(string key, string grpstr) // Print complete timing results to a file of this name void UniversalTimer::printCSV(string filename, bool excludeZeros) { - ofstream os(filename.c_str(), ios::out); + ofstream os(OS_TranslatePath(filename.c_str()), ios::out); printCSV(os, excludeZeros); os.close(); } @@ -205,7 +205,7 @@ void UniversalTimer::printCSV(ostream & os, bool excludeZeros) // Print complete timing results to a file of this name void UniversalTimer::print(string filename, bool excludeZeros) { - ofstream os(filename.c_str(), ios::out); + ofstream os(OS_TranslatePath(filename.c_str()), ios::out); print(os, excludeZeros); os.close(); } diff --git a/cppForSwig/UniversalTimer.h b/cppForSwig/UniversalTimer.h index 69a085191..da22461ba 100644 --- a/cppForSwig/UniversalTimer.h +++ b/cppForSwig/UniversalTimer.h @@ -1,6 +1,6 @@ //////////////////////////////////////////////////////////////////////////////// // // -// Copyright(C) 2011-2013, Armory Technologies, Inc. // +// Copyright (C) 2011-2014, Armory Technologies, Inc. // // Distributed under the GNU Affero General Public License (AGPL v3) // // See LICENSE or http://www.gnu.org/licenses/agpl.html // // // diff --git a/cppForSwig/cryptopp/Makefile b/cppForSwig/cryptopp/Makefile index 581d1aa12..eb56870d8 100755 --- a/cppForSwig/cryptopp/Makefile +++ b/cppForSwig/cryptopp/Makefile @@ -38,9 +38,11 @@ ifeq ($(UNAME),Darwin) CXXFLAGS += -arch x86_64 -arch i386 else #CXXFLAGS += -march=native +ifeq ($(CXX),g++) CXXFLAGS += -mtune=generic endif endif +endif ifneq ($(INTEL_COMPILER),0) CXXFLAGS += -wd68 -wd186 -wd279 -wd327 diff --git a/cppForSwig/cryptopp/authenc.cpp b/cppForSwig/cryptopp/authenc.cpp index 0ca5da60d..f93662efb 100755 --- a/cppForSwig/cryptopp/authenc.cpp +++ b/cppForSwig/cryptopp/authenc.cpp @@ -1,180 +1,180 @@ -// authenc.cpp - written and placed in the public domain by Wei Dai - -#include "pch.h" - -#ifndef CRYPTOPP_IMPORTS - -#include "authenc.h" - -NAMESPACE_BEGIN(CryptoPP) - -void AuthenticatedSymmetricCipherBase::AuthenticateData(const byte *input, size_t len) -{ - unsigned int blockSize = AuthenticationBlockSize(); - unsigned int &num = m_bufferedDataLength; - byte* data = m_buffer.begin(); - - if (num != 0) // process left over data - { - if (num+len >= blockSize) - { - memcpy(data+num, input, blockSize-num); - AuthenticateBlocks(data, blockSize); - input += (blockSize-num); - len -= (blockSize-num); - num = 0; - // drop through and do the rest - } - else - { - memcpy(data+num, input, len); - num += (unsigned int)len; - return; - } - } - - // now process the input data in blocks of blockSize bytes and save the leftovers to m_data - if (len >= blockSize) - { - size_t leftOver = AuthenticateBlocks(input, len); - input += (len - leftOver); - len = leftOver; - } - - memcpy(data, input, len); - num = (unsigned int)len; -} - -void AuthenticatedSymmetricCipherBase::SetKey(const byte *userKey, size_t keylength, const NameValuePairs ¶ms) -{ - m_bufferedDataLength = 0; - m_state = State_Start; - - SetKeyWithoutResync(userKey, keylength, params); - m_state = State_KeySet; - - size_t length; - const byte *iv = GetIVAndThrowIfInvalid(params, length); - if (iv) - Resynchronize(iv, (int)length); -} - -void AuthenticatedSymmetricCipherBase::Resynchronize(const byte *iv, int length) -{ - if (m_state < State_KeySet) - throw BadState(AlgorithmName(), "Resynchronize", "key is set"); - - m_bufferedDataLength = 0; - m_totalHeaderLength = m_totalMessageLength = m_totalFooterLength = 0; - m_state = State_KeySet; - - Resync(iv, this->ThrowIfInvalidIVLength(length)); - m_state = State_IVSet; -} - -void AuthenticatedSymmetricCipherBase::Update(const byte *input, size_t length) -{ - if (length == 0) - return; - - switch (m_state) - { - case State_Start: - case State_KeySet: - throw BadState(AlgorithmName(), "Update", "setting key and IV"); - case State_IVSet: - AuthenticateData(input, length); - m_totalHeaderLength += length; - break; - case State_AuthUntransformed: - case State_AuthTransformed: - AuthenticateLastConfidentialBlock(); - m_bufferedDataLength = 0; - m_state = State_AuthFooter; - // fall through - case State_AuthFooter: - AuthenticateData(input, length); - m_totalFooterLength += length; - break; - default: - assert(false); - } -} - -void AuthenticatedSymmetricCipherBase::ProcessData(byte *outString, const byte *inString, size_t length) -{ - m_totalMessageLength += length; - if (m_state >= State_IVSet && m_totalMessageLength > MaxMessageLength()) - throw InvalidArgument(AlgorithmName() + ": message length exceeds maximum"); - -reswitch: - switch (m_state) - { - case State_Start: - case State_KeySet: - throw BadState(AlgorithmName(), "ProcessData", "setting key and IV"); - case State_AuthFooter: - throw BadState(AlgorithmName(), "ProcessData was called after footer input has started"); - case State_IVSet: - AuthenticateLastHeaderBlock(); - m_bufferedDataLength = 0; - m_state = AuthenticationIsOnPlaintext()==IsForwardTransformation() ? State_AuthUntransformed : State_AuthTransformed; - goto reswitch; - case State_AuthUntransformed: - AuthenticateData(inString, length); - AccessSymmetricCipher().ProcessData(outString, inString, length); - break; - case State_AuthTransformed: - AccessSymmetricCipher().ProcessData(outString, inString, length); - AuthenticateData(outString, length); - break; - default: - assert(false); - } -} - -void AuthenticatedSymmetricCipherBase::TruncatedFinal(byte *mac, size_t macSize) -{ - if (m_totalHeaderLength > MaxHeaderLength()) - throw InvalidArgument(AlgorithmName() + ": header length of " + IntToString(m_totalHeaderLength) + " exceeds the maximum of " + IntToString(MaxHeaderLength())); - - if (m_totalFooterLength > MaxFooterLength()) - { - if (MaxFooterLength() == 0) - throw InvalidArgument(AlgorithmName() + ": additional authenticated data (AAD) cannot be input after data to be encrypted or decrypted"); - else - throw InvalidArgument(AlgorithmName() + ": footer length of " + IntToString(m_totalFooterLength) + " exceeds the maximum of " + IntToString(MaxFooterLength())); - } - - switch (m_state) - { - case State_Start: - case State_KeySet: - throw BadState(AlgorithmName(), "TruncatedFinal", "setting key and IV"); - - case State_IVSet: - AuthenticateLastHeaderBlock(); - m_bufferedDataLength = 0; - // fall through - - case State_AuthUntransformed: - case State_AuthTransformed: - AuthenticateLastConfidentialBlock(); - m_bufferedDataLength = 0; - // fall through - - case State_AuthFooter: - AuthenticateLastFooterBlock(mac, macSize); - m_bufferedDataLength = 0; - break; - - default: - assert(false); - } - - m_state = State_KeySet; -} - -NAMESPACE_END - -#endif +// authenc.cpp - written and placed in the public domain by Wei Dai + +#include "pch.h" + +#ifndef CRYPTOPP_IMPORTS + +#include "authenc.h" + +NAMESPACE_BEGIN(CryptoPP) + +void AuthenticatedSymmetricCipherBase::AuthenticateData(const byte *input, size_t len) +{ + unsigned int blockSize = AuthenticationBlockSize(); + unsigned int &num = m_bufferedDataLength; + byte* data = m_buffer.begin(); + + if (num != 0) // process left over data + { + if (num+len >= blockSize) + { + memcpy(data+num, input, blockSize-num); + AuthenticateBlocks(data, blockSize); + input += (blockSize-num); + len -= (blockSize-num); + num = 0; + // drop through and do the rest + } + else + { + memcpy(data+num, input, len); + num += (unsigned int)len; + return; + } + } + + // now process the input data in blocks of blockSize bytes and save the leftovers to m_data + if (len >= blockSize) + { + size_t leftOver = AuthenticateBlocks(input, len); + input += (len - leftOver); + len = leftOver; + } + + memcpy(data, input, len); + num = (unsigned int)len; +} + +void AuthenticatedSymmetricCipherBase::SetKey(const byte *userKey, size_t keylength, const NameValuePairs ¶ms) +{ + m_bufferedDataLength = 0; + m_state = State_Start; + + SetKeyWithoutResync(userKey, keylength, params); + m_state = State_KeySet; + + size_t length; + const byte *iv = GetIVAndThrowIfInvalid(params, length); + if (iv) + Resynchronize(iv, (int)length); +} + +void AuthenticatedSymmetricCipherBase::Resynchronize(const byte *iv, int length) +{ + if (m_state < State_KeySet) + throw BadState(AlgorithmName(), "Resynchronize", "key is set"); + + m_bufferedDataLength = 0; + m_totalHeaderLength = m_totalMessageLength = m_totalFooterLength = 0; + m_state = State_KeySet; + + Resync(iv, this->ThrowIfInvalidIVLength(length)); + m_state = State_IVSet; +} + +void AuthenticatedSymmetricCipherBase::Update(const byte *input, size_t length) +{ + if (length == 0) + return; + + switch (m_state) + { + case State_Start: + case State_KeySet: + throw BadState(AlgorithmName(), "Update", "setting key and IV"); + case State_IVSet: + AuthenticateData(input, length); + m_totalHeaderLength += length; + break; + case State_AuthUntransformed: + case State_AuthTransformed: + AuthenticateLastConfidentialBlock(); + m_bufferedDataLength = 0; + m_state = State_AuthFooter; + // fall through + case State_AuthFooter: + AuthenticateData(input, length); + m_totalFooterLength += length; + break; + default: + assert(false); + } +} + +void AuthenticatedSymmetricCipherBase::ProcessData(byte *outString, const byte *inString, size_t length) +{ + m_totalMessageLength += length; + if (m_state >= State_IVSet && m_totalMessageLength > MaxMessageLength()) + throw InvalidArgument(AlgorithmName() + ": message length exceeds maximum"); + +reswitch: + switch (m_state) + { + case State_Start: + case State_KeySet: + throw BadState(AlgorithmName(), "ProcessData", "setting key and IV"); + case State_AuthFooter: + throw BadState(AlgorithmName(), "ProcessData was called after footer input has started"); + case State_IVSet: + AuthenticateLastHeaderBlock(); + m_bufferedDataLength = 0; + m_state = AuthenticationIsOnPlaintext()==IsForwardTransformation() ? State_AuthUntransformed : State_AuthTransformed; + goto reswitch; + case State_AuthUntransformed: + AuthenticateData(inString, length); + AccessSymmetricCipher().ProcessData(outString, inString, length); + break; + case State_AuthTransformed: + AccessSymmetricCipher().ProcessData(outString, inString, length); + AuthenticateData(outString, length); + break; + default: + assert(false); + } +} + +void AuthenticatedSymmetricCipherBase::TruncatedFinal(byte *mac, size_t macSize) +{ + if (m_totalHeaderLength > MaxHeaderLength()) + throw InvalidArgument(AlgorithmName() + ": header length of " + IntToString(m_totalHeaderLength) + " exceeds the maximum of " + IntToString(MaxHeaderLength())); + + if (m_totalFooterLength > MaxFooterLength()) + { + if (MaxFooterLength() == 0) + throw InvalidArgument(AlgorithmName() + ": additional authenticated data (AAD) cannot be input after data to be encrypted or decrypted"); + else + throw InvalidArgument(AlgorithmName() + ": footer length of " + IntToString(m_totalFooterLength) + " exceeds the maximum of " + IntToString(MaxFooterLength())); + } + + switch (m_state) + { + case State_Start: + case State_KeySet: + throw BadState(AlgorithmName(), "TruncatedFinal", "setting key and IV"); + + case State_IVSet: + AuthenticateLastHeaderBlock(); + m_bufferedDataLength = 0; + // fall through + + case State_AuthUntransformed: + case State_AuthTransformed: + AuthenticateLastConfidentialBlock(); + m_bufferedDataLength = 0; + // fall through + + case State_AuthFooter: + AuthenticateLastFooterBlock(mac, macSize); + m_bufferedDataLength = 0; + break; + + default: + assert(false); + } + + m_state = State_KeySet; +} + +NAMESPACE_END + +#endif diff --git a/cppForSwig/cryptopp/authenc.h b/cppForSwig/cryptopp/authenc.h index f726716e7..5bb2a51c8 100755 --- a/cppForSwig/cryptopp/authenc.h +++ b/cppForSwig/cryptopp/authenc.h @@ -1,49 +1,49 @@ -#ifndef CRYPTOPP_AUTHENC_H -#define CRYPTOPP_AUTHENC_H - -#include "cryptlib.h" -#include "secblock.h" - -NAMESPACE_BEGIN(CryptoPP) - -//! . -class CRYPTOPP_DLL CRYPTOPP_NO_VTABLE AuthenticatedSymmetricCipherBase : public AuthenticatedSymmetricCipher -{ -public: - AuthenticatedSymmetricCipherBase() : m_state(State_Start) {} - - bool IsRandomAccess() const {return false;} - bool IsSelfInverting() const {return true;} - void UncheckedSetKey(const byte *,unsigned int,const CryptoPP::NameValuePairs &) {assert(false);} - - void SetKey(const byte *userKey, size_t keylength, const NameValuePairs ¶ms); - void Restart() {if (m_state > State_KeySet) m_state = State_KeySet;} - void Resynchronize(const byte *iv, int length=-1); - void Update(const byte *input, size_t length); - void ProcessData(byte *outString, const byte *inString, size_t length); - void TruncatedFinal(byte *mac, size_t macSize); - -protected: - void AuthenticateData(const byte *data, size_t len); - const SymmetricCipher & GetSymmetricCipher() const {return const_cast(this)->AccessSymmetricCipher();}; - - virtual SymmetricCipher & AccessSymmetricCipher() =0; - virtual bool AuthenticationIsOnPlaintext() const =0; - virtual unsigned int AuthenticationBlockSize() const =0; - virtual void SetKeyWithoutResync(const byte *userKey, size_t keylength, const NameValuePairs ¶ms) =0; - virtual void Resync(const byte *iv, size_t len) =0; - virtual size_t AuthenticateBlocks(const byte *data, size_t len) =0; - virtual void AuthenticateLastHeaderBlock() =0; - virtual void AuthenticateLastConfidentialBlock() {} - virtual void AuthenticateLastFooterBlock(byte *mac, size_t macSize) =0; - - enum State {State_Start, State_KeySet, State_IVSet, State_AuthUntransformed, State_AuthTransformed, State_AuthFooter}; - State m_state; - unsigned int m_bufferedDataLength; - lword m_totalHeaderLength, m_totalMessageLength, m_totalFooterLength; - AlignedSecByteBlock m_buffer; -}; - -NAMESPACE_END - -#endif +#ifndef CRYPTOPP_AUTHENC_H +#define CRYPTOPP_AUTHENC_H + +#include "cryptlib.h" +#include "secblock.h" + +NAMESPACE_BEGIN(CryptoPP) + +//! . +class CRYPTOPP_DLL CRYPTOPP_NO_VTABLE AuthenticatedSymmetricCipherBase : public AuthenticatedSymmetricCipher +{ +public: + AuthenticatedSymmetricCipherBase() : m_state(State_Start) {} + + bool IsRandomAccess() const {return false;} + bool IsSelfInverting() const {return true;} + void UncheckedSetKey(const byte *,unsigned int,const CryptoPP::NameValuePairs &) {assert(false);} + + void SetKey(const byte *userKey, size_t keylength, const NameValuePairs ¶ms); + void Restart() {if (m_state > State_KeySet) m_state = State_KeySet;} + void Resynchronize(const byte *iv, int length=-1); + void Update(const byte *input, size_t length); + void ProcessData(byte *outString, const byte *inString, size_t length); + void TruncatedFinal(byte *mac, size_t macSize); + +protected: + void AuthenticateData(const byte *data, size_t len); + const SymmetricCipher & GetSymmetricCipher() const {return const_cast(this)->AccessSymmetricCipher();}; + + virtual SymmetricCipher & AccessSymmetricCipher() =0; + virtual bool AuthenticationIsOnPlaintext() const =0; + virtual unsigned int AuthenticationBlockSize() const =0; + virtual void SetKeyWithoutResync(const byte *userKey, size_t keylength, const NameValuePairs ¶ms) =0; + virtual void Resync(const byte *iv, size_t len) =0; + virtual size_t AuthenticateBlocks(const byte *data, size_t len) =0; + virtual void AuthenticateLastHeaderBlock() =0; + virtual void AuthenticateLastConfidentialBlock() {} + virtual void AuthenticateLastFooterBlock(byte *mac, size_t macSize) =0; + + enum State {State_Start, State_KeySet, State_IVSet, State_AuthUntransformed, State_AuthTransformed, State_AuthFooter}; + State m_state; + unsigned int m_bufferedDataLength; + lword m_totalHeaderLength, m_totalMessageLength, m_totalFooterLength; + AlignedSecByteBlock m_buffer; +}; + +NAMESPACE_END + +#endif diff --git a/cppForSwig/cryptopp/ccm.cpp b/cppForSwig/cryptopp/ccm.cpp index 036878719..030828ad8 100755 --- a/cppForSwig/cryptopp/ccm.cpp +++ b/cppForSwig/cryptopp/ccm.cpp @@ -1,140 +1,140 @@ -// ccm.cpp - written and placed in the public domain by Wei Dai - -#include "pch.h" - -#ifndef CRYPTOPP_IMPORTS - -#include "ccm.h" - -NAMESPACE_BEGIN(CryptoPP) - -void CCM_Base::SetKeyWithoutResync(const byte *userKey, size_t keylength, const NameValuePairs ¶ms) -{ - BlockCipher &blockCipher = AccessBlockCipher(); - - blockCipher.SetKey(userKey, keylength, params); - - if (blockCipher.BlockSize() != REQUIRED_BLOCKSIZE) - throw InvalidArgument(AlgorithmName() + ": block size of underlying block cipher is not 16"); - - m_digestSize = params.GetIntValueWithDefault(Name::DigestSize(), DefaultDigestSize()); - if (m_digestSize % 2 > 0 || m_digestSize < 4 || m_digestSize > 16) - throw InvalidArgument(AlgorithmName() + ": DigestSize must be 4, 6, 8, 10, 12, 14, or 16"); - - m_buffer.Grow(2*REQUIRED_BLOCKSIZE); - m_L = 8; -} - -void CCM_Base::Resync(const byte *iv, size_t len) -{ - BlockCipher &cipher = AccessBlockCipher(); - - m_L = REQUIRED_BLOCKSIZE-1-(int)len; - assert(m_L >= 2); - if (m_L > 8) - m_L = 8; - - m_buffer[0] = byte(m_L-1); // flag - memcpy(m_buffer+1, iv, len); - memset(m_buffer+1+len, 0, REQUIRED_BLOCKSIZE-1-len); - - if (m_state >= State_IVSet) - m_ctr.Resynchronize(m_buffer, REQUIRED_BLOCKSIZE); - else - m_ctr.SetCipherWithIV(cipher, m_buffer); - - m_ctr.Seek(REQUIRED_BLOCKSIZE); - m_aadLength = 0; - m_messageLength = 0; -} - -void CCM_Base::UncheckedSpecifyDataLengths(lword headerLength, lword messageLength, lword footerLength) -{ - if (m_state != State_IVSet) - throw BadState(AlgorithmName(), "SpecifyDataLengths", "or after State_IVSet"); - - m_aadLength = headerLength; - m_messageLength = messageLength; - - byte *cbcBuffer = CBC_Buffer(); - const BlockCipher &cipher = GetBlockCipher(); - - cbcBuffer[0] = byte(64*(headerLength>0) + 8*((m_digestSize-2)/2) + (m_L-1)); // flag - PutWord(true, BIG_ENDIAN_ORDER, cbcBuffer+REQUIRED_BLOCKSIZE-8, m_messageLength); - memcpy(cbcBuffer+1, m_buffer+1, REQUIRED_BLOCKSIZE-1-m_L); - cipher.ProcessBlock(cbcBuffer); - - if (headerLength>0) - { - assert(m_bufferedDataLength == 0); - - if (headerLength < ((1<<16) - (1<<8))) - { - PutWord(true, BIG_ENDIAN_ORDER, m_buffer, (word16)headerLength); - m_bufferedDataLength = 2; - } - else if (headerLength < (W64LIT(1)<<32)) - { - m_buffer[0] = 0xff; - m_buffer[1] = 0xfe; - PutWord(false, BIG_ENDIAN_ORDER, m_buffer+2, (word32)headerLength); - m_bufferedDataLength = 6; - } - else - { - m_buffer[0] = 0xff; - m_buffer[1] = 0xff; - PutWord(false, BIG_ENDIAN_ORDER, m_buffer+2, headerLength); - m_bufferedDataLength = 10; - } - } -} - -size_t CCM_Base::AuthenticateBlocks(const byte *data, size_t len) -{ - byte *cbcBuffer = CBC_Buffer(); - const BlockCipher &cipher = GetBlockCipher(); - return cipher.AdvancedProcessBlocks(cbcBuffer, data, cbcBuffer, len, BlockTransformation::BT_DontIncrementInOutPointers|BlockTransformation::BT_XorInput); -} - -void CCM_Base::AuthenticateLastHeaderBlock() -{ - byte *cbcBuffer = CBC_Buffer(); - const BlockCipher &cipher = GetBlockCipher(); - - if (m_aadLength != m_totalHeaderLength) - throw InvalidArgument(AlgorithmName() + ": header length doesn't match that given in SpecifyDataLengths"); - - if (m_bufferedDataLength > 0) - { - xorbuf(cbcBuffer, m_buffer, m_bufferedDataLength); - cipher.ProcessBlock(cbcBuffer); - m_bufferedDataLength = 0; - } -} - -void CCM_Base::AuthenticateLastConfidentialBlock() -{ - byte *cbcBuffer = CBC_Buffer(); - const BlockCipher &cipher = GetBlockCipher(); - - if (m_messageLength != m_totalMessageLength) - throw InvalidArgument(AlgorithmName() + ": message length doesn't match that given in SpecifyDataLengths"); - - if (m_bufferedDataLength > 0) - { - xorbuf(cbcBuffer, m_buffer, m_bufferedDataLength); - cipher.ProcessBlock(cbcBuffer); - m_bufferedDataLength = 0; - } -} - -void CCM_Base::AuthenticateLastFooterBlock(byte *mac, size_t macSize) -{ - m_ctr.Seek(0); - m_ctr.ProcessData(mac, CBC_Buffer(), macSize); -} - -NAMESPACE_END - -#endif +// ccm.cpp - written and placed in the public domain by Wei Dai + +#include "pch.h" + +#ifndef CRYPTOPP_IMPORTS + +#include "ccm.h" + +NAMESPACE_BEGIN(CryptoPP) + +void CCM_Base::SetKeyWithoutResync(const byte *userKey, size_t keylength, const NameValuePairs ¶ms) +{ + BlockCipher &blockCipher = AccessBlockCipher(); + + blockCipher.SetKey(userKey, keylength, params); + + if (blockCipher.BlockSize() != REQUIRED_BLOCKSIZE) + throw InvalidArgument(AlgorithmName() + ": block size of underlying block cipher is not 16"); + + m_digestSize = params.GetIntValueWithDefault(Name::DigestSize(), DefaultDigestSize()); + if (m_digestSize % 2 > 0 || m_digestSize < 4 || m_digestSize > 16) + throw InvalidArgument(AlgorithmName() + ": DigestSize must be 4, 6, 8, 10, 12, 14, or 16"); + + m_buffer.Grow(2*REQUIRED_BLOCKSIZE); + m_L = 8; +} + +void CCM_Base::Resync(const byte *iv, size_t len) +{ + BlockCipher &cipher = AccessBlockCipher(); + + m_L = REQUIRED_BLOCKSIZE-1-(int)len; + assert(m_L >= 2); + if (m_L > 8) + m_L = 8; + + m_buffer[0] = byte(m_L-1); // flag + memcpy(m_buffer+1, iv, len); + memset(m_buffer+1+len, 0, REQUIRED_BLOCKSIZE-1-len); + + if (m_state >= State_IVSet) + m_ctr.Resynchronize(m_buffer, REQUIRED_BLOCKSIZE); + else + m_ctr.SetCipherWithIV(cipher, m_buffer); + + m_ctr.Seek(REQUIRED_BLOCKSIZE); + m_aadLength = 0; + m_messageLength = 0; +} + +void CCM_Base::UncheckedSpecifyDataLengths(lword headerLength, lword messageLength, lword footerLength) +{ + if (m_state != State_IVSet) + throw BadState(AlgorithmName(), "SpecifyDataLengths", "or after State_IVSet"); + + m_aadLength = headerLength; + m_messageLength = messageLength; + + byte *cbcBuffer = CBC_Buffer(); + const BlockCipher &cipher = GetBlockCipher(); + + cbcBuffer[0] = byte(64*(headerLength>0) + 8*((m_digestSize-2)/2) + (m_L-1)); // flag + PutWord(true, BIG_ENDIAN_ORDER, cbcBuffer+REQUIRED_BLOCKSIZE-8, m_messageLength); + memcpy(cbcBuffer+1, m_buffer+1, REQUIRED_BLOCKSIZE-1-m_L); + cipher.ProcessBlock(cbcBuffer); + + if (headerLength>0) + { + assert(m_bufferedDataLength == 0); + + if (headerLength < ((1<<16) - (1<<8))) + { + PutWord(true, BIG_ENDIAN_ORDER, m_buffer, (word16)headerLength); + m_bufferedDataLength = 2; + } + else if (headerLength < (W64LIT(1)<<32)) + { + m_buffer[0] = 0xff; + m_buffer[1] = 0xfe; + PutWord(false, BIG_ENDIAN_ORDER, m_buffer+2, (word32)headerLength); + m_bufferedDataLength = 6; + } + else + { + m_buffer[0] = 0xff; + m_buffer[1] = 0xff; + PutWord(false, BIG_ENDIAN_ORDER, m_buffer+2, headerLength); + m_bufferedDataLength = 10; + } + } +} + +size_t CCM_Base::AuthenticateBlocks(const byte *data, size_t len) +{ + byte *cbcBuffer = CBC_Buffer(); + const BlockCipher &cipher = GetBlockCipher(); + return cipher.AdvancedProcessBlocks(cbcBuffer, data, cbcBuffer, len, BlockTransformation::BT_DontIncrementInOutPointers|BlockTransformation::BT_XorInput); +} + +void CCM_Base::AuthenticateLastHeaderBlock() +{ + byte *cbcBuffer = CBC_Buffer(); + const BlockCipher &cipher = GetBlockCipher(); + + if (m_aadLength != m_totalHeaderLength) + throw InvalidArgument(AlgorithmName() + ": header length doesn't match that given in SpecifyDataLengths"); + + if (m_bufferedDataLength > 0) + { + xorbuf(cbcBuffer, m_buffer, m_bufferedDataLength); + cipher.ProcessBlock(cbcBuffer); + m_bufferedDataLength = 0; + } +} + +void CCM_Base::AuthenticateLastConfidentialBlock() +{ + byte *cbcBuffer = CBC_Buffer(); + const BlockCipher &cipher = GetBlockCipher(); + + if (m_messageLength != m_totalMessageLength) + throw InvalidArgument(AlgorithmName() + ": message length doesn't match that given in SpecifyDataLengths"); + + if (m_bufferedDataLength > 0) + { + xorbuf(cbcBuffer, m_buffer, m_bufferedDataLength); + cipher.ProcessBlock(cbcBuffer); + m_bufferedDataLength = 0; + } +} + +void CCM_Base::AuthenticateLastFooterBlock(byte *mac, size_t macSize) +{ + m_ctr.Seek(0); + m_ctr.ProcessData(mac, CBC_Buffer(), macSize); +} + +NAMESPACE_END + +#endif diff --git a/cppForSwig/cryptopp/ccm.h b/cppForSwig/cryptopp/ccm.h index 2f3c56b45..b1e5f00b9 100755 --- a/cppForSwig/cryptopp/ccm.h +++ b/cppForSwig/cryptopp/ccm.h @@ -1,101 +1,101 @@ -#ifndef CRYPTOPP_CCM_H -#define CRYPTOPP_CCM_H - -#include "authenc.h" -#include "modes.h" - -NAMESPACE_BEGIN(CryptoPP) - -//! . -class CRYPTOPP_DLL CRYPTOPP_NO_VTABLE CCM_Base : public AuthenticatedSymmetricCipherBase -{ -public: - CCM_Base() - : m_digestSize(0), m_L(0) {} - - // AuthenticatedSymmetricCipher - std::string AlgorithmName() const - {return GetBlockCipher().AlgorithmName() + std::string("/CCM");} - size_t MinKeyLength() const - {return GetBlockCipher().MinKeyLength();} - size_t MaxKeyLength() const - {return GetBlockCipher().MaxKeyLength();} - size_t DefaultKeyLength() const - {return GetBlockCipher().DefaultKeyLength();} - size_t GetValidKeyLength(size_t n) const - {return GetBlockCipher().GetValidKeyLength(n);} - bool IsValidKeyLength(size_t n) const - {return GetBlockCipher().IsValidKeyLength(n);} - unsigned int OptimalDataAlignment() const - {return GetBlockCipher().OptimalDataAlignment();} - IV_Requirement IVRequirement() const - {return UNIQUE_IV;} - unsigned int IVSize() const - {return 8;} - unsigned int MinIVLength() const - {return 7;} - unsigned int MaxIVLength() const - {return 13;} - unsigned int DigestSize() const - {return m_digestSize;} - lword MaxHeaderLength() const - {return W64LIT(0)-1;} - lword MaxMessageLength() const - {return m_L<8 ? (W64LIT(1)<<(8*m_L))-1 : W64LIT(0)-1;} - bool NeedsPrespecifiedDataLengths() const - {return true;} - void UncheckedSpecifyDataLengths(lword headerLength, lword messageLength, lword footerLength); - -protected: - // AuthenticatedSymmetricCipherBase - bool AuthenticationIsOnPlaintext() const - {return true;} - unsigned int AuthenticationBlockSize() const - {return GetBlockCipher().BlockSize();} - void SetKeyWithoutResync(const byte *userKey, size_t keylength, const NameValuePairs ¶ms); - void Resync(const byte *iv, size_t len); - size_t AuthenticateBlocks(const byte *data, size_t len); - void AuthenticateLastHeaderBlock(); - void AuthenticateLastConfidentialBlock(); - void AuthenticateLastFooterBlock(byte *mac, size_t macSize); - SymmetricCipher & AccessSymmetricCipher() {return m_ctr;} - - virtual BlockCipher & AccessBlockCipher() =0; - virtual int DefaultDigestSize() const =0; - - const BlockCipher & GetBlockCipher() const {return const_cast(this)->AccessBlockCipher();}; - byte *CBC_Buffer() {return m_buffer+REQUIRED_BLOCKSIZE;} - - enum {REQUIRED_BLOCKSIZE = 16}; - int m_digestSize, m_L; - word64 m_messageLength, m_aadLength; - CTR_Mode_ExternalCipher::Encryption m_ctr; -}; - -//! . -template -class CCM_Final : public CCM_Base -{ -public: - static std::string StaticAlgorithmName() - {return T_BlockCipher::StaticAlgorithmName() + std::string("/CCM");} - bool IsForwardTransformation() const - {return T_IsEncryption;} - -private: - BlockCipher & AccessBlockCipher() {return m_cipher;} - int DefaultDigestSize() const {return T_DefaultDigestSize;} - typename T_BlockCipher::Encryption m_cipher; -}; - -/// CCM -template -struct CCM : public AuthenticatedSymmetricCipherDocumentation -{ - typedef CCM_Final Encryption; - typedef CCM_Final Decryption; -}; - -NAMESPACE_END - -#endif +#ifndef CRYPTOPP_CCM_H +#define CRYPTOPP_CCM_H + +#include "authenc.h" +#include "modes.h" + +NAMESPACE_BEGIN(CryptoPP) + +//! . +class CRYPTOPP_DLL CRYPTOPP_NO_VTABLE CCM_Base : public AuthenticatedSymmetricCipherBase +{ +public: + CCM_Base() + : m_digestSize(0), m_L(0) {} + + // AuthenticatedSymmetricCipher + std::string AlgorithmName() const + {return GetBlockCipher().AlgorithmName() + std::string("/CCM");} + size_t MinKeyLength() const + {return GetBlockCipher().MinKeyLength();} + size_t MaxKeyLength() const + {return GetBlockCipher().MaxKeyLength();} + size_t DefaultKeyLength() const + {return GetBlockCipher().DefaultKeyLength();} + size_t GetValidKeyLength(size_t n) const + {return GetBlockCipher().GetValidKeyLength(n);} + bool IsValidKeyLength(size_t n) const + {return GetBlockCipher().IsValidKeyLength(n);} + unsigned int OptimalDataAlignment() const + {return GetBlockCipher().OptimalDataAlignment();} + IV_Requirement IVRequirement() const + {return UNIQUE_IV;} + unsigned int IVSize() const + {return 8;} + unsigned int MinIVLength() const + {return 7;} + unsigned int MaxIVLength() const + {return 13;} + unsigned int DigestSize() const + {return m_digestSize;} + lword MaxHeaderLength() const + {return W64LIT(0)-1;} + lword MaxMessageLength() const + {return m_L<8 ? (W64LIT(1)<<(8*m_L))-1 : W64LIT(0)-1;} + bool NeedsPrespecifiedDataLengths() const + {return true;} + void UncheckedSpecifyDataLengths(lword headerLength, lword messageLength, lword footerLength); + +protected: + // AuthenticatedSymmetricCipherBase + bool AuthenticationIsOnPlaintext() const + {return true;} + unsigned int AuthenticationBlockSize() const + {return GetBlockCipher().BlockSize();} + void SetKeyWithoutResync(const byte *userKey, size_t keylength, const NameValuePairs ¶ms); + void Resync(const byte *iv, size_t len); + size_t AuthenticateBlocks(const byte *data, size_t len); + void AuthenticateLastHeaderBlock(); + void AuthenticateLastConfidentialBlock(); + void AuthenticateLastFooterBlock(byte *mac, size_t macSize); + SymmetricCipher & AccessSymmetricCipher() {return m_ctr;} + + virtual BlockCipher & AccessBlockCipher() =0; + virtual int DefaultDigestSize() const =0; + + const BlockCipher & GetBlockCipher() const {return const_cast(this)->AccessBlockCipher();}; + byte *CBC_Buffer() {return m_buffer+REQUIRED_BLOCKSIZE;} + + enum {REQUIRED_BLOCKSIZE = 16}; + int m_digestSize, m_L; + word64 m_messageLength, m_aadLength; + CTR_Mode_ExternalCipher::Encryption m_ctr; +}; + +//! . +template +class CCM_Final : public CCM_Base +{ +public: + static std::string StaticAlgorithmName() + {return T_BlockCipher::StaticAlgorithmName() + std::string("/CCM");} + bool IsForwardTransformation() const + {return T_IsEncryption;} + +private: + BlockCipher & AccessBlockCipher() {return m_cipher;} + int DefaultDigestSize() const {return T_DefaultDigestSize;} + typename T_BlockCipher::Encryption m_cipher; +}; + +/// CCM +template +struct CCM : public AuthenticatedSymmetricCipherDocumentation +{ + typedef CCM_Final Encryption; + typedef CCM_Final Decryption; +}; + +NAMESPACE_END + +#endif diff --git a/cppForSwig/cryptopp/cmac.cpp b/cppForSwig/cryptopp/cmac.cpp index e8fa6fea6..a31d5f8b0 100755 --- a/cppForSwig/cryptopp/cmac.cpp +++ b/cppForSwig/cryptopp/cmac.cpp @@ -1,122 +1,122 @@ -// cmac.cpp - written and placed in the public domain by Wei Dai - -#include "pch.h" - -#ifndef CRYPTOPP_IMPORTS - -#include "cmac.h" - -NAMESPACE_BEGIN(CryptoPP) - -static void MulU(byte *k, unsigned int length) -{ - byte carry = 0; - - for (int i=length-1; i>=1; i-=2) - { - byte carry2 = k[i] >> 7; - k[i] += k[i] + carry; - carry = k[i-1] >> 7; - k[i-1] += k[i-1] + carry2; - } - - if (carry) - { - switch (length) - { - case 8: - k[7] ^= 0x1b; - break; - case 16: - k[15] ^= 0x87; - break; - case 32: - k[30] ^= 4; - k[31] ^= 0x23; - break; - default: - throw InvalidArgument("CMAC: " + IntToString(length) + " is not a supported cipher block size"); - } - } -} - -void CMAC_Base::UncheckedSetKey(const byte *key, unsigned int length, const NameValuePairs ¶ms) -{ - BlockCipher &cipher = AccessCipher(); - unsigned int blockSize = cipher.BlockSize(); - - cipher.SetKey(key, length, params); - m_reg.CleanNew(3*blockSize); - m_counter = 0; - - cipher.ProcessBlock(m_reg, m_reg+blockSize); - MulU(m_reg+blockSize, blockSize); - memcpy(m_reg+2*blockSize, m_reg+blockSize, blockSize); - MulU(m_reg+2*blockSize, blockSize); -} - -void CMAC_Base::Update(const byte *input, size_t length) -{ - if (!length) - return; - - BlockCipher &cipher = AccessCipher(); - unsigned int blockSize = cipher.BlockSize(); - - if (m_counter > 0) - { - unsigned int len = UnsignedMin(blockSize - m_counter, length); - xorbuf(m_reg+m_counter, input, len); - length -= len; - input += len; - m_counter += len; - - if (m_counter == blockSize && length > 0) - { - cipher.ProcessBlock(m_reg); - m_counter = 0; - } - } - - if (length > blockSize) - { - assert(m_counter == 0); - size_t leftOver = 1 + cipher.AdvancedProcessBlocks(m_reg, input, m_reg, length-1, BlockTransformation::BT_DontIncrementInOutPointers|BlockTransformation::BT_XorInput); - input += (length - leftOver); - length = leftOver; - } - - if (length > 0) - { - assert(m_counter + length <= blockSize); - xorbuf(m_reg+m_counter, input, length); - m_counter += (unsigned int)length; - } - - assert(m_counter > 0); -} - -void CMAC_Base::TruncatedFinal(byte *mac, size_t size) -{ - ThrowIfInvalidTruncatedSize(size); - - BlockCipher &cipher = AccessCipher(); - unsigned int blockSize = cipher.BlockSize(); - - if (m_counter < blockSize) - { - m_reg[m_counter] ^= 0x80; - cipher.AdvancedProcessBlocks(m_reg, m_reg+2*blockSize, m_reg, blockSize, BlockTransformation::BT_DontIncrementInOutPointers|BlockTransformation::BT_XorInput); - } - else - cipher.AdvancedProcessBlocks(m_reg, m_reg+blockSize, m_reg, blockSize, BlockTransformation::BT_DontIncrementInOutPointers|BlockTransformation::BT_XorInput); - - memcpy(mac, m_reg, size); - - m_counter = 0; - memset(m_reg, 0, blockSize); -} - -NAMESPACE_END - -#endif +// cmac.cpp - written and placed in the public domain by Wei Dai + +#include "pch.h" + +#ifndef CRYPTOPP_IMPORTS + +#include "cmac.h" + +NAMESPACE_BEGIN(CryptoPP) + +static void MulU(byte *k, unsigned int length) +{ + byte carry = 0; + + for (int i=length-1; i>=1; i-=2) + { + byte carry2 = k[i] >> 7; + k[i] += k[i] + carry; + carry = k[i-1] >> 7; + k[i-1] += k[i-1] + carry2; + } + + if (carry) + { + switch (length) + { + case 8: + k[7] ^= 0x1b; + break; + case 16: + k[15] ^= 0x87; + break; + case 32: + k[30] ^= 4; + k[31] ^= 0x23; + break; + default: + throw InvalidArgument("CMAC: " + IntToString(length) + " is not a supported cipher block size"); + } + } +} + +void CMAC_Base::UncheckedSetKey(const byte *key, unsigned int length, const NameValuePairs ¶ms) +{ + BlockCipher &cipher = AccessCipher(); + unsigned int blockSize = cipher.BlockSize(); + + cipher.SetKey(key, length, params); + m_reg.CleanNew(3*blockSize); + m_counter = 0; + + cipher.ProcessBlock(m_reg, m_reg+blockSize); + MulU(m_reg+blockSize, blockSize); + memcpy(m_reg+2*blockSize, m_reg+blockSize, blockSize); + MulU(m_reg+2*blockSize, blockSize); +} + +void CMAC_Base::Update(const byte *input, size_t length) +{ + if (!length) + return; + + BlockCipher &cipher = AccessCipher(); + unsigned int blockSize = cipher.BlockSize(); + + if (m_counter > 0) + { + unsigned int len = UnsignedMin(blockSize - m_counter, length); + xorbuf(m_reg+m_counter, input, len); + length -= len; + input += len; + m_counter += len; + + if (m_counter == blockSize && length > 0) + { + cipher.ProcessBlock(m_reg); + m_counter = 0; + } + } + + if (length > blockSize) + { + assert(m_counter == 0); + size_t leftOver = 1 + cipher.AdvancedProcessBlocks(m_reg, input, m_reg, length-1, BlockTransformation::BT_DontIncrementInOutPointers|BlockTransformation::BT_XorInput); + input += (length - leftOver); + length = leftOver; + } + + if (length > 0) + { + assert(m_counter + length <= blockSize); + xorbuf(m_reg+m_counter, input, length); + m_counter += (unsigned int)length; + } + + assert(m_counter > 0); +} + +void CMAC_Base::TruncatedFinal(byte *mac, size_t size) +{ + ThrowIfInvalidTruncatedSize(size); + + BlockCipher &cipher = AccessCipher(); + unsigned int blockSize = cipher.BlockSize(); + + if (m_counter < blockSize) + { + m_reg[m_counter] ^= 0x80; + cipher.AdvancedProcessBlocks(m_reg, m_reg+2*blockSize, m_reg, blockSize, BlockTransformation::BT_DontIncrementInOutPointers|BlockTransformation::BT_XorInput); + } + else + cipher.AdvancedProcessBlocks(m_reg, m_reg+blockSize, m_reg, blockSize, BlockTransformation::BT_DontIncrementInOutPointers|BlockTransformation::BT_XorInput); + + memcpy(mac, m_reg, size); + + m_counter = 0; + memset(m_reg, 0, blockSize); +} + +NAMESPACE_END + +#endif diff --git a/cppForSwig/cryptopp/cmac.h b/cppForSwig/cryptopp/cmac.h index ab3ecf8cc..d8a1b391d 100755 --- a/cppForSwig/cryptopp/cmac.h +++ b/cppForSwig/cryptopp/cmac.h @@ -1,52 +1,52 @@ -#ifndef CRYPTOPP_CMAC_H -#define CRYPTOPP_CMAC_H - -#include "seckey.h" -#include "secblock.h" - -NAMESPACE_BEGIN(CryptoPP) - -//! _ -class CRYPTOPP_DLL CRYPTOPP_NO_VTABLE CMAC_Base : public MessageAuthenticationCode -{ -public: - CMAC_Base() {} - - void UncheckedSetKey(const byte *key, unsigned int length, const NameValuePairs ¶ms); - void Update(const byte *input, size_t length); - void TruncatedFinal(byte *mac, size_t size); - unsigned int DigestSize() const {return GetCipher().BlockSize();} - unsigned int OptimalBlockSize() const {return GetCipher().BlockSize();} - unsigned int OptimalDataAlignment() const {return GetCipher().OptimalDataAlignment();} - -protected: - friend class EAX_Base; - - const BlockCipher & GetCipher() const {return const_cast(this)->AccessCipher();} - virtual BlockCipher & AccessCipher() =0; - - void ProcessBuf(); - SecByteBlock m_reg; - unsigned int m_counter; -}; - -/// CMAC -/*! Template parameter T should be a class derived from BlockCipherDocumentation, for example AES, with a block size of 8, 16, or 32 */ -template -class CMAC : public MessageAuthenticationCodeImpl >, public SameKeyLengthAs -{ -public: - CMAC() {} - CMAC(const byte *key, size_t length=SameKeyLengthAs::DEFAULT_KEYLENGTH) - {this->SetKey(key, length);} - - static std::string StaticAlgorithmName() {return std::string("CMAC(") + T::StaticAlgorithmName() + ")";} - -private: - BlockCipher & AccessCipher() {return m_cipher;} - typename T::Encryption m_cipher; -}; - -NAMESPACE_END - -#endif +#ifndef CRYPTOPP_CMAC_H +#define CRYPTOPP_CMAC_H + +#include "seckey.h" +#include "secblock.h" + +NAMESPACE_BEGIN(CryptoPP) + +//! _ +class CRYPTOPP_DLL CRYPTOPP_NO_VTABLE CMAC_Base : public MessageAuthenticationCode +{ +public: + CMAC_Base() {} + + void UncheckedSetKey(const byte *key, unsigned int length, const NameValuePairs ¶ms); + void Update(const byte *input, size_t length); + void TruncatedFinal(byte *mac, size_t size); + unsigned int DigestSize() const {return GetCipher().BlockSize();} + unsigned int OptimalBlockSize() const {return GetCipher().BlockSize();} + unsigned int OptimalDataAlignment() const {return GetCipher().OptimalDataAlignment();} + +protected: + friend class EAX_Base; + + const BlockCipher & GetCipher() const {return const_cast(this)->AccessCipher();} + virtual BlockCipher & AccessCipher() =0; + + void ProcessBuf(); + SecByteBlock m_reg; + unsigned int m_counter; +}; + +/// CMAC +/*! Template parameter T should be a class derived from BlockCipherDocumentation, for example AES, with a block size of 8, 16, or 32 */ +template +class CMAC : public MessageAuthenticationCodeImpl >, public SameKeyLengthAs +{ +public: + CMAC() {} + CMAC(const byte *key, size_t length=SameKeyLengthAs::DEFAULT_KEYLENGTH) + {this->SetKey(key, length);} + + static std::string StaticAlgorithmName() {return std::string("CMAC(") + T::StaticAlgorithmName() + ")";} + +private: + BlockCipher & AccessCipher() {return m_cipher;} + typename T::Encryption m_cipher; +}; + +NAMESPACE_END + +#endif diff --git a/cppForSwig/cryptopp/cryptdll.dsp b/cppForSwig/cryptopp/cryptdll.dsp index aad342ea6..4ea04f178 100755 --- a/cppForSwig/cryptopp/cryptdll.dsp +++ b/cppForSwig/cryptopp/cryptdll.dsp @@ -1,594 +1,594 @@ -# Microsoft Developer Studio Project File - Name="cryptdll" - Package Owner=<4> -# Microsoft Developer Studio Generated Build File, Format Version 6.00 -# ** DO NOT EDIT ** - -# TARGTYPE "Win32 (x86) Dynamic-Link Library" 0x0102 - -CFG=cryptdll - Win32 Debug -!MESSAGE This is not a valid makefile. To build this project using NMAKE, -!MESSAGE use the Export Makefile command and run -!MESSAGE -!MESSAGE NMAKE /f "cryptdll.mak". -!MESSAGE -!MESSAGE You can specify a configuration when running NMAKE -!MESSAGE by defining the macro CFG on the command line. For example: -!MESSAGE -!MESSAGE NMAKE /f "cryptdll.mak" CFG="cryptdll - Win32 Debug" -!MESSAGE -!MESSAGE Possible choices for configuration are: -!MESSAGE -!MESSAGE "cryptdll - Win32 Release" (based on "Win32 (x86) Dynamic-Link Library") -!MESSAGE "cryptdll - Win32 Debug" (based on "Win32 (x86) Dynamic-Link Library") -!MESSAGE - -# Begin Project -# PROP AllowPerConfigDependencies 0 -# PROP Scc_ProjName "" -# PROP Scc_LocalPath "" -CPP=cl.exe -MTL=midl.exe -RSC=rc.exe - -!IF "$(CFG)" == "cryptdll - Win32 Release" - -# PROP BASE Use_MFC 0 -# PROP BASE Use_Debug_Libraries 0 -# PROP BASE Output_Dir "cryptdll___Win32_Release" -# PROP BASE Intermediate_Dir "cryptdll___Win32_Release" -# PROP BASE Target_Dir "" -# PROP Use_MFC 0 -# PROP Use_Debug_Libraries 0 -# PROP Output_Dir "DLL_Release" -# PROP Intermediate_Dir "DLL_Release" -# PROP Ignore_Export_Lib 0 -# PROP Target_Dir "" -# ADD BASE CPP /nologo /MT /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "CRYPTDLL_EXPORTS" /YX /FD /c -# ADD CPP /nologo /G5 /MT /W3 /GR /GX /Zi /O1 /Ob2 /D "NDEBUG" /D "WIN32" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "CRYPTOPP_EXPORTS" /D CRYPTOPP_ENABLE_COMPLIANCE_WITH_FIPS_140_2=1 /D "USE_PRECOMPILED_HEADERS" /Yu"pch.h" /FD /Zm200 /c -# ADD BASE MTL /nologo /D "NDEBUG" /mktyplib203 /win32 -# ADD MTL /nologo /D "NDEBUG" /mktyplib203 /win32 -# ADD BASE RSC /l 0x409 /d "NDEBUG" -# ADD RSC /l 0x409 /d "NDEBUG" -BSC32=bscmake.exe -# ADD BASE BSC32 /nologo -# ADD BSC32 /nologo -LINK32=link.exe -# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /machine:I386 -# ADD LINK32 advapi32.lib /nologo /base:"0x42900000" /dll /map /debug /machine:I386 /out:"DLL_Release/cryptopp.dll" /opt:ref -# SUBTRACT LINK32 /pdb:none -# Begin Custom Build -OutDir=.\DLL_Release -TargetPath=.\DLL_Release\cryptopp.dll -InputPath=.\DLL_Release\cryptopp.dll -SOURCE="$(InputPath)" - -"$(OutDir)\cryptopp.mac.done" : $(SOURCE) "$(INTDIR)" "$(OUTDIR)" - CTRelease\cryptest mac_dll $(TargetPath) - echo mac done > $(OutDir)\cryptopp.mac.done - -# End Custom Build - -!ELSEIF "$(CFG)" == "cryptdll - Win32 Debug" - -# PROP BASE Use_MFC 0 -# PROP BASE Use_Debug_Libraries 1 -# PROP BASE Output_Dir "cryptdll___Win32_Debug" -# PROP BASE Intermediate_Dir "cryptdll___Win32_Debug" -# PROP BASE Target_Dir "" -# PROP Use_MFC 0 -# PROP Use_Debug_Libraries 1 -# PROP Output_Dir "DLL_Debug" -# PROP Intermediate_Dir "DLL_Debug" -# PROP Ignore_Export_Lib 0 -# PROP Target_Dir "" -# ADD BASE CPP /nologo /MTd /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "CRYPTDLL_EXPORTS" /YX /FD /GZ /c -# ADD CPP /nologo /G5 /MTd /W3 /Gm /GR /GX /Zi /Oi /D "_DEBUG" /D "WIN32" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "CRYPTOPP_EXPORTS" /D CRYPTOPP_ENABLE_COMPLIANCE_WITH_FIPS_140_2=1 /D "USE_PRECOMPILED_HEADERS" /Yu"pch.h" /FD /GZ /Zm200 /c -# ADD BASE MTL /nologo /D "_DEBUG" /mktyplib203 /win32 -# ADD MTL /nologo /D "_DEBUG" /mktyplib203 /win32 -# ADD BASE RSC /l 0x409 /d "_DEBUG" -# ADD RSC /l 0x409 /d "_DEBUG" -BSC32=bscmake.exe -# ADD BASE BSC32 /nologo -# ADD BSC32 /nologo -LINK32=link.exe -# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /debug /machine:I386 /pdbtype:sept -# ADD LINK32 advapi32.lib /nologo /base:"0x42900000" /dll /incremental:no /debug /machine:I386 /out:"DLL_Debug/cryptopp.dll" /opt:ref -# SUBTRACT LINK32 /pdb:none -# Begin Custom Build -OutDir=.\DLL_Debug -TargetPath=.\DLL_Debug\cryptopp.dll -InputPath=.\DLL_Debug\cryptopp.dll -SOURCE="$(InputPath)" - -"$(OutDir)\cryptopp.mac.done" : $(SOURCE) "$(INTDIR)" "$(OUTDIR)" - CTDebug\cryptest mac_dll $(TargetPath) - echo mac done > $(OutDir)\cryptopp.mac.done - -# End Custom Build - -!ENDIF - -# Begin Target - -# Name "cryptdll - Win32 Release" -# Name "cryptdll - Win32 Debug" -# Begin Group "Source Files" - -# PROP Default_Filter "cpp;c;cxx;rc;def;r;odl;idl;hpj;bat" -# Begin Source File - -SOURCE=.\algebra.cpp -# End Source File -# Begin Source File - -SOURCE=.\algparam.cpp -# End Source File -# Begin Source File - -SOURCE=.\asn.cpp -# End Source File -# Begin Source File - -SOURCE=.\authenc.cpp -# End Source File -# Begin Source File - -SOURCE=.\basecode.cpp -# End Source File -# Begin Source File - -SOURCE=.\cbcmac.cpp -# End Source File -# Begin Source File - -SOURCE=.\ccm.cpp -# End Source File -# Begin Source File - -SOURCE=.\channels.cpp -# End Source File -# Begin Source File - -SOURCE=.\cmac.cpp -# End Source File -# Begin Source File - -SOURCE=.\cpu.cpp -# End Source File -# Begin Source File - -SOURCE=.\cryptlib.cpp -# End Source File -# Begin Source File - -SOURCE=.\des.cpp -# End Source File -# Begin Source File - -SOURCE=.\dessp.cpp -# End Source File -# Begin Source File - -SOURCE=.\dh.cpp -# End Source File -# Begin Source File - -SOURCE=.\dll.cpp -# SUBTRACT CPP /YX /Yc /Yu -# End Source File -# Begin Source File - -SOURCE=.\dsa.cpp -# End Source File -# Begin Source File - -SOURCE=.\ec2n.cpp -# End Source File -# Begin Source File - -SOURCE=.\eccrypto.cpp -# End Source File -# Begin Source File - -SOURCE=.\ecp.cpp -# End Source File -# Begin Source File - -SOURCE=.\emsa2.cpp -# End Source File -# Begin Source File - -SOURCE=.\eprecomp.cpp -# End Source File -# Begin Source File - -SOURCE=.\files.cpp -# End Source File -# Begin Source File - -SOURCE=.\filters.cpp -# End Source File -# Begin Source File - -SOURCE=.\fips140.cpp -# End Source File -# Begin Source File - -SOURCE=.\fipstest.cpp -# End Source File -# Begin Source File - -SOURCE=.\gcm.cpp -# End Source File -# Begin Source File - -SOURCE=.\gf2n.cpp -# End Source File -# Begin Source File - -SOURCE=.\gfpcrypt.cpp -# End Source File -# Begin Source File - -SOURCE=.\hex.cpp -# End Source File -# Begin Source File - -SOURCE=.\hmac.cpp -# End Source File -# Begin Source File - -SOURCE=.\hrtimer.cpp -# End Source File -# Begin Source File - -SOURCE=.\integer.cpp -# End Source File -# Begin Source File - -SOURCE=.\iterhash.cpp -# SUBTRACT CPP /YX /Yc /Yu -# End Source File -# Begin Source File - -SOURCE=.\misc.cpp -# End Source File -# Begin Source File - -SOURCE=.\modes.cpp -# End Source File -# Begin Source File - -SOURCE=.\mqueue.cpp -# End Source File -# Begin Source File - -SOURCE=.\nbtheory.cpp -# End Source File -# Begin Source File - -SOURCE=.\oaep.cpp -# End Source File -# Begin Source File - -SOURCE=.\osrng.cpp -# End Source File -# Begin Source File - -SOURCE=.\pch.cpp -# ADD CPP /Yc"pch.h" -# End Source File -# Begin Source File - -SOURCE=.\pkcspad.cpp -# End Source File -# Begin Source File - -SOURCE=.\pssr.cpp -# End Source File -# Begin Source File - -SOURCE=.\pubkey.cpp -# End Source File -# Begin Source File - -SOURCE=.\queue.cpp -# End Source File -# Begin Source File - -SOURCE=.\randpool.cpp -# End Source File -# Begin Source File - -SOURCE=.\rdtables.cpp -# End Source File -# Begin Source File - -SOURCE=.\rijndael.cpp -# End Source File -# Begin Source File - -SOURCE=.\rng.cpp -# End Source File -# Begin Source File - -SOURCE=.\rsa.cpp -# End Source File -# Begin Source File - -SOURCE=.\rw.cpp -# End Source File -# Begin Source File - -SOURCE=.\sha.cpp -# End Source File -# Begin Source File - -SOURCE=.\simple.cpp -# End Source File -# Begin Source File - -SOURCE=.\skipjack.cpp -# End Source File -# Begin Source File - -SOURCE=.\strciphr.cpp -# End Source File -# Begin Source File - -SOURCE=.\trdlocal.cpp -# End Source File -# End Group -# Begin Group "Header Files" - -# PROP Default_Filter ".h" -# Begin Source File - -SOURCE=.\aes.h -# End Source File -# Begin Source File - -SOURCE=.\algebra.h -# End Source File -# Begin Source File - -SOURCE=.\algparam.h -# End Source File -# Begin Source File - -SOURCE=.\argnames.h -# End Source File -# Begin Source File - -SOURCE=.\asn.h -# End Source File -# Begin Source File - -SOURCE=.\authenc.h -# End Source File -# Begin Source File - -SOURCE=.\basecode.h -# End Source File -# Begin Source File - -SOURCE=.\cbcmac.h -# End Source File -# Begin Source File - -SOURCE=.\ccm.h -# End Source File -# Begin Source File - -SOURCE=.\channels.h -# End Source File -# Begin Source File - -SOURCE=.\cmac.h -# End Source File -# Begin Source File - -SOURCE=.\config.h -# End Source File -# Begin Source File - -SOURCE=.\cryptlib.h -# End Source File -# Begin Source File - -SOURCE=.\des.h -# End Source File -# Begin Source File - -SOURCE=.\dh.h -# End Source File -# Begin Source File - -SOURCE=.\dll.h -# End Source File -# Begin Source File - -SOURCE=.\dsa.h -# End Source File -# Begin Source File - -SOURCE=.\ec2n.h -# End Source File -# Begin Source File - -SOURCE=.\eccrypto.h -# End Source File -# Begin Source File - -SOURCE=.\ecp.h -# End Source File -# Begin Source File - -SOURCE=.\eprecomp.h -# End Source File -# Begin Source File - -SOURCE=.\files.h -# End Source File -# Begin Source File - -SOURCE=.\filters.h -# End Source File -# Begin Source File - -SOURCE=.\fips140.h -# End Source File -# Begin Source File - -SOURCE=.\fltrimpl.h -# End Source File -# Begin Source File - -SOURCE=.\gcm.h -# End Source File -# Begin Source File - -SOURCE=.\gf2n.h -# End Source File -# Begin Source File - -SOURCE=.\gfpcrypt.h -# End Source File -# Begin Source File - -SOURCE=.\hex.h -# End Source File -# Begin Source File - -SOURCE=.\hmac.h -# End Source File -# Begin Source File - -SOURCE=.\integer.h -# End Source File -# Begin Source File - -SOURCE=.\iterhash.h -# End Source File -# Begin Source File - -SOURCE=.\mdc.h -# End Source File -# Begin Source File - -SOURCE=.\misc.h -# End Source File -# Begin Source File - -SOURCE=.\modarith.h -# End Source File -# Begin Source File - -SOURCE=.\modes.h -# End Source File -# Begin Source File - -SOURCE=.\modexppc.h -# End Source File -# Begin Source File - -SOURCE=.\mqueue.h -# End Source File -# Begin Source File - -SOURCE=.\mqv.h -# End Source File -# Begin Source File - -SOURCE=.\nbtheory.h -# End Source File -# Begin Source File - -SOURCE=.\oaep.h -# End Source File -# Begin Source File - -SOURCE=.\oids.h -# End Source File -# Begin Source File - -SOURCE=.\osrng.h -# End Source File -# Begin Source File - -SOURCE=.\pch.h -# End Source File -# Begin Source File - -SOURCE=.\pkcspad.h -# End Source File -# Begin Source File - -SOURCE=.\pubkey.h -# End Source File -# Begin Source File - -SOURCE=.\queue.h -# End Source File -# Begin Source File - -SOURCE=.\randpool.h -# End Source File -# Begin Source File - -SOURCE=.\rijndael.h -# End Source File -# Begin Source File - -SOURCE=.\rng.h -# End Source File -# Begin Source File - -SOURCE=.\rsa.h -# End Source File -# Begin Source File - -SOURCE=.\secblock.h -# End Source File -# Begin Source File - -SOURCE=.\seckey.h -# End Source File -# Begin Source File - -SOURCE=.\sha.h -# End Source File -# Begin Source File - -SOURCE=.\simple.h -# End Source File -# Begin Source File - -SOURCE=.\skipjack.h -# End Source File -# Begin Source File - -SOURCE=.\smartptr.h -# End Source File -# Begin Source File - -SOURCE=.\stdcpp.h -# End Source File -# Begin Source File - -SOURCE=.\strciphr.h -# End Source File -# Begin Source File - -SOURCE=.\trdlocal.h -# End Source File -# Begin Source File - -SOURCE=.\words.h -# End Source File -# End Group -# Begin Source File - -SOURCE=.\cryptopp.rc -# End Source File -# End Target -# End Project +# Microsoft Developer Studio Project File - Name="cryptdll" - Package Owner=<4> +# Microsoft Developer Studio Generated Build File, Format Version 6.00 +# ** DO NOT EDIT ** + +# TARGTYPE "Win32 (x86) Dynamic-Link Library" 0x0102 + +CFG=cryptdll - Win32 Debug +!MESSAGE This is not a valid makefile. To build this project using NMAKE, +!MESSAGE use the Export Makefile command and run +!MESSAGE +!MESSAGE NMAKE /f "cryptdll.mak". +!MESSAGE +!MESSAGE You can specify a configuration when running NMAKE +!MESSAGE by defining the macro CFG on the command line. For example: +!MESSAGE +!MESSAGE NMAKE /f "cryptdll.mak" CFG="cryptdll - Win32 Debug" +!MESSAGE +!MESSAGE Possible choices for configuration are: +!MESSAGE +!MESSAGE "cryptdll - Win32 Release" (based on "Win32 (x86) Dynamic-Link Library") +!MESSAGE "cryptdll - Win32 Debug" (based on "Win32 (x86) Dynamic-Link Library") +!MESSAGE + +# Begin Project +# PROP AllowPerConfigDependencies 0 +# PROP Scc_ProjName "" +# PROP Scc_LocalPath "" +CPP=cl.exe +MTL=midl.exe +RSC=rc.exe + +!IF "$(CFG)" == "cryptdll - Win32 Release" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 0 +# PROP BASE Output_Dir "cryptdll___Win32_Release" +# PROP BASE Intermediate_Dir "cryptdll___Win32_Release" +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 0 +# PROP Output_Dir "DLL_Release" +# PROP Intermediate_Dir "DLL_Release" +# PROP Ignore_Export_Lib 0 +# PROP Target_Dir "" +# ADD BASE CPP /nologo /MT /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "CRYPTDLL_EXPORTS" /YX /FD /c +# ADD CPP /nologo /G5 /MT /W3 /GR /GX /Zi /O1 /Ob2 /D "NDEBUG" /D "WIN32" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "CRYPTOPP_EXPORTS" /D CRYPTOPP_ENABLE_COMPLIANCE_WITH_FIPS_140_2=1 /D "USE_PRECOMPILED_HEADERS" /Yu"pch.h" /FD /Zm200 /c +# ADD BASE MTL /nologo /D "NDEBUG" /mktyplib203 /win32 +# ADD MTL /nologo /D "NDEBUG" /mktyplib203 /win32 +# ADD BASE RSC /l 0x409 /d "NDEBUG" +# ADD RSC /l 0x409 /d "NDEBUG" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LINK32=link.exe +# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /machine:I386 +# ADD LINK32 advapi32.lib /nologo /base:"0x42900000" /dll /map /debug /machine:I386 /out:"DLL_Release/cryptopp.dll" /opt:ref +# SUBTRACT LINK32 /pdb:none +# Begin Custom Build +OutDir=.\DLL_Release +TargetPath=.\DLL_Release\cryptopp.dll +InputPath=.\DLL_Release\cryptopp.dll +SOURCE="$(InputPath)" + +"$(OutDir)\cryptopp.mac.done" : $(SOURCE) "$(INTDIR)" "$(OUTDIR)" + CTRelease\cryptest mac_dll $(TargetPath) + echo mac done > $(OutDir)\cryptopp.mac.done + +# End Custom Build + +!ELSEIF "$(CFG)" == "cryptdll - Win32 Debug" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 1 +# PROP BASE Output_Dir "cryptdll___Win32_Debug" +# PROP BASE Intermediate_Dir "cryptdll___Win32_Debug" +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 1 +# PROP Output_Dir "DLL_Debug" +# PROP Intermediate_Dir "DLL_Debug" +# PROP Ignore_Export_Lib 0 +# PROP Target_Dir "" +# ADD BASE CPP /nologo /MTd /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "CRYPTDLL_EXPORTS" /YX /FD /GZ /c +# ADD CPP /nologo /G5 /MTd /W3 /Gm /GR /GX /Zi /Oi /D "_DEBUG" /D "WIN32" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "CRYPTOPP_EXPORTS" /D CRYPTOPP_ENABLE_COMPLIANCE_WITH_FIPS_140_2=1 /D "USE_PRECOMPILED_HEADERS" /Yu"pch.h" /FD /GZ /Zm200 /c +# ADD BASE MTL /nologo /D "_DEBUG" /mktyplib203 /win32 +# ADD MTL /nologo /D "_DEBUG" /mktyplib203 /win32 +# ADD BASE RSC /l 0x409 /d "_DEBUG" +# ADD RSC /l 0x409 /d "_DEBUG" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LINK32=link.exe +# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /debug /machine:I386 /pdbtype:sept +# ADD LINK32 advapi32.lib /nologo /base:"0x42900000" /dll /incremental:no /debug /machine:I386 /out:"DLL_Debug/cryptopp.dll" /opt:ref +# SUBTRACT LINK32 /pdb:none +# Begin Custom Build +OutDir=.\DLL_Debug +TargetPath=.\DLL_Debug\cryptopp.dll +InputPath=.\DLL_Debug\cryptopp.dll +SOURCE="$(InputPath)" + +"$(OutDir)\cryptopp.mac.done" : $(SOURCE) "$(INTDIR)" "$(OUTDIR)" + CTDebug\cryptest mac_dll $(TargetPath) + echo mac done > $(OutDir)\cryptopp.mac.done + +# End Custom Build + +!ENDIF + +# Begin Target + +# Name "cryptdll - Win32 Release" +# Name "cryptdll - Win32 Debug" +# Begin Group "Source Files" + +# PROP Default_Filter "cpp;c;cxx;rc;def;r;odl;idl;hpj;bat" +# Begin Source File + +SOURCE=.\algebra.cpp +# End Source File +# Begin Source File + +SOURCE=.\algparam.cpp +# End Source File +# Begin Source File + +SOURCE=.\asn.cpp +# End Source File +# Begin Source File + +SOURCE=.\authenc.cpp +# End Source File +# Begin Source File + +SOURCE=.\basecode.cpp +# End Source File +# Begin Source File + +SOURCE=.\cbcmac.cpp +# End Source File +# Begin Source File + +SOURCE=.\ccm.cpp +# End Source File +# Begin Source File + +SOURCE=.\channels.cpp +# End Source File +# Begin Source File + +SOURCE=.\cmac.cpp +# End Source File +# Begin Source File + +SOURCE=.\cpu.cpp +# End Source File +# Begin Source File + +SOURCE=.\cryptlib.cpp +# End Source File +# Begin Source File + +SOURCE=.\des.cpp +# End Source File +# Begin Source File + +SOURCE=.\dessp.cpp +# End Source File +# Begin Source File + +SOURCE=.\dh.cpp +# End Source File +# Begin Source File + +SOURCE=.\dll.cpp +# SUBTRACT CPP /YX /Yc /Yu +# End Source File +# Begin Source File + +SOURCE=.\dsa.cpp +# End Source File +# Begin Source File + +SOURCE=.\ec2n.cpp +# End Source File +# Begin Source File + +SOURCE=.\eccrypto.cpp +# End Source File +# Begin Source File + +SOURCE=.\ecp.cpp +# End Source File +# Begin Source File + +SOURCE=.\emsa2.cpp +# End Source File +# Begin Source File + +SOURCE=.\eprecomp.cpp +# End Source File +# Begin Source File + +SOURCE=.\files.cpp +# End Source File +# Begin Source File + +SOURCE=.\filters.cpp +# End Source File +# Begin Source File + +SOURCE=.\fips140.cpp +# End Source File +# Begin Source File + +SOURCE=.\fipstest.cpp +# End Source File +# Begin Source File + +SOURCE=.\gcm.cpp +# End Source File +# Begin Source File + +SOURCE=.\gf2n.cpp +# End Source File +# Begin Source File + +SOURCE=.\gfpcrypt.cpp +# End Source File +# Begin Source File + +SOURCE=.\hex.cpp +# End Source File +# Begin Source File + +SOURCE=.\hmac.cpp +# End Source File +# Begin Source File + +SOURCE=.\hrtimer.cpp +# End Source File +# Begin Source File + +SOURCE=.\integer.cpp +# End Source File +# Begin Source File + +SOURCE=.\iterhash.cpp +# SUBTRACT CPP /YX /Yc /Yu +# End Source File +# Begin Source File + +SOURCE=.\misc.cpp +# End Source File +# Begin Source File + +SOURCE=.\modes.cpp +# End Source File +# Begin Source File + +SOURCE=.\mqueue.cpp +# End Source File +# Begin Source File + +SOURCE=.\nbtheory.cpp +# End Source File +# Begin Source File + +SOURCE=.\oaep.cpp +# End Source File +# Begin Source File + +SOURCE=.\osrng.cpp +# End Source File +# Begin Source File + +SOURCE=.\pch.cpp +# ADD CPP /Yc"pch.h" +# End Source File +# Begin Source File + +SOURCE=.\pkcspad.cpp +# End Source File +# Begin Source File + +SOURCE=.\pssr.cpp +# End Source File +# Begin Source File + +SOURCE=.\pubkey.cpp +# End Source File +# Begin Source File + +SOURCE=.\queue.cpp +# End Source File +# Begin Source File + +SOURCE=.\randpool.cpp +# End Source File +# Begin Source File + +SOURCE=.\rdtables.cpp +# End Source File +# Begin Source File + +SOURCE=.\rijndael.cpp +# End Source File +# Begin Source File + +SOURCE=.\rng.cpp +# End Source File +# Begin Source File + +SOURCE=.\rsa.cpp +# End Source File +# Begin Source File + +SOURCE=.\rw.cpp +# End Source File +# Begin Source File + +SOURCE=.\sha.cpp +# End Source File +# Begin Source File + +SOURCE=.\simple.cpp +# End Source File +# Begin Source File + +SOURCE=.\skipjack.cpp +# End Source File +# Begin Source File + +SOURCE=.\strciphr.cpp +# End Source File +# Begin Source File + +SOURCE=.\trdlocal.cpp +# End Source File +# End Group +# Begin Group "Header Files" + +# PROP Default_Filter ".h" +# Begin Source File + +SOURCE=.\aes.h +# End Source File +# Begin Source File + +SOURCE=.\algebra.h +# End Source File +# Begin Source File + +SOURCE=.\algparam.h +# End Source File +# Begin Source File + +SOURCE=.\argnames.h +# End Source File +# Begin Source File + +SOURCE=.\asn.h +# End Source File +# Begin Source File + +SOURCE=.\authenc.h +# End Source File +# Begin Source File + +SOURCE=.\basecode.h +# End Source File +# Begin Source File + +SOURCE=.\cbcmac.h +# End Source File +# Begin Source File + +SOURCE=.\ccm.h +# End Source File +# Begin Source File + +SOURCE=.\channels.h +# End Source File +# Begin Source File + +SOURCE=.\cmac.h +# End Source File +# Begin Source File + +SOURCE=.\config.h +# End Source File +# Begin Source File + +SOURCE=.\cryptlib.h +# End Source File +# Begin Source File + +SOURCE=.\des.h +# End Source File +# Begin Source File + +SOURCE=.\dh.h +# End Source File +# Begin Source File + +SOURCE=.\dll.h +# End Source File +# Begin Source File + +SOURCE=.\dsa.h +# End Source File +# Begin Source File + +SOURCE=.\ec2n.h +# End Source File +# Begin Source File + +SOURCE=.\eccrypto.h +# End Source File +# Begin Source File + +SOURCE=.\ecp.h +# End Source File +# Begin Source File + +SOURCE=.\eprecomp.h +# End Source File +# Begin Source File + +SOURCE=.\files.h +# End Source File +# Begin Source File + +SOURCE=.\filters.h +# End Source File +# Begin Source File + +SOURCE=.\fips140.h +# End Source File +# Begin Source File + +SOURCE=.\fltrimpl.h +# End Source File +# Begin Source File + +SOURCE=.\gcm.h +# End Source File +# Begin Source File + +SOURCE=.\gf2n.h +# End Source File +# Begin Source File + +SOURCE=.\gfpcrypt.h +# End Source File +# Begin Source File + +SOURCE=.\hex.h +# End Source File +# Begin Source File + +SOURCE=.\hmac.h +# End Source File +# Begin Source File + +SOURCE=.\integer.h +# End Source File +# Begin Source File + +SOURCE=.\iterhash.h +# End Source File +# Begin Source File + +SOURCE=.\mdc.h +# End Source File +# Begin Source File + +SOURCE=.\misc.h +# End Source File +# Begin Source File + +SOURCE=.\modarith.h +# End Source File +# Begin Source File + +SOURCE=.\modes.h +# End Source File +# Begin Source File + +SOURCE=.\modexppc.h +# End Source File +# Begin Source File + +SOURCE=.\mqueue.h +# End Source File +# Begin Source File + +SOURCE=.\mqv.h +# End Source File +# Begin Source File + +SOURCE=.\nbtheory.h +# End Source File +# Begin Source File + +SOURCE=.\oaep.h +# End Source File +# Begin Source File + +SOURCE=.\oids.h +# End Source File +# Begin Source File + +SOURCE=.\osrng.h +# End Source File +# Begin Source File + +SOURCE=.\pch.h +# End Source File +# Begin Source File + +SOURCE=.\pkcspad.h +# End Source File +# Begin Source File + +SOURCE=.\pubkey.h +# End Source File +# Begin Source File + +SOURCE=.\queue.h +# End Source File +# Begin Source File + +SOURCE=.\randpool.h +# End Source File +# Begin Source File + +SOURCE=.\rijndael.h +# End Source File +# Begin Source File + +SOURCE=.\rng.h +# End Source File +# Begin Source File + +SOURCE=.\rsa.h +# End Source File +# Begin Source File + +SOURCE=.\secblock.h +# End Source File +# Begin Source File + +SOURCE=.\seckey.h +# End Source File +# Begin Source File + +SOURCE=.\sha.h +# End Source File +# Begin Source File + +SOURCE=.\simple.h +# End Source File +# Begin Source File + +SOURCE=.\skipjack.h +# End Source File +# Begin Source File + +SOURCE=.\smartptr.h +# End Source File +# Begin Source File + +SOURCE=.\stdcpp.h +# End Source File +# Begin Source File + +SOURCE=.\strciphr.h +# End Source File +# Begin Source File + +SOURCE=.\trdlocal.h +# End Source File +# Begin Source File + +SOURCE=.\words.h +# End Source File +# End Group +# Begin Source File + +SOURCE=.\cryptopp.rc +# End Source File +# End Target +# End Project diff --git a/cppForSwig/cryptopp/cryptdll.vcproj b/cppForSwig/cryptopp/cryptdll.vcproj index 293fe0c6b..3fe28e4e7 100755 --- a/cppForSwig/cryptopp/cryptdll.vcproj +++ b/cppForSwig/cryptopp/cryptdll.vcproj @@ -1,2674 +1,2674 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cppForSwig/cryptopp/cryptest.dsp b/cppForSwig/cryptopp/cryptest.dsp index a9816421a..63a35881e 100755 --- a/cppForSwig/cryptopp/cryptest.dsp +++ b/cppForSwig/cryptopp/cryptest.dsp @@ -1,207 +1,207 @@ -# Microsoft Developer Studio Project File - Name="cryptest" - Package Owner=<4> -# Microsoft Developer Studio Generated Build File, Format Version 6.00 -# ** DO NOT EDIT ** - -# TARGTYPE "Win32 (x86) Console Application" 0x0103 - -CFG=cryptest - Win32 Debug -!MESSAGE This is not a valid makefile. To build this project using NMAKE, -!MESSAGE use the Export Makefile command and run -!MESSAGE -!MESSAGE NMAKE /f "cryptest.mak". -!MESSAGE -!MESSAGE You can specify a configuration when running NMAKE -!MESSAGE by defining the macro CFG on the command line. For example: -!MESSAGE -!MESSAGE NMAKE /f "cryptest.mak" CFG="cryptest - Win32 Debug" -!MESSAGE -!MESSAGE Possible choices for configuration are: -!MESSAGE -!MESSAGE "cryptest - Win32 DLL-Import Release" (based on "Win32 (x86) Console Application") -!MESSAGE "cryptest - Win32 DLL-Import Debug" (based on "Win32 (x86) Console Application") -!MESSAGE "cryptest - Win32 Release" (based on "Win32 (x86) Console Application") -!MESSAGE "cryptest - Win32 Debug" (based on "Win32 (x86) Console Application") -!MESSAGE - -# Begin Project -# PROP AllowPerConfigDependencies 0 -# PROP Scc_ProjName "" -# PROP Scc_LocalPath "" -CPP=cl.exe -RSC=rc.exe - -!IF "$(CFG)" == "cryptest - Win32 DLL-Import Release" - -# PROP BASE Use_MFC 0 -# PROP BASE Use_Debug_Libraries 0 -# PROP BASE Output_Dir "cryptest___Win32_FIPS_140_Release" -# PROP BASE Intermediate_Dir "cryptest___Win32_FIPS_140_Release" -# PROP BASE Ignore_Export_Lib 0 -# PROP BASE Target_Dir "" -# PROP Use_MFC 0 -# PROP Use_Debug_Libraries 0 -# PROP Output_Dir "CT_DLL_Import_Release" -# PROP Intermediate_Dir "CT_DLL_Import_Release" -# PROP Ignore_Export_Lib 0 -# PROP Target_Dir "" -# ADD BASE CPP /nologo /G5 /Gz /MT /W3 /GX /Zi /O2 /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_MBCS" /YX /FD /Zm200 /c -# ADD CPP /nologo /G5 /Gz /MT /W3 /GR /GX /Zi /O1 /Ob2 /D "NDEBUG" /D "CRYPTOPP_IMPORTS" /D "WIN32" /D "_CONSOLE" /D "_MBCS" /YX /FD /Zm400 /c -# ADD BASE RSC /l 0x409 /d "NDEBUG" -# ADD RSC /l 0x409 /d "NDEBUG" -BSC32=bscmake.exe -# ADD BASE BSC32 /nologo -# ADD BSC32 /nologo -LINK32=link.exe -# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib Ws2_32.lib /nologo /subsystem:console /debug /machine:I386 /OPT:NOWIN98 -# ADD LINK32 Ws2_32.lib /nologo /subsystem:console /debug /machine:I386 /out:"DLL_Release/cryptest.exe" /libpath:"DLL_Release" /OPT:NOWIN98 /OPT:REF /OPT:ICF -# SUBTRACT LINK32 /pdb:none /incremental:yes -# Begin Special Build Tool -SOURCE="$(InputPath)" -PreLink_Cmds=echo This configuration requires cryptopp.dll. echo You can build it yourself using the cryptdll project, or echo obtain a pre-built, FIPS 140-2 validated DLL. If you build it yourself echo the resulting DLL will not be considered FIPS validated echo unless it undergoes FIPS validation. -# End Special Build Tool - -!ELSEIF "$(CFG)" == "cryptest - Win32 DLL-Import Debug" - -# PROP BASE Use_MFC 0 -# PROP BASE Use_Debug_Libraries 1 -# PROP BASE Output_Dir "cryptest___Win32_FIPS_140_Debug" -# PROP BASE Intermediate_Dir "cryptest___Win32_FIPS_140_Debug" -# PROP BASE Ignore_Export_Lib 0 -# PROP BASE Target_Dir "" -# PROP Use_MFC 0 -# PROP Use_Debug_Libraries 1 -# PROP Output_Dir "CT_DLL_Import_Debug" -# PROP Intermediate_Dir "CT_DLL_Import_Debug" -# PROP Ignore_Export_Lib 0 -# PROP Target_Dir "" -# ADD BASE CPP /nologo /MTd /W3 /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_CONSOLE" /D "_MBCS" /YX /FD /Zm200 /c -# ADD CPP /nologo /G5 /Gz /MTd /W3 /GR /GX /Zi /Oi /D "_DEBUG" /D "CRYPTOPP_IMPORTS" /D "WIN32" /D "_CONSOLE" /D "_MBCS" /YX /FD /Zm400 /c -# ADD BASE RSC /l 0x409 /d "_DEBUG" -# ADD RSC /l 0x409 /d "_DEBUG" -BSC32=bscmake.exe -# ADD BASE BSC32 /nologo -# ADD BSC32 /nologo -LINK32=link.exe -# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib Ws2_32.lib /nologo /subsystem:console /debug /machine:I386 /pdbtype:sept /OPT:NOWIN98 -# ADD LINK32 Ws2_32.lib /nologo /subsystem:console /debug /machine:I386 /out:"DLL_Debug/cryptest.exe" /pdbtype:sept /libpath:"DLL_Debug" /OPT:NOWIN98 -# Begin Special Build Tool -SOURCE="$(InputPath)" -PreLink_Cmds=echo This configuration requires cryptopp.dll. echo You can build it yourself using the cryptdll project, or echo obtain a pre-built, FIPS 140-2 validated DLL. If you build it yourself echo the resulting DLL will not be considered FIPS validated echo unless it undergoes FIPS validation. -# End Special Build Tool - -!ELSEIF "$(CFG)" == "cryptest - Win32 Release" - -# PROP BASE Use_MFC 0 -# PROP BASE Use_Debug_Libraries 0 -# PROP BASE Output_Dir "cryptes0" -# PROP BASE Intermediate_Dir "cryptes0" -# PROP BASE Target_Dir "" -# PROP Use_MFC 0 -# PROP Use_Debug_Libraries 0 -# PROP Output_Dir "CTRelease" -# PROP Intermediate_Dir "CTRelease" -# PROP Ignore_Export_Lib 0 -# PROP Target_Dir "" -# ADD BASE CPP /nologo /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_MBCS" /YX /FD /c -# ADD CPP /nologo /MT /W3 /GR /GX /Zi /O1 /Ob2 /D "NDEBUG" /D "_CONSOLE" /D "_MBCS" /D "WIN32" /YX /FD /Zm400 /c -# ADD BASE RSC /l 0x409 /d "NDEBUG" -# ADD RSC /l 0x409 /d "NDEBUG" -BSC32=bscmake.exe -# ADD BASE BSC32 /nologo -# ADD BSC32 /nologo -LINK32=link.exe -# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:console /machine:I386 -# ADD LINK32 advapi32.lib Ws2_32.lib /nologo /subsystem:console /map /debug /machine:I386 /OPT:NOWIN98 /OPT:REF /OPT:ICF -# SUBTRACT LINK32 /pdb:none - -!ELSEIF "$(CFG)" == "cryptest - Win32 Debug" - -# PROP BASE Use_MFC 0 -# PROP BASE Use_Debug_Libraries 1 -# PROP BASE Output_Dir "cryptes1" -# PROP BASE Intermediate_Dir "cryptes1" -# PROP BASE Target_Dir "" -# PROP Use_MFC 0 -# PROP Use_Debug_Libraries 1 -# PROP Output_Dir "CTDebug" -# PROP Intermediate_Dir "CTDebug" -# PROP Ignore_Export_Lib 0 -# PROP Target_Dir "" -# ADD BASE CPP /nologo /W3 /Gm /GX /Zi /Od /D "WIN32" /D "_DEBUG" /D "_CONSOLE" /D "_MBCS" /YX /FD /c -# ADD CPP /nologo /MTd /W3 /GR /GX /Zi /Oi /D "_DEBUG" /D "_CONSOLE" /D "_MBCS" /D "WIN32" /YX /FD /Zm400 /c -# ADD BASE RSC /l 0x409 /d "_DEBUG" -# ADD RSC /l 0x409 /d "_DEBUG" -BSC32=bscmake.exe -# ADD BASE BSC32 /nologo -# ADD BSC32 /nologo -LINK32=link.exe -# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:console /debug /machine:I386 /pdbtype:sept -# ADD LINK32 advapi32.lib Ws2_32.lib /nologo /subsystem:console /debug /machine:I386 /pdbtype:sept /OPT:NOWIN98 -# SUBTRACT LINK32 /pdb:none - -!ENDIF - -# Begin Target - -# Name "cryptest - Win32 DLL-Import Release" -# Name "cryptest - Win32 DLL-Import Debug" -# Name "cryptest - Win32 Release" -# Name "cryptest - Win32 Debug" -# Begin Group "Source Code" - -# PROP Default_Filter ".cpp;.h" -# Begin Source File - -SOURCE=.\adhoc.cpp -# End Source File -# Begin Source File - -SOURCE=.\bench.cpp -# End Source File -# Begin Source File - -SOURCE=.\bench.h -# End Source File -# Begin Source File - -SOURCE=.\bench2.cpp -# End Source File -# Begin Source File - -SOURCE=.\datatest.cpp -# End Source File -# Begin Source File - -SOURCE=.\dlltest.cpp -# End Source File -# Begin Source File - -SOURCE=.\fipsalgt.cpp -# End Source File -# Begin Source File - -SOURCE=.\regtest.cpp -# End Source File -# Begin Source File - -SOURCE=.\test.cpp -# End Source File -# Begin Source File - -SOURCE=.\validat1.cpp -# End Source File -# Begin Source File - -SOURCE=.\validat2.cpp -# End Source File -# Begin Source File - -SOURCE=.\validat3.cpp -# End Source File -# Begin Source File - -SOURCE=.\validate.h -# End Source File -# End Group -# End Target -# End Project +# Microsoft Developer Studio Project File - Name="cryptest" - Package Owner=<4> +# Microsoft Developer Studio Generated Build File, Format Version 6.00 +# ** DO NOT EDIT ** + +# TARGTYPE "Win32 (x86) Console Application" 0x0103 + +CFG=cryptest - Win32 Debug +!MESSAGE This is not a valid makefile. To build this project using NMAKE, +!MESSAGE use the Export Makefile command and run +!MESSAGE +!MESSAGE NMAKE /f "cryptest.mak". +!MESSAGE +!MESSAGE You can specify a configuration when running NMAKE +!MESSAGE by defining the macro CFG on the command line. For example: +!MESSAGE +!MESSAGE NMAKE /f "cryptest.mak" CFG="cryptest - Win32 Debug" +!MESSAGE +!MESSAGE Possible choices for configuration are: +!MESSAGE +!MESSAGE "cryptest - Win32 DLL-Import Release" (based on "Win32 (x86) Console Application") +!MESSAGE "cryptest - Win32 DLL-Import Debug" (based on "Win32 (x86) Console Application") +!MESSAGE "cryptest - Win32 Release" (based on "Win32 (x86) Console Application") +!MESSAGE "cryptest - Win32 Debug" (based on "Win32 (x86) Console Application") +!MESSAGE + +# Begin Project +# PROP AllowPerConfigDependencies 0 +# PROP Scc_ProjName "" +# PROP Scc_LocalPath "" +CPP=cl.exe +RSC=rc.exe + +!IF "$(CFG)" == "cryptest - Win32 DLL-Import Release" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 0 +# PROP BASE Output_Dir "cryptest___Win32_FIPS_140_Release" +# PROP BASE Intermediate_Dir "cryptest___Win32_FIPS_140_Release" +# PROP BASE Ignore_Export_Lib 0 +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 0 +# PROP Output_Dir "CT_DLL_Import_Release" +# PROP Intermediate_Dir "CT_DLL_Import_Release" +# PROP Ignore_Export_Lib 0 +# PROP Target_Dir "" +# ADD BASE CPP /nologo /G5 /Gz /MT /W3 /GX /Zi /O2 /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_MBCS" /YX /FD /Zm200 /c +# ADD CPP /nologo /G5 /Gz /MT /W3 /GR /GX /Zi /O1 /Ob2 /D "NDEBUG" /D "CRYPTOPP_IMPORTS" /D "WIN32" /D "_CONSOLE" /D "_MBCS" /YX /FD /Zm400 /c +# ADD BASE RSC /l 0x409 /d "NDEBUG" +# ADD RSC /l 0x409 /d "NDEBUG" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LINK32=link.exe +# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib Ws2_32.lib /nologo /subsystem:console /debug /machine:I386 /OPT:NOWIN98 +# ADD LINK32 Ws2_32.lib /nologo /subsystem:console /debug /machine:I386 /out:"DLL_Release/cryptest.exe" /libpath:"DLL_Release" /OPT:NOWIN98 /OPT:REF /OPT:ICF +# SUBTRACT LINK32 /pdb:none /incremental:yes +# Begin Special Build Tool +SOURCE="$(InputPath)" +PreLink_Cmds=echo This configuration requires cryptopp.dll. echo You can build it yourself using the cryptdll project, or echo obtain a pre-built, FIPS 140-2 validated DLL. If you build it yourself echo the resulting DLL will not be considered FIPS validated echo unless it undergoes FIPS validation. +# End Special Build Tool + +!ELSEIF "$(CFG)" == "cryptest - Win32 DLL-Import Debug" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 1 +# PROP BASE Output_Dir "cryptest___Win32_FIPS_140_Debug" +# PROP BASE Intermediate_Dir "cryptest___Win32_FIPS_140_Debug" +# PROP BASE Ignore_Export_Lib 0 +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 1 +# PROP Output_Dir "CT_DLL_Import_Debug" +# PROP Intermediate_Dir "CT_DLL_Import_Debug" +# PROP Ignore_Export_Lib 0 +# PROP Target_Dir "" +# ADD BASE CPP /nologo /MTd /W3 /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_CONSOLE" /D "_MBCS" /YX /FD /Zm200 /c +# ADD CPP /nologo /G5 /Gz /MTd /W3 /GR /GX /Zi /Oi /D "_DEBUG" /D "CRYPTOPP_IMPORTS" /D "WIN32" /D "_CONSOLE" /D "_MBCS" /YX /FD /Zm400 /c +# ADD BASE RSC /l 0x409 /d "_DEBUG" +# ADD RSC /l 0x409 /d "_DEBUG" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LINK32=link.exe +# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib Ws2_32.lib /nologo /subsystem:console /debug /machine:I386 /pdbtype:sept /OPT:NOWIN98 +# ADD LINK32 Ws2_32.lib /nologo /subsystem:console /debug /machine:I386 /out:"DLL_Debug/cryptest.exe" /pdbtype:sept /libpath:"DLL_Debug" /OPT:NOWIN98 +# Begin Special Build Tool +SOURCE="$(InputPath)" +PreLink_Cmds=echo This configuration requires cryptopp.dll. echo You can build it yourself using the cryptdll project, or echo obtain a pre-built, FIPS 140-2 validated DLL. If you build it yourself echo the resulting DLL will not be considered FIPS validated echo unless it undergoes FIPS validation. +# End Special Build Tool + +!ELSEIF "$(CFG)" == "cryptest - Win32 Release" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 0 +# PROP BASE Output_Dir "cryptes0" +# PROP BASE Intermediate_Dir "cryptes0" +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 0 +# PROP Output_Dir "CTRelease" +# PROP Intermediate_Dir "CTRelease" +# PROP Ignore_Export_Lib 0 +# PROP Target_Dir "" +# ADD BASE CPP /nologo /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_MBCS" /YX /FD /c +# ADD CPP /nologo /MT /W3 /GR /GX /Zi /O1 /Ob2 /D "NDEBUG" /D "_CONSOLE" /D "_MBCS" /D "WIN32" /YX /FD /Zm400 /c +# ADD BASE RSC /l 0x409 /d "NDEBUG" +# ADD RSC /l 0x409 /d "NDEBUG" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LINK32=link.exe +# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:console /machine:I386 +# ADD LINK32 advapi32.lib Ws2_32.lib /nologo /subsystem:console /map /debug /machine:I386 /OPT:NOWIN98 /OPT:REF /OPT:ICF +# SUBTRACT LINK32 /pdb:none + +!ELSEIF "$(CFG)" == "cryptest - Win32 Debug" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 1 +# PROP BASE Output_Dir "cryptes1" +# PROP BASE Intermediate_Dir "cryptes1" +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 1 +# PROP Output_Dir "CTDebug" +# PROP Intermediate_Dir "CTDebug" +# PROP Ignore_Export_Lib 0 +# PROP Target_Dir "" +# ADD BASE CPP /nologo /W3 /Gm /GX /Zi /Od /D "WIN32" /D "_DEBUG" /D "_CONSOLE" /D "_MBCS" /YX /FD /c +# ADD CPP /nologo /MTd /W3 /GR /GX /Zi /Oi /D "_DEBUG" /D "_CONSOLE" /D "_MBCS" /D "WIN32" /YX /FD /Zm400 /c +# ADD BASE RSC /l 0x409 /d "_DEBUG" +# ADD RSC /l 0x409 /d "_DEBUG" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LINK32=link.exe +# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:console /debug /machine:I386 /pdbtype:sept +# ADD LINK32 advapi32.lib Ws2_32.lib /nologo /subsystem:console /debug /machine:I386 /pdbtype:sept /OPT:NOWIN98 +# SUBTRACT LINK32 /pdb:none + +!ENDIF + +# Begin Target + +# Name "cryptest - Win32 DLL-Import Release" +# Name "cryptest - Win32 DLL-Import Debug" +# Name "cryptest - Win32 Release" +# Name "cryptest - Win32 Debug" +# Begin Group "Source Code" + +# PROP Default_Filter ".cpp;.h" +# Begin Source File + +SOURCE=.\adhoc.cpp +# End Source File +# Begin Source File + +SOURCE=.\bench.cpp +# End Source File +# Begin Source File + +SOURCE=.\bench.h +# End Source File +# Begin Source File + +SOURCE=.\bench2.cpp +# End Source File +# Begin Source File + +SOURCE=.\datatest.cpp +# End Source File +# Begin Source File + +SOURCE=.\dlltest.cpp +# End Source File +# Begin Source File + +SOURCE=.\fipsalgt.cpp +# End Source File +# Begin Source File + +SOURCE=.\regtest.cpp +# End Source File +# Begin Source File + +SOURCE=.\test.cpp +# End Source File +# Begin Source File + +SOURCE=.\validat1.cpp +# End Source File +# Begin Source File + +SOURCE=.\validat2.cpp +# End Source File +# Begin Source File + +SOURCE=.\validat3.cpp +# End Source File +# Begin Source File + +SOURCE=.\validate.h +# End Source File +# End Group +# End Target +# End Project diff --git a/cppForSwig/cryptopp/cryptest.dsw b/cppForSwig/cryptopp/cryptest.dsw index a2f14118c..f8cf4f162 100755 --- a/cppForSwig/cryptopp/cryptest.dsw +++ b/cppForSwig/cryptopp/cryptest.dsw @@ -1,74 +1,74 @@ -Microsoft Developer Studio Workspace File, Format Version 6.00 -# WARNING: DO NOT EDIT OR DELETE THIS WORKSPACE FILE! - -############################################################################### - -Project: "cryptdll"=.\cryptdll.dsp - Package Owner=<4> - -Package=<5> -{{{ -}}} - -Package=<4> -{{{ - Begin Project Dependency - Project_Dep_Name cryptest - End Project Dependency -}}} - -############################################################################### - -Project: "cryptest"=.\cryptest.dsp - Package Owner=<4> - -Package=<5> -{{{ -}}} - -Package=<4> -{{{ - Begin Project Dependency - Project_Dep_Name cryptlib - End Project Dependency -}}} - -############################################################################### - -Project: "cryptlib"=.\cryptlib.dsp - Package Owner=<4> - -Package=<5> -{{{ -}}} - -Package=<4> -{{{ -}}} - -############################################################################### - -Project: "dlltest"=.\dlltest.dsp - Package Owner=<4> - -Package=<5> -{{{ -}}} - -Package=<4> -{{{ - Begin Project Dependency - Project_Dep_Name cryptdll - End Project Dependency -}}} - -############################################################################### - -Global: - -Package=<5> -{{{ -}}} - -Package=<3> -{{{ -}}} - -############################################################################### - +Microsoft Developer Studio Workspace File, Format Version 6.00 +# WARNING: DO NOT EDIT OR DELETE THIS WORKSPACE FILE! + +############################################################################### + +Project: "cryptdll"=.\cryptdll.dsp - Package Owner=<4> + +Package=<5> +{{{ +}}} + +Package=<4> +{{{ + Begin Project Dependency + Project_Dep_Name cryptest + End Project Dependency +}}} + +############################################################################### + +Project: "cryptest"=.\cryptest.dsp - Package Owner=<4> + +Package=<5> +{{{ +}}} + +Package=<4> +{{{ + Begin Project Dependency + Project_Dep_Name cryptlib + End Project Dependency +}}} + +############################################################################### + +Project: "cryptlib"=.\cryptlib.dsp - Package Owner=<4> + +Package=<5> +{{{ +}}} + +Package=<4> +{{{ +}}} + +############################################################################### + +Project: "dlltest"=.\dlltest.dsp - Package Owner=<4> + +Package=<5> +{{{ +}}} + +Package=<4> +{{{ + Begin Project Dependency + Project_Dep_Name cryptdll + End Project Dependency +}}} + +############################################################################### + +Global: + +Package=<5> +{{{ +}}} + +Package=<3> +{{{ +}}} + +############################################################################### + diff --git a/cppForSwig/cryptopp/cryptest.sln b/cppForSwig/cryptopp/cryptest.sln index d4ab1e039..9591f967a 100755 --- a/cppForSwig/cryptopp/cryptest.sln +++ b/cppForSwig/cryptopp/cryptest.sln @@ -1,91 +1,91 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Express 2012 for Windows Desktop -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cryptopp", "cryptdll.vcxproj", "{EBD86293-69A9-456B-B814-916E12AA9BBF}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cryptest", "cryptest.vcxproj", "{9EAFA456-89B4-4879-AD4F-C2C341184CF5}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cryptlib", "cryptlib.vcxproj", "{3423EC9A-52E4-4A4D-9753-EDEBC38785EF}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "dlltest", "dlltest.vcxproj", "{A7483CE8-2784-46CE-8CB8-8C0C1D27E232}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Win32 = Debug|Win32 - Debug|x64 = Debug|x64 - DLL-Import Debug|Win32 = DLL-Import Debug|Win32 - DLL-Import Debug|x64 = DLL-Import Debug|x64 - DLL-Import Release|Win32 = DLL-Import Release|Win32 - DLL-Import Release|x64 = DLL-Import Release|x64 - Release|Win32 = Release|Win32 - Release|x64 = Release|x64 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {EBD86293-69A9-456B-B814-916E12AA9BBF}.Debug|Win32.ActiveCfg = Debug|Win32 - {EBD86293-69A9-456B-B814-916E12AA9BBF}.Debug|Win32.Build.0 = Debug|Win32 - {EBD86293-69A9-456B-B814-916E12AA9BBF}.Debug|x64.ActiveCfg = Debug|x64 - {EBD86293-69A9-456B-B814-916E12AA9BBF}.Debug|x64.Build.0 = Debug|x64 - {EBD86293-69A9-456B-B814-916E12AA9BBF}.DLL-Import Debug|Win32.ActiveCfg = Debug|Win32 - {EBD86293-69A9-456B-B814-916E12AA9BBF}.DLL-Import Debug|Win32.Build.0 = Debug|Win32 - {EBD86293-69A9-456B-B814-916E12AA9BBF}.DLL-Import Debug|x64.ActiveCfg = Debug|x64 - {EBD86293-69A9-456B-B814-916E12AA9BBF}.DLL-Import Debug|x64.Build.0 = Debug|x64 - {EBD86293-69A9-456B-B814-916E12AA9BBF}.DLL-Import Release|Win32.ActiveCfg = Release|Win32 - {EBD86293-69A9-456B-B814-916E12AA9BBF}.DLL-Import Release|Win32.Build.0 = Release|Win32 - {EBD86293-69A9-456B-B814-916E12AA9BBF}.DLL-Import Release|x64.ActiveCfg = Release|x64 - {EBD86293-69A9-456B-B814-916E12AA9BBF}.DLL-Import Release|x64.Build.0 = Release|x64 - {EBD86293-69A9-456B-B814-916E12AA9BBF}.Release|Win32.ActiveCfg = Release|Win32 - {EBD86293-69A9-456B-B814-916E12AA9BBF}.Release|Win32.Build.0 = Release|Win32 - {EBD86293-69A9-456B-B814-916E12AA9BBF}.Release|x64.ActiveCfg = Release|x64 - {EBD86293-69A9-456B-B814-916E12AA9BBF}.Release|x64.Build.0 = Release|x64 - {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.Debug|Win32.ActiveCfg = Debug|Win32 - {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.Debug|Win32.Build.0 = Debug|Win32 - {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.Debug|x64.ActiveCfg = Debug|x64 - {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.Debug|x64.Build.0 = Debug|x64 - {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.DLL-Import Debug|Win32.ActiveCfg = DLL-Import Debug|Win32 - {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.DLL-Import Debug|Win32.Build.0 = DLL-Import Debug|Win32 - {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.DLL-Import Debug|x64.ActiveCfg = DLL-Import Debug|x64 - {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.DLL-Import Debug|x64.Build.0 = DLL-Import Debug|x64 - {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.DLL-Import Release|Win32.ActiveCfg = DLL-Import Release|Win32 - {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.DLL-Import Release|Win32.Build.0 = DLL-Import Release|Win32 - {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.DLL-Import Release|x64.ActiveCfg = DLL-Import Release|x64 - {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.DLL-Import Release|x64.Build.0 = DLL-Import Release|x64 - {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.Release|Win32.ActiveCfg = Release|Win32 - {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.Release|Win32.Build.0 = Release|Win32 - {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.Release|x64.ActiveCfg = Release|x64 - {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.Release|x64.Build.0 = Release|x64 - {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.Debug|Win32.ActiveCfg = Debug|Win32 - {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.Debug|Win32.Build.0 = Debug|Win32 - {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.Debug|x64.ActiveCfg = Debug|x64 - {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.Debug|x64.Build.0 = Debug|x64 - {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.DLL-Import Debug|Win32.ActiveCfg = DLL-Import Debug|Win32 - {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.DLL-Import Debug|Win32.Build.0 = DLL-Import Debug|Win32 - {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.DLL-Import Debug|x64.ActiveCfg = DLL-Import Debug|x64 - {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.DLL-Import Debug|x64.Build.0 = DLL-Import Debug|x64 - {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.DLL-Import Release|Win32.ActiveCfg = DLL-Import Release|Win32 - {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.DLL-Import Release|Win32.Build.0 = DLL-Import Release|Win32 - {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.DLL-Import Release|x64.ActiveCfg = DLL-Import Release|x64 - {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.DLL-Import Release|x64.Build.0 = DLL-Import Release|x64 - {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.Release|Win32.ActiveCfg = Release|Win32 - {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.Release|Win32.Build.0 = Release|Win32 - {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.Release|x64.ActiveCfg = Release|x64 - {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.Release|x64.Build.0 = Release|x64 - {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.Debug|Win32.ActiveCfg = Debug|Win32 - {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.Debug|Win32.Build.0 = Debug|Win32 - {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.Debug|x64.ActiveCfg = Debug|x64 - {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.Debug|x64.Build.0 = Debug|x64 - {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.DLL-Import Debug|Win32.ActiveCfg = Debug|Win32 - {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.DLL-Import Debug|Win32.Build.0 = Debug|Win32 - {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.DLL-Import Debug|x64.ActiveCfg = Debug|x64 - {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.DLL-Import Debug|x64.Build.0 = Debug|x64 - {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.DLL-Import Release|Win32.ActiveCfg = Release|Win32 - {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.DLL-Import Release|Win32.Build.0 = Release|Win32 - {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.DLL-Import Release|x64.ActiveCfg = Release|x64 - {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.DLL-Import Release|x64.Build.0 = Release|x64 - {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.Release|Win32.ActiveCfg = Release|Win32 - {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.Release|Win32.Build.0 = Release|Win32 - {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.Release|x64.ActiveCfg = Release|x64 - {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.Release|x64.Build.0 = Release|x64 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Express 2012 for Windows Desktop +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cryptopp", "cryptdll.vcxproj", "{EBD86293-69A9-456B-B814-916E12AA9BBF}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cryptest", "cryptest.vcxproj", "{9EAFA456-89B4-4879-AD4F-C2C341184CF5}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cryptlib", "cryptlib.vcxproj", "{3423EC9A-52E4-4A4D-9753-EDEBC38785EF}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "dlltest", "dlltest.vcxproj", "{A7483CE8-2784-46CE-8CB8-8C0C1D27E232}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Debug|x64 = Debug|x64 + DLL-Import Debug|Win32 = DLL-Import Debug|Win32 + DLL-Import Debug|x64 = DLL-Import Debug|x64 + DLL-Import Release|Win32 = DLL-Import Release|Win32 + DLL-Import Release|x64 = DLL-Import Release|x64 + Release|Win32 = Release|Win32 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EBD86293-69A9-456B-B814-916E12AA9BBF}.Debug|Win32.ActiveCfg = Debug|Win32 + {EBD86293-69A9-456B-B814-916E12AA9BBF}.Debug|Win32.Build.0 = Debug|Win32 + {EBD86293-69A9-456B-B814-916E12AA9BBF}.Debug|x64.ActiveCfg = Debug|x64 + {EBD86293-69A9-456B-B814-916E12AA9BBF}.Debug|x64.Build.0 = Debug|x64 + {EBD86293-69A9-456B-B814-916E12AA9BBF}.DLL-Import Debug|Win32.ActiveCfg = Debug|Win32 + {EBD86293-69A9-456B-B814-916E12AA9BBF}.DLL-Import Debug|Win32.Build.0 = Debug|Win32 + {EBD86293-69A9-456B-B814-916E12AA9BBF}.DLL-Import Debug|x64.ActiveCfg = Debug|x64 + {EBD86293-69A9-456B-B814-916E12AA9BBF}.DLL-Import Debug|x64.Build.0 = Debug|x64 + {EBD86293-69A9-456B-B814-916E12AA9BBF}.DLL-Import Release|Win32.ActiveCfg = Release|Win32 + {EBD86293-69A9-456B-B814-916E12AA9BBF}.DLL-Import Release|Win32.Build.0 = Release|Win32 + {EBD86293-69A9-456B-B814-916E12AA9BBF}.DLL-Import Release|x64.ActiveCfg = Release|x64 + {EBD86293-69A9-456B-B814-916E12AA9BBF}.DLL-Import Release|x64.Build.0 = Release|x64 + {EBD86293-69A9-456B-B814-916E12AA9BBF}.Release|Win32.ActiveCfg = Release|Win32 + {EBD86293-69A9-456B-B814-916E12AA9BBF}.Release|Win32.Build.0 = Release|Win32 + {EBD86293-69A9-456B-B814-916E12AA9BBF}.Release|x64.ActiveCfg = Release|x64 + {EBD86293-69A9-456B-B814-916E12AA9BBF}.Release|x64.Build.0 = Release|x64 + {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.Debug|Win32.ActiveCfg = Debug|Win32 + {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.Debug|Win32.Build.0 = Debug|Win32 + {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.Debug|x64.ActiveCfg = Debug|x64 + {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.Debug|x64.Build.0 = Debug|x64 + {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.DLL-Import Debug|Win32.ActiveCfg = DLL-Import Debug|Win32 + {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.DLL-Import Debug|Win32.Build.0 = DLL-Import Debug|Win32 + {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.DLL-Import Debug|x64.ActiveCfg = DLL-Import Debug|x64 + {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.DLL-Import Debug|x64.Build.0 = DLL-Import Debug|x64 + {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.DLL-Import Release|Win32.ActiveCfg = DLL-Import Release|Win32 + {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.DLL-Import Release|Win32.Build.0 = DLL-Import Release|Win32 + {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.DLL-Import Release|x64.ActiveCfg = DLL-Import Release|x64 + {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.DLL-Import Release|x64.Build.0 = DLL-Import Release|x64 + {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.Release|Win32.ActiveCfg = Release|Win32 + {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.Release|Win32.Build.0 = Release|Win32 + {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.Release|x64.ActiveCfg = Release|x64 + {9EAFA456-89B4-4879-AD4F-C2C341184CF5}.Release|x64.Build.0 = Release|x64 + {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.Debug|Win32.ActiveCfg = Debug|Win32 + {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.Debug|Win32.Build.0 = Debug|Win32 + {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.Debug|x64.ActiveCfg = Debug|x64 + {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.Debug|x64.Build.0 = Debug|x64 + {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.DLL-Import Debug|Win32.ActiveCfg = DLL-Import Debug|Win32 + {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.DLL-Import Debug|Win32.Build.0 = DLL-Import Debug|Win32 + {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.DLL-Import Debug|x64.ActiveCfg = DLL-Import Debug|x64 + {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.DLL-Import Debug|x64.Build.0 = DLL-Import Debug|x64 + {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.DLL-Import Release|Win32.ActiveCfg = DLL-Import Release|Win32 + {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.DLL-Import Release|Win32.Build.0 = DLL-Import Release|Win32 + {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.DLL-Import Release|x64.ActiveCfg = DLL-Import Release|x64 + {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.DLL-Import Release|x64.Build.0 = DLL-Import Release|x64 + {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.Release|Win32.ActiveCfg = Release|Win32 + {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.Release|Win32.Build.0 = Release|Win32 + {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.Release|x64.ActiveCfg = Release|x64 + {3423EC9A-52E4-4A4D-9753-EDEBC38785EF}.Release|x64.Build.0 = Release|x64 + {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.Debug|Win32.ActiveCfg = Debug|Win32 + {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.Debug|Win32.Build.0 = Debug|Win32 + {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.Debug|x64.ActiveCfg = Debug|x64 + {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.Debug|x64.Build.0 = Debug|x64 + {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.DLL-Import Debug|Win32.ActiveCfg = Debug|Win32 + {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.DLL-Import Debug|Win32.Build.0 = Debug|Win32 + {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.DLL-Import Debug|x64.ActiveCfg = Debug|x64 + {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.DLL-Import Debug|x64.Build.0 = Debug|x64 + {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.DLL-Import Release|Win32.ActiveCfg = Release|Win32 + {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.DLL-Import Release|Win32.Build.0 = Release|Win32 + {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.DLL-Import Release|x64.ActiveCfg = Release|x64 + {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.DLL-Import Release|x64.Build.0 = Release|x64 + {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.Release|Win32.ActiveCfg = Release|Win32 + {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.Release|Win32.Build.0 = Release|Win32 + {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.Release|x64.ActiveCfg = Release|x64 + {A7483CE8-2784-46CE-8CB8-8C0C1D27E232}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/cppForSwig/cryptopp/cryptest.vcproj b/cppForSwig/cryptopp/cryptest.vcproj index 03a5b8850..ef7fd86e2 100755 --- a/cppForSwig/cryptopp/cryptest.vcproj +++ b/cppForSwig/cryptopp/cryptest.vcproj @@ -1,1811 +1,1811 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cppForSwig/cryptopp/cryptest_bds.bdsgroup b/cppForSwig/cryptopp/cryptest_bds.bdsgroup index bbb081ac9..32d85c637 100755 --- a/cppForSwig/cryptopp/cryptest_bds.bdsgroup +++ b/cppForSwig/cryptopp/cryptest_bds.bdsgroup @@ -1,22 +1,22 @@ - - - - - - - - - - - cryptest_bds.bdsproj - cryptlib_bds.bdsproj - cryptest_bds.exe cryptlib_bds.lib - - - - - - - - + + + + + + + + + + + cryptest_bds.bdsproj + cryptlib_bds.bdsproj + cryptest_bds.exe cryptlib_bds.lib + + + + + + + + diff --git a/cppForSwig/cryptopp/cryptest_bds.bdsproj b/cppForSwig/cryptopp/cryptest_bds.bdsproj index 9086d3088..39751de01 100755 --- a/cppForSwig/cryptopp/cryptest_bds.bdsproj +++ b/cppForSwig/cryptopp/cryptest_bds.bdsproj @@ -1,267 +1,267 @@ - - - - - - - - - - - - cryptest_bds.bpf - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - False - False - 1 - 0 - 0 - 0 - False - False - False - False - False - 1033 - 1252 - - - - - 1.0.0.0 - - - - - - 1.0.0.0 - - - - - - - v - - True - . - D:\cvs\c5\Debug_Build\cryptest_bds.exe - - - - - False - - False - - True - False - - - Delphi 1.0 Compatibility Components - Borland C++Builder Internet Explorer 5 Components Package - - - - - - - - - - - + + + + + + + + + + + + cryptest_bds.bpf + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + False + False + 1 + 0 + 0 + 0 + False + False + False + False + False + 1033 + 1252 + + + + + 1.0.0.0 + + + + + + 1.0.0.0 + + + + + + + v + + True + . + D:\cvs\c5\Debug_Build\cryptest_bds.exe + + + + + False + + False + + True + False + + + Delphi 1.0 Compatibility Components + Borland C++Builder Internet Explorer 5 Components Package + + + + + + + + + + + diff --git a/cppForSwig/cryptopp/cryptest_bds.bpf b/cppForSwig/cryptopp/cryptest_bds.bpf index c30200be3..fa1ed3b86 100755 --- a/cppForSwig/cryptopp/cryptest_bds.bpf +++ b/cppForSwig/cryptopp/cryptest_bds.bpf @@ -1,5 +1,5 @@ -This file is used by the project manager only and should be treated like the project file - -To add a file to this project use the Project menu 'Add to Project' - +This file is used by the project manager only and should be treated like the project file + +To add a file to this project use the Project menu 'Add to Project' + main \ No newline at end of file diff --git a/cppForSwig/cryptopp/cryptlib.dsp b/cppForSwig/cryptopp/cryptlib.dsp index 8bbb6ea9b..2e467d7ad 100755 --- a/cppForSwig/cryptopp/cryptlib.dsp +++ b/cppForSwig/cryptopp/cryptlib.dsp @@ -1,1204 +1,1204 @@ -# Microsoft Developer Studio Project File - Name="cryptlib" - Package Owner=<4> -# Microsoft Developer Studio Generated Build File, Format Version 6.00 -# ** DO NOT EDIT ** - -# TARGTYPE "Win32 (x86) Static Library" 0x0104 - -CFG=cryptlib - Win32 Debug -!MESSAGE This is not a valid makefile. To build this project using NMAKE, -!MESSAGE use the Export Makefile command and run -!MESSAGE -!MESSAGE NMAKE /f "cryptlib.mak". -!MESSAGE -!MESSAGE You can specify a configuration when running NMAKE -!MESSAGE by defining the macro CFG on the command line. For example: -!MESSAGE -!MESSAGE NMAKE /f "cryptlib.mak" CFG="cryptlib - Win32 Debug" -!MESSAGE -!MESSAGE Possible choices for configuration are: -!MESSAGE -!MESSAGE "cryptlib - Win32 DLL-Import Release" (based on "Win32 (x86) Static Library") -!MESSAGE "cryptlib - Win32 DLL-Import Debug" (based on "Win32 (x86) Static Library") -!MESSAGE "cryptlib - Win32 Release" (based on "Win32 (x86) Static Library") -!MESSAGE "cryptlib - Win32 Debug" (based on "Win32 (x86) Static Library") -!MESSAGE - -# Begin Project -# PROP AllowPerConfigDependencies 0 -# PROP Scc_ProjName "" -# PROP Scc_LocalPath "" -CPP=cl.exe -RSC=rc.exe - -!IF "$(CFG)" == "cryptlib - Win32 DLL-Import Release" - -# PROP BASE Use_MFC 0 -# PROP BASE Use_Debug_Libraries 0 -# PROP BASE Output_Dir "cryptlib___Win32_FIPS_140_Release" -# PROP BASE Intermediate_Dir "cryptlib___Win32_FIPS_140_Release" -# PROP BASE Target_Dir "" -# PROP Use_MFC 0 -# PROP Use_Debug_Libraries 0 -# PROP Output_Dir "DLL_Import_Release" -# PROP Intermediate_Dir "DLL_Import_Release" -# PROP Target_Dir "" -# ADD BASE CPP /nologo /G5 /Gz /MT /W3 /GX /Zi /O2 /D "NDEBUG" /D "WIN32" /D "_WINDOWS" /D "USE_PRECOMPILED_HEADERS" /Yu"pch.h" /FD /c -# ADD CPP /nologo /G5 /Gz /MT /W3 /GR /GX /Zi /O2 /Ob2 /D "NDEBUG" /D "_WINDOWS" /D "USE_PRECOMPILED_HEADERS" /D "WIN32" /D "CRYPTOPP_IMPORTS" /Yu"pch.h" /FD /Zm400 /c -# ADD BASE RSC /l 0x409 -# ADD RSC /l 0x409 -BSC32=bscmake.exe -# ADD BASE BSC32 /nologo -# ADD BSC32 /nologo -LIB32=link.exe -lib -# ADD BASE LIB32 /nologo -# ADD LIB32 /nologo - -!ELSEIF "$(CFG)" == "cryptlib - Win32 DLL-Import Debug" - -# PROP BASE Use_MFC 0 -# PROP BASE Use_Debug_Libraries 1 -# PROP BASE Output_Dir "cryptlib___Win32_FIPS_140_Debug" -# PROP BASE Intermediate_Dir "cryptlib___Win32_FIPS_140_Debug" -# PROP BASE Target_Dir "" -# PROP Use_MFC 0 -# PROP Use_Debug_Libraries 1 -# PROP Output_Dir "DLL_Import_Debug" -# PROP Intermediate_Dir "DLL_Import_Debug" -# PROP Target_Dir "" -# ADD BASE CPP /nologo /MTd /W3 /GX /ZI /Od /D "_DEBUG" /D "WIN32" /D "_WINDOWS" /D "USE_PRECOMPILED_HEADERS" /Yu"pch.h" /FD /c -# ADD CPP /nologo /G5 /Gz /MTd /W3 /GR /GX /Zi /Oi /D "_DEBUG" /D "_WINDOWS" /D "USE_PRECOMPILED_HEADERS" /D "WIN32" /D "CRYPTOPP_IMPORTS" /Yu"pch.h" /FD /Zm400 /c -# ADD BASE RSC /l 0x409 -# ADD RSC /l 0x409 -BSC32=bscmake.exe -# ADD BASE BSC32 /nologo -# ADD BSC32 /nologo -LIB32=link.exe -lib -# ADD BASE LIB32 /nologo -# ADD LIB32 /nologo - -!ELSEIF "$(CFG)" == "cryptlib - Win32 Release" - -# PROP BASE Use_MFC 0 -# PROP BASE Use_Debug_Libraries 0 -# PROP BASE Output_Dir "cryptlib" -# PROP BASE Intermediate_Dir "cryptlib" -# PROP BASE Target_Dir "" -# PROP Use_MFC 0 -# PROP Use_Debug_Libraries 0 -# PROP Output_Dir "Release" -# PROP Intermediate_Dir "Release" -# PROP Target_Dir "" -# ADD BASE CPP /nologo /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /YX /FD /c -# ADD CPP /nologo /MT /W3 /GR /GX /Zi /O2 /Ob2 /D "NDEBUG" /D "_WINDOWS" /D "USE_PRECOMPILED_HEADERS" /D "WIN32" /Yu"pch.h" /FD /Zm400 /c -# ADD BASE RSC /l 0x409 -# ADD RSC /l 0x409 -BSC32=bscmake.exe -# ADD BASE BSC32 /nologo -# ADD BSC32 /nologo -LIB32=link.exe -lib -# ADD BASE LIB32 /nologo -# ADD LIB32 /nologo - -!ELSEIF "$(CFG)" == "cryptlib - Win32 Debug" - -# PROP BASE Use_MFC 0 -# PROP BASE Use_Debug_Libraries 1 -# PROP BASE Output_Dir "cryptli0" -# PROP BASE Intermediate_Dir "cryptli0" -# PROP BASE Target_Dir "" -# PROP Use_MFC 0 -# PROP Use_Debug_Libraries 1 -# PROP Output_Dir "Debug" -# PROP Intermediate_Dir "Debug" -# PROP Target_Dir "" -# ADD BASE CPP /nologo /W3 /GX /Z7 /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /YX /FD /c -# ADD CPP /nologo /MTd /W3 /GR /GX /Zi /Oi /D "_DEBUG" /D "_WINDOWS" /D "USE_PRECOMPILED_HEADERS" /D "WIN32" /Yu"pch.h" /FD /Zm400 /c -# ADD BASE RSC /l 0x409 -# ADD RSC /l 0x409 -BSC32=bscmake.exe -# ADD BASE BSC32 /nologo -# ADD BSC32 /nologo -LIB32=link.exe -lib -# ADD BASE LIB32 /nologo -# ADD LIB32 /nologo - -!ENDIF - -# Begin Target - -# Name "cryptlib - Win32 DLL-Import Release" -# Name "cryptlib - Win32 DLL-Import Debug" -# Name "cryptlib - Win32 Release" -# Name "cryptlib - Win32 Debug" -# Begin Group "Source Files" - -# PROP Default_Filter ".cpp" -# Begin Source File - -SOURCE=.\3way.cpp -# End Source File -# Begin Source File - -SOURCE=.\adhoc.cpp.proto - -!IF "$(CFG)" == "cryptlib - Win32 DLL-Import Release" - -# Begin Custom Build -InputPath=.\adhoc.cpp.proto - -"adhoc.cpp.copied" : $(SOURCE) "$(INTDIR)" "$(OUTDIR)" - if not exist adhoc.cpp copy "$(InputPath)" adhoc.cpp - echo: >> adhoc.cpp.copied - -# End Custom Build - -!ELSEIF "$(CFG)" == "cryptlib - Win32 DLL-Import Debug" - -# Begin Custom Build -InputPath=.\adhoc.cpp.proto - -"adhoc.cpp.copied" : $(SOURCE) "$(INTDIR)" "$(OUTDIR)" - if not exist adhoc.cpp copy "$(InputPath)" adhoc.cpp - echo: >> adhoc.cpp.copied - -# End Custom Build - -!ELSEIF "$(CFG)" == "cryptlib - Win32 Release" - -# Begin Custom Build -InputPath=.\adhoc.cpp.proto - -"adhoc.cpp.copied" : $(SOURCE) "$(INTDIR)" "$(OUTDIR)" - if not exist adhoc.cpp copy "$(InputPath)" adhoc.cpp - echo: >> adhoc.cpp.copied - -# End Custom Build - -!ELSEIF "$(CFG)" == "cryptlib - Win32 Debug" - -# Begin Custom Build -InputPath=.\adhoc.cpp.proto - -"adhoc.cpp.copied" : $(SOURCE) "$(INTDIR)" "$(OUTDIR)" - if not exist adhoc.cpp copy "$(InputPath)" adhoc.cpp - echo: >> adhoc.cpp.copied - -# End Custom Build - -!ENDIF - -# End Source File -# Begin Source File - -SOURCE=.\adler32.cpp -# End Source File -# Begin Source File - -SOURCE=.\algebra.cpp -# End Source File -# Begin Source File - -SOURCE=.\algparam.cpp -# End Source File -# Begin Source File - -SOURCE=.\arc4.cpp -# End Source File -# Begin Source File - -SOURCE=.\asn.cpp -# End Source File -# Begin Source File - -SOURCE=.\authenc.cpp -# End Source File -# Begin Source File - -SOURCE=.\base32.cpp -# End Source File -# Begin Source File - -SOURCE=.\base64.cpp -# End Source File -# Begin Source File - -SOURCE=.\basecode.cpp -# End Source File -# Begin Source File - -SOURCE=.\bfinit.cpp -# End Source File -# Begin Source File - -SOURCE=.\blowfish.cpp -# End Source File -# Begin Source File - -SOURCE=.\blumshub.cpp -# End Source File -# Begin Source File - -SOURCE=.\camellia.cpp -# End Source File -# Begin Source File - -SOURCE=.\cast.cpp -# End Source File -# Begin Source File - -SOURCE=.\casts.cpp -# End Source File -# Begin Source File - -SOURCE=.\cbcmac.cpp -# End Source File -# Begin Source File - -SOURCE=.\ccm.cpp -# End Source File -# Begin Source File - -SOURCE=.\channels.cpp -# End Source File -# Begin Source File - -SOURCE=.\cmac.cpp -# End Source File -# Begin Source File - -SOURCE=.\cpu.cpp -# End Source File -# Begin Source File - -SOURCE=.\crc.cpp -# End Source File -# Begin Source File - -SOURCE=.\cryptlib.cpp -# End Source File -# Begin Source File - -SOURCE=.\default.cpp -# End Source File -# Begin Source File - -SOURCE=.\des.cpp -# End Source File -# Begin Source File - -SOURCE=.\dessp.cpp -# End Source File -# Begin Source File - -SOURCE=.\dh.cpp -# End Source File -# Begin Source File - -SOURCE=.\dh2.cpp -# End Source File -# Begin Source File - -SOURCE=.\dll.cpp -# SUBTRACT CPP /YX /Yc /Yu -# End Source File -# Begin Source File - -SOURCE=.\dsa.cpp -# End Source File -# Begin Source File - -SOURCE=.\eax.cpp -# End Source File -# Begin Source File - -SOURCE=.\ec2n.cpp -# End Source File -# Begin Source File - -SOURCE=.\eccrypto.cpp -# End Source File -# Begin Source File - -SOURCE=.\ecp.cpp -# End Source File -# Begin Source File - -SOURCE=.\elgamal.cpp -# End Source File -# Begin Source File - -SOURCE=.\emsa2.cpp -# End Source File -# Begin Source File - -SOURCE=.\eprecomp.cpp -# End Source File -# Begin Source File - -SOURCE=.\esign.cpp -# End Source File -# Begin Source File - -SOURCE=.\files.cpp -# End Source File -# Begin Source File - -SOURCE=.\filters.cpp -# End Source File -# Begin Source File - -SOURCE=.\fips140.cpp -# End Source File -# Begin Source File - -SOURCE=.\fipstest.cpp -# End Source File -# Begin Source File - -SOURCE=.\gcm.cpp -# End Source File -# Begin Source File - -SOURCE=.\gf256.cpp -# End Source File -# Begin Source File - -SOURCE=.\gf2_32.cpp -# End Source File -# Begin Source File - -SOURCE=.\gf2n.cpp -# End Source File -# Begin Source File - -SOURCE=.\gfpcrypt.cpp -# End Source File -# Begin Source File - -SOURCE=.\gost.cpp -# End Source File -# Begin Source File - -SOURCE=.\gzip.cpp -# End Source File -# Begin Source File - -SOURCE=.\hex.cpp -# End Source File -# Begin Source File - -SOURCE=.\hmac.cpp -# End Source File -# Begin Source File - -SOURCE=.\hrtimer.cpp -# End Source File -# Begin Source File - -SOURCE=.\ida.cpp -# End Source File -# Begin Source File - -SOURCE=.\idea.cpp -# End Source File -# Begin Source File - -SOURCE=.\integer.cpp -# End Source File -# Begin Source File - -SOURCE=.\iterhash.cpp -# SUBTRACT CPP /YX /Yc /Yu -# End Source File -# Begin Source File - -SOURCE=.\luc.cpp -# End Source File -# Begin Source File - -SOURCE=.\mars.cpp -# End Source File -# Begin Source File - -SOURCE=.\marss.cpp -# End Source File -# Begin Source File - -SOURCE=.\md2.cpp -# End Source File -# Begin Source File - -SOURCE=.\md4.cpp -# End Source File -# Begin Source File - -SOURCE=.\md5.cpp -# End Source File -# Begin Source File - -SOURCE=.\misc.cpp -# End Source File -# Begin Source File - -SOURCE=.\modes.cpp -# End Source File -# Begin Source File - -SOURCE=.\mqueue.cpp -# End Source File -# Begin Source File - -SOURCE=.\mqv.cpp -# End Source File -# Begin Source File - -SOURCE=.\nbtheory.cpp -# End Source File -# Begin Source File - -SOURCE=.\network.cpp -# End Source File -# Begin Source File - -SOURCE=.\oaep.cpp -# End Source File -# Begin Source File - -SOURCE=.\osrng.cpp -# End Source File -# Begin Source File - -SOURCE=.\panama.cpp -# End Source File -# Begin Source File - -SOURCE=.\pch.cpp -# ADD CPP /Yc"pch.h" -# End Source File -# Begin Source File - -SOURCE=.\pkcspad.cpp -# End Source File -# Begin Source File - -SOURCE=.\polynomi.cpp -# End Source File -# Begin Source File - -SOURCE=.\pssr.cpp -# End Source File -# Begin Source File - -SOURCE=.\pubkey.cpp -# End Source File -# Begin Source File - -SOURCE=.\queue.cpp -# End Source File -# Begin Source File - -SOURCE=.\rabin.cpp -# End Source File -# Begin Source File - -SOURCE=.\randpool.cpp -# End Source File -# Begin Source File - -SOURCE=.\rc2.cpp -# End Source File -# Begin Source File - -SOURCE=.\rc5.cpp -# End Source File -# Begin Source File - -SOURCE=.\rc6.cpp -# End Source File -# Begin Source File - -SOURCE=.\rdtables.cpp -# End Source File -# Begin Source File - -SOURCE=.\rijndael.cpp -# End Source File -# Begin Source File - -SOURCE=.\ripemd.cpp -# End Source File -# Begin Source File - -SOURCE=.\rng.cpp -# End Source File -# Begin Source File - -SOURCE=.\rsa.cpp -# End Source File -# Begin Source File - -SOURCE=.\rw.cpp -# End Source File -# Begin Source File - -SOURCE=.\safer.cpp -# End Source File -# Begin Source File - -SOURCE=.\salsa.cpp -# End Source File -# Begin Source File - -SOURCE=.\seal.cpp -# End Source File -# Begin Source File - -SOURCE=.\seed.cpp -# End Source File -# Begin Source File - -SOURCE=.\serpent.cpp -# End Source File -# Begin Source File - -SOURCE=.\sha.cpp -# End Source File -# Begin Source File - -SOURCE=.\shacal2.cpp -# End Source File -# Begin Source File - -SOURCE=.\shark.cpp -# End Source File -# Begin Source File - -SOURCE=.\sharkbox.cpp -# End Source File -# Begin Source File - -SOURCE=.\simple.cpp -# End Source File -# Begin Source File - -SOURCE=.\skipjack.cpp -# End Source File -# Begin Source File - -SOURCE=.\socketft.cpp -# End Source File -# Begin Source File - -SOURCE=.\sosemanuk.cpp -# End Source File -# Begin Source File - -SOURCE=.\square.cpp -# End Source File -# Begin Source File - -SOURCE=.\squaretb.cpp -# End Source File -# Begin Source File - -SOURCE=.\strciphr.cpp -# End Source File -# Begin Source File - -SOURCE=.\tea.cpp -# End Source File -# Begin Source File - -SOURCE=.\tftables.cpp -# End Source File -# Begin Source File - -SOURCE=.\tiger.cpp -# End Source File -# Begin Source File - -SOURCE=.\tigertab.cpp -# End Source File -# Begin Source File - -SOURCE=.\trdlocal.cpp -# End Source File -# Begin Source File - -SOURCE=.\ttmac.cpp -# End Source File -# Begin Source File - -SOURCE=.\twofish.cpp -# End Source File -# Begin Source File - -SOURCE=.\vmac.cpp -# End Source File -# Begin Source File - -SOURCE=.\wait.cpp -# End Source File -# Begin Source File - -SOURCE=.\wake.cpp -# End Source File -# Begin Source File - -SOURCE=.\whrlpool.cpp -# End Source File -# Begin Source File - -SOURCE=.\winpipes.cpp -# End Source File -# Begin Source File - -SOURCE=.\xtr.cpp -# End Source File -# Begin Source File - -SOURCE=.\xtrcrypt.cpp -# End Source File -# Begin Source File - -SOURCE=.\zdeflate.cpp -# End Source File -# Begin Source File - -SOURCE=.\zinflate.cpp -# End Source File -# Begin Source File - -SOURCE=.\zlib.cpp -# End Source File -# End Group -# Begin Group "Header Files" - -# PROP Default_Filter ".;.h" -# Begin Source File - -SOURCE=.\3way.h -# End Source File -# Begin Source File - -SOURCE=.\adler32.h -# End Source File -# Begin Source File - -SOURCE=.\aes.h -# End Source File -# Begin Source File - -SOURCE=.\algebra.h -# End Source File -# Begin Source File - -SOURCE=.\algparam.h -# End Source File -# Begin Source File - -SOURCE=.\arc4.h -# End Source File -# Begin Source File - -SOURCE=.\argnames.h -# End Source File -# Begin Source File - -SOURCE=.\asn.h -# End Source File -# Begin Source File - -SOURCE=.\authenc.h -# End Source File -# Begin Source File - -SOURCE=.\base32.h -# End Source File -# Begin Source File - -SOURCE=.\base64.h -# End Source File -# Begin Source File - -SOURCE=.\basecode.h -# End Source File -# Begin Source File - -SOURCE=.\blowfish.h -# End Source File -# Begin Source File - -SOURCE=.\blumshub.h -# End Source File -# Begin Source File - -SOURCE=.\camellia.h -# End Source File -# Begin Source File - -SOURCE=.\cast.h -# End Source File -# Begin Source File - -SOURCE=.\cbcmac.h -# End Source File -# Begin Source File - -SOURCE=.\ccm.h -# End Source File -# Begin Source File - -SOURCE=.\channels.h -# End Source File -# Begin Source File - -SOURCE=.\cmac.h -# End Source File -# Begin Source File - -SOURCE=.\config.h -# End Source File -# Begin Source File - -SOURCE=.\crc.h -# End Source File -# Begin Source File - -SOURCE=.\cryptlib.h -# End Source File -# Begin Source File - -SOURCE=.\default.h -# End Source File -# Begin Source File - -SOURCE=.\des.h -# End Source File -# Begin Source File - -SOURCE=.\dh.h -# End Source File -# Begin Source File - -SOURCE=.\dh2.h -# End Source File -# Begin Source File - -SOURCE=.\dmac.h -# End Source File -# Begin Source File - -SOURCE=.\dsa.h -# End Source File -# Begin Source File - -SOURCE=.\dword.h -# End Source File -# Begin Source File - -SOURCE=.\eax.h -# End Source File -# Begin Source File - -SOURCE=.\ec2n.h -# End Source File -# Begin Source File - -SOURCE=.\eccrypto.h -# End Source File -# Begin Source File - -SOURCE=.\ecp.h -# End Source File -# Begin Source File - -SOURCE=.\elgamal.h -# End Source File -# Begin Source File - -SOURCE=.\emsa2.h -# End Source File -# Begin Source File - -SOURCE=.\eprecomp.h -# End Source File -# Begin Source File - -SOURCE=.\esign.h -# End Source File -# Begin Source File - -SOURCE=.\factory.h -# End Source File -# Begin Source File - -SOURCE=.\files.h -# End Source File -# Begin Source File - -SOURCE=.\filters.h -# End Source File -# Begin Source File - -SOURCE=.\fips140.h -# End Source File -# Begin Source File - -SOURCE=.\fltrimpl.h -# End Source File -# Begin Source File - -SOURCE=.\gcm.h -# End Source File -# Begin Source File - -SOURCE=.\gf256.h -# End Source File -# Begin Source File - -SOURCE=.\gf2_32.h -# End Source File -# Begin Source File - -SOURCE=.\gf2n.h -# End Source File -# Begin Source File - -SOURCE=.\gfpcrypt.h -# End Source File -# Begin Source File - -SOURCE=.\gost.h -# End Source File -# Begin Source File - -SOURCE=.\gzip.h -# End Source File -# Begin Source File - -SOURCE=.\hex.h -# End Source File -# Begin Source File - -SOURCE=.\hmac.h -# End Source File -# Begin Source File - -SOURCE=.\hrtimer.h -# End Source File -# Begin Source File - -SOURCE=.\ida.h -# End Source File -# Begin Source File - -SOURCE=.\idea.h -# End Source File -# Begin Source File - -SOURCE=.\integer.h -# End Source File -# Begin Source File - -SOURCE=.\iterhash.h -# End Source File -# Begin Source File - -SOURCE=.\lubyrack.h -# End Source File -# Begin Source File - -SOURCE=.\luc.h -# End Source File -# Begin Source File - -SOURCE=.\mars.h -# End Source File -# Begin Source File - -SOURCE=.\md2.h -# End Source File -# Begin Source File - -SOURCE=.\md4.h -# End Source File -# Begin Source File - -SOURCE=.\md5.h -# End Source File -# Begin Source File - -SOURCE=.\mdc.h -# End Source File -# Begin Source File - -SOURCE=.\misc.h -# End Source File -# Begin Source File - -SOURCE=.\modarith.h -# End Source File -# Begin Source File - -SOURCE=.\modes.h -# End Source File -# Begin Source File - -SOURCE=.\modexppc.h -# End Source File -# Begin Source File - -SOURCE=.\mqueue.h -# End Source File -# Begin Source File - -SOURCE=.\mqv.h -# End Source File -# Begin Source File - -SOURCE=.\nbtheory.h -# End Source File -# Begin Source File - -SOURCE=.\network.h -# End Source File -# Begin Source File - -SOURCE=.\nr.h -# End Source File -# Begin Source File - -SOURCE=.\oaep.h -# End Source File -# Begin Source File - -SOURCE=.\oids.h -# End Source File -# Begin Source File - -SOURCE=.\osrng.h -# End Source File -# Begin Source File - -SOURCE=.\panama.h -# End Source File -# Begin Source File - -SOURCE=.\pch.h -# End Source File -# Begin Source File - -SOURCE=.\pkcspad.h -# End Source File -# Begin Source File - -SOURCE=.\polynomi.h -# End Source File -# Begin Source File - -SOURCE=.\pssr.h -# End Source File -# Begin Source File - -SOURCE=.\pubkey.h -# End Source File -# Begin Source File - -SOURCE=.\pwdbased.h -# End Source File -# Begin Source File - -SOURCE=.\queue.h -# End Source File -# Begin Source File - -SOURCE=.\rabin.h -# End Source File -# Begin Source File - -SOURCE=.\randpool.h -# End Source File -# Begin Source File - -SOURCE=.\rc2.h -# End Source File -# Begin Source File - -SOURCE=.\rc5.h -# End Source File -# Begin Source File - -SOURCE=.\rc6.h -# End Source File -# Begin Source File - -SOURCE=.\rijndael.h -# End Source File -# Begin Source File - -SOURCE=.\ripemd.h -# End Source File -# Begin Source File - -SOURCE=.\rng.h -# End Source File -# Begin Source File - -SOURCE=.\rsa.h -# End Source File -# Begin Source File - -SOURCE=.\rw.h -# End Source File -# Begin Source File - -SOURCE=.\safer.h -# End Source File -# Begin Source File - -SOURCE=.\salsa.h -# End Source File -# Begin Source File - -SOURCE=.\seal.h -# End Source File -# Begin Source File - -SOURCE=.\secblock.h -# End Source File -# Begin Source File - -SOURCE=.\seckey.h -# End Source File -# Begin Source File - -SOURCE=.\seed.h -# End Source File -# Begin Source File - -SOURCE=.\serpent.h -# End Source File -# Begin Source File - -SOURCE=.\sha.h -# End Source File -# Begin Source File - -SOURCE=.\shacal2.h -# End Source File -# Begin Source File - -SOURCE=.\shark.h -# End Source File -# Begin Source File - -SOURCE=.\simple.h -# End Source File -# Begin Source File - -SOURCE=.\skipjack.h -# End Source File -# Begin Source File - -SOURCE=.\smartptr.h -# End Source File -# Begin Source File - -SOURCE=.\socketft.h -# End Source File -# Begin Source File - -SOURCE=.\square.h -# End Source File -# Begin Source File - -SOURCE=.\strciphr.h -# End Source File -# Begin Source File - -SOURCE=.\tea.h -# End Source File -# Begin Source File - -SOURCE=.\tiger.h -# End Source File -# Begin Source File - -SOURCE=.\trdlocal.h -# End Source File -# Begin Source File - -SOURCE=.\trunhash.h -# End Source File -# Begin Source File - -SOURCE=.\ttmac.h -# End Source File -# Begin Source File - -SOURCE=.\twofish.h -# End Source File -# Begin Source File - -SOURCE=.\wait.h -# End Source File -# Begin Source File - -SOURCE=.\wake.h -# End Source File -# Begin Source File - -SOURCE=.\whrlpool.h -# End Source File -# Begin Source File - -SOURCE=.\winpipes.h -# End Source File -# Begin Source File - -SOURCE=.\words.h -# End Source File -# Begin Source File - -SOURCE=.\xtr.h -# End Source File -# Begin Source File - -SOURCE=.\xtrcrypt.h -# End Source File -# Begin Source File - -SOURCE=.\zdeflate.h -# End Source File -# Begin Source File - -SOURCE=.\zinflate.h -# End Source File -# Begin Source File - -SOURCE=.\zlib.h -# End Source File -# End Group -# Begin Group "Miscellaneous" - -# PROP Default_Filter "" -# Begin Source File - -SOURCE=.\Doxyfile -# End Source File -# Begin Source File - -SOURCE=.\GNUmakefile -# End Source File -# Begin Source File - -SOURCE=.\license.txt -# End Source File -# Begin Source File - -SOURCE=.\readme.txt -# End Source File -# End Group -# End Target -# End Project +# Microsoft Developer Studio Project File - Name="cryptlib" - Package Owner=<4> +# Microsoft Developer Studio Generated Build File, Format Version 6.00 +# ** DO NOT EDIT ** + +# TARGTYPE "Win32 (x86) Static Library" 0x0104 + +CFG=cryptlib - Win32 Debug +!MESSAGE This is not a valid makefile. To build this project using NMAKE, +!MESSAGE use the Export Makefile command and run +!MESSAGE +!MESSAGE NMAKE /f "cryptlib.mak". +!MESSAGE +!MESSAGE You can specify a configuration when running NMAKE +!MESSAGE by defining the macro CFG on the command line. For example: +!MESSAGE +!MESSAGE NMAKE /f "cryptlib.mak" CFG="cryptlib - Win32 Debug" +!MESSAGE +!MESSAGE Possible choices for configuration are: +!MESSAGE +!MESSAGE "cryptlib - Win32 DLL-Import Release" (based on "Win32 (x86) Static Library") +!MESSAGE "cryptlib - Win32 DLL-Import Debug" (based on "Win32 (x86) Static Library") +!MESSAGE "cryptlib - Win32 Release" (based on "Win32 (x86) Static Library") +!MESSAGE "cryptlib - Win32 Debug" (based on "Win32 (x86) Static Library") +!MESSAGE + +# Begin Project +# PROP AllowPerConfigDependencies 0 +# PROP Scc_ProjName "" +# PROP Scc_LocalPath "" +CPP=cl.exe +RSC=rc.exe + +!IF "$(CFG)" == "cryptlib - Win32 DLL-Import Release" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 0 +# PROP BASE Output_Dir "cryptlib___Win32_FIPS_140_Release" +# PROP BASE Intermediate_Dir "cryptlib___Win32_FIPS_140_Release" +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 0 +# PROP Output_Dir "DLL_Import_Release" +# PROP Intermediate_Dir "DLL_Import_Release" +# PROP Target_Dir "" +# ADD BASE CPP /nologo /G5 /Gz /MT /W3 /GX /Zi /O2 /D "NDEBUG" /D "WIN32" /D "_WINDOWS" /D "USE_PRECOMPILED_HEADERS" /Yu"pch.h" /FD /c +# ADD CPP /nologo /G5 /Gz /MT /W3 /GR /GX /Zi /O2 /Ob2 /D "NDEBUG" /D "_WINDOWS" /D "USE_PRECOMPILED_HEADERS" /D "WIN32" /D "CRYPTOPP_IMPORTS" /Yu"pch.h" /FD /Zm400 /c +# ADD BASE RSC /l 0x409 +# ADD RSC /l 0x409 +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LIB32=link.exe -lib +# ADD BASE LIB32 /nologo +# ADD LIB32 /nologo + +!ELSEIF "$(CFG)" == "cryptlib - Win32 DLL-Import Debug" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 1 +# PROP BASE Output_Dir "cryptlib___Win32_FIPS_140_Debug" +# PROP BASE Intermediate_Dir "cryptlib___Win32_FIPS_140_Debug" +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 1 +# PROP Output_Dir "DLL_Import_Debug" +# PROP Intermediate_Dir "DLL_Import_Debug" +# PROP Target_Dir "" +# ADD BASE CPP /nologo /MTd /W3 /GX /ZI /Od /D "_DEBUG" /D "WIN32" /D "_WINDOWS" /D "USE_PRECOMPILED_HEADERS" /Yu"pch.h" /FD /c +# ADD CPP /nologo /G5 /Gz /MTd /W3 /GR /GX /Zi /Oi /D "_DEBUG" /D "_WINDOWS" /D "USE_PRECOMPILED_HEADERS" /D "WIN32" /D "CRYPTOPP_IMPORTS" /Yu"pch.h" /FD /Zm400 /c +# ADD BASE RSC /l 0x409 +# ADD RSC /l 0x409 +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LIB32=link.exe -lib +# ADD BASE LIB32 /nologo +# ADD LIB32 /nologo + +!ELSEIF "$(CFG)" == "cryptlib - Win32 Release" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 0 +# PROP BASE Output_Dir "cryptlib" +# PROP BASE Intermediate_Dir "cryptlib" +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 0 +# PROP Output_Dir "Release" +# PROP Intermediate_Dir "Release" +# PROP Target_Dir "" +# ADD BASE CPP /nologo /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /YX /FD /c +# ADD CPP /nologo /MT /W3 /GR /GX /Zi /O2 /Ob2 /D "NDEBUG" /D "_WINDOWS" /D "USE_PRECOMPILED_HEADERS" /D "WIN32" /Yu"pch.h" /FD /Zm400 /c +# ADD BASE RSC /l 0x409 +# ADD RSC /l 0x409 +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LIB32=link.exe -lib +# ADD BASE LIB32 /nologo +# ADD LIB32 /nologo + +!ELSEIF "$(CFG)" == "cryptlib - Win32 Debug" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 1 +# PROP BASE Output_Dir "cryptli0" +# PROP BASE Intermediate_Dir "cryptli0" +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 1 +# PROP Output_Dir "Debug" +# PROP Intermediate_Dir "Debug" +# PROP Target_Dir "" +# ADD BASE CPP /nologo /W3 /GX /Z7 /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /YX /FD /c +# ADD CPP /nologo /MTd /W3 /GR /GX /Zi /Oi /D "_DEBUG" /D "_WINDOWS" /D "USE_PRECOMPILED_HEADERS" /D "WIN32" /Yu"pch.h" /FD /Zm400 /c +# ADD BASE RSC /l 0x409 +# ADD RSC /l 0x409 +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LIB32=link.exe -lib +# ADD BASE LIB32 /nologo +# ADD LIB32 /nologo + +!ENDIF + +# Begin Target + +# Name "cryptlib - Win32 DLL-Import Release" +# Name "cryptlib - Win32 DLL-Import Debug" +# Name "cryptlib - Win32 Release" +# Name "cryptlib - Win32 Debug" +# Begin Group "Source Files" + +# PROP Default_Filter ".cpp" +# Begin Source File + +SOURCE=.\3way.cpp +# End Source File +# Begin Source File + +SOURCE=.\adhoc.cpp.proto + +!IF "$(CFG)" == "cryptlib - Win32 DLL-Import Release" + +# Begin Custom Build +InputPath=.\adhoc.cpp.proto + +"adhoc.cpp.copied" : $(SOURCE) "$(INTDIR)" "$(OUTDIR)" + if not exist adhoc.cpp copy "$(InputPath)" adhoc.cpp + echo: >> adhoc.cpp.copied + +# End Custom Build + +!ELSEIF "$(CFG)" == "cryptlib - Win32 DLL-Import Debug" + +# Begin Custom Build +InputPath=.\adhoc.cpp.proto + +"adhoc.cpp.copied" : $(SOURCE) "$(INTDIR)" "$(OUTDIR)" + if not exist adhoc.cpp copy "$(InputPath)" adhoc.cpp + echo: >> adhoc.cpp.copied + +# End Custom Build + +!ELSEIF "$(CFG)" == "cryptlib - Win32 Release" + +# Begin Custom Build +InputPath=.\adhoc.cpp.proto + +"adhoc.cpp.copied" : $(SOURCE) "$(INTDIR)" "$(OUTDIR)" + if not exist adhoc.cpp copy "$(InputPath)" adhoc.cpp + echo: >> adhoc.cpp.copied + +# End Custom Build + +!ELSEIF "$(CFG)" == "cryptlib - Win32 Debug" + +# Begin Custom Build +InputPath=.\adhoc.cpp.proto + +"adhoc.cpp.copied" : $(SOURCE) "$(INTDIR)" "$(OUTDIR)" + if not exist adhoc.cpp copy "$(InputPath)" adhoc.cpp + echo: >> adhoc.cpp.copied + +# End Custom Build + +!ENDIF + +# End Source File +# Begin Source File + +SOURCE=.\adler32.cpp +# End Source File +# Begin Source File + +SOURCE=.\algebra.cpp +# End Source File +# Begin Source File + +SOURCE=.\algparam.cpp +# End Source File +# Begin Source File + +SOURCE=.\arc4.cpp +# End Source File +# Begin Source File + +SOURCE=.\asn.cpp +# End Source File +# Begin Source File + +SOURCE=.\authenc.cpp +# End Source File +# Begin Source File + +SOURCE=.\base32.cpp +# End Source File +# Begin Source File + +SOURCE=.\base64.cpp +# End Source File +# Begin Source File + +SOURCE=.\basecode.cpp +# End Source File +# Begin Source File + +SOURCE=.\bfinit.cpp +# End Source File +# Begin Source File + +SOURCE=.\blowfish.cpp +# End Source File +# Begin Source File + +SOURCE=.\blumshub.cpp +# End Source File +# Begin Source File + +SOURCE=.\camellia.cpp +# End Source File +# Begin Source File + +SOURCE=.\cast.cpp +# End Source File +# Begin Source File + +SOURCE=.\casts.cpp +# End Source File +# Begin Source File + +SOURCE=.\cbcmac.cpp +# End Source File +# Begin Source File + +SOURCE=.\ccm.cpp +# End Source File +# Begin Source File + +SOURCE=.\channels.cpp +# End Source File +# Begin Source File + +SOURCE=.\cmac.cpp +# End Source File +# Begin Source File + +SOURCE=.\cpu.cpp +# End Source File +# Begin Source File + +SOURCE=.\crc.cpp +# End Source File +# Begin Source File + +SOURCE=.\cryptlib.cpp +# End Source File +# Begin Source File + +SOURCE=.\default.cpp +# End Source File +# Begin Source File + +SOURCE=.\des.cpp +# End Source File +# Begin Source File + +SOURCE=.\dessp.cpp +# End Source File +# Begin Source File + +SOURCE=.\dh.cpp +# End Source File +# Begin Source File + +SOURCE=.\dh2.cpp +# End Source File +# Begin Source File + +SOURCE=.\dll.cpp +# SUBTRACT CPP /YX /Yc /Yu +# End Source File +# Begin Source File + +SOURCE=.\dsa.cpp +# End Source File +# Begin Source File + +SOURCE=.\eax.cpp +# End Source File +# Begin Source File + +SOURCE=.\ec2n.cpp +# End Source File +# Begin Source File + +SOURCE=.\eccrypto.cpp +# End Source File +# Begin Source File + +SOURCE=.\ecp.cpp +# End Source File +# Begin Source File + +SOURCE=.\elgamal.cpp +# End Source File +# Begin Source File + +SOURCE=.\emsa2.cpp +# End Source File +# Begin Source File + +SOURCE=.\eprecomp.cpp +# End Source File +# Begin Source File + +SOURCE=.\esign.cpp +# End Source File +# Begin Source File + +SOURCE=.\files.cpp +# End Source File +# Begin Source File + +SOURCE=.\filters.cpp +# End Source File +# Begin Source File + +SOURCE=.\fips140.cpp +# End Source File +# Begin Source File + +SOURCE=.\fipstest.cpp +# End Source File +# Begin Source File + +SOURCE=.\gcm.cpp +# End Source File +# Begin Source File + +SOURCE=.\gf256.cpp +# End Source File +# Begin Source File + +SOURCE=.\gf2_32.cpp +# End Source File +# Begin Source File + +SOURCE=.\gf2n.cpp +# End Source File +# Begin Source File + +SOURCE=.\gfpcrypt.cpp +# End Source File +# Begin Source File + +SOURCE=.\gost.cpp +# End Source File +# Begin Source File + +SOURCE=.\gzip.cpp +# End Source File +# Begin Source File + +SOURCE=.\hex.cpp +# End Source File +# Begin Source File + +SOURCE=.\hmac.cpp +# End Source File +# Begin Source File + +SOURCE=.\hrtimer.cpp +# End Source File +# Begin Source File + +SOURCE=.\ida.cpp +# End Source File +# Begin Source File + +SOURCE=.\idea.cpp +# End Source File +# Begin Source File + +SOURCE=.\integer.cpp +# End Source File +# Begin Source File + +SOURCE=.\iterhash.cpp +# SUBTRACT CPP /YX /Yc /Yu +# End Source File +# Begin Source File + +SOURCE=.\luc.cpp +# End Source File +# Begin Source File + +SOURCE=.\mars.cpp +# End Source File +# Begin Source File + +SOURCE=.\marss.cpp +# End Source File +# Begin Source File + +SOURCE=.\md2.cpp +# End Source File +# Begin Source File + +SOURCE=.\md4.cpp +# End Source File +# Begin Source File + +SOURCE=.\md5.cpp +# End Source File +# Begin Source File + +SOURCE=.\misc.cpp +# End Source File +# Begin Source File + +SOURCE=.\modes.cpp +# End Source File +# Begin Source File + +SOURCE=.\mqueue.cpp +# End Source File +# Begin Source File + +SOURCE=.\mqv.cpp +# End Source File +# Begin Source File + +SOURCE=.\nbtheory.cpp +# End Source File +# Begin Source File + +SOURCE=.\network.cpp +# End Source File +# Begin Source File + +SOURCE=.\oaep.cpp +# End Source File +# Begin Source File + +SOURCE=.\osrng.cpp +# End Source File +# Begin Source File + +SOURCE=.\panama.cpp +# End Source File +# Begin Source File + +SOURCE=.\pch.cpp +# ADD CPP /Yc"pch.h" +# End Source File +# Begin Source File + +SOURCE=.\pkcspad.cpp +# End Source File +# Begin Source File + +SOURCE=.\polynomi.cpp +# End Source File +# Begin Source File + +SOURCE=.\pssr.cpp +# End Source File +# Begin Source File + +SOURCE=.\pubkey.cpp +# End Source File +# Begin Source File + +SOURCE=.\queue.cpp +# End Source File +# Begin Source File + +SOURCE=.\rabin.cpp +# End Source File +# Begin Source File + +SOURCE=.\randpool.cpp +# End Source File +# Begin Source File + +SOURCE=.\rc2.cpp +# End Source File +# Begin Source File + +SOURCE=.\rc5.cpp +# End Source File +# Begin Source File + +SOURCE=.\rc6.cpp +# End Source File +# Begin Source File + +SOURCE=.\rdtables.cpp +# End Source File +# Begin Source File + +SOURCE=.\rijndael.cpp +# End Source File +# Begin Source File + +SOURCE=.\ripemd.cpp +# End Source File +# Begin Source File + +SOURCE=.\rng.cpp +# End Source File +# Begin Source File + +SOURCE=.\rsa.cpp +# End Source File +# Begin Source File + +SOURCE=.\rw.cpp +# End Source File +# Begin Source File + +SOURCE=.\safer.cpp +# End Source File +# Begin Source File + +SOURCE=.\salsa.cpp +# End Source File +# Begin Source File + +SOURCE=.\seal.cpp +# End Source File +# Begin Source File + +SOURCE=.\seed.cpp +# End Source File +# Begin Source File + +SOURCE=.\serpent.cpp +# End Source File +# Begin Source File + +SOURCE=.\sha.cpp +# End Source File +# Begin Source File + +SOURCE=.\shacal2.cpp +# End Source File +# Begin Source File + +SOURCE=.\shark.cpp +# End Source File +# Begin Source File + +SOURCE=.\sharkbox.cpp +# End Source File +# Begin Source File + +SOURCE=.\simple.cpp +# End Source File +# Begin Source File + +SOURCE=.\skipjack.cpp +# End Source File +# Begin Source File + +SOURCE=.\socketft.cpp +# End Source File +# Begin Source File + +SOURCE=.\sosemanuk.cpp +# End Source File +# Begin Source File + +SOURCE=.\square.cpp +# End Source File +# Begin Source File + +SOURCE=.\squaretb.cpp +# End Source File +# Begin Source File + +SOURCE=.\strciphr.cpp +# End Source File +# Begin Source File + +SOURCE=.\tea.cpp +# End Source File +# Begin Source File + +SOURCE=.\tftables.cpp +# End Source File +# Begin Source File + +SOURCE=.\tiger.cpp +# End Source File +# Begin Source File + +SOURCE=.\tigertab.cpp +# End Source File +# Begin Source File + +SOURCE=.\trdlocal.cpp +# End Source File +# Begin Source File + +SOURCE=.\ttmac.cpp +# End Source File +# Begin Source File + +SOURCE=.\twofish.cpp +# End Source File +# Begin Source File + +SOURCE=.\vmac.cpp +# End Source File +# Begin Source File + +SOURCE=.\wait.cpp +# End Source File +# Begin Source File + +SOURCE=.\wake.cpp +# End Source File +# Begin Source File + +SOURCE=.\whrlpool.cpp +# End Source File +# Begin Source File + +SOURCE=.\winpipes.cpp +# End Source File +# Begin Source File + +SOURCE=.\xtr.cpp +# End Source File +# Begin Source File + +SOURCE=.\xtrcrypt.cpp +# End Source File +# Begin Source File + +SOURCE=.\zdeflate.cpp +# End Source File +# Begin Source File + +SOURCE=.\zinflate.cpp +# End Source File +# Begin Source File + +SOURCE=.\zlib.cpp +# End Source File +# End Group +# Begin Group "Header Files" + +# PROP Default_Filter ".;.h" +# Begin Source File + +SOURCE=.\3way.h +# End Source File +# Begin Source File + +SOURCE=.\adler32.h +# End Source File +# Begin Source File + +SOURCE=.\aes.h +# End Source File +# Begin Source File + +SOURCE=.\algebra.h +# End Source File +# Begin Source File + +SOURCE=.\algparam.h +# End Source File +# Begin Source File + +SOURCE=.\arc4.h +# End Source File +# Begin Source File + +SOURCE=.\argnames.h +# End Source File +# Begin Source File + +SOURCE=.\asn.h +# End Source File +# Begin Source File + +SOURCE=.\authenc.h +# End Source File +# Begin Source File + +SOURCE=.\base32.h +# End Source File +# Begin Source File + +SOURCE=.\base64.h +# End Source File +# Begin Source File + +SOURCE=.\basecode.h +# End Source File +# Begin Source File + +SOURCE=.\blowfish.h +# End Source File +# Begin Source File + +SOURCE=.\blumshub.h +# End Source File +# Begin Source File + +SOURCE=.\camellia.h +# End Source File +# Begin Source File + +SOURCE=.\cast.h +# End Source File +# Begin Source File + +SOURCE=.\cbcmac.h +# End Source File +# Begin Source File + +SOURCE=.\ccm.h +# End Source File +# Begin Source File + +SOURCE=.\channels.h +# End Source File +# Begin Source File + +SOURCE=.\cmac.h +# End Source File +# Begin Source File + +SOURCE=.\config.h +# End Source File +# Begin Source File + +SOURCE=.\crc.h +# End Source File +# Begin Source File + +SOURCE=.\cryptlib.h +# End Source File +# Begin Source File + +SOURCE=.\default.h +# End Source File +# Begin Source File + +SOURCE=.\des.h +# End Source File +# Begin Source File + +SOURCE=.\dh.h +# End Source File +# Begin Source File + +SOURCE=.\dh2.h +# End Source File +# Begin Source File + +SOURCE=.\dmac.h +# End Source File +# Begin Source File + +SOURCE=.\dsa.h +# End Source File +# Begin Source File + +SOURCE=.\dword.h +# End Source File +# Begin Source File + +SOURCE=.\eax.h +# End Source File +# Begin Source File + +SOURCE=.\ec2n.h +# End Source File +# Begin Source File + +SOURCE=.\eccrypto.h +# End Source File +# Begin Source File + +SOURCE=.\ecp.h +# End Source File +# Begin Source File + +SOURCE=.\elgamal.h +# End Source File +# Begin Source File + +SOURCE=.\emsa2.h +# End Source File +# Begin Source File + +SOURCE=.\eprecomp.h +# End Source File +# Begin Source File + +SOURCE=.\esign.h +# End Source File +# Begin Source File + +SOURCE=.\factory.h +# End Source File +# Begin Source File + +SOURCE=.\files.h +# End Source File +# Begin Source File + +SOURCE=.\filters.h +# End Source File +# Begin Source File + +SOURCE=.\fips140.h +# End Source File +# Begin Source File + +SOURCE=.\fltrimpl.h +# End Source File +# Begin Source File + +SOURCE=.\gcm.h +# End Source File +# Begin Source File + +SOURCE=.\gf256.h +# End Source File +# Begin Source File + +SOURCE=.\gf2_32.h +# End Source File +# Begin Source File + +SOURCE=.\gf2n.h +# End Source File +# Begin Source File + +SOURCE=.\gfpcrypt.h +# End Source File +# Begin Source File + +SOURCE=.\gost.h +# End Source File +# Begin Source File + +SOURCE=.\gzip.h +# End Source File +# Begin Source File + +SOURCE=.\hex.h +# End Source File +# Begin Source File + +SOURCE=.\hmac.h +# End Source File +# Begin Source File + +SOURCE=.\hrtimer.h +# End Source File +# Begin Source File + +SOURCE=.\ida.h +# End Source File +# Begin Source File + +SOURCE=.\idea.h +# End Source File +# Begin Source File + +SOURCE=.\integer.h +# End Source File +# Begin Source File + +SOURCE=.\iterhash.h +# End Source File +# Begin Source File + +SOURCE=.\lubyrack.h +# End Source File +# Begin Source File + +SOURCE=.\luc.h +# End Source File +# Begin Source File + +SOURCE=.\mars.h +# End Source File +# Begin Source File + +SOURCE=.\md2.h +# End Source File +# Begin Source File + +SOURCE=.\md4.h +# End Source File +# Begin Source File + +SOURCE=.\md5.h +# End Source File +# Begin Source File + +SOURCE=.\mdc.h +# End Source File +# Begin Source File + +SOURCE=.\misc.h +# End Source File +# Begin Source File + +SOURCE=.\modarith.h +# End Source File +# Begin Source File + +SOURCE=.\modes.h +# End Source File +# Begin Source File + +SOURCE=.\modexppc.h +# End Source File +# Begin Source File + +SOURCE=.\mqueue.h +# End Source File +# Begin Source File + +SOURCE=.\mqv.h +# End Source File +# Begin Source File + +SOURCE=.\nbtheory.h +# End Source File +# Begin Source File + +SOURCE=.\network.h +# End Source File +# Begin Source File + +SOURCE=.\nr.h +# End Source File +# Begin Source File + +SOURCE=.\oaep.h +# End Source File +# Begin Source File + +SOURCE=.\oids.h +# End Source File +# Begin Source File + +SOURCE=.\osrng.h +# End Source File +# Begin Source File + +SOURCE=.\panama.h +# End Source File +# Begin Source File + +SOURCE=.\pch.h +# End Source File +# Begin Source File + +SOURCE=.\pkcspad.h +# End Source File +# Begin Source File + +SOURCE=.\polynomi.h +# End Source File +# Begin Source File + +SOURCE=.\pssr.h +# End Source File +# Begin Source File + +SOURCE=.\pubkey.h +# End Source File +# Begin Source File + +SOURCE=.\pwdbased.h +# End Source File +# Begin Source File + +SOURCE=.\queue.h +# End Source File +# Begin Source File + +SOURCE=.\rabin.h +# End Source File +# Begin Source File + +SOURCE=.\randpool.h +# End Source File +# Begin Source File + +SOURCE=.\rc2.h +# End Source File +# Begin Source File + +SOURCE=.\rc5.h +# End Source File +# Begin Source File + +SOURCE=.\rc6.h +# End Source File +# Begin Source File + +SOURCE=.\rijndael.h +# End Source File +# Begin Source File + +SOURCE=.\ripemd.h +# End Source File +# Begin Source File + +SOURCE=.\rng.h +# End Source File +# Begin Source File + +SOURCE=.\rsa.h +# End Source File +# Begin Source File + +SOURCE=.\rw.h +# End Source File +# Begin Source File + +SOURCE=.\safer.h +# End Source File +# Begin Source File + +SOURCE=.\salsa.h +# End Source File +# Begin Source File + +SOURCE=.\seal.h +# End Source File +# Begin Source File + +SOURCE=.\secblock.h +# End Source File +# Begin Source File + +SOURCE=.\seckey.h +# End Source File +# Begin Source File + +SOURCE=.\seed.h +# End Source File +# Begin Source File + +SOURCE=.\serpent.h +# End Source File +# Begin Source File + +SOURCE=.\sha.h +# End Source File +# Begin Source File + +SOURCE=.\shacal2.h +# End Source File +# Begin Source File + +SOURCE=.\shark.h +# End Source File +# Begin Source File + +SOURCE=.\simple.h +# End Source File +# Begin Source File + +SOURCE=.\skipjack.h +# End Source File +# Begin Source File + +SOURCE=.\smartptr.h +# End Source File +# Begin Source File + +SOURCE=.\socketft.h +# End Source File +# Begin Source File + +SOURCE=.\square.h +# End Source File +# Begin Source File + +SOURCE=.\strciphr.h +# End Source File +# Begin Source File + +SOURCE=.\tea.h +# End Source File +# Begin Source File + +SOURCE=.\tiger.h +# End Source File +# Begin Source File + +SOURCE=.\trdlocal.h +# End Source File +# Begin Source File + +SOURCE=.\trunhash.h +# End Source File +# Begin Source File + +SOURCE=.\ttmac.h +# End Source File +# Begin Source File + +SOURCE=.\twofish.h +# End Source File +# Begin Source File + +SOURCE=.\wait.h +# End Source File +# Begin Source File + +SOURCE=.\wake.h +# End Source File +# Begin Source File + +SOURCE=.\whrlpool.h +# End Source File +# Begin Source File + +SOURCE=.\winpipes.h +# End Source File +# Begin Source File + +SOURCE=.\words.h +# End Source File +# Begin Source File + +SOURCE=.\xtr.h +# End Source File +# Begin Source File + +SOURCE=.\xtrcrypt.h +# End Source File +# Begin Source File + +SOURCE=.\zdeflate.h +# End Source File +# Begin Source File + +SOURCE=.\zinflate.h +# End Source File +# Begin Source File + +SOURCE=.\zlib.h +# End Source File +# End Group +# Begin Group "Miscellaneous" + +# PROP Default_Filter "" +# Begin Source File + +SOURCE=.\Doxyfile +# End Source File +# Begin Source File + +SOURCE=.\GNUmakefile +# End Source File +# Begin Source File + +SOURCE=.\license.txt +# End Source File +# Begin Source File + +SOURCE=.\readme.txt +# End Source File +# End Group +# End Target +# End Project diff --git a/cppForSwig/cryptopp/cryptlib.vcproj b/cppForSwig/cryptopp/cryptlib.vcproj index c0c079baf..78d56437b 100755 --- a/cppForSwig/cryptopp/cryptlib.vcproj +++ b/cppForSwig/cryptopp/cryptlib.vcproj @@ -1,9351 +1,9351 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cppForSwig/cryptopp/cryptlib_bds.bdsproj b/cppForSwig/cryptopp/cryptlib_bds.bdsproj index 9b9c7b249..835131623 100755 --- a/cppForSwig/cryptopp/cryptlib_bds.bdsproj +++ b/cppForSwig/cryptopp/cryptlib_bds.bdsproj @@ -1,377 +1,377 @@ - - - - - - - - - - - - cryptlib_bds.cpp - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - False - False - 1 - 0 - 0 - 0 - False - False - False - False - False - 1033 - 1252 - - - - - 1.0.0.0 - - - - - - 1.0.0.0 - - - - - - - - - False - - - - - - - False - - False - - True - False - - - - - - - - - - - + + + + + + + + + + + + cryptlib_bds.cpp + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + False + False + 1 + 0 + 0 + 0 + False + False + False + False + False + 1033 + 1252 + + + + + 1.0.0.0 + + + + + + 1.0.0.0 + + + + + + + + + False + + + + + + + False + + False + + True + False + + + + + + + + + + + diff --git a/cppForSwig/cryptopp/cryptopp.rc b/cppForSwig/cryptopp/cryptopp.rc index 7c37dc62d..3f27c75c7 100755 --- a/cppForSwig/cryptopp/cryptopp.rc +++ b/cppForSwig/cryptopp/cryptopp.rc @@ -1,104 +1,104 @@ -// Microsoft Visual C++ generated resource script. -// -#include "resource.h" - -#define APSTUDIO_READONLY_SYMBOLS -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 2 resource. -// -#include "winres.h" - -///////////////////////////////////////////////////////////////////////////// -#undef APSTUDIO_READONLY_SYMBOLS - -///////////////////////////////////////////////////////////////////////////// -// English (U.S.) resources - -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -#ifdef _WIN32 -LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US -#pragma code_page(1252) -#endif //_WIN32 - -///////////////////////////////////////////////////////////////////////////// -// -// Version -// - -VS_VERSION_INFO VERSIONINFO - FILEVERSION 5,6,1,0 - PRODUCTVERSION 5,6,1,0 - FILEFLAGSMASK 0x3fL -#ifdef _DEBUG - FILEFLAGS 0x1L -#else - FILEFLAGS 0x0L -#endif - FILEOS 0x4L - FILETYPE 0x2L - FILESUBTYPE 0x0L -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "040904b0" - BEGIN - VALUE "Comments", "free crypto library, more information available at www.cryptopp.com" - VALUE "CompanyName", "Wei Dai" - VALUE "FileDescription", "Crypto++® Library DLL" - VALUE "FileVersion", "5, 6, 1, 0" - VALUE "InternalName", "cryptopp" - VALUE "LegalCopyright", "Copyright © 1995-2010 by Wei Dai" - VALUE "LegalTrademarks", "Crypto++®" - VALUE "OriginalFilename", "cryptopp.dll" - VALUE "ProductName", "Crypto++® Library" - VALUE "ProductVersion", "5, 6, 1, 0" - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x409, 1200 - END -END - - -#ifdef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// TEXTINCLUDE -// - -1 TEXTINCLUDE -BEGIN - "resource.h\0" -END - -2 TEXTINCLUDE -BEGIN - "#include ""winres.h""\r\n" - "\0" -END - -3 TEXTINCLUDE -BEGIN - "\r\n" - "\0" -END - -#endif // APSTUDIO_INVOKED - -#endif // English (U.S.) resources -///////////////////////////////////////////////////////////////////////////// - - - -#ifndef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 3 resource. -// - - -///////////////////////////////////////////////////////////////////////////// -#endif // not APSTUDIO_INVOKED - +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (U.S.) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +#ifdef _WIN32 +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) +#endif //_WIN32 + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION 5,6,1,0 + PRODUCTVERSION 5,6,1,0 + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x4L + FILETYPE 0x2L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "Comments", "free crypto library, more information available at www.cryptopp.com" + VALUE "CompanyName", "Wei Dai" + VALUE "FileDescription", "Crypto++® Library DLL" + VALUE "FileVersion", "5, 6, 1, 0" + VALUE "InternalName", "cryptopp" + VALUE "LegalCopyright", "Copyright © 1995-2010 by Wei Dai" + VALUE "LegalTrademarks", "Crypto++®" + VALUE "OriginalFilename", "cryptopp.dll" + VALUE "ProductName", "Crypto++® Library" + VALUE "ProductVersion", "5, 6, 1, 0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END + + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + +#endif // English (U.S.) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/cppForSwig/cryptopp/cryptopp.vcxproj b/cppForSwig/cryptopp/cryptopp.vcxproj old mode 100755 new mode 100644 index cb1c5a8e5..7581617a5 --- a/cppForSwig/cryptopp/cryptopp.vcxproj +++ b/cppForSwig/cryptopp/cryptopp.vcxproj @@ -17,6 +17,14 @@ Release x64 + + WinXP_32 + Win32 + + + WinXP_32 + x64 + {B1055DA3-83CE-47BF-98E6-2850E3411D39} @@ -50,6 +58,14 @@ MultiByte v110 + + v100 + StaticLibrary + MultiByte + + + v110 + @@ -86,6 +102,9 @@ false $(SolutionDir)libs\$(Platform)\ + + $(SolutionDir)libs\$(Platform)\Win_XP + @@ -146,7 +165,7 @@ MaxSpeed true true - NDEBUG;_CONSOLE;WIN32;%(PreprocessorDefinitions) + NDEBUG;_CONSOLE;WIN32;%(PreprocessorDefinitions);NDEBUG MultiThreaded cryptopp pch.h @@ -163,6 +182,12 @@ + + + Disabled + MultiThreaded + + diff --git a/cppForSwig/cryptopp/dlltest.dsp b/cppForSwig/cryptopp/dlltest.dsp index b980b7f88..9da1a2f1e 100755 --- a/cppForSwig/cryptopp/dlltest.dsp +++ b/cppForSwig/cryptopp/dlltest.dsp @@ -1,90 +1,90 @@ -# Microsoft Developer Studio Project File - Name="dlltest" - Package Owner=<4> -# Microsoft Developer Studio Generated Build File, Format Version 6.00 -# ** DO NOT EDIT ** - -# TARGTYPE "Win32 (x86) Console Application" 0x0103 - -CFG=dlltest - Win32 Debug -!MESSAGE This is not a valid makefile. To build this project using NMAKE, -!MESSAGE use the Export Makefile command and run -!MESSAGE -!MESSAGE NMAKE /f "dlltest.mak". -!MESSAGE -!MESSAGE You can specify a configuration when running NMAKE -!MESSAGE by defining the macro CFG on the command line. For example: -!MESSAGE -!MESSAGE NMAKE /f "dlltest.mak" CFG="dlltest - Win32 Debug" -!MESSAGE -!MESSAGE Possible choices for configuration are: -!MESSAGE -!MESSAGE "dlltest - Win32 Release" (based on "Win32 (x86) Console Application") -!MESSAGE "dlltest - Win32 Debug" (based on "Win32 (x86) Console Application") -!MESSAGE - -# Begin Project -# PROP AllowPerConfigDependencies 0 -# PROP Scc_ProjName "" -# PROP Scc_LocalPath "" -CPP=cl.exe -RSC=rc.exe - -!IF "$(CFG)" == "dlltest - Win32 Release" - -# PROP BASE Use_MFC 0 -# PROP BASE Use_Debug_Libraries 0 -# PROP BASE Output_Dir "dlltest___Win32_Release" -# PROP BASE Intermediate_Dir "dlltest___Win32_Release" -# PROP BASE Target_Dir "" -# PROP Use_MFC 0 -# PROP Use_Debug_Libraries 0 -# PROP Output_Dir "dlltest___Win32_Release" -# PROP Intermediate_Dir "dlltest___Win32_Release" -# PROP Ignore_Export_Lib 0 -# PROP Target_Dir "" -# ADD BASE CPP /nologo /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_MBCS" /YX /FD /c -# ADD CPP /nologo /Gz /MT /W3 /GR /GX /Zi /O1 /Ob2 /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_MBCS" /D "CRYPTOPP_DLL_ONLY" /YX /FD /c -# ADD BASE RSC /l 0x409 /d "NDEBUG" -# ADD RSC /l 0x409 /d "NDEBUG" -BSC32=bscmake.exe -# ADD BASE BSC32 /nologo -# ADD BSC32 /nologo -LINK32=link.exe -# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:console /machine:I386 -# ADD LINK32 /nologo /subsystem:console /map /debug /machine:I386 /out:"DLL_Release/dlltest.exe" /libpath:"DLL_Release" - -!ELSEIF "$(CFG)" == "dlltest - Win32 Debug" - -# PROP BASE Use_MFC 0 -# PROP BASE Use_Debug_Libraries 1 -# PROP BASE Output_Dir "dlltest___Win32_Debug" -# PROP BASE Intermediate_Dir "dlltest___Win32_Debug" -# PROP BASE Target_Dir "" -# PROP Use_MFC 0 -# PROP Use_Debug_Libraries 1 -# PROP Output_Dir "dlltest___Win32_Debug" -# PROP Intermediate_Dir "dlltest___Win32_Debug" -# PROP Ignore_Export_Lib 0 -# PROP Target_Dir "" -# ADD BASE CPP /nologo /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_CONSOLE" /D "_MBCS" /YX /FD /GZ /c -# ADD CPP /nologo /Gz /MTd /W3 /Gm /GR /GX /Zi /Oi /D "WIN32" /D "_DEBUG" /D "_CONSOLE" /D "_MBCS" /D "CRYPTOPP_DLL_ONLY" /YX /FD /GZ /c -# ADD BASE RSC /l 0x409 /d "_DEBUG" -# ADD RSC /l 0x409 /d "_DEBUG" -BSC32=bscmake.exe -# ADD BASE BSC32 /nologo -# ADD BSC32 /nologo -LINK32=link.exe -# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:console /debug /machine:I386 /pdbtype:sept -# ADD LINK32 /nologo /subsystem:console /debug /machine:I386 /out:"DLL_Debug/dlltest.exe" /pdbtype:sept /libpath:"DLL_Debug" - -!ENDIF - -# Begin Target - -# Name "dlltest - Win32 Release" -# Name "dlltest - Win32 Debug" -# Begin Source File - -SOURCE=.\dlltest.cpp -# End Source File -# End Target -# End Project +# Microsoft Developer Studio Project File - Name="dlltest" - Package Owner=<4> +# Microsoft Developer Studio Generated Build File, Format Version 6.00 +# ** DO NOT EDIT ** + +# TARGTYPE "Win32 (x86) Console Application" 0x0103 + +CFG=dlltest - Win32 Debug +!MESSAGE This is not a valid makefile. To build this project using NMAKE, +!MESSAGE use the Export Makefile command and run +!MESSAGE +!MESSAGE NMAKE /f "dlltest.mak". +!MESSAGE +!MESSAGE You can specify a configuration when running NMAKE +!MESSAGE by defining the macro CFG on the command line. For example: +!MESSAGE +!MESSAGE NMAKE /f "dlltest.mak" CFG="dlltest - Win32 Debug" +!MESSAGE +!MESSAGE Possible choices for configuration are: +!MESSAGE +!MESSAGE "dlltest - Win32 Release" (based on "Win32 (x86) Console Application") +!MESSAGE "dlltest - Win32 Debug" (based on "Win32 (x86) Console Application") +!MESSAGE + +# Begin Project +# PROP AllowPerConfigDependencies 0 +# PROP Scc_ProjName "" +# PROP Scc_LocalPath "" +CPP=cl.exe +RSC=rc.exe + +!IF "$(CFG)" == "dlltest - Win32 Release" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 0 +# PROP BASE Output_Dir "dlltest___Win32_Release" +# PROP BASE Intermediate_Dir "dlltest___Win32_Release" +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 0 +# PROP Output_Dir "dlltest___Win32_Release" +# PROP Intermediate_Dir "dlltest___Win32_Release" +# PROP Ignore_Export_Lib 0 +# PROP Target_Dir "" +# ADD BASE CPP /nologo /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_MBCS" /YX /FD /c +# ADD CPP /nologo /Gz /MT /W3 /GR /GX /Zi /O1 /Ob2 /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_MBCS" /D "CRYPTOPP_DLL_ONLY" /YX /FD /c +# ADD BASE RSC /l 0x409 /d "NDEBUG" +# ADD RSC /l 0x409 /d "NDEBUG" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LINK32=link.exe +# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:console /machine:I386 +# ADD LINK32 /nologo /subsystem:console /map /debug /machine:I386 /out:"DLL_Release/dlltest.exe" /libpath:"DLL_Release" + +!ELSEIF "$(CFG)" == "dlltest - Win32 Debug" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 1 +# PROP BASE Output_Dir "dlltest___Win32_Debug" +# PROP BASE Intermediate_Dir "dlltest___Win32_Debug" +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 1 +# PROP Output_Dir "dlltest___Win32_Debug" +# PROP Intermediate_Dir "dlltest___Win32_Debug" +# PROP Ignore_Export_Lib 0 +# PROP Target_Dir "" +# ADD BASE CPP /nologo /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_CONSOLE" /D "_MBCS" /YX /FD /GZ /c +# ADD CPP /nologo /Gz /MTd /W3 /Gm /GR /GX /Zi /Oi /D "WIN32" /D "_DEBUG" /D "_CONSOLE" /D "_MBCS" /D "CRYPTOPP_DLL_ONLY" /YX /FD /GZ /c +# ADD BASE RSC /l 0x409 /d "_DEBUG" +# ADD RSC /l 0x409 /d "_DEBUG" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LINK32=link.exe +# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:console /debug /machine:I386 /pdbtype:sept +# ADD LINK32 /nologo /subsystem:console /debug /machine:I386 /out:"DLL_Debug/dlltest.exe" /pdbtype:sept /libpath:"DLL_Debug" + +!ENDIF + +# Begin Target + +# Name "dlltest - Win32 Release" +# Name "dlltest - Win32 Debug" +# Begin Source File + +SOURCE=.\dlltest.cpp +# End Source File +# End Target +# End Project diff --git a/cppForSwig/cryptopp/dlltest.vcproj b/cppForSwig/cryptopp/dlltest.vcproj index 1e697d10e..edcf2b96f 100755 --- a/cppForSwig/cryptopp/dlltest.vcproj +++ b/cppForSwig/cryptopp/dlltest.vcproj @@ -1,395 +1,395 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cppForSwig/cryptopp/eax.cpp b/cppForSwig/cryptopp/eax.cpp index cf836632b..2728c9bcd 100755 --- a/cppForSwig/cryptopp/eax.cpp +++ b/cppForSwig/cryptopp/eax.cpp @@ -1,59 +1,59 @@ -// eax.cpp - written and placed in the public domain by Wei Dai - -#include "pch.h" -#include "eax.h" - -NAMESPACE_BEGIN(CryptoPP) - -void EAX_Base::SetKeyWithoutResync(const byte *userKey, size_t keylength, const NameValuePairs ¶ms) -{ - AccessMAC().SetKey(userKey, keylength, params); - m_buffer.New(2*AccessMAC().TagSize()); -} - -void EAX_Base::Resync(const byte *iv, size_t len) -{ - MessageAuthenticationCode &mac = AccessMAC(); - unsigned int blockSize = mac.TagSize(); - - memset(m_buffer, 0, blockSize); - mac.Update(m_buffer, blockSize); - mac.CalculateDigest(m_buffer+blockSize, iv, len); - - m_buffer[blockSize-1] = 1; - mac.Update(m_buffer, blockSize); - - m_ctr.SetCipherWithIV(AccessMAC().AccessCipher(), m_buffer+blockSize, blockSize); -} - -size_t EAX_Base::AuthenticateBlocks(const byte *data, size_t len) -{ - AccessMAC().Update(data, len); - return 0; -} - -void EAX_Base::AuthenticateLastHeaderBlock() -{ - assert(m_bufferedDataLength == 0); - MessageAuthenticationCode &mac = AccessMAC(); - unsigned int blockSize = mac.TagSize(); - - mac.Final(m_buffer); - xorbuf(m_buffer+blockSize, m_buffer, blockSize); - - memset(m_buffer, 0, blockSize); - m_buffer[blockSize-1] = 2; - mac.Update(m_buffer, blockSize); -} - -void EAX_Base::AuthenticateLastFooterBlock(byte *tag, size_t macSize) -{ - assert(m_bufferedDataLength == 0); - MessageAuthenticationCode &mac = AccessMAC(); - unsigned int blockSize = mac.TagSize(); - - mac.TruncatedFinal(m_buffer, macSize); - xorbuf(tag, m_buffer, m_buffer+blockSize, macSize); -} - -NAMESPACE_END +// eax.cpp - written and placed in the public domain by Wei Dai + +#include "pch.h" +#include "eax.h" + +NAMESPACE_BEGIN(CryptoPP) + +void EAX_Base::SetKeyWithoutResync(const byte *userKey, size_t keylength, const NameValuePairs ¶ms) +{ + AccessMAC().SetKey(userKey, keylength, params); + m_buffer.New(2*AccessMAC().TagSize()); +} + +void EAX_Base::Resync(const byte *iv, size_t len) +{ + MessageAuthenticationCode &mac = AccessMAC(); + unsigned int blockSize = mac.TagSize(); + + memset(m_buffer, 0, blockSize); + mac.Update(m_buffer, blockSize); + mac.CalculateDigest(m_buffer+blockSize, iv, len); + + m_buffer[blockSize-1] = 1; + mac.Update(m_buffer, blockSize); + + m_ctr.SetCipherWithIV(AccessMAC().AccessCipher(), m_buffer+blockSize, blockSize); +} + +size_t EAX_Base::AuthenticateBlocks(const byte *data, size_t len) +{ + AccessMAC().Update(data, len); + return 0; +} + +void EAX_Base::AuthenticateLastHeaderBlock() +{ + assert(m_bufferedDataLength == 0); + MessageAuthenticationCode &mac = AccessMAC(); + unsigned int blockSize = mac.TagSize(); + + mac.Final(m_buffer); + xorbuf(m_buffer+blockSize, m_buffer, blockSize); + + memset(m_buffer, 0, blockSize); + m_buffer[blockSize-1] = 2; + mac.Update(m_buffer, blockSize); +} + +void EAX_Base::AuthenticateLastFooterBlock(byte *tag, size_t macSize) +{ + assert(m_bufferedDataLength == 0); + MessageAuthenticationCode &mac = AccessMAC(); + unsigned int blockSize = mac.TagSize(); + + mac.TruncatedFinal(m_buffer, macSize); + xorbuf(tag, m_buffer, m_buffer+blockSize, macSize); +} + +NAMESPACE_END diff --git a/cppForSwig/cryptopp/gcm.cpp b/cppForSwig/cryptopp/gcm.cpp index 237325d9f..2304f96d8 100755 --- a/cppForSwig/cryptopp/gcm.cpp +++ b/cppForSwig/cryptopp/gcm.cpp @@ -1,828 +1,828 @@ -// gcm.cpp - written and placed in the public domain by Wei Dai - -// use "cl /EP /P /DCRYPTOPP_GENERATE_X64_MASM gcm.cpp" to generate MASM code - -#include "pch.h" - -#ifndef CRYPTOPP_IMPORTS -#ifndef CRYPTOPP_GENERATE_X64_MASM - -#include "gcm.h" -#include "cpu.h" - -NAMESPACE_BEGIN(CryptoPP) - -word16 GCM_Base::s_reductionTable[256]; -volatile bool GCM_Base::s_reductionTableInitialized = false; - -void GCM_Base::GCTR::IncrementCounterBy256() -{ - IncrementCounterByOne(m_counterArray+BlockSize()-4, 3); -} - -#if 0 -// preserved for testing -void gcm_gf_mult(const unsigned char *a, const unsigned char *b, unsigned char *c) -{ - word64 Z0=0, Z1=0, V0, V1; - - typedef BlockGetAndPut Block; - Block::Get(a)(V0)(V1); - - for (int i=0; i<16; i++) - { - for (int j=0x80; j!=0; j>>=1) - { - int x = b[i] & j; - Z0 ^= x ? V0 : 0; - Z1 ^= x ? V1 : 0; - x = (int)V1 & 1; - V1 = (V1>>1) | (V0<<63); - V0 = (V0>>1) ^ (x ? W64LIT(0xe1) << 56 : 0); - } - } - Block::Put(NULL, c)(Z0)(Z1); -} - -__m128i _mm_clmulepi64_si128(const __m128i &a, const __m128i &b, int i) -{ - word64 A[1] = {ByteReverse(((word64*)&a)[i&1])}; - word64 B[1] = {ByteReverse(((word64*)&b)[i>>4])}; - - PolynomialMod2 pa((byte *)A, 8); - PolynomialMod2 pb((byte *)B, 8); - PolynomialMod2 c = pa*pb; - - __m128i output; - for (int i=0; i<16; i++) - ((byte *)&output)[i] = c.GetByte(i); - return output; -} -#endif - -#if CRYPTOPP_BOOL_SSE2_INTRINSICS_AVAILABLE || CRYPTOPP_BOOL_SSE2_ASM_AVAILABLE -inline static void SSE2_Xor16(byte *a, const byte *b, const byte *c) -{ -#if CRYPTOPP_BOOL_SSE2_INTRINSICS_AVAILABLE - *(__m128i *)a = _mm_xor_si128(*(__m128i *)b, *(__m128i *)c); -#else - asm ("movdqa %1, %%xmm0; pxor %2, %%xmm0; movdqa %%xmm0, %0;" : "=m" (a[0]) : "m"(b[0]), "m"(c[0])); -#endif -} -#endif - -inline static void Xor16(byte *a, const byte *b, const byte *c) -{ - ((word64 *)a)[0] = ((word64 *)b)[0] ^ ((word64 *)c)[0]; - ((word64 *)a)[1] = ((word64 *)b)[1] ^ ((word64 *)c)[1]; -} - -#if CRYPTOPP_BOOL_AESNI_INTRINSICS_AVAILABLE -static CRYPTOPP_ALIGN_DATA(16) const word64 s_clmulConstants64[] = { - W64LIT(0xe100000000000000), W64LIT(0xc200000000000000), - W64LIT(0x08090a0b0c0d0e0f), W64LIT(0x0001020304050607), - W64LIT(0x0001020304050607), W64LIT(0x08090a0b0c0d0e0f)}; -static const __m128i *s_clmulConstants = (const __m128i *)s_clmulConstants64; -static const unsigned int s_clmulTableSizeInBlocks = 8; - -inline __m128i CLMUL_Reduce(__m128i c0, __m128i c1, __m128i c2, const __m128i &r) -{ - /* - The polynomial to be reduced is c0 * x^128 + c1 * x^64 + c2. c0t below refers to the most - significant half of c0 as a polynomial, which, due to GCM's bit reflection, are in the - rightmost bit positions, and the lowest byte addresses. - - c1 ^= c0t * 0xc200000000000000 - c2t ^= c0t - t = shift (c1t ^ c0b) left 1 bit - c2 ^= t * 0xe100000000000000 - c2t ^= c1b - shift c2 left 1 bit and xor in lowest bit of c1t - */ -#if 0 // MSVC 2010 workaround: see http://connect.microsoft.com/VisualStudio/feedback/details/575301 - c2 = _mm_xor_si128(c2, _mm_move_epi64(c0)); -#else - c1 = _mm_xor_si128(c1, _mm_slli_si128(c0, 8)); -#endif - c1 = _mm_xor_si128(c1, _mm_clmulepi64_si128(c0, r, 0x10)); - c0 = _mm_srli_si128(c0, 8); - c0 = _mm_xor_si128(c0, c1); - c0 = _mm_slli_epi64(c0, 1); - c0 = _mm_clmulepi64_si128(c0, r, 0); - c2 = _mm_xor_si128(c2, c0); - c2 = _mm_xor_si128(c2, _mm_srli_si128(c1, 8)); - c1 = _mm_unpacklo_epi64(c1, c2); - c1 = _mm_srli_epi64(c1, 63); - c2 = _mm_slli_epi64(c2, 1); - return _mm_xor_si128(c2, c1); -} - -inline __m128i CLMUL_GF_Mul(const __m128i &x, const __m128i &h, const __m128i &r) -{ - __m128i c0 = _mm_clmulepi64_si128(x,h,0); - __m128i c1 = _mm_xor_si128(_mm_clmulepi64_si128(x,h,1), _mm_clmulepi64_si128(x,h,0x10)); - __m128i c2 = _mm_clmulepi64_si128(x,h,0x11); - - return CLMUL_Reduce(c0, c1, c2, r); -} -#endif - -void GCM_Base::SetKeyWithoutResync(const byte *userKey, size_t keylength, const NameValuePairs ¶ms) -{ - BlockCipher &blockCipher = AccessBlockCipher(); - blockCipher.SetKey(userKey, keylength, params); - - if (blockCipher.BlockSize() != REQUIRED_BLOCKSIZE) - throw InvalidArgument(AlgorithmName() + ": block size of underlying block cipher is not 16"); - - int tableSize, i, j, k; - -#if CRYPTOPP_BOOL_AESNI_INTRINSICS_AVAILABLE - if (HasCLMUL()) - { - params.GetIntValue(Name::TableSize(), tableSize); // avoid "parameter not used" error - tableSize = s_clmulTableSizeInBlocks * REQUIRED_BLOCKSIZE; - } - else -#endif - { - if (params.GetIntValue(Name::TableSize(), tableSize)) - tableSize = (tableSize >= 64*1024) ? 64*1024 : 2*1024; - else - tableSize = (GetTablesOption() == GCM_64K_Tables) ? 64*1024 : 2*1024; - -#if defined(_MSC_VER) && (_MSC_VER >= 1300 && _MSC_VER < 1400) - // VC 2003 workaround: compiler generates bad code for 64K tables - tableSize = 2*1024; -#endif - } - - m_buffer.resize(3*REQUIRED_BLOCKSIZE + tableSize); - byte *table = MulTable(); - byte *hashKey = HashKey(); - memset(hashKey, 0, REQUIRED_BLOCKSIZE); - blockCipher.ProcessBlock(hashKey); - -#if CRYPTOPP_BOOL_AESNI_INTRINSICS_AVAILABLE - if (HasCLMUL()) - { - const __m128i r = s_clmulConstants[0]; - __m128i h0 = _mm_shuffle_epi8(_mm_load_si128((__m128i *)hashKey), s_clmulConstants[1]); - __m128i h = h0; - - for (i=0; i Block; - Block::Get(hashKey)(V0)(V1); - - if (tableSize == 64*1024) - { - for (i=0; i<128; i++) - { - k = i%8; - Block::Put(NULL, table+(i/8)*256*16+(size_t(1)<<(11-k)))(V0)(V1); - - int x = (int)V1 & 1; - V1 = (V1>>1) | (V0<<63); - V0 = (V0>>1) ^ (x ? W64LIT(0xe1) << 56 : 0); - } - - for (i=0; i<16; i++) - { - memset(table+i*256*16, 0, 16); -#if CRYPTOPP_BOOL_SSE2_INTRINSICS_AVAILABLE || CRYPTOPP_BOOL_SSE2_ASM_AVAILABLE - if (HasSSE2()) - for (j=2; j<=0x80; j*=2) - for (k=1; k>1) | (V0<<63); - V0 = (V0>>1) ^ (x ? W64LIT(0xe1) << 56 : 0); - } - - for (i=0; i<4; i++) - { - memset(table+i*256, 0, 16); - memset(table+1024+i*256, 0, 16); -#if CRYPTOPP_BOOL_SSE2_INTRINSICS_AVAILABLE || CRYPTOPP_BOOL_SSE2_ASM_AVAILABLE - if (HasSSE2()) - for (j=2; j<=8; j*=2) - for (k=1; k= HASH_BLOCKSIZE) - { - len = GCM_Base::AuthenticateBlocks(iv, len); - iv += (origLen - len); - } - - if (len > 0) - { - memcpy(m_buffer, iv, len); - memset(m_buffer+len, 0, HASH_BLOCKSIZE-len); - GCM_Base::AuthenticateBlocks(m_buffer, HASH_BLOCKSIZE); - } - - PutBlock(NULL, m_buffer)(0)(origLen*8); - GCM_Base::AuthenticateBlocks(m_buffer, HASH_BLOCKSIZE); - - ReverseHashBufferIfNeeded(); - } - - if (m_state >= State_IVSet) - m_ctr.Resynchronize(hashBuffer, REQUIRED_BLOCKSIZE); - else - m_ctr.SetCipherWithIV(cipher, hashBuffer); - - m_ctr.Seek(HASH_BLOCKSIZE); - - memset(hashBuffer, 0, HASH_BLOCKSIZE); -} - -unsigned int GCM_Base::OptimalDataAlignment() const -{ - return -#if CRYPTOPP_BOOL_SSE2_ASM_AVAILABLE || defined(CRYPTOPP_X64_MASM_AVAILABLE) - HasSSE2() ? 16 : -#endif - GetBlockCipher().OptimalDataAlignment(); -} - -#pragma warning(disable: 4731) // frame pointer register 'ebp' modified by inline assembly code - -#endif // #ifndef CRYPTOPP_GENERATE_X64_MASM - -#ifdef CRYPTOPP_X64_MASM_AVAILABLE -extern "C" { -void GCM_AuthenticateBlocks_2K(const byte *data, size_t blocks, word64 *hashBuffer, const word16 *reductionTable); -void GCM_AuthenticateBlocks_64K(const byte *data, size_t blocks, word64 *hashBuffer); -} -#endif - -#ifndef CRYPTOPP_GENERATE_X64_MASM - -size_t GCM_Base::AuthenticateBlocks(const byte *data, size_t len) -{ -#if CRYPTOPP_BOOL_AESNI_INTRINSICS_AVAILABLE - if (HasCLMUL()) - { - const __m128i *table = (const __m128i *)MulTable(); - __m128i x = _mm_load_si128((__m128i *)HashBuffer()); - const __m128i r = s_clmulConstants[0], bswapMask = s_clmulConstants[1], bswapMask2 = s_clmulConstants[2]; - - while (len >= 16) - { - size_t s = UnsignedMin(len/16, s_clmulTableSizeInBlocks), i=0; - __m128i d, d2 = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i *)(data+(s-1)*16)), bswapMask2);; - __m128i c0 = _mm_setzero_si128(); - __m128i c1 = _mm_setzero_si128(); - __m128i c2 = _mm_setzero_si128(); - - while (true) - { - __m128i h0 = _mm_load_si128(table+i); - __m128i h1 = _mm_load_si128(table+i+1); - __m128i h01 = _mm_xor_si128(h0, h1); - - if (++i == s) - { - d = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i *)data), bswapMask); - d = _mm_xor_si128(d, x); - c0 = _mm_xor_si128(c0, _mm_clmulepi64_si128(d, h0, 0)); - c2 = _mm_xor_si128(c2, _mm_clmulepi64_si128(d, h1, 1)); - d = _mm_xor_si128(d, _mm_shuffle_epi32(d, _MM_SHUFFLE(1, 0, 3, 2))); - c1 = _mm_xor_si128(c1, _mm_clmulepi64_si128(d, h01, 0)); - break; - } - - d = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i *)(data+(s-i)*16-8)), bswapMask2); - c0 = _mm_xor_si128(c0, _mm_clmulepi64_si128(d2, h0, 1)); - c2 = _mm_xor_si128(c2, _mm_clmulepi64_si128(d, h1, 1)); - d2 = _mm_xor_si128(d2, d); - c1 = _mm_xor_si128(c1, _mm_clmulepi64_si128(d2, h01, 1)); - - if (++i == s) - { - d = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i *)data), bswapMask); - d = _mm_xor_si128(d, x); - c0 = _mm_xor_si128(c0, _mm_clmulepi64_si128(d, h0, 0x10)); - c2 = _mm_xor_si128(c2, _mm_clmulepi64_si128(d, h1, 0x11)); - d = _mm_xor_si128(d, _mm_shuffle_epi32(d, _MM_SHUFFLE(1, 0, 3, 2))); - c1 = _mm_xor_si128(c1, _mm_clmulepi64_si128(d, h01, 0x10)); - break; - } - - d2 = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i *)(data+(s-i)*16-8)), bswapMask); - c0 = _mm_xor_si128(c0, _mm_clmulepi64_si128(d, h0, 0x10)); - c2 = _mm_xor_si128(c2, _mm_clmulepi64_si128(d2, h1, 0x10)); - d = _mm_xor_si128(d, d2); - c1 = _mm_xor_si128(c1, _mm_clmulepi64_si128(d, h01, 0x10)); - } - data += s*16; - len -= s*16; - - c1 = _mm_xor_si128(_mm_xor_si128(c1, c0), c2); - x = CLMUL_Reduce(c0, c1, c2, r); - } - - _mm_store_si128((__m128i *)HashBuffer(), x); - return len; - } -#endif - - typedef BlockGetAndPut Block; - word64 *hashBuffer = (word64 *)HashBuffer(); - - switch (2*(m_buffer.size()>=64*1024) -#if CRYPTOPP_BOOL_SSE2_ASM_AVAILABLE || defined(CRYPTOPP_X64_MASM_AVAILABLE) - + HasSSE2() -#endif - ) - { - case 0: // non-SSE2 and 2K tables - { - byte *table = MulTable(); - word64 x0 = hashBuffer[0], x1 = hashBuffer[1]; - - do - { - word64 y0, y1, a0, a1, b0, b1, c0, c1, d0, d1; - Block::Get(data)(y0)(y1); - x0 ^= y0; - x1 ^= y1; - - data += HASH_BLOCKSIZE; - len -= HASH_BLOCKSIZE; - - #define READ_TABLE_WORD64_COMMON(a, b, c, d) *(word64 *)(table+(a*1024)+(b*256)+c+d*8) - - #ifdef IS_LITTLE_ENDIAN - #if CRYPTOPP_BOOL_SLOW_WORD64 - word32 z0 = (word32)x0; - word32 z1 = (word32)(x0>>32); - word32 z2 = (word32)x1; - word32 z3 = (word32)(x1>>32); - #define READ_TABLE_WORD64(a, b, c, d, e) READ_TABLE_WORD64_COMMON((d%2), c, (d?(z##c>>((d?d-1:0)*4))&0xf0:(z##c&0xf)<<4), e) - #else - #define READ_TABLE_WORD64(a, b, c, d, e) READ_TABLE_WORD64_COMMON((d%2), c, ((d+8*b)?(x##a>>(((d+8*b)?(d+8*b)-1:1)*4))&0xf0:(x##a&0xf)<<4), e) - #endif - #define GF_MOST_SIG_8BITS(a) (a##1 >> 7*8) - #define GF_SHIFT_8(a) a##1 = (a##1 << 8) ^ (a##0 >> 7*8); a##0 <<= 8; - #else - #define READ_TABLE_WORD64(a, b, c, d, e) READ_TABLE_WORD64_COMMON((1-d%2), c, ((15-d-8*b)?(x##a>>(((15-d-8*b)?(15-d-8*b)-1:0)*4))&0xf0:(x##a&0xf)<<4), e) - #define GF_MOST_SIG_8BITS(a) (a##1 & 0xff) - #define GF_SHIFT_8(a) a##1 = (a##1 >> 8) ^ (a##0 << 7*8); a##0 >>= 8; - #endif - - #define GF_MUL_32BY128(op, a, b, c) \ - a0 op READ_TABLE_WORD64(a, b, c, 0, 0) ^ READ_TABLE_WORD64(a, b, c, 1, 0);\ - a1 op READ_TABLE_WORD64(a, b, c, 0, 1) ^ READ_TABLE_WORD64(a, b, c, 1, 1);\ - b0 op READ_TABLE_WORD64(a, b, c, 2, 0) ^ READ_TABLE_WORD64(a, b, c, 3, 0);\ - b1 op READ_TABLE_WORD64(a, b, c, 2, 1) ^ READ_TABLE_WORD64(a, b, c, 3, 1);\ - c0 op READ_TABLE_WORD64(a, b, c, 4, 0) ^ READ_TABLE_WORD64(a, b, c, 5, 0);\ - c1 op READ_TABLE_WORD64(a, b, c, 4, 1) ^ READ_TABLE_WORD64(a, b, c, 5, 1);\ - d0 op READ_TABLE_WORD64(a, b, c, 6, 0) ^ READ_TABLE_WORD64(a, b, c, 7, 0);\ - d1 op READ_TABLE_WORD64(a, b, c, 6, 1) ^ READ_TABLE_WORD64(a, b, c, 7, 1);\ - - GF_MUL_32BY128(=, 0, 0, 0) - GF_MUL_32BY128(^=, 0, 1, 1) - GF_MUL_32BY128(^=, 1, 0, 2) - GF_MUL_32BY128(^=, 1, 1, 3) - - word32 r = (word32)s_reductionTable[GF_MOST_SIG_8BITS(d)] << 16; - GF_SHIFT_8(d) - c0 ^= d0; c1 ^= d1; - r ^= (word32)s_reductionTable[GF_MOST_SIG_8BITS(c)] << 8; - GF_SHIFT_8(c) - b0 ^= c0; b1 ^= c1; - r ^= s_reductionTable[GF_MOST_SIG_8BITS(b)]; - GF_SHIFT_8(b) - a0 ^= b0; a1 ^= b1; - a0 ^= ConditionalByteReverse(LITTLE_ENDIAN_ORDER, r); - x0 = a0; x1 = a1; - } - while (len >= HASH_BLOCKSIZE); - - hashBuffer[0] = x0; hashBuffer[1] = x1; - return len; - } - - case 2: // non-SSE2 and 64K tables - { - byte *table = MulTable(); - word64 x0 = hashBuffer[0], x1 = hashBuffer[1]; - - do - { - word64 y0, y1, a0, a1; - Block::Get(data)(y0)(y1); - x0 ^= y0; - x1 ^= y1; - - data += HASH_BLOCKSIZE; - len -= HASH_BLOCKSIZE; - - #undef READ_TABLE_WORD64_COMMON - #undef READ_TABLE_WORD64 - - #define READ_TABLE_WORD64_COMMON(a, c, d) *(word64 *)(table+(a)*256*16+(c)+(d)*8) - - #ifdef IS_LITTLE_ENDIAN - #if CRYPTOPP_BOOL_SLOW_WORD64 - word32 z0 = (word32)x0; - word32 z1 = (word32)(x0>>32); - word32 z2 = (word32)x1; - word32 z3 = (word32)(x1>>32); - #define READ_TABLE_WORD64(b, c, d, e) READ_TABLE_WORD64_COMMON(c*4+d, (d?(z##c>>((d?d:1)*8-4))&0xff0:(z##c&0xff)<<4), e) - #else - #define READ_TABLE_WORD64(b, c, d, e) READ_TABLE_WORD64_COMMON(c*4+d, ((d+4*(c%2))?(x##b>>(((d+4*(c%2))?(d+4*(c%2)):1)*8-4))&0xff0:(x##b&0xff)<<4), e) - #endif - #else - #define READ_TABLE_WORD64(b, c, d, e) READ_TABLE_WORD64_COMMON(c*4+d, ((7-d-4*(c%2))?(x##b>>(((7-d-4*(c%2))?(7-d-4*(c%2)):1)*8-4))&0xff0:(x##b&0xff)<<4), e) - #endif - - #define GF_MUL_8BY128(op, b, c, d) \ - a0 op READ_TABLE_WORD64(b, c, d, 0);\ - a1 op READ_TABLE_WORD64(b, c, d, 1);\ - - GF_MUL_8BY128(=, 0, 0, 0) - GF_MUL_8BY128(^=, 0, 0, 1) - GF_MUL_8BY128(^=, 0, 0, 2) - GF_MUL_8BY128(^=, 0, 0, 3) - GF_MUL_8BY128(^=, 0, 1, 0) - GF_MUL_8BY128(^=, 0, 1, 1) - GF_MUL_8BY128(^=, 0, 1, 2) - GF_MUL_8BY128(^=, 0, 1, 3) - GF_MUL_8BY128(^=, 1, 2, 0) - GF_MUL_8BY128(^=, 1, 2, 1) - GF_MUL_8BY128(^=, 1, 2, 2) - GF_MUL_8BY128(^=, 1, 2, 3) - GF_MUL_8BY128(^=, 1, 3, 0) - GF_MUL_8BY128(^=, 1, 3, 1) - GF_MUL_8BY128(^=, 1, 3, 2) - GF_MUL_8BY128(^=, 1, 3, 3) - - x0 = a0; x1 = a1; - } - while (len >= HASH_BLOCKSIZE); - - hashBuffer[0] = x0; hashBuffer[1] = x1; - return len; - } -#endif // #ifndef CRYPTOPP_GENERATE_X64_MASM - -#ifdef CRYPTOPP_X64_MASM_AVAILABLE - case 1: // SSE2 and 2K tables - GCM_AuthenticateBlocks_2K(data, len/16, hashBuffer, s_reductionTable); - return len % 16; - case 3: // SSE2 and 64K tables - GCM_AuthenticateBlocks_64K(data, len/16, hashBuffer); - return len % 16; -#endif - -#if CRYPTOPP_BOOL_SSE2_ASM_AVAILABLE - case 1: // SSE2 and 2K tables - { - #ifdef __GNUC__ - __asm__ __volatile__ - ( - ".intel_syntax noprefix;" - #elif defined(CRYPTOPP_GENERATE_X64_MASM) - ALIGN 8 - GCM_AuthenticateBlocks_2K PROC FRAME - rex_push_reg rsi - push_reg rdi - push_reg rbx - .endprolog - mov rsi, r8 - mov r11, r9 - #else - AS2( mov WORD_REG(cx), data ) - AS2( mov WORD_REG(dx), len ) - AS2( mov WORD_REG(si), hashBuffer ) - AS2( shr WORD_REG(dx), 4 ) - #endif - - AS_PUSH_IF86( bx) - AS_PUSH_IF86( bp) - - #ifdef __GNUC__ - AS2( mov AS_REG_7, WORD_REG(di)) - #elif CRYPTOPP_BOOL_X86 - AS2( lea AS_REG_7, s_reductionTable) - #endif - - AS2( movdqa xmm0, [WORD_REG(si)] ) - - #define MUL_TABLE_0 WORD_REG(si) + 32 - #define MUL_TABLE_1 WORD_REG(si) + 32 + 1024 - #define RED_TABLE AS_REG_7 - - ASL(0) - AS2( movdqu xmm4, [WORD_REG(cx)] ) - AS2( pxor xmm0, xmm4 ) - - AS2( movd ebx, xmm0 ) - AS2( mov eax, AS_HEX(f0f0f0f0) ) - AS2( and eax, ebx ) - AS2( shl ebx, 4 ) - AS2( and ebx, AS_HEX(f0f0f0f0) ) - AS2( movzx edi, ah ) - AS2( movdqa xmm5, XMMWORD_PTR [MUL_TABLE_1 + WORD_REG(di)] ) - AS2( movzx edi, al ) - AS2( movdqa xmm4, XMMWORD_PTR [MUL_TABLE_1 + WORD_REG(di)] ) - AS2( shr eax, 16 ) - AS2( movzx edi, ah ) - AS2( movdqa xmm3, XMMWORD_PTR [MUL_TABLE_1 + WORD_REG(di)] ) - AS2( movzx edi, al ) - AS2( movdqa xmm2, XMMWORD_PTR [MUL_TABLE_1 + WORD_REG(di)] ) - - #define SSE2_MUL_32BITS(i) \ - AS2( psrldq xmm0, 4 )\ - AS2( movd eax, xmm0 )\ - AS2( and eax, AS_HEX(f0f0f0f0) )\ - AS2( movzx edi, bh )\ - AS2( pxor xmm5, XMMWORD_PTR [MUL_TABLE_0 + (i-1)*256 + WORD_REG(di)] )\ - AS2( movzx edi, bl )\ - AS2( pxor xmm4, XMMWORD_PTR [MUL_TABLE_0 + (i-1)*256 + WORD_REG(di)] )\ - AS2( shr ebx, 16 )\ - AS2( movzx edi, bh )\ - AS2( pxor xmm3, XMMWORD_PTR [MUL_TABLE_0 + (i-1)*256 + WORD_REG(di)] )\ - AS2( movzx edi, bl )\ - AS2( pxor xmm2, XMMWORD_PTR [MUL_TABLE_0 + (i-1)*256 + WORD_REG(di)] )\ - AS2( movd ebx, xmm0 )\ - AS2( shl ebx, 4 )\ - AS2( and ebx, AS_HEX(f0f0f0f0) )\ - AS2( movzx edi, ah )\ - AS2( pxor xmm5, XMMWORD_PTR [MUL_TABLE_1 + i*256 + WORD_REG(di)] )\ - AS2( movzx edi, al )\ - AS2( pxor xmm4, XMMWORD_PTR [MUL_TABLE_1 + i*256 + WORD_REG(di)] )\ - AS2( shr eax, 16 )\ - AS2( movzx edi, ah )\ - AS2( pxor xmm3, XMMWORD_PTR [MUL_TABLE_1 + i*256 + WORD_REG(di)] )\ - AS2( movzx edi, al )\ - AS2( pxor xmm2, XMMWORD_PTR [MUL_TABLE_1 + i*256 + WORD_REG(di)] )\ - - SSE2_MUL_32BITS(1) - SSE2_MUL_32BITS(2) - SSE2_MUL_32BITS(3) - - AS2( movzx edi, bh ) - AS2( pxor xmm5, XMMWORD_PTR [MUL_TABLE_0 + 3*256 + WORD_REG(di)] ) - AS2( movzx edi, bl ) - AS2( pxor xmm4, XMMWORD_PTR [MUL_TABLE_0 + 3*256 + WORD_REG(di)] ) - AS2( shr ebx, 16 ) - AS2( movzx edi, bh ) - AS2( pxor xmm3, XMMWORD_PTR [MUL_TABLE_0 + 3*256 + WORD_REG(di)] ) - AS2( movzx edi, bl ) - AS2( pxor xmm2, XMMWORD_PTR [MUL_TABLE_0 + 3*256 + WORD_REG(di)] ) - - AS2( movdqa xmm0, xmm3 ) - AS2( pslldq xmm3, 1 ) - AS2( pxor xmm2, xmm3 ) - AS2( movdqa xmm1, xmm2 ) - AS2( pslldq xmm2, 1 ) - AS2( pxor xmm5, xmm2 ) - - AS2( psrldq xmm0, 15 ) - AS2( movd WORD_REG(di), xmm0 ) - AS2( movzx eax, WORD PTR [RED_TABLE + WORD_REG(di)*2] ) - AS2( shl eax, 8 ) - - AS2( movdqa xmm0, xmm5 ) - AS2( pslldq xmm5, 1 ) - AS2( pxor xmm4, xmm5 ) - - AS2( psrldq xmm1, 15 ) - AS2( movd WORD_REG(di), xmm1 ) - AS2( xor ax, WORD PTR [RED_TABLE + WORD_REG(di)*2] ) - AS2( shl eax, 8 ) - - AS2( psrldq xmm0, 15 ) - AS2( movd WORD_REG(di), xmm0 ) - AS2( xor ax, WORD PTR [RED_TABLE + WORD_REG(di)*2] ) - - AS2( movd xmm0, eax ) - AS2( pxor xmm0, xmm4 ) - - AS2( add WORD_REG(cx), 16 ) - AS2( sub WORD_REG(dx), 1 ) - ASJ( jnz, 0, b ) - AS2( movdqa [WORD_REG(si)], xmm0 ) - - AS_POP_IF86( bp) - AS_POP_IF86( bx) - - #ifdef __GNUC__ - ".att_syntax prefix;" - : - : "c" (data), "d" (len/16), "S" (hashBuffer), "D" (s_reductionTable) - : "memory", "cc", "%eax" - #if CRYPTOPP_BOOL_X64 - , "%ebx", "%r11" - #endif - ); - #elif defined(CRYPTOPP_GENERATE_X64_MASM) - pop rbx - pop rdi - pop rsi - ret - GCM_AuthenticateBlocks_2K ENDP - #endif - - return len%16; - } - case 3: // SSE2 and 64K tables - { - #ifdef __GNUC__ - __asm__ __volatile__ - ( - ".intel_syntax noprefix;" - #elif defined(CRYPTOPP_GENERATE_X64_MASM) - ALIGN 8 - GCM_AuthenticateBlocks_64K PROC FRAME - rex_push_reg rsi - push_reg rdi - .endprolog - mov rsi, r8 - #else - AS2( mov WORD_REG(cx), data ) - AS2( mov WORD_REG(dx), len ) - AS2( mov WORD_REG(si), hashBuffer ) - AS2( shr WORD_REG(dx), 4 ) - #endif - - AS2( movdqa xmm0, [WORD_REG(si)] ) - - #undef MUL_TABLE - #define MUL_TABLE(i,j) WORD_REG(si) + 32 + (i*4+j)*256*16 - - ASL(1) - AS2( movdqu xmm1, [WORD_REG(cx)] ) - AS2( pxor xmm1, xmm0 ) - AS2( pxor xmm0, xmm0 ) - - #undef SSE2_MUL_32BITS - #define SSE2_MUL_32BITS(i) \ - AS2( movd eax, xmm1 )\ - AS2( psrldq xmm1, 4 )\ - AS2( movzx edi, al )\ - AS2( add WORD_REG(di), WORD_REG(di) )\ - AS2( pxor xmm0, [MUL_TABLE(i,0) + WORD_REG(di)*8] )\ - AS2( movzx edi, ah )\ - AS2( add WORD_REG(di), WORD_REG(di) )\ - AS2( pxor xmm0, [MUL_TABLE(i,1) + WORD_REG(di)*8] )\ - AS2( shr eax, 16 )\ - AS2( movzx edi, al )\ - AS2( add WORD_REG(di), WORD_REG(di) )\ - AS2( pxor xmm0, [MUL_TABLE(i,2) + WORD_REG(di)*8] )\ - AS2( movzx edi, ah )\ - AS2( add WORD_REG(di), WORD_REG(di) )\ - AS2( pxor xmm0, [MUL_TABLE(i,3) + WORD_REG(di)*8] )\ - - SSE2_MUL_32BITS(0) - SSE2_MUL_32BITS(1) - SSE2_MUL_32BITS(2) - SSE2_MUL_32BITS(3) - - AS2( add WORD_REG(cx), 16 ) - AS2( sub WORD_REG(dx), 1 ) - ASJ( jnz, 1, b ) - AS2( movdqa [WORD_REG(si)], xmm0 ) - - #ifdef __GNUC__ - ".att_syntax prefix;" - : - : "c" (data), "d" (len/16), "S" (hashBuffer) - : "memory", "cc", "%edi", "%eax" - ); - #elif defined(CRYPTOPP_GENERATE_X64_MASM) - pop rdi - pop rsi - ret - GCM_AuthenticateBlocks_64K ENDP - #endif - - return len%16; - } -#endif -#ifndef CRYPTOPP_GENERATE_X64_MASM - } - - return len%16; -} - -void GCM_Base::AuthenticateLastHeaderBlock() -{ - if (m_bufferedDataLength > 0) - { - memset(m_buffer+m_bufferedDataLength, 0, HASH_BLOCKSIZE-m_bufferedDataLength); - m_bufferedDataLength = 0; - GCM_Base::AuthenticateBlocks(m_buffer, HASH_BLOCKSIZE); - } -} - -void GCM_Base::AuthenticateLastConfidentialBlock() -{ - GCM_Base::AuthenticateLastHeaderBlock(); - PutBlock(NULL, m_buffer)(m_totalHeaderLength*8)(m_totalMessageLength*8); - GCM_Base::AuthenticateBlocks(m_buffer, HASH_BLOCKSIZE); -} - -void GCM_Base::AuthenticateLastFooterBlock(byte *mac, size_t macSize) -{ - m_ctr.Seek(0); - ReverseHashBufferIfNeeded(); - m_ctr.ProcessData(mac, HashBuffer(), macSize); -} - -NAMESPACE_END - -#endif // #ifndef CRYPTOPP_GENERATE_X64_MASM -#endif +// gcm.cpp - written and placed in the public domain by Wei Dai + +// use "cl /EP /P /DCRYPTOPP_GENERATE_X64_MASM gcm.cpp" to generate MASM code + +#include "pch.h" + +#ifndef CRYPTOPP_IMPORTS +#ifndef CRYPTOPP_GENERATE_X64_MASM + +#include "gcm.h" +#include "cpu.h" + +NAMESPACE_BEGIN(CryptoPP) + +word16 GCM_Base::s_reductionTable[256]; +volatile bool GCM_Base::s_reductionTableInitialized = false; + +void GCM_Base::GCTR::IncrementCounterBy256() +{ + IncrementCounterByOne(m_counterArray+BlockSize()-4, 3); +} + +#if 0 +// preserved for testing +void gcm_gf_mult(const unsigned char *a, const unsigned char *b, unsigned char *c) +{ + word64 Z0=0, Z1=0, V0, V1; + + typedef BlockGetAndPut Block; + Block::Get(a)(V0)(V1); + + for (int i=0; i<16; i++) + { + for (int j=0x80; j!=0; j>>=1) + { + int x = b[i] & j; + Z0 ^= x ? V0 : 0; + Z1 ^= x ? V1 : 0; + x = (int)V1 & 1; + V1 = (V1>>1) | (V0<<63); + V0 = (V0>>1) ^ (x ? W64LIT(0xe1) << 56 : 0); + } + } + Block::Put(NULL, c)(Z0)(Z1); +} + +__m128i _mm_clmulepi64_si128(const __m128i &a, const __m128i &b, int i) +{ + word64 A[1] = {ByteReverse(((word64*)&a)[i&1])}; + word64 B[1] = {ByteReverse(((word64*)&b)[i>>4])}; + + PolynomialMod2 pa((byte *)A, 8); + PolynomialMod2 pb((byte *)B, 8); + PolynomialMod2 c = pa*pb; + + __m128i output; + for (int i=0; i<16; i++) + ((byte *)&output)[i] = c.GetByte(i); + return output; +} +#endif + +#if CRYPTOPP_BOOL_SSE2_INTRINSICS_AVAILABLE || CRYPTOPP_BOOL_SSE2_ASM_AVAILABLE +inline static void SSE2_Xor16(byte *a, const byte *b, const byte *c) +{ +#if CRYPTOPP_BOOL_SSE2_INTRINSICS_AVAILABLE + *(__m128i *)a = _mm_xor_si128(*(__m128i *)b, *(__m128i *)c); +#else + asm ("movdqa %1, %%xmm0; pxor %2, %%xmm0; movdqa %%xmm0, %0;" : "=m" (a[0]) : "m"(b[0]), "m"(c[0])); +#endif +} +#endif + +inline static void Xor16(byte *a, const byte *b, const byte *c) +{ + ((word64 *)a)[0] = ((word64 *)b)[0] ^ ((word64 *)c)[0]; + ((word64 *)a)[1] = ((word64 *)b)[1] ^ ((word64 *)c)[1]; +} + +#if CRYPTOPP_BOOL_AESNI_INTRINSICS_AVAILABLE +static CRYPTOPP_ALIGN_DATA(16) const word64 s_clmulConstants64[] = { + W64LIT(0xe100000000000000), W64LIT(0xc200000000000000), + W64LIT(0x08090a0b0c0d0e0f), W64LIT(0x0001020304050607), + W64LIT(0x0001020304050607), W64LIT(0x08090a0b0c0d0e0f)}; +static const __m128i *s_clmulConstants = (const __m128i *)s_clmulConstants64; +static const unsigned int s_clmulTableSizeInBlocks = 8; + +inline __m128i CLMUL_Reduce(__m128i c0, __m128i c1, __m128i c2, const __m128i &r) +{ + /* + The polynomial to be reduced is c0 * x^128 + c1 * x^64 + c2. c0t below refers to the most + significant half of c0 as a polynomial, which, due to GCM's bit reflection, are in the + rightmost bit positions, and the lowest byte addresses. + + c1 ^= c0t * 0xc200000000000000 + c2t ^= c0t + t = shift (c1t ^ c0b) left 1 bit + c2 ^= t * 0xe100000000000000 + c2t ^= c1b + shift c2 left 1 bit and xor in lowest bit of c1t + */ +#if 0 // MSVC 2010 workaround: see http://connect.microsoft.com/VisualStudio/feedback/details/575301 + c2 = _mm_xor_si128(c2, _mm_move_epi64(c0)); +#else + c1 = _mm_xor_si128(c1, _mm_slli_si128(c0, 8)); +#endif + c1 = _mm_xor_si128(c1, _mm_clmulepi64_si128(c0, r, 0x10)); + c0 = _mm_srli_si128(c0, 8); + c0 = _mm_xor_si128(c0, c1); + c0 = _mm_slli_epi64(c0, 1); + c0 = _mm_clmulepi64_si128(c0, r, 0); + c2 = _mm_xor_si128(c2, c0); + c2 = _mm_xor_si128(c2, _mm_srli_si128(c1, 8)); + c1 = _mm_unpacklo_epi64(c1, c2); + c1 = _mm_srli_epi64(c1, 63); + c2 = _mm_slli_epi64(c2, 1); + return _mm_xor_si128(c2, c1); +} + +inline __m128i CLMUL_GF_Mul(const __m128i &x, const __m128i &h, const __m128i &r) +{ + __m128i c0 = _mm_clmulepi64_si128(x,h,0); + __m128i c1 = _mm_xor_si128(_mm_clmulepi64_si128(x,h,1), _mm_clmulepi64_si128(x,h,0x10)); + __m128i c2 = _mm_clmulepi64_si128(x,h,0x11); + + return CLMUL_Reduce(c0, c1, c2, r); +} +#endif + +void GCM_Base::SetKeyWithoutResync(const byte *userKey, size_t keylength, const NameValuePairs ¶ms) +{ + BlockCipher &blockCipher = AccessBlockCipher(); + blockCipher.SetKey(userKey, keylength, params); + + if (blockCipher.BlockSize() != REQUIRED_BLOCKSIZE) + throw InvalidArgument(AlgorithmName() + ": block size of underlying block cipher is not 16"); + + int tableSize, i, j, k; + +#if CRYPTOPP_BOOL_AESNI_INTRINSICS_AVAILABLE + if (HasCLMUL()) + { + params.GetIntValue(Name::TableSize(), tableSize); // avoid "parameter not used" error + tableSize = s_clmulTableSizeInBlocks * REQUIRED_BLOCKSIZE; + } + else +#endif + { + if (params.GetIntValue(Name::TableSize(), tableSize)) + tableSize = (tableSize >= 64*1024) ? 64*1024 : 2*1024; + else + tableSize = (GetTablesOption() == GCM_64K_Tables) ? 64*1024 : 2*1024; + +#if defined(_MSC_VER) && (_MSC_VER >= 1300 && _MSC_VER < 1400) + // VC 2003 workaround: compiler generates bad code for 64K tables + tableSize = 2*1024; +#endif + } + + m_buffer.resize(3*REQUIRED_BLOCKSIZE + tableSize); + byte *table = MulTable(); + byte *hashKey = HashKey(); + memset(hashKey, 0, REQUIRED_BLOCKSIZE); + blockCipher.ProcessBlock(hashKey); + +#if CRYPTOPP_BOOL_AESNI_INTRINSICS_AVAILABLE + if (HasCLMUL()) + { + const __m128i r = s_clmulConstants[0]; + __m128i h0 = _mm_shuffle_epi8(_mm_load_si128((__m128i *)hashKey), s_clmulConstants[1]); + __m128i h = h0; + + for (i=0; i Block; + Block::Get(hashKey)(V0)(V1); + + if (tableSize == 64*1024) + { + for (i=0; i<128; i++) + { + k = i%8; + Block::Put(NULL, table+(i/8)*256*16+(size_t(1)<<(11-k)))(V0)(V1); + + int x = (int)V1 & 1; + V1 = (V1>>1) | (V0<<63); + V0 = (V0>>1) ^ (x ? W64LIT(0xe1) << 56 : 0); + } + + for (i=0; i<16; i++) + { + memset(table+i*256*16, 0, 16); +#if CRYPTOPP_BOOL_SSE2_INTRINSICS_AVAILABLE || CRYPTOPP_BOOL_SSE2_ASM_AVAILABLE + if (HasSSE2()) + for (j=2; j<=0x80; j*=2) + for (k=1; k>1) | (V0<<63); + V0 = (V0>>1) ^ (x ? W64LIT(0xe1) << 56 : 0); + } + + for (i=0; i<4; i++) + { + memset(table+i*256, 0, 16); + memset(table+1024+i*256, 0, 16); +#if CRYPTOPP_BOOL_SSE2_INTRINSICS_AVAILABLE || CRYPTOPP_BOOL_SSE2_ASM_AVAILABLE + if (HasSSE2()) + for (j=2; j<=8; j*=2) + for (k=1; k= HASH_BLOCKSIZE) + { + len = GCM_Base::AuthenticateBlocks(iv, len); + iv += (origLen - len); + } + + if (len > 0) + { + memcpy(m_buffer, iv, len); + memset(m_buffer+len, 0, HASH_BLOCKSIZE-len); + GCM_Base::AuthenticateBlocks(m_buffer, HASH_BLOCKSIZE); + } + + PutBlock(NULL, m_buffer)(0)(origLen*8); + GCM_Base::AuthenticateBlocks(m_buffer, HASH_BLOCKSIZE); + + ReverseHashBufferIfNeeded(); + } + + if (m_state >= State_IVSet) + m_ctr.Resynchronize(hashBuffer, REQUIRED_BLOCKSIZE); + else + m_ctr.SetCipherWithIV(cipher, hashBuffer); + + m_ctr.Seek(HASH_BLOCKSIZE); + + memset(hashBuffer, 0, HASH_BLOCKSIZE); +} + +unsigned int GCM_Base::OptimalDataAlignment() const +{ + return +#if CRYPTOPP_BOOL_SSE2_ASM_AVAILABLE || defined(CRYPTOPP_X64_MASM_AVAILABLE) + HasSSE2() ? 16 : +#endif + GetBlockCipher().OptimalDataAlignment(); +} + +#pragma warning(disable: 4731) // frame pointer register 'ebp' modified by inline assembly code + +#endif // #ifndef CRYPTOPP_GENERATE_X64_MASM + +#ifdef CRYPTOPP_X64_MASM_AVAILABLE +extern "C" { +void GCM_AuthenticateBlocks_2K(const byte *data, size_t blocks, word64 *hashBuffer, const word16 *reductionTable); +void GCM_AuthenticateBlocks_64K(const byte *data, size_t blocks, word64 *hashBuffer); +} +#endif + +#ifndef CRYPTOPP_GENERATE_X64_MASM + +size_t GCM_Base::AuthenticateBlocks(const byte *data, size_t len) +{ +#if CRYPTOPP_BOOL_AESNI_INTRINSICS_AVAILABLE + if (HasCLMUL()) + { + const __m128i *table = (const __m128i *)MulTable(); + __m128i x = _mm_load_si128((__m128i *)HashBuffer()); + const __m128i r = s_clmulConstants[0], bswapMask = s_clmulConstants[1], bswapMask2 = s_clmulConstants[2]; + + while (len >= 16) + { + size_t s = UnsignedMin(len/16, s_clmulTableSizeInBlocks), i=0; + __m128i d, d2 = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i *)(data+(s-1)*16)), bswapMask2);; + __m128i c0 = _mm_setzero_si128(); + __m128i c1 = _mm_setzero_si128(); + __m128i c2 = _mm_setzero_si128(); + + while (true) + { + __m128i h0 = _mm_load_si128(table+i); + __m128i h1 = _mm_load_si128(table+i+1); + __m128i h01 = _mm_xor_si128(h0, h1); + + if (++i == s) + { + d = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i *)data), bswapMask); + d = _mm_xor_si128(d, x); + c0 = _mm_xor_si128(c0, _mm_clmulepi64_si128(d, h0, 0)); + c2 = _mm_xor_si128(c2, _mm_clmulepi64_si128(d, h1, 1)); + d = _mm_xor_si128(d, _mm_shuffle_epi32(d, _MM_SHUFFLE(1, 0, 3, 2))); + c1 = _mm_xor_si128(c1, _mm_clmulepi64_si128(d, h01, 0)); + break; + } + + d = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i *)(data+(s-i)*16-8)), bswapMask2); + c0 = _mm_xor_si128(c0, _mm_clmulepi64_si128(d2, h0, 1)); + c2 = _mm_xor_si128(c2, _mm_clmulepi64_si128(d, h1, 1)); + d2 = _mm_xor_si128(d2, d); + c1 = _mm_xor_si128(c1, _mm_clmulepi64_si128(d2, h01, 1)); + + if (++i == s) + { + d = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i *)data), bswapMask); + d = _mm_xor_si128(d, x); + c0 = _mm_xor_si128(c0, _mm_clmulepi64_si128(d, h0, 0x10)); + c2 = _mm_xor_si128(c2, _mm_clmulepi64_si128(d, h1, 0x11)); + d = _mm_xor_si128(d, _mm_shuffle_epi32(d, _MM_SHUFFLE(1, 0, 3, 2))); + c1 = _mm_xor_si128(c1, _mm_clmulepi64_si128(d, h01, 0x10)); + break; + } + + d2 = _mm_shuffle_epi8(_mm_loadu_si128((const __m128i *)(data+(s-i)*16-8)), bswapMask); + c0 = _mm_xor_si128(c0, _mm_clmulepi64_si128(d, h0, 0x10)); + c2 = _mm_xor_si128(c2, _mm_clmulepi64_si128(d2, h1, 0x10)); + d = _mm_xor_si128(d, d2); + c1 = _mm_xor_si128(c1, _mm_clmulepi64_si128(d, h01, 0x10)); + } + data += s*16; + len -= s*16; + + c1 = _mm_xor_si128(_mm_xor_si128(c1, c0), c2); + x = CLMUL_Reduce(c0, c1, c2, r); + } + + _mm_store_si128((__m128i *)HashBuffer(), x); + return len; + } +#endif + + typedef BlockGetAndPut Block; + word64 *hashBuffer = (word64 *)HashBuffer(); + + switch (2*(m_buffer.size()>=64*1024) +#if CRYPTOPP_BOOL_SSE2_ASM_AVAILABLE || defined(CRYPTOPP_X64_MASM_AVAILABLE) + + HasSSE2() +#endif + ) + { + case 0: // non-SSE2 and 2K tables + { + byte *table = MulTable(); + word64 x0 = hashBuffer[0], x1 = hashBuffer[1]; + + do + { + word64 y0, y1, a0, a1, b0, b1, c0, c1, d0, d1; + Block::Get(data)(y0)(y1); + x0 ^= y0; + x1 ^= y1; + + data += HASH_BLOCKSIZE; + len -= HASH_BLOCKSIZE; + + #define READ_TABLE_WORD64_COMMON(a, b, c, d) *(word64 *)(table+(a*1024)+(b*256)+c+d*8) + + #ifdef IS_LITTLE_ENDIAN + #if CRYPTOPP_BOOL_SLOW_WORD64 + word32 z0 = (word32)x0; + word32 z1 = (word32)(x0>>32); + word32 z2 = (word32)x1; + word32 z3 = (word32)(x1>>32); + #define READ_TABLE_WORD64(a, b, c, d, e) READ_TABLE_WORD64_COMMON((d%2), c, (d?(z##c>>((d?d-1:0)*4))&0xf0:(z##c&0xf)<<4), e) + #else + #define READ_TABLE_WORD64(a, b, c, d, e) READ_TABLE_WORD64_COMMON((d%2), c, ((d+8*b)?(x##a>>(((d+8*b)?(d+8*b)-1:1)*4))&0xf0:(x##a&0xf)<<4), e) + #endif + #define GF_MOST_SIG_8BITS(a) (a##1 >> 7*8) + #define GF_SHIFT_8(a) a##1 = (a##1 << 8) ^ (a##0 >> 7*8); a##0 <<= 8; + #else + #define READ_TABLE_WORD64(a, b, c, d, e) READ_TABLE_WORD64_COMMON((1-d%2), c, ((15-d-8*b)?(x##a>>(((15-d-8*b)?(15-d-8*b)-1:0)*4))&0xf0:(x##a&0xf)<<4), e) + #define GF_MOST_SIG_8BITS(a) (a##1 & 0xff) + #define GF_SHIFT_8(a) a##1 = (a##1 >> 8) ^ (a##0 << 7*8); a##0 >>= 8; + #endif + + #define GF_MUL_32BY128(op, a, b, c) \ + a0 op READ_TABLE_WORD64(a, b, c, 0, 0) ^ READ_TABLE_WORD64(a, b, c, 1, 0);\ + a1 op READ_TABLE_WORD64(a, b, c, 0, 1) ^ READ_TABLE_WORD64(a, b, c, 1, 1);\ + b0 op READ_TABLE_WORD64(a, b, c, 2, 0) ^ READ_TABLE_WORD64(a, b, c, 3, 0);\ + b1 op READ_TABLE_WORD64(a, b, c, 2, 1) ^ READ_TABLE_WORD64(a, b, c, 3, 1);\ + c0 op READ_TABLE_WORD64(a, b, c, 4, 0) ^ READ_TABLE_WORD64(a, b, c, 5, 0);\ + c1 op READ_TABLE_WORD64(a, b, c, 4, 1) ^ READ_TABLE_WORD64(a, b, c, 5, 1);\ + d0 op READ_TABLE_WORD64(a, b, c, 6, 0) ^ READ_TABLE_WORD64(a, b, c, 7, 0);\ + d1 op READ_TABLE_WORD64(a, b, c, 6, 1) ^ READ_TABLE_WORD64(a, b, c, 7, 1);\ + + GF_MUL_32BY128(=, 0, 0, 0) + GF_MUL_32BY128(^=, 0, 1, 1) + GF_MUL_32BY128(^=, 1, 0, 2) + GF_MUL_32BY128(^=, 1, 1, 3) + + word32 r = (word32)s_reductionTable[GF_MOST_SIG_8BITS(d)] << 16; + GF_SHIFT_8(d) + c0 ^= d0; c1 ^= d1; + r ^= (word32)s_reductionTable[GF_MOST_SIG_8BITS(c)] << 8; + GF_SHIFT_8(c) + b0 ^= c0; b1 ^= c1; + r ^= s_reductionTable[GF_MOST_SIG_8BITS(b)]; + GF_SHIFT_8(b) + a0 ^= b0; a1 ^= b1; + a0 ^= ConditionalByteReverse(LITTLE_ENDIAN_ORDER, r); + x0 = a0; x1 = a1; + } + while (len >= HASH_BLOCKSIZE); + + hashBuffer[0] = x0; hashBuffer[1] = x1; + return len; + } + + case 2: // non-SSE2 and 64K tables + { + byte *table = MulTable(); + word64 x0 = hashBuffer[0], x1 = hashBuffer[1]; + + do + { + word64 y0, y1, a0, a1; + Block::Get(data)(y0)(y1); + x0 ^= y0; + x1 ^= y1; + + data += HASH_BLOCKSIZE; + len -= HASH_BLOCKSIZE; + + #undef READ_TABLE_WORD64_COMMON + #undef READ_TABLE_WORD64 + + #define READ_TABLE_WORD64_COMMON(a, c, d) *(word64 *)(table+(a)*256*16+(c)+(d)*8) + + #ifdef IS_LITTLE_ENDIAN + #if CRYPTOPP_BOOL_SLOW_WORD64 + word32 z0 = (word32)x0; + word32 z1 = (word32)(x0>>32); + word32 z2 = (word32)x1; + word32 z3 = (word32)(x1>>32); + #define READ_TABLE_WORD64(b, c, d, e) READ_TABLE_WORD64_COMMON(c*4+d, (d?(z##c>>((d?d:1)*8-4))&0xff0:(z##c&0xff)<<4), e) + #else + #define READ_TABLE_WORD64(b, c, d, e) READ_TABLE_WORD64_COMMON(c*4+d, ((d+4*(c%2))?(x##b>>(((d+4*(c%2))?(d+4*(c%2)):1)*8-4))&0xff0:(x##b&0xff)<<4), e) + #endif + #else + #define READ_TABLE_WORD64(b, c, d, e) READ_TABLE_WORD64_COMMON(c*4+d, ((7-d-4*(c%2))?(x##b>>(((7-d-4*(c%2))?(7-d-4*(c%2)):1)*8-4))&0xff0:(x##b&0xff)<<4), e) + #endif + + #define GF_MUL_8BY128(op, b, c, d) \ + a0 op READ_TABLE_WORD64(b, c, d, 0);\ + a1 op READ_TABLE_WORD64(b, c, d, 1);\ + + GF_MUL_8BY128(=, 0, 0, 0) + GF_MUL_8BY128(^=, 0, 0, 1) + GF_MUL_8BY128(^=, 0, 0, 2) + GF_MUL_8BY128(^=, 0, 0, 3) + GF_MUL_8BY128(^=, 0, 1, 0) + GF_MUL_8BY128(^=, 0, 1, 1) + GF_MUL_8BY128(^=, 0, 1, 2) + GF_MUL_8BY128(^=, 0, 1, 3) + GF_MUL_8BY128(^=, 1, 2, 0) + GF_MUL_8BY128(^=, 1, 2, 1) + GF_MUL_8BY128(^=, 1, 2, 2) + GF_MUL_8BY128(^=, 1, 2, 3) + GF_MUL_8BY128(^=, 1, 3, 0) + GF_MUL_8BY128(^=, 1, 3, 1) + GF_MUL_8BY128(^=, 1, 3, 2) + GF_MUL_8BY128(^=, 1, 3, 3) + + x0 = a0; x1 = a1; + } + while (len >= HASH_BLOCKSIZE); + + hashBuffer[0] = x0; hashBuffer[1] = x1; + return len; + } +#endif // #ifndef CRYPTOPP_GENERATE_X64_MASM + +#ifdef CRYPTOPP_X64_MASM_AVAILABLE + case 1: // SSE2 and 2K tables + GCM_AuthenticateBlocks_2K(data, len/16, hashBuffer, s_reductionTable); + return len % 16; + case 3: // SSE2 and 64K tables + GCM_AuthenticateBlocks_64K(data, len/16, hashBuffer); + return len % 16; +#endif + +#if CRYPTOPP_BOOL_SSE2_ASM_AVAILABLE + case 1: // SSE2 and 2K tables + { + #ifdef __GNUC__ + __asm__ __volatile__ + ( + ".intel_syntax noprefix;" + #elif defined(CRYPTOPP_GENERATE_X64_MASM) + ALIGN 8 + GCM_AuthenticateBlocks_2K PROC FRAME + rex_push_reg rsi + push_reg rdi + push_reg rbx + .endprolog + mov rsi, r8 + mov r11, r9 + #else + AS2( mov WORD_REG(cx), data ) + AS2( mov WORD_REG(dx), len ) + AS2( mov WORD_REG(si), hashBuffer ) + AS2( shr WORD_REG(dx), 4 ) + #endif + + AS_PUSH_IF86( bx) + AS_PUSH_IF86( bp) + + #ifdef __GNUC__ + AS2( mov AS_REG_7, WORD_REG(di)) + #elif CRYPTOPP_BOOL_X86 + AS2( lea AS_REG_7, s_reductionTable) + #endif + + AS2( movdqa xmm0, [WORD_REG(si)] ) + + #define MUL_TABLE_0 WORD_REG(si) + 32 + #define MUL_TABLE_1 WORD_REG(si) + 32 + 1024 + #define RED_TABLE AS_REG_7 + + ASL(0) + AS2( movdqu xmm4, [WORD_REG(cx)] ) + AS2( pxor xmm0, xmm4 ) + + AS2( movd ebx, xmm0 ) + AS2( mov eax, AS_HEX(f0f0f0f0) ) + AS2( and eax, ebx ) + AS2( shl ebx, 4 ) + AS2( and ebx, AS_HEX(f0f0f0f0) ) + AS2( movzx edi, ah ) + AS2( movdqa xmm5, XMMWORD_PTR [MUL_TABLE_1 + WORD_REG(di)] ) + AS2( movzx edi, al ) + AS2( movdqa xmm4, XMMWORD_PTR [MUL_TABLE_1 + WORD_REG(di)] ) + AS2( shr eax, 16 ) + AS2( movzx edi, ah ) + AS2( movdqa xmm3, XMMWORD_PTR [MUL_TABLE_1 + WORD_REG(di)] ) + AS2( movzx edi, al ) + AS2( movdqa xmm2, XMMWORD_PTR [MUL_TABLE_1 + WORD_REG(di)] ) + + #define SSE2_MUL_32BITS(i) \ + AS2( psrldq xmm0, 4 )\ + AS2( movd eax, xmm0 )\ + AS2( and eax, AS_HEX(f0f0f0f0) )\ + AS2( movzx edi, bh )\ + AS2( pxor xmm5, XMMWORD_PTR [MUL_TABLE_0 + (i-1)*256 + WORD_REG(di)] )\ + AS2( movzx edi, bl )\ + AS2( pxor xmm4, XMMWORD_PTR [MUL_TABLE_0 + (i-1)*256 + WORD_REG(di)] )\ + AS2( shr ebx, 16 )\ + AS2( movzx edi, bh )\ + AS2( pxor xmm3, XMMWORD_PTR [MUL_TABLE_0 + (i-1)*256 + WORD_REG(di)] )\ + AS2( movzx edi, bl )\ + AS2( pxor xmm2, XMMWORD_PTR [MUL_TABLE_0 + (i-1)*256 + WORD_REG(di)] )\ + AS2( movd ebx, xmm0 )\ + AS2( shl ebx, 4 )\ + AS2( and ebx, AS_HEX(f0f0f0f0) )\ + AS2( movzx edi, ah )\ + AS2( pxor xmm5, XMMWORD_PTR [MUL_TABLE_1 + i*256 + WORD_REG(di)] )\ + AS2( movzx edi, al )\ + AS2( pxor xmm4, XMMWORD_PTR [MUL_TABLE_1 + i*256 + WORD_REG(di)] )\ + AS2( shr eax, 16 )\ + AS2( movzx edi, ah )\ + AS2( pxor xmm3, XMMWORD_PTR [MUL_TABLE_1 + i*256 + WORD_REG(di)] )\ + AS2( movzx edi, al )\ + AS2( pxor xmm2, XMMWORD_PTR [MUL_TABLE_1 + i*256 + WORD_REG(di)] )\ + + SSE2_MUL_32BITS(1) + SSE2_MUL_32BITS(2) + SSE2_MUL_32BITS(3) + + AS2( movzx edi, bh ) + AS2( pxor xmm5, XMMWORD_PTR [MUL_TABLE_0 + 3*256 + WORD_REG(di)] ) + AS2( movzx edi, bl ) + AS2( pxor xmm4, XMMWORD_PTR [MUL_TABLE_0 + 3*256 + WORD_REG(di)] ) + AS2( shr ebx, 16 ) + AS2( movzx edi, bh ) + AS2( pxor xmm3, XMMWORD_PTR [MUL_TABLE_0 + 3*256 + WORD_REG(di)] ) + AS2( movzx edi, bl ) + AS2( pxor xmm2, XMMWORD_PTR [MUL_TABLE_0 + 3*256 + WORD_REG(di)] ) + + AS2( movdqa xmm0, xmm3 ) + AS2( pslldq xmm3, 1 ) + AS2( pxor xmm2, xmm3 ) + AS2( movdqa xmm1, xmm2 ) + AS2( pslldq xmm2, 1 ) + AS2( pxor xmm5, xmm2 ) + + AS2( psrldq xmm0, 15 ) + AS2( movd WORD_REG(di), xmm0 ) + AS2( movzx eax, WORD PTR [RED_TABLE + WORD_REG(di)*2] ) + AS2( shl eax, 8 ) + + AS2( movdqa xmm0, xmm5 ) + AS2( pslldq xmm5, 1 ) + AS2( pxor xmm4, xmm5 ) + + AS2( psrldq xmm1, 15 ) + AS2( movd WORD_REG(di), xmm1 ) + AS2( xor ax, WORD PTR [RED_TABLE + WORD_REG(di)*2] ) + AS2( shl eax, 8 ) + + AS2( psrldq xmm0, 15 ) + AS2( movd WORD_REG(di), xmm0 ) + AS2( xor ax, WORD PTR [RED_TABLE + WORD_REG(di)*2] ) + + AS2( movd xmm0, eax ) + AS2( pxor xmm0, xmm4 ) + + AS2( add WORD_REG(cx), 16 ) + AS2( sub WORD_REG(dx), 1 ) + ASJ( jnz, 0, b ) + AS2( movdqa [WORD_REG(si)], xmm0 ) + + AS_POP_IF86( bp) + AS_POP_IF86( bx) + + #ifdef __GNUC__ + ".att_syntax prefix;" + : + : "c" (data), "d" (len/16), "S" (hashBuffer), "D" (s_reductionTable) + : "memory", "cc", "%eax" + #if CRYPTOPP_BOOL_X64 + , "%ebx", "%r11" + #endif + ); + #elif defined(CRYPTOPP_GENERATE_X64_MASM) + pop rbx + pop rdi + pop rsi + ret + GCM_AuthenticateBlocks_2K ENDP + #endif + + return len%16; + } + case 3: // SSE2 and 64K tables + { + #ifdef __GNUC__ + __asm__ __volatile__ + ( + ".intel_syntax noprefix;" + #elif defined(CRYPTOPP_GENERATE_X64_MASM) + ALIGN 8 + GCM_AuthenticateBlocks_64K PROC FRAME + rex_push_reg rsi + push_reg rdi + .endprolog + mov rsi, r8 + #else + AS2( mov WORD_REG(cx), data ) + AS2( mov WORD_REG(dx), len ) + AS2( mov WORD_REG(si), hashBuffer ) + AS2( shr WORD_REG(dx), 4 ) + #endif + + AS2( movdqa xmm0, [WORD_REG(si)] ) + + #undef MUL_TABLE + #define MUL_TABLE(i,j) WORD_REG(si) + 32 + (i*4+j)*256*16 + + ASL(1) + AS2( movdqu xmm1, [WORD_REG(cx)] ) + AS2( pxor xmm1, xmm0 ) + AS2( pxor xmm0, xmm0 ) + + #undef SSE2_MUL_32BITS + #define SSE2_MUL_32BITS(i) \ + AS2( movd eax, xmm1 )\ + AS2( psrldq xmm1, 4 )\ + AS2( movzx edi, al )\ + AS2( add WORD_REG(di), WORD_REG(di) )\ + AS2( pxor xmm0, [MUL_TABLE(i,0) + WORD_REG(di)*8] )\ + AS2( movzx edi, ah )\ + AS2( add WORD_REG(di), WORD_REG(di) )\ + AS2( pxor xmm0, [MUL_TABLE(i,1) + WORD_REG(di)*8] )\ + AS2( shr eax, 16 )\ + AS2( movzx edi, al )\ + AS2( add WORD_REG(di), WORD_REG(di) )\ + AS2( pxor xmm0, [MUL_TABLE(i,2) + WORD_REG(di)*8] )\ + AS2( movzx edi, ah )\ + AS2( add WORD_REG(di), WORD_REG(di) )\ + AS2( pxor xmm0, [MUL_TABLE(i,3) + WORD_REG(di)*8] )\ + + SSE2_MUL_32BITS(0) + SSE2_MUL_32BITS(1) + SSE2_MUL_32BITS(2) + SSE2_MUL_32BITS(3) + + AS2( add WORD_REG(cx), 16 ) + AS2( sub WORD_REG(dx), 1 ) + ASJ( jnz, 1, b ) + AS2( movdqa [WORD_REG(si)], xmm0 ) + + #ifdef __GNUC__ + ".att_syntax prefix;" + : + : "c" (data), "d" (len/16), "S" (hashBuffer) + : "memory", "cc", "%edi", "%eax" + ); + #elif defined(CRYPTOPP_GENERATE_X64_MASM) + pop rdi + pop rsi + ret + GCM_AuthenticateBlocks_64K ENDP + #endif + + return len%16; + } +#endif +#ifndef CRYPTOPP_GENERATE_X64_MASM + } + + return len%16; +} + +void GCM_Base::AuthenticateLastHeaderBlock() +{ + if (m_bufferedDataLength > 0) + { + memset(m_buffer+m_bufferedDataLength, 0, HASH_BLOCKSIZE-m_bufferedDataLength); + m_bufferedDataLength = 0; + GCM_Base::AuthenticateBlocks(m_buffer, HASH_BLOCKSIZE); + } +} + +void GCM_Base::AuthenticateLastConfidentialBlock() +{ + GCM_Base::AuthenticateLastHeaderBlock(); + PutBlock(NULL, m_buffer)(m_totalHeaderLength*8)(m_totalMessageLength*8); + GCM_Base::AuthenticateBlocks(m_buffer, HASH_BLOCKSIZE); +} + +void GCM_Base::AuthenticateLastFooterBlock(byte *mac, size_t macSize) +{ + m_ctr.Seek(0); + ReverseHashBufferIfNeeded(); + m_ctr.ProcessData(mac, HashBuffer(), macSize); +} + +NAMESPACE_END + +#endif // #ifndef CRYPTOPP_GENERATE_X64_MASM +#endif diff --git a/cppForSwig/cryptopp/gcm.h b/cppForSwig/cryptopp/gcm.h index 0b32524f9..272a51c9c 100755 --- a/cppForSwig/cryptopp/gcm.h +++ b/cppForSwig/cryptopp/gcm.h @@ -1,106 +1,106 @@ -#ifndef CRYPTOPP_GCM_H -#define CRYPTOPP_GCM_H - -#include "authenc.h" -#include "modes.h" - -NAMESPACE_BEGIN(CryptoPP) - -//! . -enum GCM_TablesOption {GCM_2K_Tables, GCM_64K_Tables}; - -//! . -class CRYPTOPP_DLL CRYPTOPP_NO_VTABLE GCM_Base : public AuthenticatedSymmetricCipherBase -{ -public: - // AuthenticatedSymmetricCipher - std::string AlgorithmName() const - {return GetBlockCipher().AlgorithmName() + std::string("/GCM");} - size_t MinKeyLength() const - {return GetBlockCipher().MinKeyLength();} - size_t MaxKeyLength() const - {return GetBlockCipher().MaxKeyLength();} - size_t DefaultKeyLength() const - {return GetBlockCipher().DefaultKeyLength();} - size_t GetValidKeyLength(size_t n) const - {return GetBlockCipher().GetValidKeyLength(n);} - bool IsValidKeyLength(size_t n) const - {return GetBlockCipher().IsValidKeyLength(n);} - unsigned int OptimalDataAlignment() const; - IV_Requirement IVRequirement() const - {return UNIQUE_IV;} - unsigned int IVSize() const - {return 12;} - unsigned int MinIVLength() const - {return 1;} - unsigned int MaxIVLength() const - {return UINT_MAX;} // (W64LIT(1)<<61)-1 in the standard - unsigned int DigestSize() const - {return 16;} - lword MaxHeaderLength() const - {return (W64LIT(1)<<61)-1;} - lword MaxMessageLength() const - {return ((W64LIT(1)<<39)-256)/8;} - -protected: - // AuthenticatedSymmetricCipherBase - bool AuthenticationIsOnPlaintext() const - {return false;} - unsigned int AuthenticationBlockSize() const - {return HASH_BLOCKSIZE;} - void SetKeyWithoutResync(const byte *userKey, size_t keylength, const NameValuePairs ¶ms); - void Resync(const byte *iv, size_t len); - size_t AuthenticateBlocks(const byte *data, size_t len); - void AuthenticateLastHeaderBlock(); - void AuthenticateLastConfidentialBlock(); - void AuthenticateLastFooterBlock(byte *mac, size_t macSize); - SymmetricCipher & AccessSymmetricCipher() {return m_ctr;} - - virtual BlockCipher & AccessBlockCipher() =0; - virtual GCM_TablesOption GetTablesOption() const =0; - - const BlockCipher & GetBlockCipher() const {return const_cast(this)->AccessBlockCipher();}; - byte *HashBuffer() {return m_buffer+REQUIRED_BLOCKSIZE;} - byte *HashKey() {return m_buffer+2*REQUIRED_BLOCKSIZE;} - byte *MulTable() {return m_buffer+3*REQUIRED_BLOCKSIZE;} - inline void ReverseHashBufferIfNeeded(); - - class CRYPTOPP_DLL GCTR : public CTR_Mode_ExternalCipher::Encryption - { - protected: - void IncrementCounterBy256(); - }; - - GCTR m_ctr; - static word16 s_reductionTable[256]; - static volatile bool s_reductionTableInitialized; - enum {REQUIRED_BLOCKSIZE = 16, HASH_BLOCKSIZE = 16}; -}; - -//! . -template -class GCM_Final : public GCM_Base -{ -public: - static std::string StaticAlgorithmName() - {return T_BlockCipher::StaticAlgorithmName() + std::string("/GCM");} - bool IsForwardTransformation() const - {return T_IsEncryption;} - -private: - GCM_TablesOption GetTablesOption() const {return T_TablesOption;} - BlockCipher & AccessBlockCipher() {return m_cipher;} - typename T_BlockCipher::Encryption m_cipher; -}; - -//! GCM -template -struct GCM : public AuthenticatedSymmetricCipherDocumentation -{ - typedef GCM_Final Encryption; - typedef GCM_Final Decryption; -}; - -NAMESPACE_END - -#endif +#ifndef CRYPTOPP_GCM_H +#define CRYPTOPP_GCM_H + +#include "authenc.h" +#include "modes.h" + +NAMESPACE_BEGIN(CryptoPP) + +//! . +enum GCM_TablesOption {GCM_2K_Tables, GCM_64K_Tables}; + +//! . +class CRYPTOPP_DLL CRYPTOPP_NO_VTABLE GCM_Base : public AuthenticatedSymmetricCipherBase +{ +public: + // AuthenticatedSymmetricCipher + std::string AlgorithmName() const + {return GetBlockCipher().AlgorithmName() + std::string("/GCM");} + size_t MinKeyLength() const + {return GetBlockCipher().MinKeyLength();} + size_t MaxKeyLength() const + {return GetBlockCipher().MaxKeyLength();} + size_t DefaultKeyLength() const + {return GetBlockCipher().DefaultKeyLength();} + size_t GetValidKeyLength(size_t n) const + {return GetBlockCipher().GetValidKeyLength(n);} + bool IsValidKeyLength(size_t n) const + {return GetBlockCipher().IsValidKeyLength(n);} + unsigned int OptimalDataAlignment() const; + IV_Requirement IVRequirement() const + {return UNIQUE_IV;} + unsigned int IVSize() const + {return 12;} + unsigned int MinIVLength() const + {return 1;} + unsigned int MaxIVLength() const + {return UINT_MAX;} // (W64LIT(1)<<61)-1 in the standard + unsigned int DigestSize() const + {return 16;} + lword MaxHeaderLength() const + {return (W64LIT(1)<<61)-1;} + lword MaxMessageLength() const + {return ((W64LIT(1)<<39)-256)/8;} + +protected: + // AuthenticatedSymmetricCipherBase + bool AuthenticationIsOnPlaintext() const + {return false;} + unsigned int AuthenticationBlockSize() const + {return HASH_BLOCKSIZE;} + void SetKeyWithoutResync(const byte *userKey, size_t keylength, const NameValuePairs ¶ms); + void Resync(const byte *iv, size_t len); + size_t AuthenticateBlocks(const byte *data, size_t len); + void AuthenticateLastHeaderBlock(); + void AuthenticateLastConfidentialBlock(); + void AuthenticateLastFooterBlock(byte *mac, size_t macSize); + SymmetricCipher & AccessSymmetricCipher() {return m_ctr;} + + virtual BlockCipher & AccessBlockCipher() =0; + virtual GCM_TablesOption GetTablesOption() const =0; + + const BlockCipher & GetBlockCipher() const {return const_cast(this)->AccessBlockCipher();}; + byte *HashBuffer() {return m_buffer+REQUIRED_BLOCKSIZE;} + byte *HashKey() {return m_buffer+2*REQUIRED_BLOCKSIZE;} + byte *MulTable() {return m_buffer+3*REQUIRED_BLOCKSIZE;} + inline void ReverseHashBufferIfNeeded(); + + class CRYPTOPP_DLL GCTR : public CTR_Mode_ExternalCipher::Encryption + { + protected: + void IncrementCounterBy256(); + }; + + GCTR m_ctr; + static word16 s_reductionTable[256]; + static volatile bool s_reductionTableInitialized; + enum {REQUIRED_BLOCKSIZE = 16, HASH_BLOCKSIZE = 16}; +}; + +//! . +template +class GCM_Final : public GCM_Base +{ +public: + static std::string StaticAlgorithmName() + {return T_BlockCipher::StaticAlgorithmName() + std::string("/GCM");} + bool IsForwardTransformation() const + {return T_IsEncryption;} + +private: + GCM_TablesOption GetTablesOption() const {return T_TablesOption;} + BlockCipher & AccessBlockCipher() {return m_cipher;} + typename T_BlockCipher::Encryption m_cipher; +}; + +//! GCM +template +struct GCM : public AuthenticatedSymmetricCipherDocumentation +{ + typedef GCM_Final Encryption; + typedef GCM_Final Decryption; +}; + +NAMESPACE_END + +#endif diff --git a/cppForSwig/cryptopp/seed.cpp b/cppForSwig/cryptopp/seed.cpp index 6c739b474..101902dce 100755 --- a/cppForSwig/cryptopp/seed.cpp +++ b/cppForSwig/cryptopp/seed.cpp @@ -1,104 +1,104 @@ -// seed.cpp - written and placed in the public domain by Wei Dai - -#include "pch.h" -#include "seed.h" -#include "misc.h" - -NAMESPACE_BEGIN(CryptoPP) - -static const word32 s_kc[16] = { - 0x9e3779b9, 0x3c6ef373, 0x78dde6e6, 0xf1bbcdcc, 0xe3779b99, 0xc6ef3733, 0x8dde6e67, 0x1bbcdccf, - 0x3779b99e, 0x6ef3733c, 0xdde6e678, 0xbbcdccf1, 0x779b99e3, 0xef3733c6, 0xde6e678d, 0xbcdccf1b}; - -static const byte s_s0[256] = { - 0xA9, 0x85, 0xD6, 0xD3, 0x54, 0x1D, 0xAC, 0x25, 0x5D, 0x43, 0x18, 0x1E, 0x51, 0xFC, 0xCA, 0x63, 0x28, - 0x44, 0x20, 0x9D, 0xE0, 0xE2, 0xC8, 0x17, 0xA5, 0x8F, 0x03, 0x7B, 0xBB, 0x13, 0xD2, 0xEE, 0x70, 0x8C, - 0x3F, 0xA8, 0x32, 0xDD, 0xF6, 0x74, 0xEC, 0x95, 0x0B, 0x57, 0x5C, 0x5B, 0xBD, 0x01, 0x24, 0x1C, 0x73, - 0x98, 0x10, 0xCC, 0xF2, 0xD9, 0x2C, 0xE7, 0x72, 0x83, 0x9B, 0xD1, 0x86, 0xC9, 0x60, 0x50, 0xA3, 0xEB, - 0x0D, 0xB6, 0x9E, 0x4F, 0xB7, 0x5A, 0xC6, 0x78, 0xA6, 0x12, 0xAF, 0xD5, 0x61, 0xC3, 0xB4, 0x41, 0x52, - 0x7D, 0x8D, 0x08, 0x1F, 0x99, 0x00, 0x19, 0x04, 0x53, 0xF7, 0xE1, 0xFD, 0x76, 0x2F, 0x27, 0xB0, 0x8B, - 0x0E, 0xAB, 0xA2, 0x6E, 0x93, 0x4D, 0x69, 0x7C, 0x09, 0x0A, 0xBF, 0xEF, 0xF3, 0xC5, 0x87, 0x14, 0xFE, - 0x64, 0xDE, 0x2E, 0x4B, 0x1A, 0x06, 0x21, 0x6B, 0x66, 0x02, 0xF5, 0x92, 0x8A, 0x0C, 0xB3, 0x7E, 0xD0, - 0x7A, 0x47, 0x96, 0xE5, 0x26, 0x80, 0xAD, 0xDF, 0xA1, 0x30, 0x37, 0xAE, 0x36, 0x15, 0x22, 0x38, 0xF4, - 0xA7, 0x45, 0x4C, 0x81, 0xE9, 0x84, 0x97, 0x35, 0xCB, 0xCE, 0x3C, 0x71, 0x11, 0xC7, 0x89, 0x75, 0xFB, - 0xDA, 0xF8, 0x94, 0x59, 0x82, 0xC4, 0xFF, 0x49, 0x39, 0x67, 0xC0, 0xCF, 0xD7, 0xB8, 0x0F, 0x8E, 0x42, - 0x23, 0x91, 0x6C, 0xDB, 0xA4, 0x34, 0xF1, 0x48, 0xC2, 0x6F, 0x3D, 0x2D, 0x40, 0xBE, 0x3E, 0xBC, 0xC1, - 0xAA, 0xBA, 0x4E, 0x55, 0x3B, 0xDC, 0x68, 0x7F, 0x9C, 0xD8, 0x4A, 0x56, 0x77, 0xA0, 0xED, 0x46, 0xB5, - 0x2B, 0x65, 0xFA, 0xE3, 0xB9, 0xB1, 0x9F, 0x5E, 0xF9, 0xE6, 0xB2, 0x31, 0xEA, 0x6D, 0x5F, 0xE4, 0xF0, - 0xCD, 0x88, 0x16, 0x3A, 0x58, 0xD4, 0x62, 0x29, 0x07, 0x33, 0xE8, 0x1B, 0x05, 0x79, 0x90, 0x6A, 0x2A, - 0x9A}; - -static const byte s_s1[256] = { - 0x38, 0xE8, 0x2D, 0xA6, 0xCF, 0xDE, 0xB3, 0xB8, 0xAF, 0x60, 0x55, 0xC7, 0x44, 0x6F, 0x6B, 0x5B, 0xC3, - 0x62, 0x33, 0xB5, 0x29, 0xA0, 0xE2, 0xA7, 0xD3, 0x91, 0x11, 0x06, 0x1C, 0xBC, 0x36, 0x4B, 0xEF, 0x88, - 0x6C, 0xA8, 0x17, 0xC4, 0x16, 0xF4, 0xC2, 0x45, 0xE1, 0xD6, 0x3F, 0x3D, 0x8E, 0x98, 0x28, 0x4E, 0xF6, - 0x3E, 0xA5, 0xF9, 0x0D, 0xDF, 0xD8, 0x2B, 0x66, 0x7A, 0x27, 0x2F, 0xF1, 0x72, 0x42, 0xD4, 0x41, 0xC0, - 0x73, 0x67, 0xAC, 0x8B, 0xF7, 0xAD, 0x80, 0x1F, 0xCA, 0x2C, 0xAA, 0x34, 0xD2, 0x0B, 0xEE, 0xE9, 0x5D, - 0x94, 0x18, 0xF8, 0x57, 0xAE, 0x08, 0xC5, 0x13, 0xCD, 0x86, 0xB9, 0xFF, 0x7D, 0xC1, 0x31, 0xF5, 0x8A, - 0x6A, 0xB1, 0xD1, 0x20, 0xD7, 0x02, 0x22, 0x04, 0x68, 0x71, 0x07, 0xDB, 0x9D, 0x99, 0x61, 0xBE, 0xE6, - 0x59, 0xDD, 0x51, 0x90, 0xDC, 0x9A, 0xA3, 0xAB, 0xD0, 0x81, 0x0F, 0x47, 0x1A, 0xE3, 0xEC, 0x8D, 0xBF, - 0x96, 0x7B, 0x5C, 0xA2, 0xA1, 0x63, 0x23, 0x4D, 0xC8, 0x9E, 0x9C, 0x3A, 0x0C, 0x2E, 0xBA, 0x6E, 0x9F, - 0x5A, 0xF2, 0x92, 0xF3, 0x49, 0x78, 0xCC, 0x15, 0xFB, 0x70, 0x75, 0x7F, 0x35, 0x10, 0x03, 0x64, 0x6D, - 0xC6, 0x74, 0xD5, 0xB4, 0xEA, 0x09, 0x76, 0x19, 0xFE, 0x40, 0x12, 0xE0, 0xBD, 0x05, 0xFA, 0x01, 0xF0, - 0x2A, 0x5E, 0xA9, 0x56, 0x43, 0x85, 0x14, 0x89, 0x9B, 0xB0, 0xE5, 0x48, 0x79, 0x97, 0xFC, 0x1E, 0x82, - 0x21, 0x8C, 0x1B, 0x5F, 0x77, 0x54, 0xB2, 0x1D, 0x25, 0x4F, 0x00, 0x46, 0xED, 0x58, 0x52, 0xEB, 0x7E, - 0xDA, 0xC9, 0xFD, 0x30, 0x95, 0x65, 0x3C, 0xB6, 0xE4, 0xBB, 0x7C, 0x0E, 0x50, 0x39, 0x26, 0x32, 0x84, - 0x69, 0x93, 0x37, 0xE7, 0x24, 0xA4, 0xCB, 0x53, 0x0A, 0x87, 0xD9, 0x4C, 0x83, 0x8F, 0xCE, 0x3B, 0x4A, - 0xB7}; - -#define SS0(x) ((s_s0[x]*0x01010101UL) & 0x3FCFF3FC) -#define SS1(x) ((s_s1[x]*0x01010101UL) & 0xFC3FCFF3) -#define SS2(x) ((s_s0[x]*0x01010101UL) & 0xF3FC3FCF) -#define SS3(x) ((s_s1[x]*0x01010101UL) & 0xCFF3FC3F) -#define G(x) (SS0(GETBYTE(x, 0)) ^ SS1(GETBYTE(x, 1)) ^ SS2(GETBYTE(x, 2)) ^ SS3(GETBYTE(x, 3))) - -void SEED::Base::UncheckedSetKey(const byte *userKey, unsigned int length, const NameValuePairs ¶ms) -{ - AssertValidKeyLength(length); - - word64 key01, key23; - GetBlock get(userKey); - get(key01)(key23); - word32 *k = m_k; - size_t kInc = 2; - if (!IsForwardTransformation()) - { - k = k+30; - kInc = 0-kInc; - } - - for (int i=0; i>32) + word32(key23>>32) - s_kc[i]; - word32 t1 = word32(key01) - word32(key23) + s_kc[i]; - k[0] = G(t0); - k[1] = G(t1); - k+=kInc; - if (i&1) - key23 = rotlFixed(key23, 8); - else - key01 = rotrFixed(key01, 8); - } -} - -void SEED::Base::ProcessAndXorBlock(const byte *inBlock, const byte *xorBlock, byte *outBlock) const -{ - typedef BlockGetAndPut Block; - word32 a0, a1, b0, b1, t0, t1; - Block::Get(inBlock)(a0)(a1)(b0)(b1); - - for (int i=0; i get(userKey); + get(key01)(key23); + word32 *k = m_k; + size_t kInc = 2; + if (!IsForwardTransformation()) + { + k = k+30; + kInc = 0-kInc; + } + + for (int i=0; i>32) + word32(key23>>32) - s_kc[i]; + word32 t1 = word32(key01) - word32(key23) + s_kc[i]; + k[0] = G(t0); + k[1] = G(t1); + k+=kInc; + if (i&1) + key23 = rotlFixed(key23, 8); + else + key01 = rotrFixed(key01, 8); + } +} + +void SEED::Base::ProcessAndXorBlock(const byte *inBlock, const byte *xorBlock, byte *outBlock) const +{ + typedef BlockGetAndPut Block; + word32 a0, a1, b0, b1, t0, t1; + Block::Get(inBlock)(a0)(a1)(b0)(b1); + + for (int i=0; i -#include -#include -#include "gtest.h" - -#include "../log.h" -#include "../BinaryData.h" -#include "../BtcUtils.h" -#include "../BlockObj.h" -#include "../StoredBlockObj.h" -#include "../PartialMerkle.h" -#include "../leveldb_wrapper.h" -#include "../BlockUtils.h" - -#ifdef _MSC_VER - #include "win32_posix.h" -#endif - -#define READHEX BinaryData::CreateFromHex -#define TheBDM BlockDataManager_LevelDB::GetInstance() - - -/* This didn't work at all -class BitcoinEnvironment : public ::testing::Environment -{ -public: - // Override this to define how to set up the environment. - virtual void SetUp() - { - rawHead_ = READHEX( - "01000000" - "1d8f4ec0443e1f19f305e488c1085c95de7cc3fd25e0d2c5bb5d000000000000" - "9762547903d36881a86751f3f5049e23050113f779735ef82734ebf0b4450081" - "d8c8c84d" - "b3936a1a" - "334b035b"); - headHashLE_ = READHEX( - "1195e67a7a6d0674bbd28ae096d602e1f038c8254b49dfe79d47000000000000"); - headHashBE_ = READHEX( - "000000000000479de7df494b25c838f0e102d696e08ad2bb74066d7a7ae69511"); - - rawTx0_ = READHEX( - "01000000016290dce984203b6a5032e543e9e272d8bce934c7de4d15fa0fe44d" - "d49ae4ece9010000008b48304502204f2fa458d439f957308bca264689aa175e" - "3b7c5f78a901cb450ebd20936b2c500221008ea3883a5b80128e55c9c6070aa6" - "264e1e0ce3d18b7cd7e85108ce3d18b7419a0141044202550a5a6d3bb81549c4" - "a7803b1ad59cdbba4770439a4923624a8acfc7d34900beb54a24188f7f0a4068" - "9d905d4847cc7d6c8d808a457d833c2d44ef83f76bffffffff0242582c0a0000" - "00001976a914c1b4695d53b6ee57a28647ce63e45665df6762c288ac80d1f008" - "000000001976a9140e0aec36fe2545fb31a41164fb6954adcd96b34288ac0000" - "0000"); - rawTx1_ = READHEX( - "0100000001f658dbc28e703d86ee17c9a2d3b167a8508b082fa0745f55be5144" - "a4369873aa010000008c49304602210041e1186ca9a41fdfe1569d5d807ca7ff" - "6c5ffd19d2ad1be42f7f2a20cdc8f1cc0221003366b5d64fe81e53910e156914" - "091d12646bc0d1d662b7a65ead3ebe4ab8f6c40141048d103d81ac9691cf13f3" - "fc94e44968ef67b27f58b27372c13108552d24a6ee04785838f34624b294afee" - "83749b64478bb8480c20b242c376e77eea2b3dc48b4bffffffff0200e1f50500" - "0000001976a9141b00a2f6899335366f04b277e19d777559c35bc888ac40aeeb" - "02000000001976a9140e0aec36fe2545fb31a41164fb6954adcd96b34288ac00" - "000000"); - - rawBlock_ = READHEX( - "01000000eb10c9a996a2340a4d74eaab41421ed8664aa49d18538bab59010000" - "000000005a2f06efa9f2bd804f17877537f2080030cadbfa1eb50e02338117cc" - "604d91b9b7541a4ecfbb0a1a64f1ade703010000000100000000000000000000" - "00000000000000000000000000000000000000000000ffffffff0804cfbb0a1a" - "02360affffffff0100f2052a01000000434104c2239c4eedb3beb26785753463" - "be3ec62b82f6acd62efb65f452f8806f2ede0b338e31d1f69b1ce449558d7061" - "aa1648ddc2bf680834d3986624006a272dc21cac000000000100000003e8caa1" - "2bcb2e7e86499c9de49c45c5a1c6167ea4b894c8c83aebba1b6100f343010000" - "008c493046022100e2f5af5329d1244807f8347a2c8d9acc55a21a5db769e927" - "4e7e7ba0bb605b26022100c34ca3350df5089f3415d8af82364d7f567a6a297f" - "cc2c1d2034865633238b8c014104129e422ac490ddfcb7b1c405ab9fb4244124" - "6c4bca578de4f27b230de08408c64cad03af71ee8a3140b40408a7058a1984a9" - "f246492386113764c1ac132990d1ffffffff5b55c18864e16c08ef9989d31c7a" - "343e34c27c30cd7caa759651b0e08cae0106000000008c4930460221009ec9aa" - "3e0caf7caa321723dea561e232603e00686d4bfadf46c5c7352b07eb00022100" - "a4f18d937d1e2354b2e69e02b18d11620a6a9332d563e9e2bbcb01cee559680a" - "014104411b35dd963028300e36e82ee8cf1b0c8d5bf1fc4273e970469f5cb931" - "ee07759a2de5fef638961726d04bd5eb4e5072330b9b371e479733c942964bb8" - "6e2b22ffffffff3de0c1e913e6271769d8c0172cea2f00d6d3240afc3a20f9fa" - "247ce58af30d2a010000008c493046022100b610e169fd15ac9f60fe2b507529" - "281cf2267673f4690ba428cbb2ba3c3811fd022100ffbe9e3d71b21977a8e97f" - "de4c3ba47b896d08bc09ecb9d086bb59175b5b9f03014104ff07a1833fd8098b" - "25f48c66dcf8fde34cbdbcc0f5f21a8c2005b160406cbf34cc432842c6b37b25" - "90d16b165b36a3efc9908d65fb0e605314c9b278f40f3e1affffffff0240420f" - "00000000001976a914adfa66f57ded1b655eb4ccd96ee07ca62bc1ddfd88ac00" - "7d6a7d040000001976a914981a0c9ae61fa8f8c96ae6f8e383d6e07e77133e88" - "ac00000000010000000138e7586e0784280df58bd3dc5e3d350c9036b1ec4107" - "951378f45881799c92a4000000008a47304402207c945ae0bbdaf9dadba07bdf" - "23faa676485a53817af975ddf85a104f764fb93b02201ac6af32ddf597e610b4" - "002e41f2de46664587a379a0161323a85389b4f82dda014104ec8883d3e4f7a3" - "9d75c9f5bb9fd581dc9fb1b7cdf7d6b5a665e4db1fdb09281a74ab138a2dba25" - "248b5be38bf80249601ae688c90c6e0ac8811cdb740fcec31dffffffff022f66" - "ac61050000001976a914964642290c194e3bfab661c1085e47d67786d2d388ac" - "2f77e200000000001976a9141486a7046affd935919a3cb4b50a8a0c233c286c" - "88ac00000000"); - - rawTxUnfrag_ = READHEX( - // Version - "01000000" - // NumTxIn - "02" - // Start TxIn0 - "0044fbc929d78e4203eed6f1d3d39c0157d8e5c100bbe0886779c0" - "ebf6a69324010000008a47304402206568144ed5e7064d6176c74738b04c08ca" - "19ca54ddeb480084b77f45eebfe57802207927d6975a5ac0e1bb36f5c05356dc" - "da1f521770511ee5e03239c8e1eecf3aed0141045d74feae58c4c36d7c35beac" - "05eddddc78b3ce4b02491a2eea72043978056a8bc439b99ddaad327207b09ef1" - "6a8910828e805b0cc8c11fba5caea2ee939346d7ffffffff" - // Start TxIn1 - "45c866b219b17695" - "2508f8e5aea728f950186554fc4a5807e2186a8e1c4009e5000000008c493046" - "022100bd5d41662f98cfddc46e86ea7e4a3bc8fe9f1dfc5c4836eaf7df582596" - "cfe0e9022100fc459ae4f59b8279d679003b88935896acd10021b6e2e4619377" - "e336b5296c5e014104c00bab76a708ba7064b2315420a1c533ca9945eeff9754" - "cdc574224589e9113469b4e71752146a10028079e04948ecdf70609bf1b9801f" - "6b73ab75947ac339e5ffffffff" - // NumTxOut - "02" - // Start TxOut0 - "ac4c8bd5000000001976a9148dce8946f1c7763bb60ea5cf16ef514cbed0633b88ac" - // Start TxOut1 - "002f6859000000001976a9146a59ac0e8f553f292dfe5e9f3aaa1da93499c15e88ac" - // Locktime - "00000000"); - - rawTxFragged_ = READHEX( - //"01000000020044fbc929d78e4203eed6f1d3d39c0157d8e5c100bbe0886779c0" - //"ebf6a69324010000008a47304402206568144ed5e7064d6176c74738b04c08ca" - //"19ca54ddeb480084b77f45eebfe57802207927d6975a5ac0e1bb36f5c05356dc" - //"da1f521770511ee5e03239c8e1eecf3aed0141045d74feae58c4c36d7c35beac" - //"05eddddc78b3ce4b02491a2eea72043978056a8bc439b99ddaad327207b09ef1" - //"6a8910828e805b0cc8c11fba5caea2ee939346d7ffffffff45c866b219b17695" - //"2508f8e5aea728f950186554fc4a5807e2186a8e1c4009e5000000008c493046" - //"022100bd5d41662f98cfddc46e86ea7e4a3bc8fe9f1dfc5c4836eaf7df582596" - //"cfe0e9022100fc459ae4f59b8279d679003b88935896acd10021b6e2e4619377" - //"e336b5296c5e014104c00bab76a708ba7064b2315420a1c533ca9945eeff9754" - //"cdc574224589e9113469b4e71752146a10028079e04948ecdf70609bf1b9801f" - //"6b73ab75947ac339e5ffffffff0200000000"); - // Version - "01000000" - // NumTxIn - "02" - // Start TxIn0 - "0044fbc929d78e4203eed6f1d3d39c0157d8e5c100bbe0886779c0" - "ebf6a69324010000008a47304402206568144ed5e7064d6176c74738b04c08ca" - "19ca54ddeb480084b77f45eebfe57802207927d6975a5ac0e1bb36f5c05356dc" - "da1f521770511ee5e03239c8e1eecf3aed0141045d74feae58c4c36d7c35beac" - "05eddddc78b3ce4b02491a2eea72043978056a8bc439b99ddaad327207b09ef1" - "6a8910828e805b0cc8c11fba5caea2ee939346d7ffffffff" - // Start TxIn1 - "45c866b219b17695" - "2508f8e5aea728f950186554fc4a5807e2186a8e1c4009e5000000008c493046" - "022100bd5d41662f98cfddc46e86ea7e4a3bc8fe9f1dfc5c4836eaf7df582596" - "cfe0e9022100fc459ae4f59b8279d679003b88935896acd10021b6e2e4619377" - "e336b5296c5e014104c00bab76a708ba7064b2315420a1c533ca9945eeff9754" - "cdc574224589e9113469b4e71752146a10028079e04948ecdf70609bf1b9801f" - "6b73ab75947ac339e5ffffffff" - // NumTxOut - "02" - // ... TxOuts fragged out - // Locktime - "00000000"); - - rawTxOut0_ = READHEX( - // Value - "ac4c8bd500000000" - // Script size (var_int) - "19" - // Script - "76""a9""14""8dce8946f1c7763bb60ea5cf16ef514cbed0633b""88""ac"); - rawTxOut1_ = READHEX( - // Value - "002f685900000000" - // Script size (var_int) - "19" - // Script - "76""a9""14""6a59ac0e8f553f292dfe5e9f3aaa1da93499c15e""88""ac"); - - bh_.unserialize(rawHead_); - tx1_.unserialize(rawTx0_); - tx2_.unserialize(rawTx1_); - - - sbh_.unserialize(rawHead_); - - // Make sure the global DB type and prune type are reset for each test - DBUtils.setArmoryDbType(ARMORY_DB_FULL); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - } - - BinaryData rawHead_; - BinaryData headHashLE_; - BinaryData headHashBE_; - - BinaryData rawBlock_; - - BinaryData rawTx0_; - BinaryData rawTx1_; - - BlockHeader bh_; - Tx tx1_; - Tx tx2_; - - BinaryData rawTxUnfrag_; - BinaryData rawTxFragged_; - BinaryData rawTxOut0_; - BinaryData rawTxOut1_; - - StoredHeader sbh_; - -}; - - -::testing::Environment* const btcenv = - ::testing::AddGlobalTestEnvironment(new BitcoinEnvironment); -*/ - - - - -//////////////////////////////////////////////////////////////////////////////// -class BinaryDataTest : public ::testing::Test -{ -protected: - virtual void SetUp(void) - { - str0_ = ""; - str4_ = "1234abcd"; - str5_ = "1234abcdef"; - - bd0_ = READHEX(str0_); - bd4_ = READHEX(str4_); - bd5_ = READHEX(str5_); - } - - string str0_; - string str4_; - string str5_; - - BinaryData bd0_; - BinaryData bd4_; - BinaryData bd5_; - -}; - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BinaryDataTest, Constructor) -{ - uint8_t* ptr = new uint8_t[4]; - - BinaryData a; - BinaryData b(4); - BinaryData c(ptr, 2); - BinaryData d(ptr, 4); - BinaryData e(b); - BinaryData f(string("xyza")); - - EXPECT_EQ(a.getSize(), 0); - EXPECT_EQ(b.getSize(), 4); - EXPECT_EQ(c.getSize(), 2); - EXPECT_EQ(d.getSize(), 4); - EXPECT_EQ(e.getSize(), 4); - EXPECT_EQ(f.getSize(), 4); - - EXPECT_TRUE( a.isNull()); - EXPECT_FALSE(b.isNull()); - EXPECT_FALSE(c.isNull()); - EXPECT_FALSE(d.isNull()); - EXPECT_FALSE(e.isNull()); - - BinaryDataRef g(f); - BinaryDataRef h(d); - BinaryData i(g); - - EXPECT_EQ( g.getSize(), 4); - EXPECT_EQ( i.getSize(), 4); - EXPECT_TRUE( g==f); - EXPECT_FALSE(g==h); - EXPECT_TRUE( i==g); - - delete[] ptr; -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BinaryDataTest, CopyFrom) -{ - BinaryData a,b,c,d,e,f; - a.copyFrom((uint8_t*)bd0_.getPtr(), bd0_.getSize()); - b.copyFrom((uint8_t*)bd4_.getPtr(), (uint8_t*)bd4_.getPtr()+4); - c.copyFrom((uint8_t*)bd4_.getPtr(), bd4_.getSize()); - d.copyFrom(str5_); - e.copyFrom(a); - - BinaryDataRef i(b); - f.copyFrom(i); - - EXPECT_EQ(a.getSize(), 0); - EXPECT_EQ(b.getSize(), 4); - EXPECT_EQ(c.getSize(), 4); - EXPECT_EQ(a,e); - EXPECT_EQ(b,c); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BinaryDataTest, CopyTo) -{ - BinaryData a,b,c,d,e,f,g,h; - bd0_.copyTo(a); - bd4_.copyTo(b); - - c.resize(bd5_.getSize()); - bd5_.copyTo(c.getPtr()); - - size_t sz = 2; - d.resize(sz); - e.resize(sz); - bd5_.copyTo(d.getPtr(), sz); - bd5_.copyTo(e.getPtr(), bd5_.getSize()-sz, sz); - - f.copyFrom(bd5_.getPtr(), bd5_.getPtr()+sz); - - EXPECT_TRUE(a==bd0_); - EXPECT_TRUE(b==bd4_); - EXPECT_TRUE(c==bd5_); - EXPECT_TRUE(bd5_.startsWith(d)); - EXPECT_TRUE(bd5_.endsWith(e)); - EXPECT_TRUE(d==f); - - EXPECT_EQ(a.getSize(), 0); - EXPECT_EQ(b.getSize(), 4); - EXPECT_EQ(c.getSize(), 5); - EXPECT_EQ(d.getSize(), 2); - EXPECT_NE(b,c); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BinaryDataTest, Fill) -{ - BinaryData a(0), b(1), c(4); - BinaryData aAns = READHEX(""); - BinaryData bAns = READHEX("aa"); - BinaryData cAns = READHEX("aaaaaaaa"); - - a.fill(0xaa); - b.fill(0xaa); - c.fill(0xaa); - - EXPECT_EQ(a, aAns); - EXPECT_EQ(b, bAns); - EXPECT_EQ(c, cAns); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BinaryDataTest, IndexOp) -{ - EXPECT_EQ(bd4_[0], 0x12); - EXPECT_EQ(bd4_[1], 0x34); - EXPECT_EQ(bd4_[2], 0xab); - EXPECT_EQ(bd4_[3], 0xcd); - - EXPECT_EQ(bd4_[-4], 0x12); - EXPECT_EQ(bd4_[-3], 0x34); - EXPECT_EQ(bd4_[-2], 0xab); - EXPECT_EQ(bd4_[-1], 0xcd); - - bd4_[1] = 0xff; - EXPECT_EQ(bd4_[0], 0x12); - EXPECT_EQ(bd4_[1], 0xff); - EXPECT_EQ(bd4_[2], 0xab); - EXPECT_EQ(bd4_[3], 0xcd); - - EXPECT_EQ(bd4_[-4], 0x12); - EXPECT_EQ(bd4_[-3], 0xff); - EXPECT_EQ(bd4_[-2], 0xab); - EXPECT_EQ(bd4_[-1], 0xcd); - - EXPECT_EQ(bd4_.toHexStr(), string("12ffabcd")); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BinaryDataTest, StartsEndsWith) -{ - BinaryData a = READHEX("abcd"); - EXPECT_TRUE( bd0_.startsWith(bd0_)); - EXPECT_TRUE( bd4_.startsWith(bd0_)); - EXPECT_TRUE( bd5_.startsWith(bd4_)); - EXPECT_TRUE( bd5_.startsWith(bd5_)); - EXPECT_FALSE(bd4_.startsWith(bd5_)); - EXPECT_TRUE( bd0_.startsWith(bd0_)); - EXPECT_FALSE(bd0_.startsWith(bd4_)); - EXPECT_FALSE(bd5_.endsWith(a)); - EXPECT_TRUE( bd4_.endsWith(a)); - EXPECT_FALSE(bd0_.endsWith(a)); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BinaryDataTest, Append) -{ - BinaryData a = READHEX("ef"); - - BinaryData static4 = bd4_; - - BinaryData b = bd4_ + a; - BinaryData c = bd4_.append(a); - - BinaryDataRef d(a); - bd4_.copyFrom(static4); - BinaryData e = bd4_.append(d); - bd4_.copyFrom(static4); - BinaryData f = bd4_.append(a.getPtr(), 1); - bd4_.copyFrom(static4); - BinaryData g = bd4_.append(0xef); - - BinaryData h = bd0_ + a; - BinaryData i = bd0_.append(a); - bd0_.resize(0); - BinaryData j = bd0_.append(a.getPtr(), 1); - bd0_.resize(0); - BinaryData k = bd0_.append(0xef); - - EXPECT_EQ(bd5_, b); - EXPECT_EQ(bd5_, c); - EXPECT_EQ(bd5_, e); - EXPECT_EQ(bd5_, f); - EXPECT_EQ(bd5_, g); - - EXPECT_NE(bd5_, h); - EXPECT_EQ(a, h); - EXPECT_EQ(a, i); - EXPECT_EQ(a, j); - EXPECT_EQ(a, k); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BinaryDataTest, Inequality) -{ - EXPECT_FALSE(bd0_ < bd0_); - EXPECT_TRUE( bd0_ < bd4_); - EXPECT_TRUE( bd0_ < bd5_); - - EXPECT_FALSE(bd4_ < bd0_); - EXPECT_FALSE(bd4_ < bd4_); - EXPECT_TRUE( bd4_ < bd5_); - - EXPECT_FALSE(bd5_ < bd0_); - EXPECT_FALSE(bd5_ < bd4_); - EXPECT_FALSE(bd5_ < bd5_); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BinaryDataTest, Equality) -{ - EXPECT_TRUE( bd0_==bd0_); - EXPECT_TRUE( bd4_==bd4_); - EXPECT_FALSE(bd4_==bd5_); - EXPECT_TRUE( bd0_!=bd4_); - EXPECT_TRUE( bd0_!=bd5_); - EXPECT_TRUE( bd4_!=bd5_); - EXPECT_FALSE(bd4_!=bd4_); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BinaryDataTest, ToString) -{ - EXPECT_EQ(bd0_.toHexStr(), str0_); - EXPECT_EQ(bd4_.toHexStr(), str4_); - EXPECT_EQ(bd4_.toHexStr(), str4_); - - string a,b; - bd0_.copyTo(a); - bd4_.copyTo(b); - EXPECT_EQ(bd0_.toBinStr(), a); - EXPECT_EQ(bd4_.toBinStr(), b); - - string stra("cdab3412"); - BinaryData bda = READHEX(stra); - - EXPECT_EQ(bd4_.toHexStr(true), stra); - EXPECT_EQ(bd4_.toBinStr(true), bda.toBinStr()); - -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BinaryDataTest, Endianness) -{ - BinaryData a = READHEX("cdab3412"); - BinaryData b = READHEX("1234cdab"); - - BinaryData static4 = bd4_; - - EXPECT_EQ( a.copySwapEndian(), bd4_); - EXPECT_EQ(bd4_.copySwapEndian(), a); - EXPECT_EQ(bd0_.copySwapEndian(), bd0_); - - - bd4_ = static4; - bd4_.swapEndian(); - EXPECT_EQ(bd4_, a); - - bd4_ = static4; - bd4_.swapEndian(2); - EXPECT_EQ(bd4_, b); - - bd4_ = static4; - bd4_.swapEndian(2,2); - EXPECT_EQ(bd4_, b); - - bd4_ = static4; - bd4_.swapEndian(2,4); - EXPECT_EQ(bd4_, b); -} - - -TEST_F(BinaryDataTest, IntToBinData) -{ - // 0x1234 in src code is always interpreted by the compiler as - // big-endian, regardless of the underlying architecture. So - // writing 0x1234 will be interpretted as an integer with value - // 4660 on all architectures. - BinaryData a,b; - - a = BinaryData::IntToStrLE(0xab); - b = BinaryData::IntToStrBE(0xab); - EXPECT_EQ(a, READHEX("ab")); - EXPECT_EQ(b, READHEX("ab")); - - a = BinaryData::IntToStrLE(0xabcd); - b = BinaryData::IntToStrBE(0xabcd); - EXPECT_EQ(a, READHEX("cdab")); - EXPECT_EQ(b, READHEX("abcd")); - - a = BinaryData::IntToStrLE((uint16_t)0xabcd); - b = BinaryData::IntToStrBE((uint16_t)0xabcd); - EXPECT_EQ(a, READHEX("cdab")); - EXPECT_EQ(b, READHEX("abcd")); - - // This fails b/c it auto "promotes" non-suffix literals to 4-byte ints - a = BinaryData::IntToStrLE(0xabcd); - b = BinaryData::IntToStrBE(0xabcd); - EXPECT_NE(a, READHEX("cdab")); - EXPECT_NE(b, READHEX("abcd")); - - a = BinaryData::IntToStrLE(0xfec38a11); - b = BinaryData::IntToStrBE(0xfec38a11); - EXPECT_EQ(a, READHEX("118ac3fe")); - EXPECT_EQ(b, READHEX("fec38a11")); - - a = BinaryData::IntToStrLE(0x00000000fec38a11ULL); - b = BinaryData::IntToStrBE(0x00000000fec38a11ULL); - EXPECT_EQ(a, READHEX("118ac3fe00000000")); - EXPECT_EQ(b, READHEX("00000000fec38a11")); - -} - -TEST_F(BinaryDataTest, BinDataToInt) -{ - uint8_t a8, b8; - uint16_t a16, b16; - uint32_t a32, b32; - uint64_t a64, b64; - - a8 = BinaryData::StrToIntBE(READHEX("ab")); - b8 = BinaryData::StrToIntLE(READHEX("ab")); - EXPECT_EQ(a8, 0xab); - EXPECT_EQ(b8, 0xab); - - a16 = BinaryData::StrToIntBE(READHEX("abcd")); - b16 = BinaryData::StrToIntLE(READHEX("abcd")); - EXPECT_EQ(a16, 0xabcd); - EXPECT_EQ(b16, 0xcdab); - - a32 = BinaryData::StrToIntBE(READHEX("fec38a11")); - b32 = BinaryData::StrToIntLE(READHEX("fec38a11")); - EXPECT_EQ(a32, 0xfec38a11); - EXPECT_EQ(b32, 0x118ac3fe); - - a64 = BinaryData::StrToIntBE(READHEX("00000000fec38a11")); - b64 = BinaryData::StrToIntLE(READHEX("00000000fec38a11")); - EXPECT_EQ(a64, 0x00000000fec38a11); - EXPECT_EQ(b64, 0x118ac3fe00000000); - - // These are really just identical tests, I have no idea whether it - // was worth spending the time to write these, and even this comment - // here explaining how it was probably a waste of time... - a8 = READ_UINT8_BE(READHEX("ab")); - b8 = READ_UINT8_LE(READHEX("ab")); - EXPECT_EQ(a8, 0xab); - EXPECT_EQ(b8, 0xab); - - a16 = READ_UINT16_BE(READHEX("abcd")); - b16 = READ_UINT16_LE(READHEX("abcd")); - EXPECT_EQ(a16, 0xabcd); - EXPECT_EQ(b16, 0xcdab); - - a32 = READ_UINT32_BE(READHEX("fec38a11")); - b32 = READ_UINT32_LE(READHEX("fec38a11")); - EXPECT_EQ(a32, 0xfec38a11); - EXPECT_EQ(b32, 0x118ac3fe); - - a64 = READ_UINT64_BE(READHEX("00000000fec38a11")); - b64 = READ_UINT64_LE(READHEX("00000000fec38a11")); - EXPECT_EQ(a64, 0x00000000fec38a11); - EXPECT_EQ(b64, 0x118ac3fe00000000); - - // Test the all-on-one read-int macros - a8 = READ_UINT8_HEX_BE("ab"); - b8 = READ_UINT8_HEX_LE("ab"); - EXPECT_EQ(a8, 0xab); - EXPECT_EQ(b8, 0xab); - - a16 = READ_UINT16_HEX_BE("abcd"); - b16 = READ_UINT16_HEX_LE("abcd"); - EXPECT_EQ(a16, 0xabcd); - EXPECT_EQ(b16, 0xcdab); - - a32 = READ_UINT32_HEX_BE("fec38a11"); - b32 = READ_UINT32_HEX_LE("fec38a11"); - EXPECT_EQ(a32, 0xfec38a11); - EXPECT_EQ(b32, 0x118ac3fe); - - a64 = READ_UINT64_HEX_BE("00000000fec38a11"); - b64 = READ_UINT64_HEX_LE("00000000fec38a11"); - EXPECT_EQ(a64, 0x00000000fec38a11); - EXPECT_EQ(b64, 0x118ac3fe00000000); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BinaryDataTest, Find) -{ - BinaryData a = READHEX("12"); - BinaryData b = READHEX("34"); - BinaryData c = READHEX("abcd"); - BinaryData d = READHEX("ff"); - - EXPECT_EQ(bd0_.find(bd0_), 0); - EXPECT_EQ(bd0_.find(bd4_), -1); - EXPECT_EQ(bd0_.find(bd4_, 2), -1); - EXPECT_EQ(bd4_.find(bd0_), 0); - EXPECT_EQ(bd4_.find(bd0_, 2), 2); - - EXPECT_EQ(bd4_.find(a), 0); - EXPECT_EQ(bd4_.find(b), 1); - EXPECT_EQ(bd4_.find(c), 2); - EXPECT_EQ(bd4_.find(d), -1); - - EXPECT_EQ(bd4_.find(a, 0), 0); - EXPECT_EQ(bd4_.find(b, 0), 1); - EXPECT_EQ(bd4_.find(c, 0), 2); - EXPECT_EQ(bd4_.find(d, 0), -1); - - EXPECT_EQ(bd4_.find(a, 1), -1); - EXPECT_EQ(bd4_.find(b, 1), 1); - EXPECT_EQ(bd4_.find(c, 1), 2); - EXPECT_EQ(bd4_.find(d, 1), -1); - - EXPECT_EQ(bd4_.find(a, 4), -1); - EXPECT_EQ(bd4_.find(b, 4), -1); - EXPECT_EQ(bd4_.find(c, 4), -1); - EXPECT_EQ(bd4_.find(d, 4), -1); - - EXPECT_EQ(bd4_.find(a, 8), -1); - EXPECT_EQ(bd4_.find(b, 8), -1); - EXPECT_EQ(bd4_.find(c, 8), -1); - EXPECT_EQ(bd4_.find(d, 8), -1); -} - - -TEST_F(BinaryDataTest, Contains) -{ - BinaryData a = READHEX("12"); - BinaryData b = READHEX("34"); - BinaryData c = READHEX("abcd"); - BinaryData d = READHEX("ff"); - - EXPECT_TRUE( bd0_.contains(bd0_)); - EXPECT_FALSE(bd0_.contains(bd4_)); - EXPECT_FALSE(bd0_.contains(bd4_, 2)); - - EXPECT_TRUE( bd4_.contains(a)); - EXPECT_TRUE( bd4_.contains(b)); - EXPECT_TRUE( bd4_.contains(c)); - EXPECT_FALSE(bd4_.contains(d)); - - EXPECT_TRUE( bd4_.contains(a, 0)); - EXPECT_TRUE( bd4_.contains(b, 0)); - EXPECT_TRUE( bd4_.contains(c, 0)); - EXPECT_FALSE(bd4_.contains(d, 0)); - - EXPECT_FALSE(bd4_.contains(a, 1)); - EXPECT_TRUE( bd4_.contains(b, 1)); - EXPECT_TRUE( bd4_.contains(c, 1)); - EXPECT_FALSE(bd4_.contains(d, 1)); - - EXPECT_FALSE(bd4_.contains(a, 4)); - EXPECT_FALSE(bd4_.contains(b, 4)); - EXPECT_FALSE(bd4_.contains(c, 4)); - EXPECT_FALSE(bd4_.contains(d, 4)); - - EXPECT_FALSE(bd4_.contains(a, 8)); - EXPECT_FALSE(bd4_.contains(b, 8)); - EXPECT_FALSE(bd4_.contains(c, 8)); - EXPECT_FALSE(bd4_.contains(d, 8)); -} - -//////////////////////////////////////////////////////////////////////////////// -//TEST_F(BinaryDataTest, GenerateRandom) -//{ - // Yeah, this would be a fun one to try to test... -//} - - -//////////////////////////////////////////////////////////////////////////////// -//TEST_F(BinaryDataTest, ReadFile) -//{ - //ofstream os("test -//} - - - -//////////////////////////////////////////////////////////////////////////////// -class BinaryDataRefTest : public ::testing::Test -{ -protected: - virtual void SetUp(void) - { - str0_ = ""; - str4_ = "1234abcd"; - str5_ = "1234abcdef"; - - bd0_ = READHEX(str0_); - bd4_ = READHEX(str4_); - bd5_ = READHEX(str5_); - - bdr__ = BinaryDataRef(); - bdr0_ = BinaryDataRef(bd0_); - bdr4_ = BinaryDataRef(bd4_); - bdr5_ = BinaryDataRef(bd5_); - } - - string str0_; - string str4_; - string str5_; - - BinaryData bd0_; - BinaryData bd4_; - BinaryData bd5_; - - BinaryDataRef bdr__; - BinaryDataRef bdr0_; - BinaryDataRef bdr4_; - BinaryDataRef bdr5_; -}; - - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BinaryDataRefTest, Constructor) -{ - BinaryDataRef a; - BinaryDataRef b((uint8_t*)bd0_.getPtr(), bd0_.getSize()); - BinaryDataRef c((uint8_t*)bd0_.getPtr(), (uint8_t*)bd0_.getPtr()); - BinaryDataRef d((uint8_t*)bd4_.getPtr(), bd4_.getSize()); - BinaryDataRef e((uint8_t*)bd4_.getPtr(), (uint8_t*)bd4_.getPtr()+4); - BinaryDataRef f(bd0_); - BinaryDataRef g(bd4_); - BinaryDataRef h(str0_); - BinaryDataRef i(str4_); - - EXPECT_TRUE(a.getPtr()==NULL); - EXPECT_EQ(a.getSize(), 0); - - EXPECT_TRUE(b.getPtr()==NULL); - EXPECT_EQ(b.getSize(), 0); - - EXPECT_TRUE(c.getPtr()==NULL); - EXPECT_EQ(c.getSize(), 0); - - EXPECT_FALSE(d.getPtr()==NULL); - EXPECT_EQ(d.getSize(), 4); - - EXPECT_FALSE(e.getPtr()==NULL); - EXPECT_EQ(e.getSize(), 4); - - EXPECT_TRUE(f.getPtr()==NULL); - EXPECT_EQ(f.getSize(), 0); - - EXPECT_FALSE(g.getPtr()==NULL); - EXPECT_EQ(g.getSize(), 4); - - EXPECT_TRUE(h.getPtr()==NULL); - EXPECT_EQ(h.getSize(), 0); - - EXPECT_FALSE(i.getPtr()==NULL); - EXPECT_EQ(i.getSize(), 8); - - EXPECT_TRUE( a.isNull()); - EXPECT_TRUE( b.isNull()); - EXPECT_TRUE( c.isNull()); - EXPECT_FALSE(d.isNull()); - EXPECT_FALSE(e.isNull()); - EXPECT_TRUE( f.isNull()); - EXPECT_FALSE(g.isNull()); - EXPECT_TRUE( h.isNull()); - EXPECT_FALSE(i.isNull()); -} - - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BinaryDataRefTest, PostConstruct) -{ - BinaryDataRef a,b,c,d,e,f,g,h,i; - - b.setRef((uint8_t*)bd0_.getPtr(), bd0_.getSize()); - c.setRef((uint8_t*)bd0_.getPtr(), (uint8_t*)bd0_.getPtr()); - d.setRef((uint8_t*)bd4_.getPtr(), bd4_.getSize()); - e.setRef((uint8_t*)bd4_.getPtr(), (uint8_t*)bd4_.getPtr()+4); - f.setRef(bd0_); - g.setRef(bd4_); - h.setRef(str0_); - i.setRef(str4_); - - EXPECT_TRUE(a.getPtr()==NULL); - EXPECT_EQ(a.getSize(), 0); - - EXPECT_TRUE(b.getPtr()==NULL); - EXPECT_EQ(b.getSize(), 0); - - EXPECT_TRUE(c.getPtr()==NULL); - EXPECT_EQ(c.getSize(), 0); - - EXPECT_FALSE(d.getPtr()==NULL); - EXPECT_EQ(d.getSize(), 4); - - EXPECT_FALSE(e.getPtr()==NULL); - EXPECT_EQ(e.getSize(), 4); - - EXPECT_TRUE(f.getPtr()==NULL); - EXPECT_EQ(f.getSize(), 0); - - EXPECT_FALSE(g.getPtr()==NULL); - EXPECT_EQ(g.getSize(), 4); - - EXPECT_FALSE(h.getPtr()==NULL); - EXPECT_EQ(h.getSize(), 0); - - EXPECT_FALSE(i.getPtr()==NULL); - EXPECT_EQ(i.getSize(), 8); - - EXPECT_TRUE( a.isNull()); - EXPECT_TRUE( b.isNull()); - EXPECT_TRUE( c.isNull()); - EXPECT_FALSE(d.isNull()); - EXPECT_FALSE(e.isNull()); - EXPECT_TRUE( f.isNull()); - EXPECT_FALSE(g.isNull()); - EXPECT_FALSE(h.isNull()); - EXPECT_FALSE(i.isNull()); -} - - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BinaryDataRefTest, CopyTo) -{ - BinaryData a,b,c,d,e,f,g,h; - bdr0_.copyTo(a); - bdr4_.copyTo(b); - - c.resize(bdr5_.getSize()); - bdr5_.copyTo(c.getPtr()); - - size_t sz = 2; - d.resize(sz); - e.resize(sz); - bdr5_.copyTo(d.getPtr(), sz); - bdr5_.copyTo(e.getPtr(), bdr5_.getSize()-sz, sz); - - f.copyFrom(bdr5_.getPtr(), bdr5_.getPtr()+sz); - - EXPECT_TRUE(a==bdr0_); - EXPECT_TRUE(b==bdr4_); - EXPECT_TRUE(c==bdr5_); - EXPECT_TRUE(bdr5_.startsWith(d)); - EXPECT_TRUE(bdr5_.endsWith(e)); - EXPECT_TRUE(d==f); - - EXPECT_EQ(a.getSize(), 0); - EXPECT_EQ(b.getSize(), 4); - EXPECT_EQ(c.getSize(), 5); - EXPECT_EQ(d.getSize(), 2); - EXPECT_NE(b,c); - - g = bdr0_.copy(); - h = bdr4_.copy(); - - EXPECT_EQ(g, bdr0_); - EXPECT_EQ(h, bdr4_); - EXPECT_EQ(g, bdr0_.copy()); - EXPECT_EQ(h, bdr4_.copy()); - - EXPECT_EQ(bdr0_, g); - EXPECT_EQ(bdr4_, h); - EXPECT_EQ(bdr0_.copy(), g); - EXPECT_EQ(bdr4_.copy(), h); -} - - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BinaryDataRefTest, ToString) -{ - EXPECT_EQ(bdr0_.toHexStr(), str0_); - EXPECT_EQ(bdr4_.toHexStr(), str4_); - EXPECT_EQ(bdr4_.toHexStr(), str4_); - - string a,b; - bdr0_.copyTo(a); - bdr4_.copyTo(b); - EXPECT_EQ(bd0_.toBinStr(), a); - EXPECT_EQ(bd4_.toBinStr(), b); - - string stra("cdab3412"); - BinaryData bda = READHEX(stra); - - EXPECT_EQ(bdr4_.toHexStr(true), stra); - EXPECT_EQ(bdr4_.toBinStr(true), bda.toBinStr()); - -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BinaryDataRefTest, Find) -{ - BinaryData a = READHEX("12"); - BinaryData b = READHEX("34"); - BinaryData c = READHEX("abcd"); - BinaryData d = READHEX("ff"); - - EXPECT_EQ(bdr0_.find(bdr0_), 0); - EXPECT_EQ(bdr0_.find(bdr4_), -1); - EXPECT_EQ(bdr0_.find(bdr4_, 2), -1); - EXPECT_EQ(bdr4_.find(bdr0_), 0); - EXPECT_EQ(bdr4_.find(bdr0_, 2), 2); - - EXPECT_EQ(bdr4_.find(a), 0); - EXPECT_EQ(bdr4_.find(b), 1); - EXPECT_EQ(bdr4_.find(c), 2); - EXPECT_EQ(bdr4_.find(d), -1); - - EXPECT_EQ(bdr4_.find(a, 0), 0); - EXPECT_EQ(bdr4_.find(b, 0), 1); - EXPECT_EQ(bdr4_.find(c, 0), 2); - EXPECT_EQ(bdr4_.find(d, 0), -1); - - EXPECT_EQ(bdr4_.find(a, 1), -1); - EXPECT_EQ(bdr4_.find(b, 1), 1); - EXPECT_EQ(bdr4_.find(c, 1), 2); - EXPECT_EQ(bdr4_.find(d, 1), -1); - - EXPECT_EQ(bdr4_.find(a, 4), -1); - EXPECT_EQ(bdr4_.find(b, 4), -1); - EXPECT_EQ(bdr4_.find(c, 4), -1); - EXPECT_EQ(bdr4_.find(d, 4), -1); - - EXPECT_EQ(bdr4_.find(a, 8), -1); - EXPECT_EQ(bdr4_.find(b, 8), -1); - EXPECT_EQ(bdr4_.find(c, 8), -1); - EXPECT_EQ(bdr4_.find(d, 8), -1); - - EXPECT_EQ(bdr4_.find(a.getRef(), 0), 0); - EXPECT_EQ(bdr4_.find(b.getRef(), 0), 1); - EXPECT_EQ(bdr4_.find(c.getRef(), 0), 2); - EXPECT_EQ(bdr4_.find(d.getRef(), 0), -1); -} - - -TEST_F(BinaryDataRefTest, Contains) -{ - BinaryData a = READHEX("12"); - BinaryData b = READHEX("34"); - BinaryData c = READHEX("abcd"); - BinaryData d = READHEX("ff"); - - EXPECT_TRUE( bdr0_.contains(bdr0_)); - EXPECT_FALSE(bdr0_.contains(bdr4_)); - EXPECT_FALSE(bdr0_.contains(bdr4_, 2)); - - EXPECT_TRUE( bdr4_.contains(a)); - EXPECT_TRUE( bdr4_.contains(b)); - EXPECT_TRUE( bdr4_.contains(c)); - EXPECT_FALSE(bdr4_.contains(d)); - - EXPECT_TRUE( bdr4_.contains(a, 0)); - EXPECT_TRUE( bdr4_.contains(b, 0)); - EXPECT_TRUE( bdr4_.contains(c, 0)); - EXPECT_FALSE(bdr4_.contains(d, 0)); - - EXPECT_FALSE(bdr4_.contains(a, 1)); - EXPECT_TRUE( bdr4_.contains(b, 1)); - EXPECT_TRUE( bdr4_.contains(c, 1)); - EXPECT_FALSE(bdr4_.contains(d, 1)); - - EXPECT_FALSE(bdr4_.contains(a, 4)); - EXPECT_FALSE(bdr4_.contains(b, 4)); - EXPECT_FALSE(bdr4_.contains(c, 4)); - EXPECT_FALSE(bdr4_.contains(d, 4)); - - EXPECT_FALSE(bdr4_.contains(a, 8)); - EXPECT_FALSE(bdr4_.contains(b, 8)); - EXPECT_FALSE(bdr4_.contains(c, 8)); - EXPECT_FALSE(bdr4_.contains(d, 8)); - - EXPECT_TRUE( bdr4_.contains(a.getRef(), 0)); - EXPECT_TRUE( bdr4_.contains(b.getRef(), 0)); - EXPECT_TRUE( bdr4_.contains(c.getRef(), 0)); - EXPECT_FALSE(bdr4_.contains(d.getRef(), 0)); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BinaryDataRefTest, StartsEndsWith) -{ - BinaryData a = READHEX("abcd"); - EXPECT_TRUE( bdr0_.startsWith(bdr0_)); - EXPECT_TRUE( bdr4_.startsWith(bdr0_)); - EXPECT_TRUE( bdr5_.startsWith(bdr4_)); - EXPECT_TRUE( bdr5_.startsWith(bdr5_)); - EXPECT_FALSE(bdr4_.startsWith(bdr5_)); - EXPECT_TRUE( bdr0_.startsWith(bdr0_)); - EXPECT_FALSE(bdr0_.startsWith(bdr4_)); - - EXPECT_TRUE( bdr0_.startsWith(bd0_)); - EXPECT_TRUE( bdr4_.startsWith(bd0_)); - EXPECT_TRUE( bdr5_.startsWith(bd4_)); - EXPECT_TRUE( bdr5_.startsWith(bd5_)); - EXPECT_FALSE(bdr4_.startsWith(bd5_)); - EXPECT_TRUE( bdr0_.startsWith(bd0_)); - EXPECT_FALSE(bdr0_.startsWith(bd4_)); - EXPECT_FALSE(bdr5_.endsWith(a)); - EXPECT_TRUE( bdr4_.endsWith(a)); - EXPECT_FALSE(bdr0_.endsWith(a)); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BinaryDataRefTest, Inequality) -{ - EXPECT_FALSE(bdr0_ < bdr0_); - EXPECT_TRUE( bdr0_ < bdr4_); - EXPECT_TRUE( bdr0_ < bdr5_); - - EXPECT_FALSE(bdr4_ < bdr0_); - EXPECT_FALSE(bdr4_ < bdr4_); - EXPECT_TRUE( bdr4_ < bdr5_); - - EXPECT_FALSE(bdr5_ < bdr0_); - EXPECT_FALSE(bdr5_ < bdr4_); - EXPECT_FALSE(bdr5_ < bdr5_); - - EXPECT_FALSE(bdr0_ < bd0_); - EXPECT_TRUE( bdr0_ < bd4_); - EXPECT_TRUE( bdr0_ < bd5_); - - EXPECT_FALSE(bdr4_ < bd0_); - EXPECT_FALSE(bdr4_ < bd4_); - EXPECT_TRUE( bdr4_ < bd5_); - - EXPECT_FALSE(bdr5_ < bd0_); - EXPECT_FALSE(bdr5_ < bd4_); - EXPECT_FALSE(bdr5_ < bd5_); - - EXPECT_FALSE(bdr0_ > bdr0_); - EXPECT_TRUE( bdr4_ > bdr0_); - EXPECT_TRUE( bdr5_ > bdr0_); - - EXPECT_FALSE(bdr0_ > bdr4_); - EXPECT_FALSE(bdr4_ > bdr4_); - EXPECT_TRUE( bdr5_ > bdr4_); - - EXPECT_FALSE(bdr0_ > bdr5_); - EXPECT_FALSE(bdr4_ > bdr5_); - EXPECT_FALSE(bdr5_ > bdr5_); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BinaryDataRefTest, Equality) -{ - EXPECT_TRUE( bdr0_==bdr0_); - EXPECT_TRUE( bdr4_==bdr4_); - EXPECT_FALSE(bdr4_==bdr5_); - EXPECT_TRUE( bdr0_!=bdr4_); - EXPECT_TRUE( bdr0_!=bdr5_); - EXPECT_TRUE( bdr4_!=bdr5_); - EXPECT_FALSE(bdr4_!=bdr4_); - - EXPECT_TRUE( bdr0_==bd0_); - EXPECT_TRUE( bdr4_==bd4_); - EXPECT_FALSE(bdr4_==bd5_); - EXPECT_TRUE( bdr0_!=bd4_); - EXPECT_TRUE( bdr0_!=bd5_); - EXPECT_TRUE( bdr4_!=bd5_); - EXPECT_FALSE(bdr4_!=bd4_); -} - - -//////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -TEST(BitReadWriteTest, Writer8) -{ - BitPacker bitp; - - //EXPECT_EQ( bitp.getValue(), 0); - EXPECT_EQ( bitp.getBitsUsed(), 0); - EXPECT_EQ( bitp.getBinaryData(), READHEX("00")); - - bitp.putBit(true); - //EXPECT_EQ( bitp.getValue(), 128); - EXPECT_EQ( bitp.getBitsUsed(), 1); - EXPECT_EQ( bitp.getBinaryData(), READHEX("80")); - - bitp.putBit(false); - //EXPECT_EQ( bitp.getValue(), 128); - EXPECT_EQ( bitp.getBitsUsed(), 2); - EXPECT_EQ( bitp.getBinaryData(), READHEX("80")); - - bitp.putBit(true); - //EXPECT_EQ( bitp.getValue(), 160); - EXPECT_EQ( bitp.getBitsUsed(), 3); - EXPECT_EQ( bitp.getBinaryData(), READHEX("a0")); - - bitp.putBits(0, 2); - //EXPECT_EQ( bitp.getValue(), 160); - EXPECT_EQ( bitp.getBitsUsed(), 5); - EXPECT_EQ( bitp.getBinaryData(), READHEX("a0")); - - bitp.putBits(3, 3); - //EXPECT_EQ( bitp.getValue(), 163); - EXPECT_EQ( bitp.getBitsUsed(), 8); - EXPECT_EQ( bitp.getBinaryData(), READHEX("a3")); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST(BitReadWriteTest, Writer16) -{ - BitPacker bitp; - - //EXPECT_EQ( bitp.getValue(), 0); - EXPECT_EQ( bitp.getBitsUsed(), 0); - EXPECT_EQ( bitp.getBinaryData(), READHEX("0000")); - - bitp.putBit(true); - //EXPECT_EQ( bitp.getValue(), 0x8000); - EXPECT_EQ( bitp.getBitsUsed(), 1); - EXPECT_EQ( bitp.getBinaryData(), READHEX("8000")); - - bitp.putBit(false); - //EXPECT_EQ( bitp.getValue(), 0x8000); - EXPECT_EQ( bitp.getBitsUsed(), 2); - EXPECT_EQ( bitp.getBinaryData(), READHEX("8000")); - - bitp.putBit(true); - //EXPECT_EQ( bitp.getValue(), 0xa000); - EXPECT_EQ( bitp.getBitsUsed(), 3); - EXPECT_EQ( bitp.getBinaryData(), READHEX("a000")); - - bitp.putBits(0, 2); - //EXPECT_EQ( bitp.getValue(), 0xa000); - EXPECT_EQ( bitp.getBitsUsed(), 5); - EXPECT_EQ( bitp.getBinaryData(), READHEX("a000")); - - bitp.putBits(3, 3); - //EXPECT_EQ( bitp.getValue(), 0xa300); - EXPECT_EQ( bitp.getBitsUsed(), 8); - EXPECT_EQ( bitp.getBinaryData(), READHEX("a300")); - - bitp.putBits(3, 8); - //EXPECT_EQ( bitp.getValue(), 0xa303); - EXPECT_EQ( bitp.getBitsUsed(), 16); - EXPECT_EQ( bitp.getBinaryData(), READHEX("a303")); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST(BitReadWriteTest, Writer32) -{ - BitPacker bitp; - - bitp.putBits(0xffffff00, 32); - //EXPECT_EQ( bitp.getValue(), 0xffffff00); - EXPECT_EQ( bitp.getBitsUsed(), 32); - EXPECT_EQ( bitp.getBinaryData(), READHEX("ffffff00")); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST(BitReadWriteTest, Writer64) -{ - BitPacker bitp; - - bitp.putBits(0xffffff00ffffffaaULL, 64); - //EXPECT_EQ( bitp.getValue(), 0xffffff00ffffffaaULL); - EXPECT_EQ( bitp.getBitsUsed(), 64); - EXPECT_EQ( bitp.getBinaryData(), READHEX("ffffff00ffffffaa")); - - BitPacker bitp2; - bitp2.putBits(0xff, 32); - bitp2.putBits(0xff, 32); - //EXPECT_EQ( bitp2.getValue(), 0x000000ff000000ffULL); - EXPECT_EQ( bitp2.getBitsUsed(), 64); - EXPECT_EQ( bitp2.getBinaryData(), READHEX("000000ff000000ff")); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST(BitReadWriteTest, Reader8) -{ - BitUnpacker bitu; - - bitu.setValue(0xa3); - EXPECT_TRUE( bitu.getBit()); - EXPECT_FALSE(bitu.getBit()); - EXPECT_TRUE( bitu.getBit()); - EXPECT_EQ( bitu.getBits(2), 0); - EXPECT_EQ( bitu.getBits(3), 3); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST(BitReadWriteTest, Reader16) -{ - BitUnpacker bitu; - - bitu.setValue(0xa303); - - EXPECT_TRUE( bitu.getBit()); - EXPECT_FALSE(bitu.getBit()); - EXPECT_TRUE( bitu.getBit()); - EXPECT_EQ( bitu.getBits(2), 0); - EXPECT_EQ( bitu.getBits(3), 3); - EXPECT_EQ( bitu.getBits(8), 3); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST(BitReadWriteTest, Reader32) -{ - BitUnpacker bitu(0xffffff00); - EXPECT_EQ(bitu.getBits(32), 0xffffff00); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST(BitReadWriteTest, Reader64) -{ - BitUnpacker bitu(0xffffff00ffffffaaULL); - EXPECT_EQ( bitu.getBits(64), 0xffffff00ffffffaaULL); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST(BinaryReadWriteTest, Writer) -{ - BinaryData out = READHEX("01""0100""013200aa""ff00ff00ff00ff00" - "ab""fdffff""fe013200aa""ffff00ff00ff00ff00"); - - BinaryWriter bw; - bw.put_uint8_t(1); EXPECT_EQ(bw.getSize(), 1); - bw.put_uint16_t(1); EXPECT_EQ(bw.getSize(), 3); - bw.put_uint32_t(0xaa003201); EXPECT_EQ(bw.getSize(), 7); - bw.put_uint64_t(0x00ff00ff00ff00ffULL); EXPECT_EQ(bw.getSize(), 15); - bw.put_var_int(0xab); EXPECT_EQ(bw.getSize(), 16); - bw.put_var_int(0xffff); EXPECT_EQ(bw.getSize(), 19); - bw.put_var_int(0xaa003201); EXPECT_EQ(bw.getSize(), 24); - bw.put_var_int(0x00ff00ff00ff00ffULL); EXPECT_EQ(bw.getSize(), 33); - - EXPECT_EQ(bw.getData(), out); - EXPECT_EQ(bw.getDataRef(), out.getRef()); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST(BinaryReadWriteTest, WriterEndian) -{ - BinaryData out = READHEX("01""0100""013200aa""ff00ff00ff00ff00" - "ab""fdffff""fe013200aa""ffff00ff00ff00ff00"); - - BinaryWriter bw; - bw.put_uint8_t(1); EXPECT_EQ(bw.getSize(), 1); - bw.put_uint16_t(0x0100, BE); EXPECT_EQ(bw.getSize(), 3); - bw.put_uint32_t(0x013200aa, BE); EXPECT_EQ(bw.getSize(), 7); - bw.put_uint64_t(0xff00ff00ff00ff00ULL, BE); EXPECT_EQ(bw.getSize(), 15); - bw.put_var_int(0xab); EXPECT_EQ(bw.getSize(), 16); - bw.put_var_int(0xffff); EXPECT_EQ(bw.getSize(), 19); - bw.put_var_int(0xaa003201); EXPECT_EQ(bw.getSize(), 24); - bw.put_var_int(0x00ff00ff00ff00ffULL); EXPECT_EQ(bw.getSize(), 33); - EXPECT_EQ(bw.getData(), out); - EXPECT_EQ(bw.getDataRef(), out.getRef()); - - BinaryWriter bw2; - bw2.put_uint8_t(1); EXPECT_EQ(bw2.getSize(), 1); - bw2.put_uint16_t(0x0001, LE); EXPECT_EQ(bw2.getSize(), 3); - bw2.put_uint32_t(0xaa003201, LE); EXPECT_EQ(bw2.getSize(), 7); - bw2.put_uint64_t(0x00ff00ff00ff00ffULL, LE); EXPECT_EQ(bw2.getSize(), 15); - bw2.put_var_int(0xab); EXPECT_EQ(bw2.getSize(), 16); - bw2.put_var_int(0xffff); EXPECT_EQ(bw2.getSize(), 19); - bw2.put_var_int(0xaa003201); EXPECT_EQ(bw2.getSize(), 24); - bw2.put_var_int(0x00ff00ff00ff00ffULL); EXPECT_EQ(bw2.getSize(), 33); - EXPECT_EQ(bw2.getData(), out); - EXPECT_EQ(bw2.getDataRef(), out.getRef()); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST(BinaryReadWriteTest, Reader) -{ - BinaryData in = READHEX("01""0100""013200aa""ff00ff00ff00ff00" - "ab""fdffff""fe013200aa""ffff00ff00ff00ff00"); - - BinaryReader br(in); - EXPECT_EQ(br.get_uint8_t(), 1); - EXPECT_EQ(br.get_uint16_t(), 1); - EXPECT_EQ(br.get_uint32_t(), 0xaa003201); - EXPECT_EQ(br.get_uint64_t(), 0x00ff00ff00ff00ffULL); - EXPECT_EQ(br.get_var_int(), 0xab); - EXPECT_EQ(br.get_var_int(), 0xffff); - EXPECT_EQ(br.get_var_int(), 0xaa003201); - EXPECT_EQ(br.get_var_int(), 0x00ff00ff00ff00ffULL); - - BinaryRefReader brr(in); - EXPECT_EQ(brr.get_uint8_t(), 1); - EXPECT_EQ(brr.get_uint16_t(), 1); - EXPECT_EQ(brr.get_uint32_t(), 0xaa003201); - EXPECT_EQ(brr.get_uint64_t(), 0x00ff00ff00ff00ffULL); - EXPECT_EQ(brr.get_var_int(), 0xab); - EXPECT_EQ(brr.get_var_int(), 0xffff); - EXPECT_EQ(brr.get_var_int(), 0xaa003201); - EXPECT_EQ(brr.get_var_int(), 0x00ff00ff00ff00ffULL); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST(BinaryReadWriteTest, ReaderEndian) -{ - BinaryData in = READHEX("01""0100""013200aa""ff00ff00ff00ff00" - "ab""fdffff""fe013200aa""ffff00ff00ff00ff00"); - - BinaryReader br(in); - EXPECT_EQ(br.get_uint8_t(LE), 1); - EXPECT_EQ(br.get_uint16_t(LE), 1); - EXPECT_EQ(br.get_uint32_t(LE), 0xaa003201); - EXPECT_EQ(br.get_uint64_t(LE), 0x00ff00ff00ff00ffULL); - EXPECT_EQ(br.get_var_int(), 0xab); - EXPECT_EQ(br.get_var_int(), 0xffff); - EXPECT_EQ(br.get_var_int(), 0xaa003201); - EXPECT_EQ(br.get_var_int(), 0x00ff00ff00ff00ffULL); - - BinaryRefReader brr(in); - EXPECT_EQ(brr.get_uint8_t(LE), 1); - EXPECT_EQ(brr.get_uint16_t(LE), 1); - EXPECT_EQ(brr.get_uint32_t(LE), 0xaa003201); - EXPECT_EQ(brr.get_uint64_t(LE), 0x00ff00ff00ff00ffULL); - EXPECT_EQ(brr.get_var_int(), 0xab); - EXPECT_EQ(brr.get_var_int(), 0xffff); - EXPECT_EQ(brr.get_var_int(), 0xaa003201); - EXPECT_EQ(brr.get_var_int(), 0x00ff00ff00ff00ffULL); - - BinaryReader br2(in); - EXPECT_EQ(br2.get_uint8_t(LITTLEENDIAN), 1); - EXPECT_EQ(br2.get_uint16_t(LITTLEENDIAN), 1); - EXPECT_EQ(br2.get_uint32_t(LITTLEENDIAN), 0xaa003201); - EXPECT_EQ(br2.get_uint64_t(LITTLEENDIAN), 0x00ff00ff00ff00ffULL); - EXPECT_EQ(br2.get_var_int(), 0xab); - EXPECT_EQ(br2.get_var_int(), 0xffff); - EXPECT_EQ(br2.get_var_int(), 0xaa003201); - EXPECT_EQ(br2.get_var_int(), 0x00ff00ff00ff00ffULL); - - BinaryRefReader brr2(in); - EXPECT_EQ(brr2.get_uint8_t(LITTLEENDIAN), 1); - EXPECT_EQ(brr2.get_uint16_t(LITTLEENDIAN), 1); - EXPECT_EQ(brr2.get_uint32_t(LITTLEENDIAN), 0xaa003201); - EXPECT_EQ(brr2.get_uint64_t(LITTLEENDIAN), 0x00ff00ff00ff00ffULL); - EXPECT_EQ(brr2.get_var_int(), 0xab); - EXPECT_EQ(brr2.get_var_int(), 0xffff); - EXPECT_EQ(brr2.get_var_int(), 0xaa003201); - EXPECT_EQ(brr2.get_var_int(), 0x00ff00ff00ff00ffULL); - - BinaryReader brBE(in); - EXPECT_EQ(brBE.get_uint8_t(BE), 1); - EXPECT_EQ(brBE.get_uint16_t(BE), 0x0100); - EXPECT_EQ(brBE.get_uint32_t(BE), 0x013200aa); - EXPECT_EQ(brBE.get_uint64_t(BE), 0xff00ff00ff00ff00ULL); - EXPECT_EQ(brBE.get_var_int(), 0xab); - EXPECT_EQ(brBE.get_var_int(), 0xffff); - EXPECT_EQ(brBE.get_var_int(), 0xaa003201); - EXPECT_EQ(brBE.get_var_int(), 0x00ff00ff00ff00ffULL); - - BinaryRefReader brrBE(in); - EXPECT_EQ(brrBE.get_uint8_t(BE), 1); - EXPECT_EQ(brrBE.get_uint16_t(BE), 0x0100); - EXPECT_EQ(brrBE.get_uint32_t(BE), 0x013200aa); - EXPECT_EQ(brrBE.get_uint64_t(BE), 0xff00ff00ff00ff00ULL); - EXPECT_EQ(brrBE.get_var_int(), 0xab); - EXPECT_EQ(brrBE.get_var_int(), 0xffff); - EXPECT_EQ(brrBE.get_var_int(), 0xaa003201); - EXPECT_EQ(brrBE.get_var_int(), 0x00ff00ff00ff00ffULL); - - BinaryReader brBE2(in); - EXPECT_EQ(brBE2.get_uint8_t(BIGENDIAN), 1); - EXPECT_EQ(brBE2.get_uint16_t(BIGENDIAN), 0x0100); - EXPECT_EQ(brBE2.get_uint32_t(BIGENDIAN), 0x013200aa); - EXPECT_EQ(brBE2.get_uint64_t(BIGENDIAN), 0xff00ff00ff00ff00ULL); - EXPECT_EQ(brBE2.get_var_int(), 0xab); - EXPECT_EQ(brBE2.get_var_int(), 0xffff); - EXPECT_EQ(brBE2.get_var_int(), 0xaa003201); - EXPECT_EQ(brBE2.get_var_int(), 0x00ff00ff00ff00ffULL); - - BinaryRefReader brrBE2(in); - EXPECT_EQ(brrBE2.get_uint8_t(BIGENDIAN), 1); - EXPECT_EQ(brrBE2.get_uint16_t(BIGENDIAN), 0x0100); - EXPECT_EQ(brrBE2.get_uint32_t(BIGENDIAN), 0x013200aa); - EXPECT_EQ(brrBE2.get_uint64_t(BIGENDIAN), 0xff00ff00ff00ff00ULL); - EXPECT_EQ(brrBE2.get_var_int(), 0xab); - EXPECT_EQ(brrBE2.get_var_int(), 0xffff); - EXPECT_EQ(brrBE2.get_var_int(), 0xaa003201); - EXPECT_EQ(brrBE2.get_var_int(), 0x00ff00ff00ff00ffULL); -} - -//////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -class BtcUtilsTest : public ::testing::Test -{ -protected: - virtual void SetUp(void) - { - rawHead_ = READHEX( - "010000001d8f4ec0443e1f19f305e488c1085c95de7cc3fd25e0d2c5bb5d0000" - "000000009762547903d36881a86751f3f5049e23050113f779735ef82734ebf0" - "b4450081d8c8c84db3936a1a334b035b"); - headHashLE_ = READHEX( - "1195e67a7a6d0674bbd28ae096d602e1f038c8254b49dfe79d47000000000000"); - headHashBE_ = READHEX( - "000000000000479de7df494b25c838f0e102d696e08ad2bb74066d7a7ae69511"); - - satoshiPubKey_ = READHEX( "04" - "fc9702847840aaf195de8442ebecedf5b095cdbb9bc716bda9110971b28a49e0" - "ead8564ff0db22209e0374782c093bb899692d524e9d6a6956e7c5ecbcd68284"); - satoshiHash160_ = READHEX("65a4358f4691660849d9f235eb05f11fabbd69fa"); - - prevHashCB_ = READHEX( - "0000000000000000000000000000000000000000000000000000000000000000"); - prevHashReg_ = READHEX( - "894862e362905c6075074d9ec4b4e2dc34720089b1e9ef4738ee1b13f3bdcdb7"); - } - - BinaryData rawHead_; - BinaryData headHashLE_; - BinaryData headHashBE_; - - BinaryData satoshiPubKey_; - BinaryData satoshiHash160_; - - BinaryData prevHashCB_; - BinaryData prevHashReg_; -}; - - - - -TEST_F(BtcUtilsTest, ReadVarInt) -{ - BinaryData vi0 = READHEX("00"); - BinaryData vi1 = READHEX("21"); - BinaryData vi3 = READHEX("fdff00"); - BinaryData vi5 = READHEX("fe00000100"); - BinaryData vi9 = READHEX("ff0010a5d4e8000000"); - - uint64_t v = 0; - uint64_t w = 33; - uint64_t x = 255; - uint64_t y = 65536; - uint64_t z = 1000000000000ULL; - - BinaryRefReader brr; - pair a; - - brr.setNewData(vi0); - a = BtcUtils::readVarInt(brr); - EXPECT_EQ(a.first, v); - EXPECT_EQ(a.second, 1); - - brr.setNewData(vi1); - a = BtcUtils::readVarInt(brr); - EXPECT_EQ(a.first, w); - EXPECT_EQ(a.second, 1); - - brr.setNewData(vi3); - a = BtcUtils::readVarInt(brr); - EXPECT_EQ(a.first, x); - EXPECT_EQ(a.second, 3); - - brr.setNewData(vi5); - a = BtcUtils::readVarInt(brr); - EXPECT_EQ(a.first, y); - EXPECT_EQ(a.second, 5); - - brr.setNewData(vi9); - a = BtcUtils::readVarInt(brr); - EXPECT_EQ(a.first, z); - EXPECT_EQ(a.second, 9); - - // Just the length - EXPECT_EQ(BtcUtils::readVarIntLength(vi0.getPtr()), 1); - EXPECT_EQ(BtcUtils::readVarIntLength(vi1.getPtr()), 1); - EXPECT_EQ(BtcUtils::readVarIntLength(vi3.getPtr()), 3); - EXPECT_EQ(BtcUtils::readVarIntLength(vi5.getPtr()), 5); - EXPECT_EQ(BtcUtils::readVarIntLength(vi9.getPtr()), 9); - - EXPECT_EQ(BtcUtils::calcVarIntSize(v), 1); - EXPECT_EQ(BtcUtils::calcVarIntSize(w), 1); - EXPECT_EQ(BtcUtils::calcVarIntSize(x), 3); - EXPECT_EQ(BtcUtils::calcVarIntSize(y), 5); - EXPECT_EQ(BtcUtils::calcVarIntSize(z), 9); -} - - -TEST_F(BtcUtilsTest, Num2Str) -{ - EXPECT_EQ(BtcUtils::numToStrWCommas(0), string("0")); - EXPECT_EQ(BtcUtils::numToStrWCommas(100), string("100")); - EXPECT_EQ(BtcUtils::numToStrWCommas(-100), string("-100")); - EXPECT_EQ(BtcUtils::numToStrWCommas(999), string("999")); - EXPECT_EQ(BtcUtils::numToStrWCommas(1234), string("1,234")); - EXPECT_EQ(BtcUtils::numToStrWCommas(-1234), string("-1,234")); - EXPECT_EQ(BtcUtils::numToStrWCommas(12345678), string("12,345,678")); - EXPECT_EQ(BtcUtils::numToStrWCommas(-12345678), string("-12,345,678")); -} - - - -TEST_F(BtcUtilsTest, PackBits) -{ - list::iterator iter, iter2; - list bitList; - - bitList = BtcUtils::UnpackBits( READHEX("00"), 0); - EXPECT_EQ(bitList.size(), 0); - - bitList = BtcUtils::UnpackBits( READHEX("00"), 3); - EXPECT_EQ(bitList.size(), 3); - iter = bitList.begin(); - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - - - bitList = BtcUtils::UnpackBits( READHEX("00"), 8); - EXPECT_EQ(bitList.size(), 8); - iter = bitList.begin(); - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - - bitList = BtcUtils::UnpackBits( READHEX("017f"), 8); - EXPECT_EQ(bitList.size(), 8); - iter = bitList.begin(); - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_TRUE( *iter); iter++; - - - bitList = BtcUtils::UnpackBits( READHEX("017f"), 12); - EXPECT_EQ(bitList.size(), 12); - iter = bitList.begin(); - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_TRUE( *iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_TRUE( *iter); iter++; - EXPECT_TRUE( *iter); iter++; - EXPECT_TRUE( *iter); iter++; - - bitList = BtcUtils::UnpackBits( READHEX("017f"), 16); - EXPECT_EQ(bitList.size(), 16); - iter = bitList.begin(); - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_TRUE( *iter); iter++; - EXPECT_FALSE(*iter); iter++; - EXPECT_TRUE( *iter); iter++; - EXPECT_TRUE( *iter); iter++; - EXPECT_TRUE( *iter); iter++; - EXPECT_TRUE( *iter); iter++; - EXPECT_TRUE( *iter); iter++; - EXPECT_TRUE( *iter); iter++; - EXPECT_TRUE( *iter); iter++; - - - BinaryData packed; - packed = BtcUtils::PackBits(bitList); - EXPECT_EQ(packed, READHEX("017f")); - - bitList = BtcUtils::UnpackBits( READHEX("017f"), 12); - packed = BtcUtils::PackBits(bitList); - EXPECT_EQ(packed, READHEX("0170")); -} - - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BtcUtilsTest, SimpleHash) -{ - BinaryData hashOut; - - // sha256(sha256(X)); - BtcUtils::getHash256(rawHead_.getPtr(), rawHead_.getSize(), hashOut); - EXPECT_EQ(hashOut, headHashLE_); - EXPECT_EQ(hashOut, headHashBE_.copySwapEndian()); - - BtcUtils::getHash256_NoSafetyCheck(rawHead_.getPtr(), rawHead_.getSize(), hashOut); - EXPECT_EQ(hashOut, headHashLE_); - EXPECT_EQ(hashOut, headHashBE_.copySwapEndian()); - - hashOut = BtcUtils::getHash256(rawHead_.getPtr(), rawHead_.getSize()); - EXPECT_EQ(hashOut, headHashLE_); - - BtcUtils::getHash256(rawHead_, hashOut); - EXPECT_EQ(hashOut, headHashLE_); - - BtcUtils::getHash256(rawHead_.getRef(), hashOut); - EXPECT_EQ(hashOut, headHashLE_); - - hashOut = BtcUtils::getHash256(rawHead_); - EXPECT_EQ(hashOut, headHashLE_); - - - // ripemd160(sha256(X)); - BtcUtils::getHash160(satoshiPubKey_.getPtr(), satoshiPubKey_.getSize(), hashOut); - EXPECT_EQ(hashOut, satoshiHash160_); - - BtcUtils::getHash160(satoshiPubKey_.getPtr(), satoshiPubKey_.getSize(), hashOut); - EXPECT_EQ(hashOut, satoshiHash160_); - - hashOut = BtcUtils::getHash160(satoshiPubKey_.getPtr(), satoshiPubKey_.getSize()); - EXPECT_EQ(hashOut, satoshiHash160_); - - BtcUtils::getHash160(satoshiPubKey_, hashOut); - EXPECT_EQ(hashOut, satoshiHash160_); - - BtcUtils::getHash160(satoshiPubKey_.getRef(), hashOut); - EXPECT_EQ(hashOut, satoshiHash160_); - - hashOut = BtcUtils::getHash160(satoshiPubKey_); - EXPECT_EQ(hashOut, satoshiHash160_); -} - - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BtcUtilsTest, TxOutScriptID_Hash160) -{ - //TXOUT_SCRIPT_STDHASH160, - //TXOUT_SCRIPT_STDPUBKEY65, - //TXOUT_SCRIPT_STDPUBKEY33, - //TXOUT_SCRIPT_MULTISIG, - //TXOUT_SCRIPT_P2SH, - //TXOUT_SCRIPT_NONSTANDARD, - - BinaryData script = READHEX("76a914a134408afa258a50ed7a1d9817f26b63cc9002cc88ac"); - BinaryData a160 = READHEX( "a134408afa258a50ed7a1d9817f26b63cc9002cc"); - BinaryData unique = READHEX("00a134408afa258a50ed7a1d9817f26b63cc9002cc"); - TXOUT_SCRIPT_TYPE scrType = BtcUtils::getTxOutScriptType(script); - EXPECT_EQ(scrType, TXOUT_SCRIPT_STDHASH160 ); - EXPECT_EQ(BtcUtils::getTxOutRecipientAddr(script), a160 ); - EXPECT_EQ(BtcUtils::getTxOutRecipientAddr(script, scrType), a160 ); - EXPECT_EQ(BtcUtils::getTxOutScrAddr(script), unique ); - EXPECT_EQ(BtcUtils::getTxOutScrAddr(script, scrType), unique ); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BtcUtilsTest, TxOutScriptID_PubKey65) -{ - BinaryData script = READHEX( - "4104b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb1" - "6e6537a576782eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7bac"); - BinaryData a160 = READHEX( "e24b86bff5112623ba67c63b6380636cbdf1a66d"); - BinaryData unique = READHEX("00e24b86bff5112623ba67c63b6380636cbdf1a66d"); - TXOUT_SCRIPT_TYPE scrType = BtcUtils::getTxOutScriptType(script); - EXPECT_EQ(scrType, TXOUT_SCRIPT_STDPUBKEY65 ); - EXPECT_EQ(BtcUtils::getTxOutRecipientAddr(script), a160 ); - EXPECT_EQ(BtcUtils::getTxOutRecipientAddr(script, scrType), a160 ); - EXPECT_EQ(BtcUtils::getTxOutScrAddr(script), unique ); - EXPECT_EQ(BtcUtils::getTxOutScrAddr(script, scrType), unique ); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BtcUtilsTest, TxOutScriptID_PubKey33) -{ - BinaryData script = READHEX( - "21024005c945d86ac6b01fb04258345abea7a845bd25689edb723d5ad4068ddd3036ac"); - BinaryData a160 = READHEX( "0c1b83d01d0ffb2bccae606963376cca3863a7ce"); - BinaryData unique = READHEX("000c1b83d01d0ffb2bccae606963376cca3863a7ce"); - TXOUT_SCRIPT_TYPE scrType = BtcUtils::getTxOutScriptType(script); - EXPECT_EQ(scrType, TXOUT_SCRIPT_STDPUBKEY33 ); - EXPECT_EQ(BtcUtils::getTxOutRecipientAddr(script), a160 ); - EXPECT_EQ(BtcUtils::getTxOutRecipientAddr(script, scrType), a160 ); - EXPECT_EQ(BtcUtils::getTxOutScrAddr(script), unique ); - EXPECT_EQ(BtcUtils::getTxOutScrAddr(script, scrType), unique ); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BtcUtilsTest, TxOutScriptID_NonStd) -{ - // This was from block 150951 which was erroneously produced by MagicalTux - // This is not only non-standard, it's non-spendable - BinaryData script = READHEX("76a90088ac"); - BinaryData a160 = BtcUtils::BadAddress_; - BinaryData unique = READHEX("ff") + BtcUtils::getHash160(READHEX("76a90088ac")); - TXOUT_SCRIPT_TYPE scrType = BtcUtils::getTxOutScriptType(script); - EXPECT_EQ(scrType, TXOUT_SCRIPT_NONSTANDARD ); - EXPECT_EQ(BtcUtils::getTxOutRecipientAddr(script), a160 ); - EXPECT_EQ(BtcUtils::getTxOutRecipientAddr(script, scrType), a160 ); - EXPECT_EQ(BtcUtils::getTxOutScrAddr(script), unique ); - EXPECT_EQ(BtcUtils::getTxOutScrAddr(script, scrType), unique ); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BtcUtilsTest, TxOutScriptID_P2SH) -{ - // P2SH script from tx: 4ac04b4830d115eb9a08f320ef30159cc107dfb72b29bbc2f370093f962397b4 (TxOut: 1) - // Spent in tx: fd16d6bbf1a3498ca9777b9d31ceae883eb8cb6ede1fafbdd218bae107de66fe (TxIn: 1) - // P2SH address: 3Lip6sxQymNr9LD2cAVp6wLrw8xdKBdYFG - // Hash160: d0c15a7d41500976056b3345f542d8c944077c8a - BinaryData script = READHEX("a914d0c15a7d41500976056b3345f542d8c944077c8a87"); // send to P2SH - BinaryData a160 = READHEX( "d0c15a7d41500976056b3345f542d8c944077c8a"); - BinaryData unique = READHEX("05d0c15a7d41500976056b3345f542d8c944077c8a"); - TXOUT_SCRIPT_TYPE scrType = BtcUtils::getTxOutScriptType(script); - EXPECT_EQ(scrType, TXOUT_SCRIPT_P2SH); - EXPECT_EQ(BtcUtils::getTxOutRecipientAddr(script), a160 ); - EXPECT_EQ(BtcUtils::getTxOutRecipientAddr(script, scrType), a160 ); - EXPECT_EQ(BtcUtils::getTxOutScrAddr(script), unique ); - EXPECT_EQ(BtcUtils::getTxOutScrAddr(script, scrType), unique ); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BtcUtilsTest, TxOutScriptID_Multisig) -{ - BinaryData script = READHEX( - "5221034758cefcb75e16e4dfafb32383b709fa632086ea5ca982712de6add93" - "060b17a2103fe96237629128a0ae8c3825af8a4be8fe3109b16f62af19cec0b1" - "eb93b8717e252ae"); - BinaryData pub1 = READHEX("034758cefcb75e16e4dfafb32383b709fa632086ea5ca982712de6add93060b17a"); - BinaryData pub2 = READHEX("03fe96237629128a0ae8c3825af8a4be8fe3109b16f62af19cec0b1eb93b8717e2"); - BinaryData addr1 = READHEX("785652a6b8e721e80ffa353e5dfd84f0658284a9"); - BinaryData addr2 = READHEX("b3348abf9dd2d1491359f937e2af64b1bb6d525a"); - BinaryData a160 = BtcUtils::BadAddress_; - BinaryData unique = READHEX( - "fe0202785652a6b8e721e80ffa353e5dfd84f0658284a9b3348abf9dd2d14913" - "59f937e2af64b1bb6d525a"); - - TXOUT_SCRIPT_TYPE scrType = BtcUtils::getTxOutScriptType(script); - EXPECT_EQ(scrType, TXOUT_SCRIPT_MULTISIG); - EXPECT_EQ(BtcUtils::getTxOutRecipientAddr(script), a160 ); - EXPECT_EQ(BtcUtils::getTxOutRecipientAddr(script, scrType), a160 ); - EXPECT_EQ(BtcUtils::getTxOutScrAddr(script), unique ); - EXPECT_EQ(BtcUtils::getTxOutScrAddr(script, scrType), unique ); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BtcUtilsTest, TxOutScriptID_MultiList) -{ - BinaryData script = READHEX( - "5221034758cefcb75e16e4dfafb32383b709fa632086ea5ca982712de6add930" - "60b17a2103fe96237629128a0ae8c3825af8a4be8fe3109b16f62af19cec0b1e" - "b93b8717e252ae"); - BinaryData addr0 = READHEX("785652a6b8e721e80ffa353e5dfd84f0658284a9"); - BinaryData addr1 = READHEX("b3348abf9dd2d1491359f937e2af64b1bb6d525a"); - BinaryData a160 = BtcUtils::BadAddress_; - BinaryData unique = READHEX( - "fe0202785652a6b8e721e80ffa353e5dfd84f0658284a9b3348abf9dd2d14913" - "59f937e2af64b1bb6d525a"); - - vector a160List; - uint32_t M; - - M = BtcUtils::getMultisigAddrList(script, a160List); - EXPECT_EQ(M, 2); - EXPECT_EQ(a160List.size(), 2); // N - - EXPECT_EQ(a160List[0], addr0); - EXPECT_EQ(a160List[1], addr1); -} - - -//TEST_F(BtcUtilsTest, TxInScriptID) -//{ - //TXIN_SCRIPT_STDUNCOMPR, - //TXIN_SCRIPT_STDCOMPR, - //TXIN_SCRIPT_COINBASE, - //TXIN_SCRIPT_SPENDPUBKEY, - //TXIN_SCRIPT_SPENDMULTI, - //TXIN_SCRIPT_SPENDP2SH, - //TXIN_SCRIPT_NONSTANDARD -//} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BtcUtilsTest, TxInScriptID_StdUncompr) -{ - BinaryData script = READHEX( - "493046022100b9daf2733055be73ae00ee0c5d78ca639d554fe779f163396c1a" - "39b7913e7eac02210091f0deeb2e510c74354afb30cc7d8fbac81b1ca8b39406" - "13379adc41a6ffd226014104b1537fa5bc2242d25ebf54f31e76ebabe0b3de4a" - "4dccd9004f058d6c2caa5d31164252e1e04e5df627fae7adec27fa9d40c271fc" - "4d30ff375ef6b26eba192bac"); - BinaryData a160 = READHEX("c42a8290196b2c5bcb35471b45aa0dc096baed5e"); - BinaryData prevHash = prevHashReg_; - - TXIN_SCRIPT_TYPE scrType = BtcUtils::getTxInScriptType( script, prevHash); - EXPECT_EQ(scrType, TXIN_SCRIPT_STDUNCOMPR); - EXPECT_EQ(BtcUtils::getTxInAddr(script, prevHash), a160); - EXPECT_EQ(BtcUtils::getTxInAddr(script, prevHash, scrType), a160); - EXPECT_EQ(BtcUtils::getTxInAddrFromType(script, scrType), a160); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BtcUtilsTest, TxInScriptID_StdCompr) -{ - BinaryData script = READHEX( - "47304402205299224886e5e3402b0e9fa3527bcfe1d73c4e2040f18de8dd17f1" - "16e3365a1102202590dcc16c4b711daae6c37977ba579ca65bcaa8fba2bd7168" - "a984be727ccf7a01210315122ff4d41d9fe3538a0a8c6c7f813cf12a901069a4" - "3d6478917246dc92a782"); - BinaryData a160 = READHEX("03214fc1433a287e964d6c4242093c34e4ed0001"); - BinaryData prevHash = prevHashReg_; - - TXIN_SCRIPT_TYPE scrType = BtcUtils::getTxInScriptType(script, prevHash); - EXPECT_EQ(scrType, TXIN_SCRIPT_STDCOMPR); - EXPECT_EQ(BtcUtils::getTxInAddr(script, prevHash), a160); - EXPECT_EQ(BtcUtils::getTxInAddr(script, prevHash, scrType), a160); - EXPECT_EQ(BtcUtils::getTxInAddrFromType(script, scrType), a160); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BtcUtilsTest, TxInScriptID_Coinbase) -{ - BinaryData script = READHEX( - "0310920304000071c3124d696e656420627920425443204775696c640800b75f950e000000"); - BinaryData a160 = BtcUtils::BadAddress_; - BinaryData prevHash = prevHashCB_; - - TXIN_SCRIPT_TYPE scrType = BtcUtils::getTxInScriptType(script, prevHash); - EXPECT_EQ(scrType, TXIN_SCRIPT_COINBASE); - EXPECT_EQ(BtcUtils::getTxInAddr(script, prevHash), a160); - EXPECT_EQ(BtcUtils::getTxInAddr(script, prevHash, scrType), a160); - EXPECT_EQ(BtcUtils::getTxInAddrFromType(script, scrType), a160); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BtcUtilsTest, TxInScriptID_SpendPubKey) -{ - BinaryData script = READHEX( - "47304402201ffc44394e5a3dd9c8b55bdc12147e18574ac945d15dac026793bf" - "3b8ff732af022035fd832549b5176126f735d87089c8c1c1319447a458a09818" - "e173eaf0c2eef101"); - BinaryData a160 = BtcUtils::BadAddress_; - BinaryData prevHash = prevHashReg_; - - TXIN_SCRIPT_TYPE scrType = BtcUtils::getTxInScriptType(script, prevHash); - EXPECT_EQ(scrType, TXIN_SCRIPT_SPENDPUBKEY); - EXPECT_EQ(BtcUtils::getTxInAddr(script, prevHash), a160); - EXPECT_EQ(BtcUtils::getTxInAddr(script, prevHash, scrType), a160); - EXPECT_EQ(BtcUtils::getTxInAddrFromType(script, scrType), a160); - //txInHash160s.push_back( READHEX("957efec6af757ccbbcf9a436f0083c5ddaa3bf1d")); // this one can't be determined -} - - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BtcUtilsTest, TxInScriptID_SpendMultisig) -{ - - BinaryData script = READHEX( - "004830450221009254113fa46918f299b1d18ec918613e56cffbeba0960db05f" - "66b51496e5bf3802201e229de334bd753a2b08b36cc3f38f5263a23e9714a737" - "520db45494ec095ce80148304502206ee62f539d5cd94f990b7abfda77750f58" - "ff91043c3f002501e5448ef6dba2520221009d29229cdfedda1dd02a1a90bb71" - "b30b77e9c3fc28d1353f054c86371f6c2a8101"); - BinaryData a160 = BtcUtils::BadAddress_; - BinaryData prevHash = prevHashReg_; - TXIN_SCRIPT_TYPE scrType = BtcUtils::getTxInScriptType(script, prevHash); - EXPECT_EQ(scrType, TXIN_SCRIPT_SPENDMULTI); - EXPECT_EQ(BtcUtils::getTxInAddr(script, prevHash), a160); - EXPECT_EQ(BtcUtils::getTxInAddr(script, prevHash, scrType), a160); - EXPECT_EQ(BtcUtils::getTxInAddrFromType(script, scrType), a160); - - - vector scrParts = BtcUtils::splitPushOnlyScriptRefs(script); - BinaryData zero = READHEX("00"); - BinaryData sig1 = READHEX( - "30450221009254113fa46918f299b1d18ec918613e56cffbeba0960db05f66b5" - "1496e5bf3802201e229de334bd753a2b08b36cc3f38f5263a23e9714a737520d" - "b45494ec095ce801"); - BinaryData sig2 = READHEX( - "304502206ee62f539d5cd94f990b7abfda77750f58ff91043c3f002501e5448e" - "f6dba2520221009d29229cdfedda1dd02a1a90bb71b30b77e9c3fc28d1353f05" - "4c86371f6c2a8101"); - - EXPECT_EQ(scrParts.size(), 3); - EXPECT_EQ(scrParts[0], zero); - EXPECT_EQ(scrParts[1], sig1); - EXPECT_EQ(scrParts[2], sig2); - - //BinaryData p2sh = READHEX("5221034758cefcb75e16e4dfafb32383b709fa632086ea5ca982712de6add93060b17a2103fe96237629128a0ae8c3825af8a4be8fe3109b16f62af19cec0b1eb93b8717e252ae"); - //BinaryData pub1 = READHEX("034758cefcb75e16e4dfafb32383b709fa632086ea5ca982712de6add93060b17a"); - //BinaryData pub1 = READHEX("03fe96237629128a0ae8c3825af8a4be8fe3109b16f62af19cec0b1eb93b8717e2"); - - -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BtcUtilsTest, TxInScriptID_SpendP2SH) -{ - - // Spending P2SH output as above: fd16d6bbf1a3498ca9777b9d31ceae883eb8cb6ede1fafbdd218bae107de66fe (TxIn: 1, 219 B) - // Leading 0x00 byte is required due to a bug in OP_CHECKMULTISIG - BinaryData script = READHEX( - "004830450221009254113fa46918f299b1d18ec918613e56cffbeba0960db05f" - "66b51496e5bf3802201e229de334bd753a2b08b36cc3f38f5263a23e9714a737" - "520db45494ec095ce80148304502206ee62f539d5cd94f990b7abfda77750f58" - "ff91043c3f002501e5448ef6dba2520221009d29229cdfedda1dd02a1a90bb71" - "b30b77e9c3fc28d1353f054c86371f6c2a8101475221034758cefcb75e16e4df" - "afb32383b709fa632086ea5ca982712de6add93060b17a2103fe96237629128a" - "0ae8c3825af8a4be8fe3109b16f62af19cec0b1eb93b8717e252ae"); - BinaryData a160 = READHEX("d0c15a7d41500976056b3345f542d8c944077c8a"); - BinaryData prevHash = prevHashReg_; - TXIN_SCRIPT_TYPE scrType = BtcUtils::getTxInScriptType(script, prevHash); - EXPECT_EQ(scrType, TXIN_SCRIPT_SPENDP2SH); - EXPECT_EQ(BtcUtils::getTxInAddr(script, prevHash), a160); - EXPECT_EQ(BtcUtils::getTxInAddr(script, prevHash, scrType), a160); - EXPECT_EQ(BtcUtils::getTxInAddrFromType(script, scrType), a160); -} - - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BtcUtilsTest, BitsToDifficulty) -{ - - double a = BtcUtils::convertDiffBitsToDouble(READHEX("ffff001d")); - double b = BtcUtils::convertDiffBitsToDouble(READHEX("be2f021a")); - double c = BtcUtils::convertDiffBitsToDouble(READHEX("3daa011a")); - - EXPECT_DOUBLE_EQ(a, 1.0); - EXPECT_DOUBLE_EQ(b, 7672999.920164138); - EXPECT_DOUBLE_EQ(c, 10076292.883418716); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BtcUtilsTest, ScriptToOpCodes) -{ - BinaryData complexScript = READHEX( - "526b006b7dac7ca9143cd1def404e12a85ead2b4d3f5f9f817fb0d46ef879a6c" - "936b7dac7ca9146a4e7d5f798e90e84db9244d4805459f87275943879a6c936b" - "7dac7ca914486efdd300987a054510b4ce1148d4ad290d911e879a6c936b6c6ca2"); - - vector opstr; - opstr.reserve(40); - opstr.push_back(string("OP_2")); - opstr.push_back(string("OP_TOALTSTACK")); - opstr.push_back(string("OP_0")); - opstr.push_back(string("OP_TOALTSTACK")); - opstr.push_back(string("OP_TUCK")); - opstr.push_back(string("OP_CHECKSIG")); - opstr.push_back(string("OP_SWAP")); - opstr.push_back(string("OP_HASH160")); - opstr.push_back(string("[PUSHDATA -- 20 BYTES:]")); - opstr.push_back(string("3cd1def404e12a85ead2b4d3f5f9f817fb0d46ef")); - opstr.push_back(string("OP_EQUAL")); - opstr.push_back(string("OP_BOOLAND")); - opstr.push_back(string("OP_FROMALTSTACK")); - opstr.push_back(string("OP_ADD")); - opstr.push_back(string("OP_TOALTSTACK")); - opstr.push_back(string("OP_TUCK")); - opstr.push_back(string("OP_CHECKSIG")); - opstr.push_back(string("OP_SWAP")); - opstr.push_back(string("OP_HASH160")); - opstr.push_back(string("[PUSHDATA -- 20 BYTES:]")); - opstr.push_back(string("6a4e7d5f798e90e84db9244d4805459f87275943")); - opstr.push_back(string("OP_EQUAL")); - opstr.push_back(string("OP_BOOLAND")); - opstr.push_back(string("OP_FROMALTSTACK")); - opstr.push_back(string("OP_ADD")); - opstr.push_back(string("OP_TOALTSTACK")); - opstr.push_back(string("OP_TUCK")); - opstr.push_back(string("OP_CHECKSIG")); - opstr.push_back(string("OP_SWAP")); - opstr.push_back(string("OP_HASH160")); - opstr.push_back(string("[PUSHDATA -- 20 BYTES:]")); - opstr.push_back(string("486efdd300987a054510b4ce1148d4ad290d911e")); - opstr.push_back(string("OP_EQUAL")); - opstr.push_back(string("OP_BOOLAND")); - opstr.push_back(string("OP_FROMALTSTACK")); - opstr.push_back(string("OP_ADD")); - opstr.push_back(string("OP_TOALTSTACK")); - opstr.push_back(string("OP_FROMALTSTACK")); - opstr.push_back(string("OP_FROMALTSTACK")); - opstr.push_back(string("OP_GREATERTHANOREQUAL")); - - vector output = BtcUtils::convertScriptToOpStrings(complexScript); - ASSERT_EQ(output.size(), opstr.size()); - for(uint32_t i=0; i txins(7); - txins[0] = TxIn(rawTxIn_.getPtr()); - txins[1] = TxIn(rawTxIn_.getPtr(), len); - txins[2] = TxIn(rawTxIn_.getPtr(), len, TxRef(), 12); - txins[3].unserialize(rawTxIn_.getPtr()); - txins[4].unserialize(rawTxIn_.getRef()); - txins[5].unserialize(brr); - txins[6].unserialize_swigsafe_(rawTxIn_); - - for(uint32_t i=0; i<7; i++) - { - EXPECT_TRUE( txins[i].isInitialized()); - EXPECT_EQ( txins[i].serialize().getSize(), len); - EXPECT_EQ( txins[i].getScriptType(), TXIN_SCRIPT_STDUNCOMPR); - EXPECT_EQ( txins[i].getScriptSize(), len-(36+1+4)); - EXPECT_TRUE( txins[i].isStandard()); - EXPECT_FALSE(txins[i].isCoinbase()); - EXPECT_EQ( txins[i].getSequence(), UINT32_MAX); - EXPECT_EQ( txins[i].getSenderScrAddrIfAvail(), srcAddr); - EXPECT_EQ( txins[i].getOutPoint().serialize(), rawOP); - - EXPECT_FALSE(txins[i].getParentTxRef().isInitialized()); - EXPECT_EQ( txins[i].getParentHeight(), UINT32_MAX); - EXPECT_EQ( txins[i].getParentHash(), BinaryData(0)); - EXPECT_EQ( txins[i].serialize(), rawTxIn_); - if(i==2) - EXPECT_EQ(txins[i].getIndex(), 12); - else - EXPECT_EQ(txins[i].getIndex(), UINT32_MAX); - } -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockObjTest, TxOutUnserialize) -{ - BinaryRefReader brr(rawTxOut_); - uint32_t len = rawTxOut_.getSize(); - BinaryData dstAddr = READHEX("8dce8946f1c7763bb60ea5cf16ef514cbed0633b"); - - vector txouts(7); - txouts[0] = TxOut(rawTxOut_.getPtr()); - txouts[1] = TxOut(rawTxOut_.getPtr(), len); - txouts[2] = TxOut(rawTxOut_.getPtr(), len, TxRef(), 12); - txouts[3].unserialize(rawTxOut_.getPtr()); - txouts[4].unserialize(rawTxOut_.getRef()); - txouts[5].unserialize(brr); - txouts[6].unserialize_swigsafe_(rawTxOut_); - - for(uint32_t i=0; i<7; i++) - { - EXPECT_TRUE( txouts[i].isInitialized()); - EXPECT_EQ( txouts[i].getSize(), len); - EXPECT_EQ( txouts[i].getScriptType(), TXOUT_SCRIPT_STDHASH160); - EXPECT_EQ( txouts[i].getScriptSize(), 25); - EXPECT_TRUE( txouts[i].isStandard()); - EXPECT_EQ( txouts[i].getValue(), 0x00000000d58b4cac); - EXPECT_EQ( txouts[i].getScrAddressStr(), HASH160PREFIX+dstAddr); - - EXPECT_TRUE( txouts[i].isScriptStandard()); - EXPECT_TRUE( txouts[i].isScriptStdHash160()); - EXPECT_FALSE(txouts[i].isScriptStdPubKey65()); - EXPECT_FALSE(txouts[i].isScriptStdPubKey33()); - EXPECT_FALSE(txouts[i].isScriptP2SH()); - EXPECT_FALSE(txouts[i].isScriptNonStd()); - - EXPECT_FALSE(txouts[i].getParentTxRef().isInitialized()); - EXPECT_EQ( txouts[i].getParentHeight(), UINT32_MAX); - EXPECT_EQ( txouts[i].getParentHash(), BinaryData(0)); - EXPECT_EQ( txouts[i].serialize(), rawTxOut_); - if(i==2) - EXPECT_EQ(txouts[i].getIndex(), 12); - else - EXPECT_EQ(txouts[i].getIndex(), UINT32_MAX); - } -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockObjTest, TxNoInit) -{ - Tx tx; - - EXPECT_FALSE(tx.isInitialized()); - - // Actually, why even bother with all these no-init tests? We should always - // check whether the tx is initialized before using it. If you don't, you - // deserve to seg fault :) - //EXPECT_EQ( tx.getSize(), UINT32_MAX); - //EXPECT_TRUE( tx.isStandard()); - //EXPECT_EQ( tx.getValue(), 0x00000000d58b4cac); - //EXPECT_EQ( tx.getRecipientAddr(), dstAddr); - - //EXPECT_TRUE( tx.isScriptStandard()); - //EXPECT_TRUE( tx.isScriptStdHash160()); - //EXPECT_FALSE(tx.isScriptStdPubKey65()); - //EXPECT_FALSE(tx.isScriptStdPubKey33()); - //EXPECT_FALSE(tx.isScriptP2SH()); - //EXPECT_FALSE(tx.isScriptNonStd()); - -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockObjTest, TxUnserialize) -{ - uint32_t len = rawTx0_.getSize(); - BinaryData tx0hash = READHEX( - "aa739836a44451be555f74a02f088b50a867b1d3a2c917ee863d708ec2db58f6"); - - BinaryData tx0_In0 = READHEX("aff189b24a36a1b93de2ea4d157c13d18251270a"); - BinaryData tx0_Out0 = READHEX("c1b4695d53b6ee57a28647ce63e45665df6762c2"); - BinaryData tx0_Out1 = READHEX("0e0aec36fe2545fb31a41164fb6954adcd96b342"); - BinaryData tx0_Val0 = READHEX("42582c0a00000000"); - BinaryData tx0_Val1 = READHEX("80d1f00800000000"); - BinaryRefReader brr(rawTx0_); - - uint64_t v0 = *(uint64_t*)tx0_Val0.getPtr(); - uint64_t v1 = *(uint64_t*)tx0_Val1.getPtr(); - - Tx tx; - vector txs(10); - txs[0] = Tx(rawTx0_.getPtr()); - txs[1] = Tx(brr); brr.resetPosition(); - txs[2] = Tx(rawTx0_); - txs[3] = Tx(rawTx0_.getRef()); - txs[4].unserialize(rawTx0_.getPtr()); - txs[5].unserialize(rawTx0_); - txs[6].unserialize(rawTx0_.getRef()); - txs[7].unserialize(brr); brr.resetPosition(); - txs[8].unserialize_swigsafe_(rawTx0_); - txs[9] = Tx::createFromStr(rawTx0_); - - for(uint32_t i=0; i<10; i++) - { - EXPECT_TRUE( txs[i].isInitialized()); - EXPECT_EQ( txs[i].getSize(), len); - - EXPECT_EQ( txs[i].getVersion(), 1); - EXPECT_EQ( txs[i].getNumTxIn(), 1); - EXPECT_EQ( txs[i].getNumTxOut(), 2); - EXPECT_EQ( txs[i].getThisHash(), tx0hash.copySwapEndian()); - //EXPECT_FALSE(txs[i].isMainBranch()); - - EXPECT_EQ( txs[i].getTxInOffset(0), 5); - EXPECT_EQ( txs[i].getTxInOffset(1), 185); - EXPECT_EQ( txs[i].getTxOutOffset(0), 186); - EXPECT_EQ( txs[i].getTxOutOffset(1), 220); - EXPECT_EQ( txs[i].getTxOutOffset(2), 254); - - EXPECT_EQ( txs[i].getLockTime(), 0); - - EXPECT_EQ( txs[i].serialize(), rawTx0_); - EXPECT_EQ( txs[0].getTxInCopy(0).getSenderScrAddrIfAvail(), tx0_In0); - EXPECT_EQ( txs[i].getTxOutCopy(0).getScrAddressStr(), HASH160PREFIX+tx0_Out0); - EXPECT_EQ( txs[i].getTxOutCopy(1).getScrAddressStr(), HASH160PREFIX+tx0_Out1); - EXPECT_EQ( txs[i].getScrAddrForTxOut(0), HASH160PREFIX+tx0_Out0); - EXPECT_EQ( txs[i].getScrAddrForTxOut(1), HASH160PREFIX+tx0_Out1); - EXPECT_EQ( txs[i].getTxOutCopy(0).getValue(), v0); - EXPECT_EQ( txs[i].getTxOutCopy(1).getValue(), v1); - EXPECT_EQ( txs[i].getSumOfOutputs(), v0+v1); - - EXPECT_EQ( txs[i].getBlockTxIndex(), UINT16_MAX); - } -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockObjTest, DISABLED_FullBlock) -{ - EXPECT_TRUE(false); - - BinaryRefReader brr(rawBlock_); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockObjTest, DISABLED_TxIOPairStuff) -{ - EXPECT_TRUE(false); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockObjTest, DISABLED_RegisteredTxStuff) -{ - EXPECT_TRUE(false); -} - - - -//////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -class StoredBlockObjTest : public ::testing::Test -{ -protected: - virtual void SetUp(void) - { - rawHead_ = READHEX( - "01000000" - "1d8f4ec0443e1f19f305e488c1085c95de7cc3fd25e0d2c5bb5d000000000000" - "9762547903d36881a86751f3f5049e23050113f779735ef82734ebf0b4450081" - "d8c8c84d" - "b3936a1a" - "334b035b"); - headHashLE_ = READHEX( - "1195e67a7a6d0674bbd28ae096d602e1f038c8254b49dfe79d47000000000000"); - headHashBE_ = READHEX( - "000000000000479de7df494b25c838f0e102d696e08ad2bb74066d7a7ae69511"); - - rawTx0_ = READHEX( - "01000000016290dce984203b6a5032e543e9e272d8bce934c7de4d15fa0fe44d" - "d49ae4ece9010000008b48304502204f2fa458d439f957308bca264689aa175e" - "3b7c5f78a901cb450ebd20936b2c500221008ea3883a5b80128e55c9c6070aa6" - "264e1e0ce3d18b7cd7e85108ce3d18b7419a0141044202550a5a6d3bb81549c4" - "a7803b1ad59cdbba4770439a4923624a8acfc7d34900beb54a24188f7f0a4068" - "9d905d4847cc7d6c8d808a457d833c2d44ef83f76bffffffff0242582c0a0000" - "00001976a914c1b4695d53b6ee57a28647ce63e45665df6762c288ac80d1f008" - "000000001976a9140e0aec36fe2545fb31a41164fb6954adcd96b34288ac0000" - "0000"); - rawTx1_ = READHEX( - "0100000001f658dbc28e703d86ee17c9a2d3b167a8508b082fa0745f55be5144" - "a4369873aa010000008c49304602210041e1186ca9a41fdfe1569d5d807ca7ff" - "6c5ffd19d2ad1be42f7f2a20cdc8f1cc0221003366b5d64fe81e53910e156914" - "091d12646bc0d1d662b7a65ead3ebe4ab8f6c40141048d103d81ac9691cf13f3" - "fc94e44968ef67b27f58b27372c13108552d24a6ee04785838f34624b294afee" - "83749b64478bb8480c20b242c376e77eea2b3dc48b4bffffffff0200e1f50500" - "0000001976a9141b00a2f6899335366f04b277e19d777559c35bc888ac40aeeb" - "02000000001976a9140e0aec36fe2545fb31a41164fb6954adcd96b34288ac00" - "000000"); - - rawBlock_ = READHEX( - "01000000eb10c9a996a2340a4d74eaab41421ed8664aa49d18538bab59010000" - "000000005a2f06efa9f2bd804f17877537f2080030cadbfa1eb50e02338117cc" - "604d91b9b7541a4ecfbb0a1a64f1ade703010000000100000000000000000000" - "00000000000000000000000000000000000000000000ffffffff0804cfbb0a1a" - "02360affffffff0100f2052a01000000434104c2239c4eedb3beb26785753463" - "be3ec62b82f6acd62efb65f452f8806f2ede0b338e31d1f69b1ce449558d7061" - "aa1648ddc2bf680834d3986624006a272dc21cac000000000100000003e8caa1" - "2bcb2e7e86499c9de49c45c5a1c6167ea4b894c8c83aebba1b6100f343010000" - "008c493046022100e2f5af5329d1244807f8347a2c8d9acc55a21a5db769e927" - "4e7e7ba0bb605b26022100c34ca3350df5089f3415d8af82364d7f567a6a297f" - "cc2c1d2034865633238b8c014104129e422ac490ddfcb7b1c405ab9fb4244124" - "6c4bca578de4f27b230de08408c64cad03af71ee8a3140b40408a7058a1984a9" - "f246492386113764c1ac132990d1ffffffff5b55c18864e16c08ef9989d31c7a" - "343e34c27c30cd7caa759651b0e08cae0106000000008c4930460221009ec9aa" - "3e0caf7caa321723dea561e232603e00686d4bfadf46c5c7352b07eb00022100" - "a4f18d937d1e2354b2e69e02b18d11620a6a9332d563e9e2bbcb01cee559680a" - "014104411b35dd963028300e36e82ee8cf1b0c8d5bf1fc4273e970469f5cb931" - "ee07759a2de5fef638961726d04bd5eb4e5072330b9b371e479733c942964bb8" - "6e2b22ffffffff3de0c1e913e6271769d8c0172cea2f00d6d3240afc3a20f9fa" - "247ce58af30d2a010000008c493046022100b610e169fd15ac9f60fe2b507529" - "281cf2267673f4690ba428cbb2ba3c3811fd022100ffbe9e3d71b21977a8e97f" - "de4c3ba47b896d08bc09ecb9d086bb59175b5b9f03014104ff07a1833fd8098b" - "25f48c66dcf8fde34cbdbcc0f5f21a8c2005b160406cbf34cc432842c6b37b25" - "90d16b165b36a3efc9908d65fb0e605314c9b278f40f3e1affffffff0240420f" - "00000000001976a914adfa66f57ded1b655eb4ccd96ee07ca62bc1ddfd88ac00" - "7d6a7d040000001976a914981a0c9ae61fa8f8c96ae6f8e383d6e07e77133e88" - "ac00000000010000000138e7586e0784280df58bd3dc5e3d350c9036b1ec4107" - "951378f45881799c92a4000000008a47304402207c945ae0bbdaf9dadba07bdf" - "23faa676485a53817af975ddf85a104f764fb93b02201ac6af32ddf597e610b4" - "002e41f2de46664587a379a0161323a85389b4f82dda014104ec8883d3e4f7a3" - "9d75c9f5bb9fd581dc9fb1b7cdf7d6b5a665e4db1fdb09281a74ab138a2dba25" - "248b5be38bf80249601ae688c90c6e0ac8811cdb740fcec31dffffffff022f66" - "ac61050000001976a914964642290c194e3bfab661c1085e47d67786d2d388ac" - "2f77e200000000001976a9141486a7046affd935919a3cb4b50a8a0c233c286c" - "88ac00000000"); - - rawTxUnfrag_ = READHEX( - // Version - "01000000" - // NumTxIn - "02" - // Start TxIn0 - "0044fbc929d78e4203eed6f1d3d39c0157d8e5c100bbe0886779c0" - "ebf6a69324010000008a47304402206568144ed5e7064d6176c74738b04c08ca" - "19ca54ddeb480084b77f45eebfe57802207927d6975a5ac0e1bb36f5c05356dc" - "da1f521770511ee5e03239c8e1eecf3aed0141045d74feae58c4c36d7c35beac" - "05eddddc78b3ce4b02491a2eea72043978056a8bc439b99ddaad327207b09ef1" - "6a8910828e805b0cc8c11fba5caea2ee939346d7ffffffff" - // Start TxIn1 - "45c866b219b17695" - "2508f8e5aea728f950186554fc4a5807e2186a8e1c4009e5000000008c493046" - "022100bd5d41662f98cfddc46e86ea7e4a3bc8fe9f1dfc5c4836eaf7df582596" - "cfe0e9022100fc459ae4f59b8279d679003b88935896acd10021b6e2e4619377" - "e336b5296c5e014104c00bab76a708ba7064b2315420a1c533ca9945eeff9754" - "cdc574224589e9113469b4e71752146a10028079e04948ecdf70609bf1b9801f" - "6b73ab75947ac339e5ffffffff" - // NumTxOut - "02" - // Start TxOut0 - "ac4c8bd5000000001976a9148dce8946f1c7763bb60ea5cf16ef514cbed0633b88ac" - // Start TxOut1 - "002f6859000000001976a9146a59ac0e8f553f292dfe5e9f3aaa1da93499c15e88ac" - // Locktime - "00000000"); - - rawTxFragged_ = READHEX( - //"01000000020044fbc929d78e4203eed6f1d3d39c0157d8e5c100bbe0886779c0" - //"ebf6a69324010000008a47304402206568144ed5e7064d6176c74738b04c08ca" - //"19ca54ddeb480084b77f45eebfe57802207927d6975a5ac0e1bb36f5c05356dc" - //"da1f521770511ee5e03239c8e1eecf3aed0141045d74feae58c4c36d7c35beac" - //"05eddddc78b3ce4b02491a2eea72043978056a8bc439b99ddaad327207b09ef1" - //"6a8910828e805b0cc8c11fba5caea2ee939346d7ffffffff45c866b219b17695" - //"2508f8e5aea728f950186554fc4a5807e2186a8e1c4009e5000000008c493046" - //"022100bd5d41662f98cfddc46e86ea7e4a3bc8fe9f1dfc5c4836eaf7df582596" - //"cfe0e9022100fc459ae4f59b8279d679003b88935896acd10021b6e2e4619377" - //"e336b5296c5e014104c00bab76a708ba7064b2315420a1c533ca9945eeff9754" - //"cdc574224589e9113469b4e71752146a10028079e04948ecdf70609bf1b9801f" - //"6b73ab75947ac339e5ffffffff0200000000"); - // Version - "01000000" - // NumTxIn - "02" - // Start TxIn0 - "0044fbc929d78e4203eed6f1d3d39c0157d8e5c100bbe0886779c0" - "ebf6a69324010000008a47304402206568144ed5e7064d6176c74738b04c08ca" - "19ca54ddeb480084b77f45eebfe57802207927d6975a5ac0e1bb36f5c05356dc" - "da1f521770511ee5e03239c8e1eecf3aed0141045d74feae58c4c36d7c35beac" - "05eddddc78b3ce4b02491a2eea72043978056a8bc439b99ddaad327207b09ef1" - "6a8910828e805b0cc8c11fba5caea2ee939346d7ffffffff" - // Start TxIn1 - "45c866b219b17695" - "2508f8e5aea728f950186554fc4a5807e2186a8e1c4009e5000000008c493046" - "022100bd5d41662f98cfddc46e86ea7e4a3bc8fe9f1dfc5c4836eaf7df582596" - "cfe0e9022100fc459ae4f59b8279d679003b88935896acd10021b6e2e4619377" - "e336b5296c5e014104c00bab76a708ba7064b2315420a1c533ca9945eeff9754" - "cdc574224589e9113469b4e71752146a10028079e04948ecdf70609bf1b9801f" - "6b73ab75947ac339e5ffffffff" - // NumTxOut - "02" - // ... TxOuts fragged out - // Locktime - "00000000"); - - rawTxOut0_ = READHEX( - // Value - "ac4c8bd500000000" - // Script size (var_int) - "19" - // Script - "76""a9""14""8dce8946f1c7763bb60ea5cf16ef514cbed0633b""88""ac"); - rawTxOut1_ = READHEX( - // Value - "002f685900000000" - // Script size (var_int) - "19" - // Script - "76""a9""14""6a59ac0e8f553f292dfe5e9f3aaa1da93499c15e""88""ac"); - - bh_.unserialize(rawHead_); - tx1_.unserialize(rawTx0_); - tx2_.unserialize(rawTx1_); - - - sbh_.unserialize(rawHead_); - - // Make sure the global DB type and prune type are reset for each test - DBUtils.setArmoryDbType(ARMORY_DB_FULL); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - } - - BinaryData PREFBYTE(DB_PREFIX pref) - { - BinaryWriter bw; - bw.put_uint8_t((uint8_t)pref); - return bw.getData(); - } - - BinaryData rawHead_; - BinaryData headHashLE_; - BinaryData headHashBE_; - - BinaryData rawBlock_; - - BinaryData rawTx0_; - BinaryData rawTx1_; - - BlockHeader bh_; - Tx tx1_; - Tx tx2_; - - BinaryData rawTxUnfrag_; - BinaryData rawTxFragged_; - BinaryData rawTxOut0_; - BinaryData rawTxOut1_; - - - - StoredHeader sbh_; -}; - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, StoredObjNoInit) -{ - StoredHeader sbh; - StoredTx stx; - StoredTxOut stxo; - StoredScriptHistory ssh; - StoredUndoData sud; - StoredHeadHgtList hhl; - StoredTxHints sths; - - EXPECT_FALSE( sbh.isInitialized() ); - EXPECT_FALSE( stx.isInitialized() ); - EXPECT_FALSE( stxo.isInitialized() ); - EXPECT_FALSE( ssh.isInitialized() ); - EXPECT_FALSE( sud.isInitialized() ); - EXPECT_FALSE( hhl.isInitialized() ); - EXPECT_FALSE( sths.isInitialized() ); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, GetDBKeys) -{ - StoredHeader sbh; - StoredTx stx; - StoredTxOut stxo; - StoredScriptHistory ssh1; - StoredScriptHistory ssh2; - StoredUndoData sud; - StoredHeadHgtList hhl; - StoredTxHints sths; - - BinaryData key = READHEX("aaaaffff"); - uint32_t hgt = 123000; - uint8_t dup = 15; - uint8_t txi = 7; - uint8_t txo = 1; - BinaryData hgtx = READHEX("01e0780f"); - BinaryData txidx = WRITE_UINT16_BE(txi); - BinaryData txoidx = WRITE_UINT16_BE(txo); - - sbh.blockHeight_ = hgt; - sbh.duplicateID_ = dup; - - stx.blockHeight_ = hgt; - stx.duplicateID_ = dup; - stx.txIndex_ = txi; - - stxo.blockHeight_ = hgt; - stxo.duplicateID_ = dup; - stxo.txIndex_ = txi; - stxo.txOutIndex_ = txo; - - ssh1.uniqueKey_ = key; - ssh2.uniqueKey_ = key; - sud.blockHeight_ = hgt; - sud.duplicateID_ = dup; - hhl.height_ = hgt; - sths.txHashPrefix_= key; - - BinaryData TXB = PREFBYTE(DB_PREFIX_TXDATA); - BinaryData SSB = PREFBYTE(DB_PREFIX_SCRIPT); - BinaryData UDB = PREFBYTE(DB_PREFIX_UNDODATA); - BinaryData HHB = PREFBYTE(DB_PREFIX_HEADHGT); - BinaryData THB = PREFBYTE(DB_PREFIX_TXHINTS); - EXPECT_EQ(sbh.getDBKey( true ), TXB + hgtx); - EXPECT_EQ(stx.getDBKey( true ), TXB + hgtx + txidx); - EXPECT_EQ(stxo.getDBKey( true ), TXB + hgtx + txidx + txoidx); - EXPECT_EQ(ssh1.getDBKey( true ), SSB + key); - EXPECT_EQ(ssh2.getDBKey( true ), SSB + key); - EXPECT_EQ(sud.getDBKey( true ), UDB + hgtx); - EXPECT_EQ(hhl.getDBKey( true ), HHB + WRITE_UINT32_BE(hgt)); - EXPECT_EQ(sths.getDBKey( true ), THB + key); - - EXPECT_EQ(sbh.getDBKey( false ), hgtx); - EXPECT_EQ(stx.getDBKey( false ), hgtx + txidx); - EXPECT_EQ(stxo.getDBKey( false ), hgtx + txidx + txoidx); - EXPECT_EQ(ssh1.getDBKey( false ), key); - EXPECT_EQ(ssh2.getDBKey( false ), key); - EXPECT_EQ(sud.getDBKey( false ), hgtx); - EXPECT_EQ(hhl.getDBKey( false ), WRITE_UINT32_BE(hgt)); - EXPECT_EQ(sths.getDBKey( false ), key); -} - - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, LengthUnfrag) -{ - StoredTx tx; - vector offin, offout; - - uint32_t lenUnfrag = BtcUtils::StoredTxCalcLength( rawTxUnfrag_.getPtr(), - false, - &offin, - &offout); - ASSERT_EQ(lenUnfrag, 438); - - ASSERT_EQ(offin.size(), 3); - EXPECT_EQ(offin[0], 5); - EXPECT_EQ(offin[1], 184); - EXPECT_EQ(offin[2], 365); - - ASSERT_EQ(offout.size(), 3); - EXPECT_EQ(offout[0], 366); - EXPECT_EQ(offout[1], 400); - EXPECT_EQ(offout[2], 434); -} - - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, LengthFragged) -{ - vector offin, offout; - - uint32_t lenFragged = BtcUtils::StoredTxCalcLength( rawTxFragged_.getPtr(), - true, - &offin, - &offout); - ASSERT_EQ(lenFragged, 370); - - ASSERT_EQ(offin.size(), 3); - EXPECT_EQ(offin[0], 5); - EXPECT_EQ(offin[1], 184); - EXPECT_EQ(offin[2], 365); - - ASSERT_EQ(offout.size(), 3); - EXPECT_EQ(offout[0], 366); - EXPECT_EQ(offout[1], 366); - EXPECT_EQ(offout[2], 366); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, BlkDataKeys) -{ - uint32_t hgt = 0x001a332b; - uint8_t dup = 0x01; - uint16_t tix = 0x0102; - uint16_t tox = 0x0021; - - EXPECT_EQ(DBUtils.getBlkDataKey(hgt, dup), - READHEX("031a332b01")); - EXPECT_EQ(DBUtils.getBlkDataKey(hgt, dup, tix), - READHEX("031a332b010102")); - EXPECT_EQ(DBUtils.getBlkDataKey(hgt, dup, tix, tox), - READHEX("031a332b0101020021")); - - EXPECT_EQ(DBUtils.getBlkDataKeyNoPrefix(hgt, dup), - READHEX("1a332b01")); - EXPECT_EQ(DBUtils.getBlkDataKeyNoPrefix(hgt, dup, tix), - READHEX("1a332b010102")); - EXPECT_EQ(DBUtils.getBlkDataKeyNoPrefix(hgt, dup, tix, tox), - READHEX("1a332b0101020021")); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, ReadBlkKeyData) -{ - BinaryData TXP = WRITE_UINT8_BE((uint8_t)DB_PREFIX_TXDATA); - BinaryData key5p = TXP + READHEX("01e078""0f"); - BinaryData key7p = TXP + READHEX("01e078""0f""0007"); - BinaryData key9p = TXP + READHEX("01e078""0f""0007""0001"); - BinaryData key5 = READHEX("01e078""0f"); - BinaryData key7 = READHEX("01e078""0f""0007"); - BinaryData key9 = READHEX("01e078""0f""0007""0001"); - BinaryRefReader brr; - - uint32_t hgt; - uint8_t dup; - uint16_t txi; - uint16_t txo; - - BLKDATA_TYPE bdtype; - - ///////////////////////////////////////////////////////////////////////////// - // 5 bytes, with prefix - brr.setNewData(key5p); - bdtype = DBUtils.readBlkDataKey(brr, hgt, dup); - EXPECT_EQ( hgt, 123000); - EXPECT_EQ( dup, 15); - EXPECT_EQ( brr.getSizeRemaining(), 0); - EXPECT_EQ( bdtype, BLKDATA_HEADER); - - brr.setNewData(key5p); - bdtype = DBUtils.readBlkDataKey(brr, hgt, dup, txi); - EXPECT_EQ( hgt, 123000); - EXPECT_EQ( dup, 15); - EXPECT_EQ( txi, UINT16_MAX); - EXPECT_EQ( brr.getSizeRemaining(), 0); - EXPECT_EQ( bdtype, BLKDATA_HEADER); - - brr.setNewData(key5p); - bdtype = DBUtils.readBlkDataKey(brr, hgt, dup, txi, txo); - EXPECT_EQ( hgt, 123000); - EXPECT_EQ( dup, 15); - EXPECT_EQ( txi, UINT16_MAX); - EXPECT_EQ( txo, UINT16_MAX); - EXPECT_EQ( brr.getSizeRemaining(), 0); - EXPECT_EQ( bdtype, BLKDATA_HEADER); - - - ///////////////////////////////////////////////////////////////////////////// - // 7 bytes, with prefix - brr.setNewData(key7p); - bdtype = DBUtils.readBlkDataKey(brr, hgt, dup); - EXPECT_EQ( hgt, 123000); - EXPECT_EQ( dup, 15); - EXPECT_EQ( brr.getSizeRemaining(), 0); - EXPECT_EQ( bdtype, BLKDATA_TX); - - brr.setNewData(key7p); - bdtype = DBUtils.readBlkDataKey(brr, hgt, dup, txi); - EXPECT_EQ( hgt, 123000); - EXPECT_EQ( dup, 15); - EXPECT_EQ( txi, 7); - EXPECT_EQ( brr.getSizeRemaining(), 0); - EXPECT_EQ( bdtype, BLKDATA_TX); - - brr.setNewData(key7p); - bdtype = DBUtils.readBlkDataKey(brr, hgt, dup, txi, txo); - EXPECT_EQ( hgt, 123000); - EXPECT_EQ( dup, 15); - EXPECT_EQ( txi, 7); - EXPECT_EQ( txo, UINT16_MAX); - EXPECT_EQ( brr.getSizeRemaining(), 0); - EXPECT_EQ( bdtype, BLKDATA_TX); - - - ///////////////////////////////////////////////////////////////////////////// - // 9 bytes, with prefix - brr.setNewData(key9p); - bdtype = DBUtils.readBlkDataKey(brr, hgt, dup); - EXPECT_EQ( hgt, 123000); - EXPECT_EQ( dup, 15); - EXPECT_EQ( brr.getSizeRemaining(), 0); - EXPECT_EQ( bdtype, BLKDATA_TXOUT); - - brr.setNewData(key9p); - bdtype = DBUtils.readBlkDataKey(brr, hgt, dup, txi); - EXPECT_EQ( hgt, 123000); - EXPECT_EQ( dup, 15); - EXPECT_EQ( txi, 7); - EXPECT_EQ( brr.getSizeRemaining(), 0); - EXPECT_EQ( bdtype, BLKDATA_TXOUT); - - brr.setNewData(key9p); - bdtype = DBUtils.readBlkDataKey(brr, hgt, dup, txi, txo); - EXPECT_EQ( hgt, 123000); - EXPECT_EQ( dup, 15); - EXPECT_EQ( txi, 7); - EXPECT_EQ( txo, 1); - EXPECT_EQ( brr.getSizeRemaining(), 0); - EXPECT_EQ( bdtype, BLKDATA_TXOUT); - - - ///////////////////////////////////////////////////////////////////////////// - // 5 bytes, no prefix - brr.setNewData(key5); - bdtype = DBUtils.readBlkDataKeyNoPrefix(brr, hgt, dup); - EXPECT_EQ( hgt, 123000); - EXPECT_EQ( dup, 15); - EXPECT_EQ( brr.getSizeRemaining(), 0); - EXPECT_EQ( bdtype, BLKDATA_HEADER); - - brr.setNewData(key5); - bdtype = DBUtils.readBlkDataKeyNoPrefix(brr, hgt, dup, txi); - EXPECT_EQ( hgt, 123000); - EXPECT_EQ( dup, 15); - EXPECT_EQ( txi, UINT16_MAX); - EXPECT_EQ( brr.getSizeRemaining(), 0); - EXPECT_EQ( bdtype, BLKDATA_HEADER); - - brr.setNewData(key5); - bdtype = DBUtils.readBlkDataKeyNoPrefix(brr, hgt, dup, txi, txo); - EXPECT_EQ( hgt, 123000); - EXPECT_EQ( dup, 15); - EXPECT_EQ( txi, UINT16_MAX); - EXPECT_EQ( txo, UINT16_MAX); - EXPECT_EQ( brr.getSizeRemaining(), 0); - EXPECT_EQ( bdtype, BLKDATA_HEADER); - - - ///////////////////////////////////////////////////////////////////////////// - // 7 bytes, no prefix - brr.setNewData(key7); - bdtype = DBUtils.readBlkDataKeyNoPrefix(brr, hgt, dup); - EXPECT_EQ( hgt, 123000); - EXPECT_EQ( dup, 15); - EXPECT_EQ( brr.getSizeRemaining(), 0); - EXPECT_EQ( bdtype, BLKDATA_TX); - - brr.setNewData(key7); - bdtype = DBUtils.readBlkDataKeyNoPrefix(brr, hgt, dup, txi); - EXPECT_EQ( hgt, 123000); - EXPECT_EQ( dup, 15); - EXPECT_EQ( txi, 7); - EXPECT_EQ( brr.getSizeRemaining(), 0); - EXPECT_EQ( bdtype, BLKDATA_TX); - - brr.setNewData(key7); - bdtype = DBUtils.readBlkDataKeyNoPrefix(brr, hgt, dup, txi, txo); - EXPECT_EQ( hgt, 123000); - EXPECT_EQ( dup, 15); - EXPECT_EQ( txi, 7); - EXPECT_EQ( txo, UINT16_MAX); - EXPECT_EQ( brr.getSizeRemaining(), 0); - EXPECT_EQ( bdtype, BLKDATA_TX); - - - ///////////////////////////////////////////////////////////////////////////// - // 9 bytes, no prefix - brr.setNewData(key9); - bdtype = DBUtils.readBlkDataKeyNoPrefix(brr, hgt, dup); - EXPECT_EQ( hgt, 123000); - EXPECT_EQ( dup, 15); - EXPECT_EQ( brr.getSizeRemaining(), 0); - EXPECT_EQ( bdtype, BLKDATA_TXOUT); - - brr.setNewData(key9); - bdtype = DBUtils.readBlkDataKeyNoPrefix(brr, hgt, dup, txi); - EXPECT_EQ( hgt, 123000); - EXPECT_EQ( dup, 15); - EXPECT_EQ( txi, 7); - EXPECT_EQ( brr.getSizeRemaining(), 0); - EXPECT_EQ( bdtype, BLKDATA_TXOUT); - - brr.setNewData(key9); - bdtype = DBUtils.readBlkDataKeyNoPrefix(brr, hgt, dup, txi, txo); - EXPECT_EQ( hgt, 123000); - EXPECT_EQ( dup, 15); - EXPECT_EQ( txi, 7); - EXPECT_EQ( txo, 1); - EXPECT_EQ( brr.getSizeRemaining(), 0); - EXPECT_EQ( bdtype, BLKDATA_TXOUT); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, SHeaderUnserialize) -{ - // SetUp already contains sbh_.unserialize(rawHead_); - EXPECT_TRUE( sbh_.isInitialized()); - EXPECT_FALSE(sbh_.isMainBranch_); - EXPECT_FALSE(sbh_.haveFullBlock()); - EXPECT_FALSE(sbh_.isMerkleCreated()); - EXPECT_EQ( sbh_.numTx_, UINT32_MAX); - EXPECT_EQ( sbh_.numBytes_, UINT32_MAX); - EXPECT_EQ( sbh_.blockHeight_, UINT32_MAX); - EXPECT_EQ( sbh_.duplicateID_, UINT8_MAX); - EXPECT_EQ( sbh_.merkle_.getSize(), 0); - EXPECT_EQ( sbh_.stxMap_.size(), 0); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, SHeaderDBSerFull_H) -{ - DBUtils.setArmoryDbType(ARMORY_DB_FULL); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - - sbh_.blockHeight_ = 65535; - sbh_.duplicateID_ = 1; - sbh_.merkle_ = READHEX("deadbeef"); - sbh_.merkleIsPartial_ = false; - sbh_.isMainBranch_ = true; - sbh_.numTx_ = 15; - sbh_.numBytes_ = 65535; - - // SetUp already contains sbh_.unserialize(rawHead_); - BinaryData last4 = READHEX("00ffff01"); - EXPECT_EQ(sbh_.serializeDBValue(HEADERS), rawHead_ + last4); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, SHeaderDBSerFull_B1) -{ - // ARMORY_DB_FULL means no merkle string (cause all Tx are in the DB - // so the merkle tree would be redundant. - DBUtils.setArmoryDbType(ARMORY_DB_FULL); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - - sbh_.blockHeight_ = 65535; - sbh_.duplicateID_ = 1; - sbh_.merkle_ = READHEX("deadbeef"); - sbh_.merkleIsPartial_ = false; - sbh_.isMainBranch_ = true; - sbh_.numTx_ = 15; - sbh_.numBytes_ = 65535; - - // SetUp already contains sbh_.unserialize(rawHead_); - BinaryData flags = READHEX("01340000"); - BinaryData ntx = READHEX("0f000000"); - BinaryData nbyte = READHEX("ffff0000"); - - BinaryData headBlkData = flags + rawHead_ + ntx + nbyte; - EXPECT_EQ(sbh_.serializeDBValue(BLKDATA), headBlkData); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, SHeaderDBSerFull_B2) -{ - // With merkle string - DBUtils.setArmoryDbType(ARMORY_DB_PARTIAL); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - - BinaryWriter bw; - - sbh_.blockHeight_ = 65535; - sbh_.duplicateID_ = 1; - sbh_.merkle_ = READHEX("deadbeef"); - sbh_.merkleIsPartial_ = false; - sbh_.isMainBranch_ = true; - sbh_.numTx_ = 15; - sbh_.numBytes_ = 65535; - - // SetUp already contains sbh_.unserialize(rawHead_); - BinaryData flags = READHEX("01260000"); - BinaryData ntx = READHEX("0f000000"); - BinaryData nbyte = READHEX("ffff0000"); - - BinaryData headBlkData = flags + rawHead_ + ntx + nbyte + sbh_.merkle_; - EXPECT_EQ(sbh_.serializeDBValue(BLKDATA), headBlkData); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, SHeaderDBSerFull_B3) -{ - DBUtils.setArmoryDbType(ARMORY_DB_LITE); - DBUtils.setDbPruneType(DB_PRUNE_ALL); - - BinaryWriter bw; - - sbh_.blockHeight_ = 65535; - sbh_.duplicateID_ = 1; - sbh_.merkle_ = BinaryData(0); - sbh_.merkleIsPartial_ = false; - sbh_.isMainBranch_ = true; - sbh_.numTx_ = 15; - sbh_.numBytes_ = 65535; - - // SetUp already contains sbh_.unserialize(rawHead_); - BinaryData flags = READHEX("01100000"); - BinaryData ntx = READHEX("0f000000"); - BinaryData nbyte = READHEX("ffff0000"); - - BinaryData headBlkData = flags + rawHead_ + ntx + nbyte; - EXPECT_EQ(sbh_.serializeDBValue(BLKDATA), headBlkData); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, SHeaderDBUnserFull_H) -{ - BinaryData dbval = READHEX( - "010000001d8f4ec0443e1f19f305e488c1085c95de7cc3fd25e0d2c5bb5d0000" - "000000009762547903d36881a86751f3f5049e23050113f779735ef82734ebf0" - "b4450081d8c8c84db3936a1a334b035b00ffff01"); - - BinaryRefReader brr(dbval); - sbh_.unserializeDBValue(HEADERS, brr); - - EXPECT_EQ(sbh_.blockHeight_, 65535); - EXPECT_EQ(sbh_.duplicateID_, 1); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, SHeaderDBUnserFull_B1) -{ - BinaryData dbval = READHEX( - "01340000010000001d8f4ec0443e1f19f305e488c1085c95de7cc3fd25e0d2c5" - "bb5d0000000000009762547903d36881a86751f3f5049e23050113f779735ef8" - "2734ebf0b4450081d8c8c84db3936a1a334b035b0f000000ffff0000"); - - BinaryRefReader brr(dbval); - sbh_.unserializeDBValue(BLKDATA, brr); - sbh_.setHeightAndDup(65535, 1); - - EXPECT_EQ(sbh_.blockHeight_, 65535); - EXPECT_EQ(sbh_.duplicateID_, 1); - EXPECT_EQ(sbh_.merkle_ , READHEX("")); - EXPECT_EQ(sbh_.numTx_ , 15); - EXPECT_EQ(sbh_.numBytes_ , 65535); - EXPECT_EQ(sbh_.unserArmVer_, 0x00); - EXPECT_EQ(sbh_.unserBlkVer_, 1); - EXPECT_EQ(sbh_.unserDbType_, ARMORY_DB_FULL); - EXPECT_EQ(sbh_.unserPrType_, DB_PRUNE_NONE); - EXPECT_EQ(sbh_.unserMkType_, MERKLE_SER_NONE); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, SHeaderDBUnserFull_B2) -{ - BinaryData dbval = READHEX( - "01260000010000001d8f4ec0443e1f19f305e488c1085c95de7cc3fd25e0d2c5" - "bb5d0000000000009762547903d36881a86751f3f5049e23050113f779735ef8" - "2734ebf0b4450081d8c8c84db3936a1a334b035b0f000000ffff0000deadbeef"); - - BinaryRefReader brr(dbval); - sbh_.unserializeDBValue(BLKDATA, brr); - sbh_.setHeightAndDup(65535, 1); - - EXPECT_EQ(sbh_.blockHeight_ , 65535); - EXPECT_EQ(sbh_.duplicateID_ , 1); - EXPECT_EQ(sbh_.merkle_ , READHEX("deadbeef")); - EXPECT_EQ(sbh_.numTx_ , 15); - EXPECT_EQ(sbh_.numBytes_ , 65535); - EXPECT_EQ(sbh_.unserArmVer_, 0x00); - EXPECT_EQ(sbh_.unserBlkVer_, 1); - EXPECT_EQ(sbh_.unserDbType_, ARMORY_DB_PARTIAL); - EXPECT_EQ(sbh_.unserPrType_, DB_PRUNE_NONE); - EXPECT_EQ(sbh_.unserMkType_, MERKLE_SER_FULL); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, SHeaderDBUnserFull_B3) -{ - BinaryData dbval = READHEX( - "01100000010000001d8f4ec0443e1f19f305e488c1085c95de7cc3fd25e0d2c5" - "bb5d0000000000009762547903d36881a86751f3f5049e23050113f779735ef8" - "2734ebf0b4450081d8c8c84db3936a1a334b035b0f000000ffff0000"); - - BinaryRefReader brr(dbval); - sbh_.unserializeDBValue(BLKDATA, brr); - sbh_.setHeightAndDup(65535, 1); - - EXPECT_EQ(sbh_.blockHeight_, 65535); - EXPECT_EQ(sbh_.duplicateID_, 1); - EXPECT_EQ(sbh_.merkle_ , READHEX("")); - EXPECT_EQ(sbh_.numTx_ , 15); - EXPECT_EQ(sbh_.numBytes_ , 65535); - EXPECT_EQ(sbh_.unserArmVer_, 0x00); - EXPECT_EQ(sbh_.unserBlkVer_, 1); - EXPECT_EQ(sbh_.unserDbType_, ARMORY_DB_LITE); - EXPECT_EQ(sbh_.unserPrType_, DB_PRUNE_ALL); - EXPECT_EQ(sbh_.unserMkType_, MERKLE_SER_NONE); -} - - - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, STxUnserUnfrag) -{ - Tx regTx(rawTx0_); - - StoredTx stx; - stx.createFromTx(regTx, false); - - EXPECT_TRUE( stx.isInitialized()); - EXPECT_TRUE( stx.haveAllTxOut()); - EXPECT_FALSE(stx.isFragged_); - EXPECT_EQ( stx.version_, 1); - EXPECT_EQ( stx.blockHeight_, UINT32_MAX); - EXPECT_EQ( stx.duplicateID_, UINT8_MAX); - EXPECT_EQ( stx.txIndex_, UINT16_MAX); - EXPECT_EQ( stx.dataCopy_.getSize(), 258); - EXPECT_EQ( stx.numBytes_, 258); - EXPECT_EQ( stx.fragBytes_, 190); - - ASSERT_EQ( stx.stxoMap_.size(), 2); - EXPECT_TRUE( stx.stxoMap_[0].isInitialized()); - EXPECT_TRUE( stx.stxoMap_[1].isInitialized()); - EXPECT_EQ( stx.stxoMap_[0].txIndex_, UINT16_MAX); - EXPECT_EQ( stx.stxoMap_[1].txIndex_, UINT16_MAX); - EXPECT_EQ( stx.stxoMap_[0].txOutIndex_, 0); - EXPECT_EQ( stx.stxoMap_[1].txOutIndex_, 1); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, STxUnserFragged) -{ - Tx regTx(rawTx0_); - - StoredTx stx; - stx.createFromTx(regTx, true); - - EXPECT_TRUE( stx.isInitialized()); - EXPECT_TRUE( stx.haveAllTxOut()); - EXPECT_TRUE( stx.isFragged_); - EXPECT_EQ( stx.version_, 1); - EXPECT_EQ( stx.blockHeight_, UINT32_MAX); - EXPECT_EQ( stx.duplicateID_, UINT8_MAX); - EXPECT_EQ( stx.txIndex_, UINT16_MAX); - EXPECT_EQ( stx.dataCopy_.getSize(), 190); - - ASSERT_EQ( stx.stxoMap_.size(), 2); - EXPECT_TRUE( stx.stxoMap_[0].isInitialized()); - EXPECT_TRUE( stx.stxoMap_[1].isInitialized()); - EXPECT_EQ( stx.stxoMap_[0].txIndex_, UINT16_MAX); - EXPECT_EQ( stx.stxoMap_[1].txIndex_, UINT16_MAX); - EXPECT_EQ( stx.stxoMap_[0].txOutIndex_, 0); - EXPECT_EQ( stx.stxoMap_[1].txOutIndex_, 1); -} - - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, STxReconstruct) -{ - Tx regTx, reconTx; - StoredTx stx; - - // Reconstruct an unfragged tx - regTx.unserialize(rawTx0_); - stx.createFromTx(regTx, false); - - reconTx = stx.getTxCopy(); - EXPECT_EQ(reconTx.serialize(), rawTx0_); - EXPECT_EQ(stx.getSerializedTx(), rawTx0_); - - // Reconstruct an fragged tx - regTx.unserialize(rawTx0_); - stx.createFromTx(regTx, true); - - reconTx = stx.getTxCopy(); - EXPECT_EQ(reconTx.serialize(), rawTx0_); - EXPECT_EQ(stx.getSerializedTx(), rawTx0_); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, STxSerUnfragToFrag) -{ - StoredTx stx; - stx.unserialize(rawTxUnfrag_); - - EXPECT_EQ(stx.getSerializedTx(), rawTxUnfrag_); - EXPECT_EQ(stx.getSerializedTxFragged(), rawTxFragged_); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, STxSerDBValue_1) -{ - DBUtils.setArmoryDbType(ARMORY_DB_FULL); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - - Tx origTx(rawTxUnfrag_); - - StoredTx stx; - stx.unserialize(rawTxUnfrag_); - - // 0123 45 67 01 23 4567 - // |----| |--| |-- --| - // DBVer TxVer TxSer - // - // For this example: DBVer=0, TxVer=1, TxSer=FRAGGED[1] - // 0000 01 00 01 -- ---- - BinaryData first2 = READHEX("0440"); // little-endian, of course - BinaryData txHash = origTx.getThisHash(); - BinaryData fragged = stx.getSerializedTxFragged(); - BinaryData output = first2 + txHash + fragged; - EXPECT_EQ(stx.serializeDBValue(), output); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, DISABLED_STxSerDBValue_2) -{ - // I modified the ARMORY_DB_SUPER code to frag, as well. There's no - // mode that doesn't frag, now. - DBUtils.setArmoryDbType(ARMORY_DB_SUPER); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - - Tx origTx(rawTxUnfrag_); - - StoredTx stx; - stx.unserialize(rawTxUnfrag_); - - // 0123 45 67 01 23 4567 - // |----| |--| |-- --| - // DBVer TxVer TxSer - // - // For this example: DBVer=0, TxVer=1, TxSer=FRAGGED[1] - // 0000 01 00 00 -- ---- - BinaryData first2 = READHEX("0400"); // little-endian, of course - BinaryData txHash = origTx.getThisHash(); - BinaryData fragged = stx.getSerializedTx(); // Full Tx this time - BinaryData output = first2 + txHash + fragged; - EXPECT_EQ(stx.serializeDBValue(), output); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, STxUnserDBValue_1) -{ - Tx origTx(rawTxUnfrag_); - - BinaryData toUnser = READHEX( - "0440e471262336aa67391e57c8c6fe03bae29734079e06ff75c7fa4d0a873c83" - "f03c01000000020044fbc929d78e4203eed6f1d3d39c0157d8e5c100bbe08867" - "79c0ebf6a69324010000008a47304402206568144ed5e7064d6176c74738b04c" - "08ca19ca54ddeb480084b77f45eebfe57802207927d6975a5ac0e1bb36f5c053" - "56dcda1f521770511ee5e03239c8e1eecf3aed0141045d74feae58c4c36d7c35" - "beac05eddddc78b3ce4b02491a2eea72043978056a8bc439b99ddaad327207b0" - "9ef16a8910828e805b0cc8c11fba5caea2ee939346d7ffffffff45c866b219b1" - "76952508f8e5aea728f950186554fc4a5807e2186a8e1c4009e5000000008c49" - "3046022100bd5d41662f98cfddc46e86ea7e4a3bc8fe9f1dfc5c4836eaf7df58" - "2596cfe0e9022100fc459ae4f59b8279d679003b88935896acd10021b6e2e461" - "9377e336b5296c5e014104c00bab76a708ba7064b2315420a1c533ca9945eeff" - "9754cdc574224589e9113469b4e71752146a10028079e04948ecdf70609bf1b9" - "801f6b73ab75947ac339e5ffffffff0200000000"); - - BinaryRefReader brr(toUnser); - - StoredTx stx; - stx.unserializeDBValue(brr); - - EXPECT_TRUE( stx.isInitialized()); - EXPECT_EQ( stx.thisHash_, origTx.getThisHash()); - EXPECT_EQ( stx.lockTime_, origTx.getLockTime()); - EXPECT_EQ( stx.dataCopy_, rawTxFragged_); - EXPECT_TRUE( stx.isFragged_); - EXPECT_EQ( stx.version_, 1); - EXPECT_EQ( stx.blockHeight_, UINT32_MAX); - EXPECT_EQ( stx.duplicateID_, UINT8_MAX); - EXPECT_EQ( stx.txIndex_, UINT16_MAX); - EXPECT_EQ( stx.numTxOut_, origTx.getNumTxOut()); - EXPECT_EQ( stx.numBytes_, UINT32_MAX); - EXPECT_EQ( stx.fragBytes_, 370); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, STxUnserDBValue_2) -{ - Tx origTx(rawTxUnfrag_); - - BinaryData toUnser = READHEX( - "0004e471262336aa67391e57c8c6fe03bae29734079e06ff75c7fa4d0a873c83" - "f03c01000000020044fbc929d78e4203eed6f1d3d39c0157d8e5c100bbe08867" - "79c0ebf6a69324010000008a47304402206568144ed5e7064d6176c74738b04c" - "08ca19ca54ddeb480084b77f45eebfe57802207927d6975a5ac0e1bb36f5c053" - "56dcda1f521770511ee5e03239c8e1eecf3aed0141045d74feae58c4c36d7c35" - "beac05eddddc78b3ce4b02491a2eea72043978056a8bc439b99ddaad327207b0" - "9ef16a8910828e805b0cc8c11fba5caea2ee939346d7ffffffff45c866b219b1" - "76952508f8e5aea728f950186554fc4a5807e2186a8e1c4009e5000000008c49" - "3046022100bd5d41662f98cfddc46e86ea7e4a3bc8fe9f1dfc5c4836eaf7df58" - "2596cfe0e9022100fc459ae4f59b8279d679003b88935896acd10021b6e2e461" - "9377e336b5296c5e014104c00bab76a708ba7064b2315420a1c533ca9945eeff" - "9754cdc574224589e9113469b4e71752146a10028079e04948ecdf70609bf1b9" - "801f6b73ab75947ac339e5ffffffff02ac4c8bd5000000001976a9148dce8946" - "f1c7763bb60ea5cf16ef514cbed0633b88ac002f6859000000001976a9146a59" - "ac0e8f553f292dfe5e9f3aaa1da93499c15e88ac00000000"); - - BinaryRefReader brr(toUnser); - - StoredTx stx; - stx.unserializeDBValue(brr); - - EXPECT_TRUE( stx.isInitialized()); - EXPECT_EQ( stx.thisHash_, origTx.getThisHash()); - EXPECT_EQ( stx.lockTime_, origTx.getLockTime()); - EXPECT_EQ( stx.dataCopy_, rawTxUnfrag_); - EXPECT_FALSE(stx.isFragged_); - EXPECT_EQ( stx.version_, 1); - EXPECT_EQ( stx.blockHeight_, UINT32_MAX); - EXPECT_EQ( stx.duplicateID_, UINT8_MAX); - EXPECT_EQ( stx.txIndex_, UINT16_MAX); - EXPECT_EQ( stx.numTxOut_, origTx.getNumTxOut()); - EXPECT_EQ( stx.numBytes_, origTx.getSize()); - EXPECT_EQ( stx.fragBytes_, 370); -} - - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, STxOutUnserialize) -{ - TxOut txo0, txo1; - StoredTxOut stxo0, stxo1; - - stxo0.unserialize(rawTxOut0_); - stxo1.unserialize(rawTxOut1_); - txo0.unserialize(rawTxOut0_); - txo1.unserialize(rawTxOut1_); - - uint64_t val0 = READ_UINT64_HEX_LE("ac4c8bd500000000"); - uint64_t val1 = READ_UINT64_HEX_LE("002f685900000000"); - - EXPECT_EQ(stxo0.getSerializedTxOut(), rawTxOut0_); - EXPECT_EQ(stxo0.getSerializedTxOut(), txo0.serialize()); - EXPECT_EQ(stxo1.getSerializedTxOut(), rawTxOut1_); - EXPECT_EQ(stxo1.getSerializedTxOut(), txo1.serialize()); - - EXPECT_EQ(stxo0.getValue(), val0); - EXPECT_EQ(stxo1.getValue(), val1); - - TxOut txoRecon = stxo0.getTxOutCopy(); - EXPECT_EQ(txoRecon.serialize(), rawTxOut0_); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, STxOutSerDBValue_1) -{ - DBUtils.setArmoryDbType(ARMORY_DB_FULL); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - - StoredTxOut stxo0; - - stxo0.unserialize(rawTxOut0_); - - stxo0.txVersion_ = 1; - stxo0.spentness_ = TXOUT_UNSPENT; - - // 0123 45 67 0 123 4567 - // |----| |--| |--| |-| - // DBVer TxVer Spnt CB - // - // For this example: DBVer=0, TxVer=1, TxSer=FRAGGED[1] - // 0000 01 00 0 --- ---- - EXPECT_EQ(stxo0.serializeDBValue(), READHEX("0400") + rawTxOut0_); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, STxOutSerDBValue_2) -{ - DBUtils.setArmoryDbType(ARMORY_DB_FULL); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - - StoredTxOut stxo0; - stxo0.unserialize(rawTxOut0_); - stxo0.txVersion_ = 1; - stxo0.spentness_ = TXOUT_UNSPENT; - - // Test a spent TxOut - // 0000 01 01 0 --- ---- - BinaryData spentStr = DBUtils.getBlkDataKeyNoPrefix( 100000, 1, 127, 15); - stxo0.spentness_ = TXOUT_SPENT; - stxo0.spentByTxInKey_ = spentStr; - EXPECT_EQ(stxo0.serializeDBValue(), READHEX("0500")+rawTxOut0_+spentStr); -} - - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, STxOutSerDBValue_3) -{ - DBUtils.setArmoryDbType(ARMORY_DB_FULL); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - - StoredTxOut stxo0; - stxo0.unserialize(rawTxOut0_); - stxo0.txVersion_ = 1; - stxo0.isCoinbase_ = true; - - // Test a spent TxOut but in lite mode where we don't record spentness - // 0000 01 01 1 --- ---- - DBUtils.setArmoryDbType(ARMORY_DB_LITE); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - BinaryData spentStr = DBUtils.getBlkDataKeyNoPrefix( 100000, 1, 127, 15); - stxo0.spentness_ = TXOUT_SPENT; - stxo0.spentByTxInKey_ = spentStr; - EXPECT_EQ(stxo0.serializeDBValue(), READHEX("0680")+rawTxOut0_); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, STxOutUnserDBValue_1) -{ - BinaryData input = READHEX( "0400ac4c8bd5000000001976a9148dce8946f1c7763b" - "b60ea5cf16ef514cbed0633b88ac"); - StoredTxOut stxo; - stxo.unserializeDBValue(input); - - EXPECT_TRUE( stxo.isInitialized()); - EXPECT_EQ( stxo.txVersion_, 1); - EXPECT_EQ( stxo.dataCopy_, rawTxOut0_); - EXPECT_EQ( stxo.blockHeight_, UINT32_MAX); - EXPECT_EQ( stxo.duplicateID_, UINT8_MAX); - EXPECT_EQ( stxo.txIndex_, UINT16_MAX); - EXPECT_EQ( stxo.txOutIndex_, UINT16_MAX); - EXPECT_EQ( stxo.spentness_, TXOUT_UNSPENT); - EXPECT_EQ( stxo.spentByTxInKey_.getSize(), 0); - EXPECT_FALSE(stxo.isCoinbase_); - EXPECT_EQ( stxo.unserArmVer_, 0); -} -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, STxOutUnserDBValue_2) -{ - BinaryData input = READHEX( "0500ac4c8bd5000000001976a9148dce8946f1c7763b" - "b60ea5cf16ef514cbed0633b88ac01a086017f000f00"); - StoredTxOut stxo; - stxo.unserializeDBValue(input); - - EXPECT_TRUE( stxo.isInitialized()); - EXPECT_EQ( stxo.txVersion_, 1); - EXPECT_EQ( stxo.dataCopy_, rawTxOut0_); - EXPECT_EQ( stxo.blockHeight_, UINT32_MAX); - EXPECT_EQ( stxo.duplicateID_, UINT8_MAX); - EXPECT_EQ( stxo.txIndex_, UINT16_MAX); - EXPECT_EQ( stxo.txOutIndex_, UINT16_MAX); - EXPECT_EQ( stxo.spentness_, TXOUT_SPENT); - EXPECT_FALSE(stxo.isCoinbase_); - EXPECT_EQ( stxo.spentByTxInKey_, READHEX("01a086017f000f00")); - EXPECT_EQ( stxo.unserArmVer_, 0); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, STxOutUnserDBValue_3) -{ - BinaryData input = READHEX( "0680ac4c8bd5000000001976a9148dce8946f1c7763b" - "b60ea5cf16ef514cbed0633b88ac"); - StoredTxOut stxo; - stxo.unserializeDBValue(input); - - EXPECT_TRUE( stxo.isInitialized()); - EXPECT_EQ( stxo.txVersion_, 1); - EXPECT_EQ( stxo.dataCopy_, rawTxOut0_); - EXPECT_EQ( stxo.blockHeight_, UINT32_MAX); - EXPECT_EQ( stxo.duplicateID_, UINT8_MAX); - EXPECT_EQ( stxo.txIndex_, UINT16_MAX); - EXPECT_EQ( stxo.txOutIndex_, UINT16_MAX); - EXPECT_EQ( stxo.spentness_, TXOUT_SPENTUNK); - EXPECT_TRUE( stxo.isCoinbase_); - EXPECT_EQ( stxo.spentByTxInKey_.getSize(), 0); - EXPECT_EQ( stxo.unserArmVer_, 0); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, SHeaderFullBlock) -{ - // I'll make this more robust later... kind of tired of writing tests... - StoredHeader sbh; - sbh.unserializeFullBlock(rawBlock_.getRef()); - - BinaryWriter bw; - sbh.serializeFullBlock(bw); - - EXPECT_EQ(bw.getDataRef(), rawBlock_.getRef()); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, SUndoDataSer) -{ - DBUtils.setArmoryDbType(ARMORY_DB_FULL); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - - BinaryData arbHash = READHEX("11112221111222111122222211112222" - "11112221111222111122211112221111"); - BinaryData op0_str = READHEX("aaaabbbbaaaabbbbaaaabbbbaaaabbbb" - "aaaabbbbaaaabbbbaaaabbbbaaaabbbb"); - BinaryData op1_str = READHEX("ffffbbbbffffbbbbffffbbbbffffbbbb" - "ffffbbbbffffbbbbffffbbbbffffbbbb"); - - - StoredUndoData sud; - OutPoint op0(op0_str, 1); - OutPoint op1(op1_str, 2); - - StoredTxOut stxo0, stxo1; - stxo0.unserialize(rawTxOut0_); - stxo1.unserialize(rawTxOut1_); - - stxo0.txVersion_ = 1; - stxo1.txVersion_ = 1; - stxo0.blockHeight_ = 100000; - stxo1.blockHeight_ = 100000; - stxo0.duplicateID_ = 2; - stxo1.duplicateID_ = 2; - stxo0.txIndex_ = 17; - stxo1.txIndex_ = 17; - stxo0.parentHash_ = arbHash; - stxo1.parentHash_ = arbHash; - stxo0.txOutIndex_ = 5; - stxo1.txOutIndex_ = 5; - - sud.stxOutsRemovedByBlock_.clear(); - sud.stxOutsRemovedByBlock_.push_back(stxo0); - sud.stxOutsRemovedByBlock_.push_back(stxo1); - sud.outPointsAddedByBlock_.clear(); - sud.outPointsAddedByBlock_.push_back(op0); - sud.outPointsAddedByBlock_.push_back(op1); - - sud.blockHash_ = arbHash; - sud.blockHeight_ = 123000; // unused for this test - sud.duplicateID_ = 15; // unused for this test - - BinaryData flags = READHEX("34"); - BinaryData str2 = WRITE_UINT32_LE(2); - BinaryData str5 = WRITE_UINT32_LE(5); - BinaryData answer = - arbHash + - str2 + - flags + stxo0.getDBKey(false) + arbHash + str5 + rawTxOut0_ + - flags + stxo1.getDBKey(false) + arbHash + str5 + rawTxOut1_ + - str2 + - op0.serialize() + - op1.serialize(); - - EXPECT_EQ(sud.serializeDBValue(), answer); -} - - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, SUndoDataUnser) -{ - DBUtils.setArmoryDbType(ARMORY_DB_FULL); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - - BinaryData arbHash = READHEX("11112221111222111122222211112222" - "11112221111222111122211112221111"); - BinaryData op0_str = READHEX("aaaabbbbaaaabbbbaaaabbbbaaaabbbb" - "aaaabbbbaaaabbbbaaaabbbbaaaabbbb"); - BinaryData op1_str = READHEX("ffffbbbbffffbbbbffffbbbbffffbbbb" - "ffffbbbbffffbbbbffffbbbbffffbbbb"); - OutPoint op0(op0_str, 1); - OutPoint op1(op1_str, 2); - - //BinaryData sudToUnser = READHEX( - //"1111222111122211112222221111222211112221111222111122211112221111" - //"0200000024111122211112221111222222111122221111222111122211112221" - //"111222111105000000ac4c8bd5000000001976a9148dce8946f1c7763bb60ea5" - //"cf16ef514cbed0633b88ac241111222111122211112222221111222211112221" - //"11122211112221111222111105000000002f6859000000001976a9146a59ac0e" - //"8f553f292dfe5e9f3aaa1da93499c15e88ac02000000aaaabbbbaaaabbbbaaaa" - //"bbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbb01000000ffffbbbbffff" - //"bbbbffffbbbbffffbbbbffffbbbbffffbbbbffffbbbbffffbbbb02000000"); - - BinaryData sudToUnser = READHEX( - "1111222111122211112222221111222211112221111222111122211112221111" - "02000000240186a0020011000511112221111222111122222211112222111122" - "2111122211112221111222111105000000ac4c8bd5000000001976a9148dce89" - "46f1c7763bb60ea5cf16ef514cbed0633b88ac240186a0020011000511112221" - "1112221111222222111122221111222111122211112221111222111105000000" - "002f6859000000001976a9146a59ac0e8f553f292dfe5e9f3aaa1da93499c15e" - "88ac02000000aaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaa" - "bbbbaaaabbbb01000000ffffbbbbffffbbbbffffbbbbffffbbbbffffbbbbffff" - "bbbbffffbbbbffffbbbb02000000"); - - StoredUndoData sud; - sud.unserializeDBValue(sudToUnser); - - ASSERT_EQ(sud.outPointsAddedByBlock_.size(), 2); - ASSERT_EQ(sud.stxOutsRemovedByBlock_.size(), 2); - - EXPECT_EQ(sud.outPointsAddedByBlock_[0].serialize(), op0.serialize()); - EXPECT_EQ(sud.outPointsAddedByBlock_[1].serialize(), op1.serialize()); - EXPECT_EQ(sud.stxOutsRemovedByBlock_[0].getSerializedTxOut(), rawTxOut0_); - EXPECT_EQ(sud.stxOutsRemovedByBlock_[1].getSerializedTxOut(), rawTxOut1_); - - EXPECT_EQ(sud.stxOutsRemovedByBlock_[0].parentHash_, arbHash); - EXPECT_EQ(sud.stxOutsRemovedByBlock_[1].parentHash_, arbHash); - - EXPECT_EQ(sud.stxOutsRemovedByBlock_[0].blockHeight_, 100000); - EXPECT_EQ(sud.stxOutsRemovedByBlock_[1].blockHeight_, 100000); - EXPECT_EQ(sud.stxOutsRemovedByBlock_[0].duplicateID_, 2); - EXPECT_EQ(sud.stxOutsRemovedByBlock_[1].duplicateID_, 2); - EXPECT_EQ(sud.stxOutsRemovedByBlock_[0].txIndex_, 17); - EXPECT_EQ(sud.stxOutsRemovedByBlock_[1].txIndex_, 17); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, STxHintsSer) -{ - BinaryData hint0 = DBUtils.getBlkDataKeyNoPrefix(123000, 7, 255); - BinaryData hint1 = DBUtils.getBlkDataKeyNoPrefix(123000, 15, 127); - BinaryData hint2 = DBUtils.getBlkDataKeyNoPrefix(183922, 15, 3); - - StoredTxHints sths; - sths.txHashPrefix_ = READHEX("aaaaffff"); - sths.dbKeyList_.clear(); - - ///// - BinaryWriter ans0; - ans0.put_var_int(0); - EXPECT_EQ(sths.serializeDBValue(), ans0.getData()); - - ///// - sths.dbKeyList_.push_back(hint0); - sths.preferredDBKey_ = hint0; - BinaryWriter ans1; - ans1.put_var_int(1); - ans1.put_BinaryData(hint0); - EXPECT_EQ(sths.dbKeyList_.size(), 1); - EXPECT_EQ(sths.preferredDBKey_, hint0); - EXPECT_EQ(sths.serializeDBValue(), ans1.getData()); - - ///// - sths.dbKeyList_.push_back(hint1); - sths.dbKeyList_.push_back(hint2); - BinaryWriter ans3; - ans3.put_var_int(3); - ans3.put_BinaryData(hint0); - ans3.put_BinaryData(hint1); - ans3.put_BinaryData(hint2); - EXPECT_EQ(sths.dbKeyList_.size(), 3); - EXPECT_EQ(sths.preferredDBKey_, hint0); - EXPECT_EQ(sths.serializeDBValue(), ans3.getData()); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, STxHintsReorder) -{ - BinaryData hint0 = DBUtils.getBlkDataKeyNoPrefix(123000, 7, 255); - BinaryData hint1 = DBUtils.getBlkDataKeyNoPrefix(123000, 15, 127); - BinaryData hint2 = DBUtils.getBlkDataKeyNoPrefix(183922, 15, 3); - - StoredTxHints sths; - sths.txHashPrefix_ = READHEX("aaaaffff"); - sths.dbKeyList_.clear(); - sths.dbKeyList_.push_back(hint0); - sths.dbKeyList_.push_back(hint1); - sths.dbKeyList_.push_back(hint2); - sths.preferredDBKey_ = hint1; - - BinaryWriter expectedOut; - expectedOut.put_var_int(3); - expectedOut.put_BinaryData(hint1); - expectedOut.put_BinaryData(hint0); - expectedOut.put_BinaryData(hint2); - - EXPECT_EQ(sths.serializeDBValue(), expectedOut.getData()); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, STxHintsUnser) -{ - BinaryData hint0 = DBUtils.getBlkDataKeyNoPrefix(123000, 7, 255); - BinaryData hint1 = DBUtils.getBlkDataKeyNoPrefix(123000, 15, 127); - BinaryData hint2 = DBUtils.getBlkDataKeyNoPrefix(183922, 15, 3); - - BinaryData in0 = READHEX("00"); - BinaryData in1 = READHEX("01""01e0780700ff"); - BinaryData in3 = READHEX("03""01e0780700ff""01e0780f007f""02ce720f0003"); - - StoredTxHints sths0, sths1, sths3; - - sths0.unserializeDBValue(in0); - - EXPECT_EQ(sths0.dbKeyList_.size(), 0); - EXPECT_EQ(sths0.preferredDBKey_.getSize(), 0); - - sths1.unserializeDBValue(in1); - - EXPECT_EQ(sths1.dbKeyList_.size(), 1); - EXPECT_EQ(sths1.dbKeyList_[0], hint0); - EXPECT_EQ(sths1.preferredDBKey_, hint0); - - sths3.unserializeDBValue(in3); - EXPECT_EQ(sths3.dbKeyList_.size(), 3); - EXPECT_EQ(sths3.dbKeyList_[0], hint0); - EXPECT_EQ(sths3.dbKeyList_[1], hint1); - EXPECT_EQ(sths3.dbKeyList_[2], hint2); - EXPECT_EQ(sths3.preferredDBKey_, hint0); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, SHeadHgtListSer) -{ - StoredHeadHgtList baseHHL, testHHL; - baseHHL.height_ = 123000; - baseHHL.dupAndHashList_.resize(0); - BinaryData hash0 = READHEX("aaaabbbbaaaabbbbaaaabbbbaaaabbbb" - "aaaabbbbaaaabbbbaaaabbbbaaaabbbb"); - BinaryData hash1 = READHEX("2222bbbb2222bbbb2222bbbb2222bbbb" - "2222bbbb2222bbbb2222bbbb2222bbbb"); - BinaryData hash2 = READHEX("2222ffff2222ffff2222ffff2222ffff" - "2222ffff2222ffff2222ffff2222ffff"); - - uint8_t dup0 = 0; - uint8_t dup1 = 1; - uint8_t dup2 = 7; - - BinaryWriter expectOut; - - // Test writing empty list - expectOut.reset(); - expectOut.put_uint8_t(0); - EXPECT_EQ(testHHL.serializeDBValue(), expectOut.getData()); - - - // Test writing list with one entry but no preferred dupID - expectOut.reset(); - testHHL = baseHHL; - testHHL.dupAndHashList_.push_back(pair(dup0, hash0)); - expectOut.put_uint8_t(1); - expectOut.put_uint8_t(dup0); - expectOut.put_BinaryData(hash0); - EXPECT_EQ(testHHL.serializeDBValue(), expectOut.getData()); - - // Test writing list with one entry which is a preferred dupID - expectOut.reset(); - testHHL = baseHHL; - testHHL.preferredDup_ = 0; - testHHL.dupAndHashList_.push_back(pair(dup0, hash0)); - expectOut.put_uint8_t(1); - expectOut.put_uint8_t(dup0 | 0x80); - expectOut.put_BinaryData(hash0); - EXPECT_EQ(testHHL.serializeDBValue(), expectOut.getData()); - - // Test writing list with one entry preferred dupID but that dup isn't avail - expectOut.reset(); - testHHL = baseHHL; - testHHL.preferredDup_ = 1; - testHHL.dupAndHashList_.push_back(pair(dup0, hash0)); - expectOut.put_uint8_t(1); - expectOut.put_uint8_t(dup0); - expectOut.put_BinaryData(hash0); - EXPECT_EQ(testHHL.serializeDBValue(), expectOut.getData()); - - // Test writing with three entries, no preferred - expectOut.reset(); - testHHL = baseHHL; - testHHL.dupAndHashList_.push_back(pair(dup0, hash0)); - testHHL.dupAndHashList_.push_back(pair(dup1, hash1)); - testHHL.dupAndHashList_.push_back(pair(dup2, hash2)); - expectOut.put_uint8_t(3); - expectOut.put_uint8_t(dup0); expectOut.put_BinaryData(hash0); - expectOut.put_uint8_t(dup1); expectOut.put_BinaryData(hash1); - expectOut.put_uint8_t(dup2); expectOut.put_BinaryData(hash2); - EXPECT_EQ(testHHL.serializeDBValue(), expectOut.getData()); - - - // Test writing with three entries, with preferred - expectOut.reset(); - testHHL = baseHHL; - testHHL.dupAndHashList_.push_back(pair(dup0, hash0)); - testHHL.dupAndHashList_.push_back(pair(dup1, hash1)); - testHHL.dupAndHashList_.push_back(pair(dup2, hash2)); - testHHL.preferredDup_ = 1; - expectOut.put_uint8_t(3); - expectOut.put_uint8_t(dup1 | 0x80); expectOut.put_BinaryData(hash1); - expectOut.put_uint8_t(dup0); expectOut.put_BinaryData(hash0); - expectOut.put_uint8_t(dup2); expectOut.put_BinaryData(hash2); - EXPECT_EQ(testHHL.serializeDBValue(), expectOut.getData()); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(StoredBlockObjTest, SHeadHgtListUnser) -{ - BinaryData hash0 = READHEX("aaaabbbbaaaabbbbaaaabbbbaaaabbbb" - "aaaabbbbaaaabbbbaaaabbbbaaaabbbb"); - BinaryData hash1 = READHEX("2222bbbb2222bbbb2222bbbb2222bbbb" - "2222bbbb2222bbbb2222bbbb2222bbbb"); - BinaryData hash2 = READHEX("2222ffff2222ffff2222ffff2222ffff" - "2222ffff2222ffff2222ffff2222ffff"); - - vector tests; - tests.push_back( READHEX( - "0100aaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbb")); - tests.push_back( READHEX( - "0180aaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbb")); - tests.push_back( READHEX( - "0300aaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaa" - "bbbb012222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb22" - "22bbbb072222ffff2222ffff2222ffff2222ffff2222ffff2222ffff2222ffff" - "2222ffff")); - tests.push_back( READHEX( - "03812222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222" - "bbbb00aaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaa" - "aabbbb072222ffff2222ffff2222ffff2222ffff2222ffff2222ffff2222ffff" - "2222ffff")); - - uint8_t dup0 = 0; - uint8_t dup1 = 1; - uint8_t dup2 = 7; - - for(uint32_t i=0; isetValidDupIDForHeight(hgt,dup); - - ssh.uniqueKey_ = HASH160PREFIX + a160; - ssh.version_ = 1; - ssh.alreadyScannedUpToBlk_ = UINT32_MAX; - - ssh.markTxOutUnspent(txio0); - - // Check the initial state matches expectations - EXPECT_EQ(ssh.serializeDBValue(), expectSSH_orig); - - ssh.insertTxio(txio1); - // Mark the first output spent (second one was already marked spent) - ssh.markTxOutSpent( dbKey0, dbKey5); - EXPECT_EQ(ssh.serializeDBValue(), expectSSH_bothspent); - - // Undo the last operation - ssh.markTxOutUnspent(dbKey0); - EXPECT_EQ(ssh.serializeDBValue(), expectSSH_orig); - - - ssh.markTxOutUnspent(dbKey1); - EXPECT_EQ(ssh.serializeDBValue(), expectSSH_bothunspent); - - ssh.markTxOutSpent( dbKey1, dbKey2); - EXPECT_EQ(ssh.serializeDBValue(), expectSSH_orig); - - - ssh.eraseTxio(dbKey1); - EXPECT_EQ(ssh.serializeDBValue(), expectSSH_afterrm); - -} -*/ - -//////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -class LevelDBTest : public ::testing::Test -{ -protected: - virtual void SetUp(void) - { - iface_ = LevelDBWrapper::GetInterfacePtr(); - magic_ = READHEX(MAINNET_MAGIC_BYTES); - ghash_ = READHEX(MAINNET_GENESIS_HASH_HEX); - gentx_ = READHEX(MAINNET_GENESIS_TX_HASH_HEX); - zeros_ = READHEX("00000000"); - DBUtils.setArmoryDbType(ARMORY_DB_FULL); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - - rawHead_ = READHEX( - "01000000" - "1d8f4ec0443e1f19f305e488c1085c95de7cc3fd25e0d2c5bb5d000000000000" - "9762547903d36881a86751f3f5049e23050113f779735ef82734ebf0b4450081" - "d8c8c84d" - "b3936a1a" - "334b035b"); - headHashLE_ = READHEX( - "1195e67a7a6d0674bbd28ae096d602e1f038c8254b49dfe79d47000000000000"); - headHashBE_ = READHEX( - "000000000000479de7df494b25c838f0e102d696e08ad2bb74066d7a7ae69511"); - - rawTx0_ = READHEX( - "01000000016290dce984203b6a5032e543e9e272d8bce934c7de4d15fa0fe44d" - "d49ae4ece9010000008b48304502204f2fa458d439f957308bca264689aa175e" - "3b7c5f78a901cb450ebd20936b2c500221008ea3883a5b80128e55c9c6070aa6" - "264e1e0ce3d18b7cd7e85108ce3d18b7419a0141044202550a5a6d3bb81549c4" - "a7803b1ad59cdbba4770439a4923624a8acfc7d34900beb54a24188f7f0a4068" - "9d905d4847cc7d6c8d808a457d833c2d44ef83f76bffffffff0242582c0a0000" - "00001976a914c1b4695d53b6ee57a28647ce63e45665df6762c288ac80d1f008" - "000000001976a9140e0aec36fe2545fb31a41164fb6954adcd96b34288ac0000" - "0000"); - rawTx1_ = READHEX( - "0100000001f658dbc28e703d86ee17c9a2d3b167a8508b082fa0745f55be5144" - "a4369873aa010000008c49304602210041e1186ca9a41fdfe1569d5d807ca7ff" - "6c5ffd19d2ad1be42f7f2a20cdc8f1cc0221003366b5d64fe81e53910e156914" - "091d12646bc0d1d662b7a65ead3ebe4ab8f6c40141048d103d81ac9691cf13f3" - "fc94e44968ef67b27f58b27372c13108552d24a6ee04785838f34624b294afee" - "83749b64478bb8480c20b242c376e77eea2b3dc48b4bffffffff0200e1f50500" - "0000001976a9141b00a2f6899335366f04b277e19d777559c35bc888ac40aeeb" - "02000000001976a9140e0aec36fe2545fb31a41164fb6954adcd96b34288ac00" - "000000"); - - rawBlock_ = READHEX( - // Header - "01000000eb10c9a996a2340a4d74eaab41421ed8664aa49d18538bab59010000" - "000000005a2f06efa9f2bd804f17877537f2080030cadbfa1eb50e02338117cc" - "604d91b9b7541a4ecfbb0a1a64f1ade7" - // 3 transactions - "03" - ///// Tx0, version - "01000000" - "01" - // Tx0, Txin0 - "0000000000000000000000000000000000000000000000000000000000000000" - "ffffffff" - "08""04cfbb0a1a02360a""ffffffff" - // Tx0, 1 TxOut - "01" - // Tx0, TxOut0 - "00f2052a01000000" - "434104c2239c4eedb3beb26785753463be3ec62b82f6acd62efb65f452f8806f" - "2ede0b338e31d1f69b1ce449558d7061aa1648ddc2bf680834d3986624006a27" - "2dc21cac" - // Tx0, Locktime - "00000000" - ///// Tx1, Version - "01000000" - // Tx1, 3 txins - "03" - // Tx1, TxIn0 - "e8caa12bcb2e7e86499c9de49c45c5a1c6167ea4b894c8c83aebba1b6100f343" - "01000000" - "8c493046022100e2f5af5329d1244807f8347a2c8d9acc55a21a5db769e9274e" - "7e7ba0bb605b26022100c34ca3350df5089f3415d8af82364d7f567a6a297fcc" - "2c1d2034865633238b8c014104129e422ac490ddfcb7b1c405ab9fb42441246c" - "4bca578de4f27b230de08408c64cad03af71ee8a3140b40408a7058a1984a9f2" - "46492386113764c1ac132990d1""ffffffff" - // Tx1, TxIn1 - "5b55c18864e16c08ef9989d31c7a343e34c27c30cd7caa759651b0e08cae0106" - "00000000" - "8c4930460221009ec9aa3e0caf7caa321723dea561e232603e00686d4bfadf46" - "c5c7352b07eb00022100a4f18d937d1e2354b2e69e02b18d11620a6a9332d563" - "e9e2bbcb01cee559680a014104411b35dd963028300e36e82ee8cf1b0c8d5bf1" - "fc4273e970469f5cb931ee07759a2de5fef638961726d04bd5eb4e5072330b9b" - "371e479733c942964bb86e2b22""ffffffff" - // Tx1, TxIn2 - "3de0c1e913e6271769d8c0172cea2f00d6d3240afc3a20f9fa247ce58af30d2a" - "01000000" - "8c493046022100b610e169fd15ac9f60fe2b507529281cf2267673f4690ba428" - "cbb2ba3c3811fd022100ffbe9e3d71b21977a8e97fde4c3ba47b896d08bc09ec" - "b9d086bb59175b5b9f03014104ff07a1833fd8098b25f48c66dcf8fde34cbdbc" - "c0f5f21a8c2005b160406cbf34cc432842c6b37b2590d16b165b36a3efc9908d" - "65fb0e605314c9b278f40f3e1a""ffffffff" - // Tx1, 2 TxOuts - "02" - // Tx1, TxOut0 - "40420f0000000000""19""76a914adfa66f57ded1b655eb4ccd96ee07ca62bc1ddfd88ac" - // Tx1, TxOut1 - "007d6a7d04000000""19""76a914981a0c9ae61fa8f8c96ae6f8e383d6e07e77133e88ac" - // Tx1 Locktime - "00000000" - ///// Tx2 Version - "01000000" - // Tx2 1 TxIn - "01" - "38e7586e0784280df58bd3dc5e3d350c9036b1ec4107951378f45881799c92a4" - "00000000" - "8a47304402207c945ae0bbdaf9dadba07bdf23faa676485a53817af975ddf85a" - "104f764fb93b02201ac6af32ddf597e610b4002e41f2de46664587a379a01613" - "23a85389b4f82dda014104ec8883d3e4f7a39d75c9f5bb9fd581dc9fb1b7cdf7" - "d6b5a665e4db1fdb09281a74ab138a2dba25248b5be38bf80249601ae688c90c" - "6e0ac8811cdb740fcec31d""ffffffff" - // Tx2, 2 TxOuts - "02" - // Tx2, TxOut0 - "2f66ac6105000000""19""76a914964642290c194e3bfab661c1085e47d67786d2d388ac" - // Tx2, TxOut1 - "2f77e20000000000""19""76a9141486a7046affd935919a3cb4b50a8a0c233c286c88ac" - // Tx2 Locktime - "00000000"); - - rawTxUnfrag_ = READHEX( - // Version - "01000000" - // NumTxIn - "02" - // Start TxIn0 - "0044fbc929d78e4203eed6f1d3d39c0157d8e5c100bbe0886779c0" - "ebf6a69324010000008a47304402206568144ed5e7064d6176c74738b04c08ca" - "19ca54ddeb480084b77f45eebfe57802207927d6975a5ac0e1bb36f5c05356dc" - "da1f521770511ee5e03239c8e1eecf3aed0141045d74feae58c4c36d7c35beac" - "05eddddc78b3ce4b02491a2eea72043978056a8bc439b99ddaad327207b09ef1" - "6a8910828e805b0cc8c11fba5caea2ee939346d7ffffffff" - // Start TxIn1 - "45c866b219b17695" - "2508f8e5aea728f950186554fc4a5807e2186a8e1c4009e5000000008c493046" - "022100bd5d41662f98cfddc46e86ea7e4a3bc8fe9f1dfc5c4836eaf7df582596" - "cfe0e9022100fc459ae4f59b8279d679003b88935896acd10021b6e2e4619377" - "e336b5296c5e014104c00bab76a708ba7064b2315420a1c533ca9945eeff9754" - "cdc574224589e9113469b4e71752146a10028079e04948ecdf70609bf1b9801f" - "6b73ab75947ac339e5ffffffff" - // NumTxOut - "02" - // Start TxOut0 - "ac4c8bd5000000001976a9148dce8946f1c7763bb60ea5cf16ef514cbed0633b88ac" - // Start TxOut1 - "002f6859000000001976a9146a59ac0e8f553f292dfe5e9f3aaa1da93499c15e88ac" - // Locktime - "00000000"); - - rawTxFragged_ = READHEX( - // Version - "01000000" - // NumTxIn - "02" - // Start TxIn0 - "0044fbc929d78e4203eed6f1d3d39c0157d8e5c100bbe0886779c0" - "ebf6a69324010000008a47304402206568144ed5e7064d6176c74738b04c08ca" - "19ca54ddeb480084b77f45eebfe57802207927d6975a5ac0e1bb36f5c05356dc" - "da1f521770511ee5e03239c8e1eecf3aed0141045d74feae58c4c36d7c35beac" - "05eddddc78b3ce4b02491a2eea72043978056a8bc439b99ddaad327207b09ef1" - "6a8910828e805b0cc8c11fba5caea2ee939346d7ffffffff" - // Start TxIn1 - "45c866b219b17695" - "2508f8e5aea728f950186554fc4a5807e2186a8e1c4009e5000000008c493046" - "022100bd5d41662f98cfddc46e86ea7e4a3bc8fe9f1dfc5c4836eaf7df582596" - "cfe0e9022100fc459ae4f59b8279d679003b88935896acd10021b6e2e4619377" - "e336b5296c5e014104c00bab76a708ba7064b2315420a1c533ca9945eeff9754" - "cdc574224589e9113469b4e71752146a10028079e04948ecdf70609bf1b9801f" - "6b73ab75947ac339e5ffffffff" - // NumTxOut - "02" - // ... TxOuts fragged out - // Locktime - "00000000"); - - rawTxOut0_ = READHEX( - // Value - "ac4c8bd500000000" - // Script size (var_int) - "19" - // Script - "76""a9""14""8dce8946f1c7763bb60ea5cf16ef514cbed0633b""88""ac"); - rawTxOut1_ = READHEX( - // Value - "002f685900000000" - // Script size (var_int) - "19" - // Script - "76""a9""14""6a59ac0e8f553f292dfe5e9f3aaa1da93499c15e""88""ac"); - - bh_.unserialize(rawHead_); - tx1_.unserialize(rawTx0_); - tx2_.unserialize(rawTx1_); - sbh_.unserialize(rawHead_); - - // Make sure the global DB type and prune type are reset for each test - DBUtils.setArmoryDbType(ARMORY_DB_FULL); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - } - - ///// - virtual void TearDown(void) - { - // This seem to be the best way to remove a dir tree in C++ (in Linux) - iface_->closeDatabases(); - iface_ = NULL; - - #ifdef _MSC_VER - rmdir("./ldbtestdir/level*"); - #else - system("rm -rf ./ldbtestdir/level*"); - #endif - } - - ///// - void addOutPairH(BinaryData key, BinaryData val) - { - expectOutH_.push_back( pair(key,val)); - } - - ///// - void addOutPairB(BinaryData key, BinaryData val) - { - expectOutB_.push_back( pair(key,val)); - } - - ///// - void replaceTopOutPairB(BinaryData key, BinaryData val) - { - uint32_t last = expectOutB_.size() -1; - expectOutB_[last] = pair(key,val); - } - - ///// - void printOutPairs(void) - { - cout << "Num Houts: " << expectOutH_.size() << endl; - for(uint32_t i=0; igetAllDatabaseEntries(HEADERS); - - if(fromDB.size() < endplus1H || expectOutH_.size() < endplus1H) - { - LOGERR << "Headers DB not the correct size"; - LOGERR << "DB size: " << (int)fromDB.size(); - LOGERR << "Expected: " << (int)expectOutH_.size(); - return false; - } - - for(uint32_t i=startH; igetAllDatabaseEntries(BLKDATA); - if(fromDB.size() < endplus1B || expectOutB_.size() < endplus1B) - { - LOGERR << "BLKDATA DB not the correct size"; - LOGERR << "DB size: " << (int)fromDB.size(); - LOGERR << "Expected: " << (int)expectOutB_.size(); - return false; - } - - for(uint32_t i=startB; iopenDatabases( string("ldbtestdir"), - ghash_, gentx_, magic_, - ARMORY_DB_FULL, DB_PRUNE_NONE); - - BinaryData DBINFO = StoredDBInfo().getDBKey(); - BinaryData flags = READHEX("03100000"); - BinaryData val0 = magic_+flags+zeros_+zeros_+ghash_; - addOutPairH(DBINFO, val0); - addOutPairB(DBINFO, val0); - - return iface_->databasesAreOpen(); - } - - - InterfaceToLDB* iface_; - vector > expectOutH_; - vector > expectOutB_; - - BinaryData magic_; - BinaryData ghash_; - BinaryData gentx_; - BinaryData zeros_; - - BinaryData rawHead_; - BinaryData headHashLE_; - BinaryData headHashBE_; - BinaryData rawBlock_; - BinaryData rawTx0_; - BinaryData rawTx1_; - BlockHeader bh_; - Tx tx1_; - Tx tx2_; - StoredHeader sbh_; - BinaryData rawTxUnfrag_; - BinaryData rawTxFragged_; - BinaryData rawTxOut0_; - BinaryData rawTxOut1_; - -}; - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(LevelDBTest, OpenClose) -{ - iface_->openDatabases( string("ldbtestdir"), - ghash_, - gentx_, - magic_, - ARMORY_DB_FULL, - DB_PRUNE_NONE); - - ASSERT_TRUE(iface_->databasesAreOpen()); - - EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 0); - EXPECT_EQ(iface_->getTopBlockHash(HEADERS), READHEX(MAINNET_GENESIS_HASH_HEX)); - - KVLIST HList = iface_->getAllDatabaseEntries(HEADERS); - KVLIST BList = iface_->getAllDatabaseEntries(BLKDATA); - - // 0123 4567 0123 4567 - // 0000 0010 0001 ---- ---- ---- ---- ---- - BinaryData flags = READHEX("03100000"); - - for(uint32_t i=0; icloseDatabases(); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(LevelDBTest, OpenCloseOpenNominal) -{ - // 0123 4567 0123 4567 - // 0000 0010 0001 ---- ---- ---- ---- ---- - BinaryData flags = READHEX("03100000"); - - iface_->openDatabases( string("ldbtestdir"), - ghash_, - gentx_, - magic_, - ARMORY_DB_FULL, - DB_PRUNE_NONE); - - - iface_->closeDatabases(); - - iface_->openDatabases( string("ldbtestdir"), - ghash_, - gentx_, - magic_, - ARMORY_DB_FULL, - DB_PRUNE_NONE); - ASSERT_TRUE(iface_->databasesAreOpen()); - - KVLIST HList = iface_->getAllDatabaseEntries(HEADERS); - KVLIST BList = iface_->getAllDatabaseEntries(BLKDATA); - - for(uint32_t i=0; icloseDatabases(); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(LevelDBTest, DISABLED_OpenCloseOpenMismatch) -{ - LOGERR << "Expecting four error messages here: this is normal"; - iface_->openDatabases( string("ldbtestdir"), - ghash_, - gentx_, - magic_, - ARMORY_DB_FULL, - DB_PRUNE_NONE); - EXPECT_TRUE(iface_->databasesAreOpen()); - iface_->closeDatabases(); - - iface_->openDatabases( string("ldbtestdir"), - ghash_.getSliceCopy(0, 31) + READHEX("00"), - gentx_, - magic_, - ARMORY_DB_FULL, - DB_PRUNE_NONE); - EXPECT_TRUE(iface_->databasesAreOpen()); - iface_->closeDatabases(); - - iface_->openDatabases( string("ldbtestdir"), - ghash_, - gentx_, - magic_.getSliceCopy(0,3) + READHEX("00"), - ARMORY_DB_FULL, - DB_PRUNE_NONE); - EXPECT_FALSE(iface_->databasesAreOpen()); - - iface_->openDatabases( string("ldbtestdir"), - ghash_, - gentx_, - magic_, - ARMORY_DB_SUPER, - DB_PRUNE_WHATEVER); - EXPECT_FALSE(iface_->databasesAreOpen()); - - iface_->openDatabases( string("ldbtestdir"), - ghash_, - gentx_, - magic_); - ASSERT_TRUE( iface_->databasesAreOpen()); - - EXPECT_EQ( DBUtils.getArmoryDbType(), ARMORY_DB_FULL); - EXPECT_EQ( DBUtils.getDbPruneType(), DB_PRUNE_NONE); - - KVLIST HList = iface_->getAllDatabaseEntries(HEADERS); - KVLIST BList = iface_->getAllDatabaseEntries(BLKDATA); - - EXPECT_EQ(HList.begin()->first, READHEX("00")); - EXPECT_EQ(BList.begin()->first, READHEX("00")); - - iface_->closeDatabases(); - - -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(LevelDBTest, PutGetDelete) -{ - BinaryData flags = READHEX("03100000"); - - iface_->openDatabases( string("ldbtestdir"), - ghash_, - gentx_, - magic_, - ARMORY_DB_FULL, - DB_PRUNE_NONE); - ASSERT_TRUE(iface_->databasesAreOpen()); - - DB_PREFIX TXDATA = DB_PREFIX_TXDATA; - BinaryData DBINFO = StoredDBInfo().getDBKey(); - BinaryData PREFIX = WRITE_UINT8_BE((uint8_t)TXDATA); - BinaryData val0 = magic_+flags+zeros_+zeros_+ghash_; - BinaryData commonValue = READHEX("abcd1234"); - BinaryData keyAB = READHEX("0000"); - BinaryData nothing = BinaryData(0); - - addOutPairH(DBINFO, val0); - - addOutPairB(DBINFO, val0); - addOutPairB( keyAB, commonValue); - addOutPairB(PREFIX + keyAB, commonValue); - - ASSERT_TRUE( compareKVListRange(0,1, 0,1)); - - iface_->putValue(BLKDATA, keyAB, commonValue); - ASSERT_TRUE( compareKVListRange(0,1, 0,2)); - - iface_->putValue(BLKDATA, DB_PREFIX_TXDATA, keyAB, commonValue); - ASSERT_TRUE( compareKVListRange(0,1, 0,3)); - - // Now test a bunch of get* methods - ASSERT_EQ( iface_->getValue( BLKDATA, PREFIX+keyAB), commonValue); - ASSERT_EQ( iface_->getValue( BLKDATA, DB_PREFIX_DBINFO, nothing),val0); - ASSERT_EQ( iface_->getValue( BLKDATA, DBINFO), val0); - ASSERT_EQ( iface_->getValueRef( BLKDATA, PREFIX+keyAB), commonValue); - ASSERT_EQ( iface_->getValueRef( BLKDATA, TXDATA, keyAB), commonValue); - ASSERT_EQ( iface_->getValueReader(BLKDATA, PREFIX+keyAB).getRawRef(), commonValue); - ASSERT_EQ( iface_->getValueReader(BLKDATA, TXDATA, keyAB).getRawRef(),commonValue); - - iface_->deleteValue(BLKDATA, DB_PREFIX_TXDATA, keyAB); - ASSERT_TRUE( compareKVListRange(0,1, 0,2)); - - iface_->deleteValue(BLKDATA, PREFIX+ keyAB); - ASSERT_TRUE( compareKVListRange(0,1, 0,1)); - - iface_->deleteValue(BLKDATA, PREFIX+ keyAB); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(LevelDBTest, STxOutPutGet) -{ - BinaryData TXP = WRITE_UINT8_BE((uint8_t)DB_PREFIX_TXDATA); - BinaryData stxoVal = READHEX("0400") + rawTxOut0_; - BinaryData stxoKey = TXP + READHEX("01e078""0f""0007""0001"); - - ASSERT_TRUE(standardOpenDBs()); - - StoredTxOut stxo0; - stxo0.txVersion_ = 1; - stxo0.spentness_ = TXOUT_UNSPENT; - stxo0.blockHeight_ = 123000; - stxo0.duplicateID_ = 15; - stxo0.txIndex_ = 7; - stxo0.txOutIndex_ = 1; - stxo0.unserialize(rawTxOut0_); - iface_->putStoredTxOut(stxo0); - - // Construct expected output - addOutPairB(stxoKey, stxoVal); - ASSERT_TRUE(compareKVListRange(0,1, 0,2)); - - StoredTxOut stxoGet; - iface_->getStoredTxOut(stxoGet, 123000, 15, 7, 1); - EXPECT_EQ(stxoGet.serializeDBValue(), stxo0.serializeDBValue()); - - //iface_->validDupByHeight_[123000] = 15; - //iface_->getStoredTxOut(stxoGet, 123000, 7, 1); - //EXPECT_EQ(stxoGet.serializeDBValue(), stxo0.serializeDBValue()); - - StoredTxOut stxo1; - stxo1.txVersion_ = 1; - stxo1.spentness_ = TXOUT_UNSPENT; - stxo1.blockHeight_ = 200333; - stxo1.duplicateID_ = 3; - stxo1.txIndex_ = 7; - stxo1.txOutIndex_ = 1; - stxo1.unserialize(rawTxOut1_); - stxoVal = READHEX("0400") + rawTxOut1_; - stxoKey = TXP + READHEX("030e8d""03""00070001"); - iface_->putStoredTxOut(stxo1); - - iface_->getStoredTxOut(stxoGet, 123000, 15, 7, 1); - EXPECT_EQ(stxoGet.serializeDBValue(), stxo0.serializeDBValue()); - iface_->getStoredTxOut(stxoGet, 200333, 3, 7, 1); - EXPECT_EQ(stxoGet.serializeDBValue(), stxo1.serializeDBValue()); - - addOutPairB(stxoKey, stxoVal); - ASSERT_TRUE(compareKVListRange(0,1, 0,3)); - -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(LevelDBTest, PutFullTxNoOuts) -{ - DBUtils.setArmoryDbType(ARMORY_DB_FULL); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - - ASSERT_TRUE(standardOpenDBs()); - - StoredTx stx; - stx.createFromTx(rawTxUnfrag_); - stx.setKeyData(123000, 15, 7); - - BinaryData TXP = WRITE_UINT8_BE((uint8_t)DB_PREFIX_TXDATA); - BinaryData stxKey = TXP + READHEX("01e078""0f""0007"); - BinaryData stxVal = READHEX("0440") + stx.thisHash_ + rawTxFragged_; - - iface_->putStoredTx(stx, false); - addOutPairB(stxKey, stxVal); - EXPECT_TRUE(compareKVListRange(0,1, 0,2)); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(LevelDBTest, PutFullTx) -{ - DBUtils.setArmoryDbType(ARMORY_DB_FULL); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - - BinaryData TXP = WRITE_UINT8_BE((uint8_t)DB_PREFIX_TXDATA); - BinaryData stxoVal = READHEX("0400") + rawTxOut0_; - BinaryData stxKey = TXP + READHEX("01e078""0f""0007"); - BinaryData stxo0Key = TXP + READHEX("01e078""0f""0007""0000"); - BinaryData stxo1Key = TXP + READHEX("01e078""0f""0007""0001"); - BinaryData stxo0raw = READHEX( - "ac4c8bd5000000001976a9148dce8946f1c7763bb60ea5cf16ef514cbed0633b88ac"); - BinaryData stxo1raw = READHEX( - "002f6859000000001976a9146a59ac0e8f553f292dfe5e9f3aaa1da93499c15e88ac"); - - ASSERT_TRUE(standardOpenDBs()); - - StoredTx stx; - stx.createFromTx(rawTxUnfrag_); - stx.setKeyData(123000, 15, 7); - - ASSERT_EQ(stx.stxoMap_.size(), 2); - for(uint32_t i=0; i<2; i++) - { - stx.stxoMap_[i].spentness_ = TXOUT_UNSPENT; - stx.stxoMap_[i].isCoinbase_ = false; - - ASSERT_EQ(stx.stxoMap_[i].blockHeight_, 123000); - ASSERT_EQ(stx.stxoMap_[i].duplicateID_, 15); - ASSERT_EQ(stx.stxoMap_[i].txIndex_, 7); - ASSERT_EQ(stx.stxoMap_[i].txOutIndex_, i); - ASSERT_EQ(stx.stxoMap_[i].isCoinbase_, false); - } - - BinaryData stxVal = READHEX("0440") + stx.thisHash_ + rawTxFragged_; - BinaryData stxo0Val = READHEX("0400") + stxo0raw; - BinaryData stxo1Val = READHEX("0400") + stxo1raw; - - iface_->putStoredTx(stx); - addOutPairB(stxKey, stxVal); - addOutPairB(stxo0Key, stxo0Val); - addOutPairB(stxo1Key, stxo1Val); - EXPECT_TRUE(compareKVListRange(0,1, 0,4)); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(LevelDBTest, PutFullBlockNoTx) -{ - DBUtils.setArmoryDbType(ARMORY_DB_FULL); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - - StoredHeader sbh; - BinaryRefReader brr(rawBlock_); - sbh.unserializeFullBlock(brr); - sbh.setKeyData(123000, UINT8_MAX); - - StoredHeadHgtList hhl; - hhl.height_ = 123000; - hhl.dupAndHashList_.push_back( pair(0, sbh.thisHash_)); - hhl.preferredDup_ = UINT8_MAX; - - BinaryData TXP = WRITE_UINT8_BE((uint8_t)DB_PREFIX_TXDATA); - BinaryData HHP = WRITE_UINT8_BE((uint8_t)DB_PREFIX_HEADHASH); - BinaryData HGP = WRITE_UINT8_BE((uint8_t)DB_PREFIX_HEADHGT); - BinaryData hgtx = READHEX("01e078""00"); - BinaryData sbh_HH_key = HHP + sbh.thisHash_; - BinaryData sbh_HH_val = sbh.dataCopy_ + hgtx; - BinaryData sbh_HG_key = hhl.getDBKey(); - BinaryData sbh_HG_val = hhl.serializeDBValue(); - - ASSERT_TRUE(standardOpenDBs()); - - addOutPairH( sbh_HH_key, sbh_HH_val); - addOutPairH( sbh_HG_key, sbh_HG_val); - - uint8_t sdup = iface_->putStoredHeader(sbh, false); - EXPECT_TRUE(compareKVListRange(0,3, 0,1)); - EXPECT_EQ(sdup, 0); - - // Try adding it again and see if get the correct dup again, and no touch DB - sdup = iface_->putStoredHeader(sbh, false); - EXPECT_TRUE(compareKVListRange(0,3, 0,1)); - EXPECT_EQ(sdup, 0); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(LevelDBTest, PutGetBareHeader) -{ - DBUtils.setArmoryDbType(ARMORY_DB_FULL); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - - StoredHeader sbh; - BinaryRefReader brr(rawBlock_); - sbh.unserializeFullBlock(brr); - sbh.setKeyData(123000, UINT8_MAX); - BinaryData header0 = sbh.thisHash_; - - ASSERT_TRUE(standardOpenDBs()); - - uint8_t sdup = iface_->putBareHeader(sbh); - EXPECT_EQ(sdup, 0); - EXPECT_EQ(sbh.duplicateID_, 0); - - // Try adding it again and see if get the correct dup again, and no touch DB - sdup = iface_->putStoredHeader(sbh, false); - EXPECT_EQ(sdup, 0); - EXPECT_EQ(sbh.duplicateID_, 0); - - // Add a new header and make sure duplicate ID is done correctly - BinaryData newHeader = READHEX( - "0000000105d3571220ef5f87c6ac0bc8bf5b33c02a9e6edf83c84d840109592c" - "0000000027523728e15f5fe1ac507bff92499eada4af8a0c485d5178e3f96568" - "c18f84994e0e4efc1c0175d646a91ad4"); - BinaryData header1 = BtcUtils::getHash256(newHeader); - - StoredHeader sbh2; - sbh2.unserialize(newHeader); - sbh2.setKeyData(123000, UINT8_MAX); - - uint8_t newDup = iface_->putBareHeader(sbh2); - EXPECT_EQ(newDup, 1); - EXPECT_EQ(sbh2.duplicateID_, 1); - - // Now add a new, isMainBranch_ header - StoredHeader sbh3; - BinaryData anotherHead = READHEX( - "010000001d8f4ec0443e1f19f305e488c1085c95de7cc3fd25e0d2c5bb5d0000" - "000000009762547903d36881a86751f3f5049e23050113f779735ef82734ebf0" - "b4450081d8c8c84db3936a1a334b035b"); - BinaryData header2 = BtcUtils::getHash256(anotherHead); - - sbh3.unserialize(anotherHead); - sbh3.setKeyData(123000, UINT8_MAX); - sbh3.isMainBranch_ = true; - uint8_t anotherDup = iface_->putBareHeader(sbh3); - EXPECT_EQ(anotherDup, 2); - EXPECT_EQ(sbh3.duplicateID_, 2); - EXPECT_EQ(iface_->getValidDupIDForHeight(123000), 2); - - // Now test getting bare headers - StoredHeader sbh4; - iface_->getBareHeader(sbh4, 123000); - EXPECT_EQ(sbh4.thisHash_, header2); - EXPECT_EQ(sbh4.duplicateID_, 2); - - iface_->getBareHeader(sbh4, 123000, 1); - EXPECT_EQ(sbh4.thisHash_, header1); - EXPECT_EQ(sbh4.duplicateID_, 1); - - // Re-add the same SBH3, make sure nothing changes - iface_->putBareHeader(sbh3); - EXPECT_EQ(sbh3.duplicateID_, 2); - EXPECT_EQ(iface_->getValidDupIDForHeight(123000), 2); - -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(LevelDBTest, PutFullBlock) -{ - DBUtils.setArmoryDbType(ARMORY_DB_FULL); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - ASSERT_TRUE(standardOpenDBs()); - - StoredHeader sbh; - BinaryRefReader brr(rawBlock_); - sbh.unserializeFullBlock(brr); - sbh.setKeyData(123000); - - BinaryData rawHeader = READHEX( - "01000000eb10c9a996a2340a4d74eaab41421ed8664aa49d18538bab59010000" - "000000005a2f06efa9f2bd804f17877537f2080030cadbfa1eb50e02338117cc" - "604d91b9b7541a4ecfbb0a1a64f1ade7"); - - // Compute and write the headers to the expected-output - StoredHeadHgtList hhl; - hhl.height_ = 123000; - hhl.dupAndHashList_.push_back( pair(0, sbh.thisHash_)); - hhl.preferredDup_ = UINT8_MAX; - BinaryData HHP = WRITE_UINT8_BE((uint8_t)DB_PREFIX_HEADHASH); - BinaryData HGP = WRITE_UINT8_BE((uint8_t)DB_PREFIX_HEADHGT); - BinaryData sbh_HH_key = HHP + sbh.thisHash_; - BinaryData sbh_HH_val = rawHeader + READHEX("01e078""00"); - BinaryData sbh_HG_key = hhl.getDBKey(); - BinaryData sbh_HG_val = hhl.serializeDBValue(); - // Only HEADHASH and HEADHGT entries get written - addOutPairH( sbh_HH_key, sbh_HH_val); - addOutPairH( sbh_HG_key, sbh_HG_val); - - // Now compute and write the BLKDATA - BinaryData TXP = WRITE_UINT8_BE((uint8_t)DB_PREFIX_TXDATA); - BinaryData sbhKey = TXP + READHEX("01e078""00"); - BinaryData stx0Key = sbhKey + READHEX("0000"); - BinaryData stx1Key = sbhKey + READHEX("0001"); - BinaryData stx2Key = sbhKey + READHEX("0002"); - BinaryData stxo00Key = stx0Key + READHEX("0000"); - BinaryData stxo10Key = stx1Key + READHEX("0000"); - BinaryData stxo11Key = stx1Key + READHEX("0001"); - BinaryData stxo20Key = stx2Key + READHEX("0000"); - BinaryData stxo21Key = stx2Key + READHEX("0001"); - BinaryData stxo00Raw = READHEX( "00f2052a01000000""434104" - "c2239c4eedb3beb26785753463be3ec62b82f6acd62efb65f452f8806f2ede0b" - "338e31d1f69b1ce449558d7061aa1648ddc2bf680834d3986624006a272dc21cac"); - BinaryData stxo10Raw = READHEX( "40420f0000000000" - "19""76a914adfa66f57ded1b655eb4ccd96ee07ca62bc1ddfd88ac"); - BinaryData stxo11Raw = READHEX( "007d6a7d04000000" - "19""76a914981a0c9ae61fa8f8c96ae6f8e383d6e07e77133e88ac"); - BinaryData stxo20Raw = READHEX( "2f66ac6105000000" - "19""76a914964642290c194e3bfab661c1085e47d67786d2d388ac"); - BinaryData stxo21Raw = READHEX( "2f77e20000000000" - "19""76a9141486a7046affd935919a3cb4b50a8a0c233c286c88ac"); - - StoredTx & stx0 = sbh.stxMap_[0]; - StoredTx & stx1 = sbh.stxMap_[1]; - StoredTx & stx2 = sbh.stxMap_[2]; - BinaryData hflags = READHEX("01340000"); - BinaryData ntx = READHEX("03000000"); - BinaryData nbyte = READHEX("46040000"); - - // Add header to BLKDATA - addOutPairB(sbhKey, hflags + rawHeader + ntx + nbyte); - - // Add Tx0 to BLKDATA - addOutPairB(stx0Key, READHEX("0440") + stx0.thisHash_ + stx0.getSerializedTxFragged()); - addOutPairB(stxo00Key, READHEX("0480") + stxo00Raw); // is coinbase - - // Add Tx1 to BLKDATA - addOutPairB(stx1Key, READHEX("0440") + stx1.thisHash_ + stx1.getSerializedTxFragged()); - addOutPairB(stxo10Key, READHEX("0400") + stxo10Raw); - addOutPairB(stxo11Key, READHEX("0400") + stxo11Raw); - - // Add Tx2 to BLKDATA - addOutPairB(stx2Key, READHEX("0440") + stx2.thisHash_ + stx2.getSerializedTxFragged()); - addOutPairB(stxo20Key, READHEX("0400") + stxo20Raw); - addOutPairB(stxo21Key, READHEX("0400") + stxo21Raw); - - // DuplicateID values get set when we putStoredHeader since we don't know - // what dupIDs have been taken until we try to put it in the database. - ASSERT_EQ(sbh.stxMap_.size(), 3); - for(uint32_t i=0; i<3; i++) - { - ASSERT_EQ(sbh.stxMap_[i].blockHeight_, 123000); - ASSERT_EQ(sbh.stxMap_[i].duplicateID_, UINT8_MAX); - ASSERT_EQ(sbh.stxMap_[i].txIndex_, i); - for(uint32_t j=0; jputStoredHeader(sbh); - - ASSERT_TRUE(compareKVListRange(0,3, 0,9)); -} - - - -//////////////////////////////////////////////////////////////////////////////// -// Of course, this test only works if the previous test passes (but doesn't -// require a saved state, it just re-puts the full block into the DB). I -// did it this way, because I wasn't comfortable committing the pre-filled -// DB to the repo. -TEST_F(LevelDBTest, GetFullBlock) -{ - DBUtils.setArmoryDbType(ARMORY_DB_FULL); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - ASSERT_TRUE(standardOpenDBs()); - - StoredHeader sbh; - BinaryRefReader brr(rawBlock_); - sbh.unserializeFullBlock(brr); - sbh.setKeyData(123000); - sbh.isMainBranch_ = true; - - // Check the DBInfo before and after putting the valid header - StoredDBInfo sdbi; - iface_->getStoredDBInfo(HEADERS, sdbi); - EXPECT_EQ(sdbi.topBlkHgt_, 0); - EXPECT_EQ(sdbi.topBlkHash_, iface_->getGenesisBlockHash()); - - iface_->putStoredHeader(sbh); - - // Since we marked this block main-branch, it should have non-MAX validDup - EXPECT_EQ(iface_->getValidDupIDForHeight(123000), 0); - BinaryData headerHash = READHEX( - "7d97d862654e03d6c43b77820a40df894e3d6890784528e9cd05000000000000"); - iface_->getStoredDBInfo(HEADERS, sdbi); - EXPECT_EQ(sdbi.topBlkHgt_, 123000); - EXPECT_EQ(sdbi.topBlkHash_, headerHash); - - BinaryData rawHeader = READHEX( - "01000000eb10c9a996a2340a4d74eaab41421ed8664aa49d18538bab59010000" - "000000005a2f06efa9f2bd804f17877537f2080030cadbfa1eb50e02338117cc" - "604d91b9b7541a4ecfbb0a1a64f1ade7"); - - //////////////////// - // Now test the get methods - - for(uint32_t i=0; i<4; i++) - { - StoredHeader sbhGet; - - if(i==0) - EXPECT_TRUE(iface_->getStoredHeader(sbhGet, 123000, 0, false)); - else if(i==1) - EXPECT_TRUE(iface_->getStoredHeader(sbhGet, headerHash, false)); - else if(i==2) - EXPECT_TRUE(iface_->getStoredHeader(sbhGet, 123000, 0, true)); - else if(i==3) - EXPECT_TRUE(iface_->getStoredHeader(sbhGet, headerHash, true)); - - EXPECT_TRUE( sbhGet.isInitialized()); - EXPECT_EQ( sbhGet.dataCopy_, rawHeader); - EXPECT_EQ( sbhGet.thisHash_, headerHash); - EXPECT_EQ( sbhGet.numTx_, 3); - EXPECT_EQ( sbhGet.numBytes_, 1094); - EXPECT_EQ( sbhGet.blockHeight_, 123000); - EXPECT_EQ( sbhGet.duplicateID_, 0); - EXPECT_EQ( sbhGet.merkle_.getSize(), 0); - EXPECT_FALSE(sbhGet.merkleIsPartial_); - EXPECT_EQ( sbhGet.unserArmVer_, 0); - EXPECT_EQ( sbhGet.unserBlkVer_, 1); - EXPECT_EQ( sbhGet.unserDbType_, ARMORY_DB_FULL); - EXPECT_EQ( sbhGet.unserPrType_, DB_PRUNE_NONE); - - if(i<2) - EXPECT_EQ( sbhGet.stxMap_.size(), 0); - else - { - EXPECT_EQ( sbhGet.stxMap_.size(), 3); - EXPECT_EQ( sbhGet.stxMap_[1].blockHeight_, 123000); - EXPECT_EQ( sbhGet.stxMap_[1].duplicateID_, 0); - EXPECT_EQ( sbhGet.stxMap_[1].txIndex_, 1); - EXPECT_EQ( sbhGet.stxMap_[1].numTxOut_, 2); - EXPECT_EQ( sbhGet.stxMap_[1].numBytes_, 621); - EXPECT_EQ( sbhGet.stxMap_[0].stxoMap_[0].isCoinbase_, true); - EXPECT_EQ( sbhGet.stxMap_[0].stxoMap_[0].spentness_, TXOUT_SPENTUNK); - EXPECT_EQ( sbhGet.stxMap_[1].stxoMap_[0].isCoinbase_, false); - EXPECT_EQ( sbhGet.stxMap_[1].stxoMap_[0].blockHeight_, 123000); - EXPECT_EQ( sbhGet.stxMap_[1].stxoMap_[0].duplicateID_, 0); - EXPECT_EQ( sbhGet.stxMap_[1].stxoMap_[0].txIndex_, 1); - EXPECT_EQ( sbhGet.stxMap_[1].stxoMap_[0].txOutIndex_, 0); - } - } - -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(LevelDBTest, PutGetStoredTxHints) -{ - ASSERT_TRUE(standardOpenDBs()); - - BinaryData prefix = READHEX("aabbccdd"); - - StoredTxHints sths; - EXPECT_FALSE(iface_->getStoredTxHints(sths, prefix)); - - sths.txHashPrefix_ = prefix; - - ASSERT_TRUE(iface_->putStoredTxHints(sths)); - - BinaryData THP = WRITE_UINT8_BE((uint8_t)DB_PREFIX_TXHINTS); - addOutPairB(THP + prefix, READHEX("00")); - - compareKVListRange(0,1, 0,2); - - ///// - sths.dbKeyList_.push_back(READHEX("abcd1234ffff")); - replaceTopOutPairB(THP + prefix, READHEX("01""abcd1234ffff")); - EXPECT_TRUE(iface_->putStoredTxHints(sths)); - compareKVListRange(0,1, 0,2); - - ///// - sths.dbKeyList_.push_back(READHEX("00002222aaaa")); - replaceTopOutPairB(THP + prefix, READHEX("02""abcd1234ffff""00002222aaaa")); - EXPECT_TRUE(iface_->putStoredTxHints(sths)); - compareKVListRange(0,1, 0,2); - - ///// - sths.preferredDBKey_ = READHEX("00002222aaaa"); - replaceTopOutPairB(THP + prefix, READHEX("02""00002222aaaa""abcd1234ffff")); - EXPECT_TRUE(iface_->putStoredTxHints(sths)); - compareKVListRange(0,1, 0,2); - - // Now test the get methods - EXPECT_TRUE( iface_->getStoredTxHints(sths, prefix)); - EXPECT_EQ( sths.txHashPrefix_, prefix); - EXPECT_EQ( sths.dbKeyList_.size(), 2); - EXPECT_EQ( sths.preferredDBKey_, READHEX("00002222aaaa")); - - // - sths.dbKeyList_.resize(0); - sths.preferredDBKey_.resize(0); - EXPECT_TRUE( iface_->putStoredTxHints(sths)); - EXPECT_TRUE( iface_->getStoredTxHints(sths, prefix)); - EXPECT_EQ( sths.txHashPrefix_, prefix); - EXPECT_EQ( sths.dbKeyList_.size(), 0); - EXPECT_EQ( sths.preferredDBKey_.getSize(), 0); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(LevelDBTest, PutGetStoredScriptHistory) -{ - ASSERT_TRUE(standardOpenDBs()); - - /////////////////////////////////////////////////////////////////////////// - // A whole bunch of setup stuff we need for SSH operations to work right - InterfaceToLDB * iface = LevelDBWrapper::GetInterfacePtr(); - iface->setValidDupIDForHeight(255,0); - iface->setValidDupIDForHeight(256,0); - - BinaryData dbkey0 = READHEX("0000ff00""0001""0001"); - BinaryData dbkey1 = READHEX("0000ff00""0002""0002"); - BinaryData dbkey2 = READHEX("00010000""0004""0004"); - BinaryData dbkey3 = READHEX("00010000""0006""0006"); - uint64_t val0 = READ_UINT64_HEX_LE("0100000000000000"); - uint64_t val1 = READ_UINT64_HEX_LE("0002000000000000"); - uint64_t val2 = READ_UINT64_HEX_LE("0000030000000000"); - uint64_t val3 = READ_UINT64_HEX_LE("0000000400000000"); - - BinaryData PREFIX = READHEX("03"); - BinaryData RAWTX = READHEX( - "01000000016290dce984203b6a5032e543e9e272d8bce934c7de4d15fa0fe44d" - "d49ae4ece9010000008b48304502204f2fa458d439f957308bca264689aa175e" - "3b7c5f78a901cb450ebd20936b2c500221008ea3883a5b80128e55c9c6070aa6" - "264e1e0ce3d18b7cd7e85108ce3d18b7419a0141044202550a5a6d3bb81549c4" - "a7803b1ad59cdbba4770439a4923624a8acfc7d34900beb54a24188f7f0a4068" - "9d905d4847cc7d6c8d808a457d833c2d44ef83f76bffffffff0242582c0a0000" - "00001976a914c1b4695d53b6ee57a28647ce63e45665df6762c288ac80d1f008" - "000000001976a9140e0aec36fe2545fb31a41164fb6954adcd96b34288ac0000" - "0000"); - iface->putValue(BLKDATA, DB_PREFIX_TXDATA, dbkey0, RAWTX); - iface->putValue(BLKDATA, DB_PREFIX_TXDATA, dbkey1, RAWTX); - iface->putValue(BLKDATA, DB_PREFIX_TXDATA, dbkey2, RAWTX); - iface->putValue(BLKDATA, DB_PREFIX_TXDATA, dbkey3, RAWTX); - - TxIOPair txio0(dbkey0, val0); - TxIOPair txio1(dbkey1, val1); - TxIOPair txio2(dbkey2, val2); - TxIOPair txio3(dbkey3, val3); - txio3.setMultisig(true); - /////////////////////////////////////////////////////////////////////////// - - StoredSubHistory * subptr; - TxIOPair * txioptr; - - StoredScriptHistory ssh, sshorig, sshtemp; - BinaryData hgtX0 = READHEX("0000ff00"); - BinaryData hgtX1 = READHEX("00010000"); - BinaryData uniq = READHEX("00""0000ffff0000ffff0000ffff0000ffff0000ffff"); - sshorig.uniqueKey_ = uniq; - uint32_t blk = READ_UINT32_HEX_LE("ffff0000"); - sshorig.alreadyScannedUpToBlk_ = blk; - sshorig.version_ = 1; - - ///////////////////////////////////////////////////////////////////////////// - // Haven't actually done anything yet... - ssh = sshorig; - EXPECT_EQ(ssh.uniqueKey_, uniq); - EXPECT_EQ(ssh.alreadyScannedUpToBlk_, blk); - EXPECT_EQ(ssh.subHistMap_.size(), 0); - - ///////////////////////////////////////////////////////////////////////////// - // An empty SSH -- this shouldn't happen in production, but test it anyway - iface_->putStoredScriptHistory(ssh); - iface_->getStoredScriptHistory(sshtemp, uniq); - - EXPECT_EQ(sshtemp.uniqueKey_, uniq); - EXPECT_EQ(sshtemp.alreadyScannedUpToBlk_, blk); - EXPECT_EQ(sshtemp.subHistMap_.size(), 0); - - ///////////////////////////////////////////////////////////////////////////// - // A single txio - ssh = sshorig; - ssh.insertTxio(txio0); - - iface_->putStoredScriptHistory(ssh); - iface_->getStoredScriptHistory(sshtemp, uniq); - - EXPECT_EQ(sshtemp.uniqueKey_, uniq); - EXPECT_EQ(sshtemp.alreadyScannedUpToBlk_, blk); - EXPECT_EQ(sshtemp.totalTxioCount_, 1); - EXPECT_EQ(sshtemp.totalUnspent_, val0); - EXPECT_EQ(sshtemp.subHistMap_.size(), 1); - ASSERT_NE(sshtemp.subHistMap_.find(hgtX0), sshtemp.subHistMap_.end()); - subptr = &sshtemp.subHistMap_[hgtX0]; - EXPECT_EQ(subptr->uniqueKey_, uniq); - EXPECT_EQ(subptr->hgtX_, hgtX0); - ASSERT_EQ(subptr->txioSet_.size(), 1); - ASSERT_NE(subptr->txioSet_.find(dbkey0), subptr->txioSet_.end()); - txioptr = &subptr->txioSet_[dbkey0]; - EXPECT_EQ(txioptr->getDBKeyOfOutput(), dbkey0); - EXPECT_EQ(txioptr->getValue(), val0); - EXPECT_FALSE(txioptr->isMultisig()); - - ///////////////////////////////////////////////////////////////////////////// - // Two TxIOPairs in one sub history - ssh = sshorig; - sshtemp = StoredScriptHistory(); - ssh.insertTxio(txio0); - ssh.insertTxio(txio1); - - iface_->putStoredScriptHistory(ssh); - iface_->getStoredScriptHistory(sshtemp, uniq); - - EXPECT_EQ(sshtemp.uniqueKey_, uniq); - EXPECT_EQ(sshtemp.alreadyScannedUpToBlk_, blk); - EXPECT_EQ(sshtemp.totalTxioCount_, 2); - EXPECT_EQ(sshtemp.totalUnspent_, val0+val1); - EXPECT_EQ(sshtemp.subHistMap_.size(), 1); - ASSERT_NE(sshtemp.subHistMap_.find(hgtX0), sshtemp.subHistMap_.end()); - subptr = &sshtemp.subHistMap_[hgtX0]; - EXPECT_EQ(subptr->uniqueKey_, uniq); - EXPECT_EQ(subptr->hgtX_, hgtX0); - ASSERT_EQ(subptr->txioSet_.size(), 2); - ASSERT_NE(subptr->txioSet_.find(dbkey0), subptr->txioSet_.end()); - txioptr = &subptr->txioSet_[dbkey0]; - EXPECT_EQ(txioptr->getDBKeyOfOutput(), dbkey0); - EXPECT_EQ(txioptr->getValue(), val0); - EXPECT_FALSE(txioptr->isMultisig()); - txioptr = &subptr->txioSet_[dbkey1]; - EXPECT_EQ(txioptr->getDBKeyOfOutput(), dbkey1); - EXPECT_EQ(txioptr->getValue(), val1); - EXPECT_FALSE(txioptr->isMultisig()); - - - ///////////////////////////////////////////////////////////////////////////// - // Add new sub-history with multisig - ssh = sshorig; - ssh.insertTxio(txio0); - ssh.insertTxio(txio1); - ssh.insertTxio(txio3); - - iface_->putStoredScriptHistory(ssh); - iface_->getStoredScriptHistory(sshtemp, uniq); - - EXPECT_EQ(sshtemp.uniqueKey_, uniq); - EXPECT_EQ(sshtemp.alreadyScannedUpToBlk_, blk); - EXPECT_EQ(sshtemp.totalTxioCount_, 3); - EXPECT_EQ(sshtemp.totalUnspent_, val0+val1); - EXPECT_EQ(sshtemp.subHistMap_.size(), 2); - - ASSERT_NE(sshtemp.subHistMap_.find(hgtX0), sshtemp.subHistMap_.end()); - subptr = &sshtemp.subHistMap_[hgtX0]; - EXPECT_EQ(subptr->uniqueKey_, uniq); - EXPECT_EQ(subptr->hgtX_, hgtX0); - ASSERT_EQ(subptr->txioSet_.size(), 2); - ASSERT_NE(subptr->txioSet_.find(dbkey0), subptr->txioSet_.end()); - txioptr = &subptr->txioSet_[dbkey0]; - EXPECT_EQ(txioptr->getDBKeyOfOutput(), dbkey0); - EXPECT_EQ(txioptr->getValue(), val0); - txioptr = &subptr->txioSet_[dbkey1]; - EXPECT_EQ(txioptr->getDBKeyOfOutput(), dbkey1); - EXPECT_EQ(txioptr->getValue(), val1); - EXPECT_FALSE(txioptr->isMultisig()); - - ASSERT_NE(sshtemp.subHistMap_.find(hgtX1), sshtemp.subHistMap_.end()); - subptr = &sshtemp.subHistMap_[hgtX1]; - EXPECT_EQ(subptr->uniqueKey_, uniq); - EXPECT_EQ(subptr->hgtX_, hgtX1); - ASSERT_EQ(subptr->txioSet_.size(), 1); - ASSERT_NE(subptr->txioSet_.find(dbkey3), subptr->txioSet_.end()); - txioptr = &subptr->txioSet_[dbkey3]; - EXPECT_EQ(txioptr->getDBKeyOfOutput(), dbkey3); - EXPECT_EQ(txioptr->getValue(), val3); - EXPECT_TRUE(txioptr->isMultisig()); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(LevelDBTest, DISABLED_PutGetStoredUndoData) -{ - // We don't actually use undo data at all yet, so I'll skip the tests for now -} - - -TEST_F(LevelDBTest, HeaderDump) -{ - // We don't actually use undo data at all yet, so I'll skip the tests for now -} -//////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -class TxRefTest : public ::testing::Test -{ -protected: -}; - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(TxRefTest, TxRefNoInit) -{ - TxRef txr; - EXPECT_FALSE(txr.isInitialized()); - //EXPECT_FALSE(txr.isBound()); - - EXPECT_EQ(txr.getDBKey(), BinaryData(0)); - EXPECT_EQ(txr.getDBKeyRef(), BinaryDataRef()); - //EXPECT_EQ(txr.getBlockTimestamp(), UINT32_MAX); - EXPECT_EQ(txr.getBlockHeight(), UINT32_MAX); - EXPECT_EQ(txr.getDuplicateID(), UINT8_MAX ); - EXPECT_EQ(txr.getBlockTxIndex(), UINT16_MAX); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(TxRefTest, TxRefKeyParts) -{ - TxRef txr; - BinaryData newKey = READHEX("e3c4027f000f"); - BinaryDataRef newRef(newKey); - - - txr.setDBKey(newKey); - EXPECT_EQ(txr.getDBKey(), newKey); - EXPECT_EQ(txr.getDBKeyRef(), newRef); - - EXPECT_EQ(txr.getBlockHeight(), 0xe3c402); - EXPECT_EQ(txr.getDuplicateID(), 127); - EXPECT_EQ(txr.getBlockTxIndex(), 15); -} - - -//////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -// TODO: These tests were taken directly from the BlockUtilsSuper.cpp where -// they previously ran without issue. After bringing them over to here, -// they now seg-fault. Disabled for now, since the PartialMerkleTrees -// are not actually in use anywhere yet. -class DISABLED_PartialMerkleTest : public ::testing::Test -{ -protected: - - virtual void SetUp(void) - { - vector txList_(7); - // The "abcd" quartets are to trigger endianness errors -- without them, - // these hashes are palindromes that work regardless of your endian-handling - txList_[0] = READHEX("00000000000000000000000000000000" - "000000000000000000000000abcd0000"); - txList_[1] = READHEX("11111111111111111111111111111111" - "111111111111111111111111abcd1111"); - txList_[2] = READHEX("22222222222222222222222222222222" - "222222222222222222222222abcd2222"); - txList_[3] = READHEX("33333333333333333333333333333333" - "333333333333333333333333abcd3333"); - txList_[4] = READHEX("44444444444444444444444444444444" - "444444444444444444444444abcd4444"); - txList_[5] = READHEX("55555555555555555555555555555555" - "555555555555555555555555abcd5555"); - txList_[6] = READHEX("66666666666666666666666666666666" - "666666666666666666666666abcd6666"); - - vector merkleTree_ = BtcUtils::calculateMerkleTree(txList_); - - /* - cout << "Merkle Tree looks like the following (7 tx): " << endl; - cout << "The ** indicates the nodes we care about for partial tree test" << endl; - cout << " \n"; - cout << " _____0a10_____ \n"; - cout << " / \\ \n"; - cout << " _/ \\_ \n"; - cout << " 65df b4d6 \n"; - cout << " / \\ / \\ \n"; - cout << " 6971 22dc 5675 d0b6 \n"; - cout << " / \\ / \\ / \\ / \n"; - cout << " 0000 1111 2222 3333 4444 5555 6666 \n"; - cout << " ** ** \n"; - cout << " " << endl; - cout << endl; - - cout << "Full Merkle Tree (this one has been unit tested before):" << endl; - for(uint32_t i=0; i txList_; - vector merkleTree_; -}; - - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(DISABLED_PartialMerkleTest, FullTree) -{ - vector isOurs(7); - isOurs[0] = true; - isOurs[1] = true; - isOurs[2] = true; - isOurs[3] = true; - isOurs[4] = true; - isOurs[5] = true; - isOurs[6] = true; - - //cout << "Start serializing a full tree" << endl; - PartialMerkleTree pmtFull(7, &isOurs, &txList_); - BinaryData pmtSerFull = pmtFull.serialize(); - - //cout << "Finished serializing (full)" << endl; - //cout << "Merkle Root: " << pmtFull.getMerkleRoot().toHexStr() << endl; - - //cout << "Starting unserialize (full):" << endl; - //cout << "Serialized: " << pmtSerFull.toHexStr() << endl; - PartialMerkleTree pmtFull2(7); - pmtFull2.unserialize(pmtSerFull); - BinaryData pmtSerFull2 = pmtFull2.serialize(); - //cout << "Reserializ: " << pmtSerFull2.toHexStr() << endl; - //cout << "Equal? " << (pmtSerFull==pmtSerFull2 ? "True" : "False") << endl; - - //cout << "Print Tree:" << endl; - //pmtFull2.pprintTree(); - EXPECT_EQ(pmtSerFull, pmtSerFull2); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(DISABLED_PartialMerkleTest, SingleLeaf) -{ - vector isOurs(7); - ///////////////////////////////////////////////////////////////////////////// - // Test all 7 single-flagged trees - for(uint32_t i=0; i<7; i++) - { - for(uint32_t j=0; j<7; j++) - isOurs[j] = i==j; - - PartialMerkleTree pmt(7, &isOurs, &txList_); - //cout << "Serializing (partial)" << endl; - BinaryData pmtSer = pmt.serialize(); - PartialMerkleTree pmt2(7); - //cout << "Unserializing (partial)" << endl; - pmt2.unserialize(pmtSer); - //cout << "Reserializing (partial)" << endl; - BinaryData pmtSer2 = pmt2.serialize(); - //cout << "Serialized (Partial): " << pmtSer.toHexStr() << endl; - //cout << "Reserializ (Partial): " << pmtSer.toHexStr() << endl; - //cout << "Equal? " << (pmtSer==pmtSer2 ? "True" : "False") << endl; - - //cout << "Print Tree:" << endl; - //pmt2.pprintTree(); - EXPECT_EQ(pmtSer, pmtSer2); - } -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(DISABLED_PartialMerkleTest, MultiLeaf) -{ - // Use deterministic seed - srand(0); - - vector isOurs(7); - - ///////////////////////////////////////////////////////////////////////////// - // Test a variety of 3-flagged trees - for(uint32_t i=0; i<512; i++) - { - if(i<256) - { - // 2/3 of leaves will be selected - for(uint32_t j=0; j<7; j++) - isOurs[j] = (rand() % 3 < 2); - } - else - { - // 1/3 of leaves will be selected - for(uint32_t j=0; j<7; j++) - isOurs[j] = (rand() % 3 < 1); - } - - PartialMerkleTree pmt(7, &isOurs, &txList_); - //cout << "Serializing (partial)" << endl; - BinaryData pmtSer = pmt.serialize(); - PartialMerkleTree pmt2(7); - //cout << "Unserializing (partial)" << endl; - pmt2.unserialize(pmtSer); - //cout << "Reserializing (partial)" << endl; - BinaryData pmtSer2 = pmt2.serialize(); - //cout << "Serialized (Partial): " << pmtSer.toHexStr() << endl; - //cout << "Reserializ (Partial): " << pmtSer.toHexStr() << endl; - cout << "Equal? " << (pmtSer==pmtSer2 ? "True" : "False") << endl; - - //cout << "Print Tree:" << endl; - //pmt2.pprintTree(); - EXPECT_EQ(pmtSer, pmtSer2); - } -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(DISABLED_PartialMerkleTest, EmptyTree) -{ - vector isOurs(7); - isOurs[0] = false; - isOurs[1] = false; - isOurs[2] = false; - isOurs[3] = false; - isOurs[4] = false; - isOurs[5] = false; - isOurs[6] = false; - - //cout << "Start serializing a full tree" << endl; - PartialMerkleTree pmtFull(7, &isOurs, &txList_); - BinaryData pmtSerFull = pmtFull.serialize(); - - //cout << "Finished serializing (full)" << endl; - //cout << "Merkle Root: " << pmtFull.getMerkleRoot().toHexStr() << endl; - - //cout << "Starting unserialize (full):" << endl; - //cout << "Serialized: " << pmtSerFull.toHexStr() << endl; - PartialMerkleTree pmtFull2(7); - pmtFull2.unserialize(pmtSerFull); - BinaryData pmtSerFull2 = pmtFull2.serialize(); - //cout << "Reserializ: " << pmtSerFull2.toHexStr() << endl; - //cout << "Equal? " << (pmtSerFull==pmtSerFull2 ? "True" : "False") << endl; - - //cout << "Print Tree:" << endl; - //pmtFull2.pprintTree(); - EXPECT_EQ(pmtSerFull, pmtSerFull2); - -} - -//////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -// THESE ARE ARMORY_DB_BARE tests. Identical to above except for the mode. -//////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -class BlockUtilsBare : public ::testing::Test -{ -protected: - - ///////////////////////////////////////////////////////////////////////////// - virtual void SetUp(void) - { - LOGDISABLESTDOUT(); - iface_ = LevelDBWrapper::GetInterfacePtr(); - magic_ = READHEX(MAINNET_MAGIC_BYTES); - ghash_ = READHEX(MAINNET_GENESIS_HASH_HEX); - gentx_ = READHEX(MAINNET_GENESIS_TX_HASH_HEX); - zeros_ = READHEX("00000000"); - DBUtils.setArmoryDbType(ARMORY_DB_BARE); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - - blkdir_ = string("./blkfiletest"); - homedir_ = string("./fakehomedir"); - ldbdir_ = string("./ldbtestdir"); - - iface_->openDatabases( ldbdir_, ghash_, gentx_, magic_, - ARMORY_DB_BARE, DB_PRUNE_NONE); - if(!iface_->databasesAreOpen()) - LOGERR << "ERROR OPENING DATABASES FOR TESTING!"; - - mkdir(blkdir_); - mkdir(homedir_); - - // Put the first 5 blocks into the blkdir - blk0dat_ = BtcUtils::getBlkFilename(blkdir_, 0); - BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_); - - TheBDM.SelectNetwork("Main"); - TheBDM.SetBlkFileLocation(blkdir_); - TheBDM.SetHomeDirLocation(homedir_); - TheBDM.SetLevelDBLocation(ldbdir_); - - blkHash0 = READHEX("6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000"); - blkHash1 = READHEX("1b5514b83257d924be7f10c65b95b1f3c0e50081e1dfd8943eece5eb00000000"); - blkHash2 = READHEX("979fc39616bf1dc6b1f88167f76383d44d65ccd0fc99b7f91bcb2c9500000000"); - blkHash3 = READHEX("50f8231e5fd476f470e1ba4937bc97cb304136c96c765339308935bc00000000"); - blkHash4 = READHEX("8e121ba0d275f49a21bbc171d7d49890de13c9b9733e0104654d262f00000000"); - blkHash3A= READHEX("dd63f62ef59d5b6a6da2a36407f76e4e28026a3fd3a46700d284424700000000"); - blkHash4A= READHEX("bfa204022816102169b4e1d4f78cdf77258048f6d14282144cc01d5500000000"); - blkHash5A= READHEX("4e049fd71ef7381a73e4f550d97812d1eb0fbd1489c1774e18855f1900000000"); - - addrA_ = READHEX("62e907b15cbf27d5425399ebf6f0fb50ebb88f18"); - addrB_ = READHEX("ee26c56fc1d942be8d7a24b2a1001dd894693980"); - addrC_ = READHEX("cb2abde8bccacc32e893df3a054b9ef7f227a4ce"); - addrD_ = READHEX("c522664fb0e55cdc5c0cea73b4aad97ec8343232"); - - scrAddrA_ = HASH160PREFIX + addrA_; - scrAddrB_ = HASH160PREFIX + addrB_; - scrAddrC_ = HASH160PREFIX + addrC_; - scrAddrD_ = HASH160PREFIX + addrD_; - - } - - - ///////////////////////////////////////////////////////////////////////////// - virtual void TearDown(void) - { - BlockDataManager_LevelDB::DestroyInstance(); - - rmdir(blkdir_); - rmdir(homedir_); - - char* delstr = new char[4096]; - sprintf(delstr, "%s/level*", ldbdir_.c_str()); - rmdir(delstr); - delete[] delstr; - - LOGENABLESTDOUT(); - } - - - -#if ! defined(_MSC_VER) && ! defined(__MINGW32__) - - ///////////////////////////////////////////////////////////////////////////// - void rmdir(string src) - { - char* syscmd = new char[4096]; - sprintf(syscmd, "rm -rf %s", src.c_str()); - system(syscmd); - delete[] syscmd; - } - - ///////////////////////////////////////////////////////////////////////////// - void mkdir(string newdir) - { - char* syscmd = new char[4096]; - sprintf(syscmd, "mkdir -p %s", newdir.c_str()); - system(syscmd); - delete[] syscmd; - } -#endif - - InterfaceToLDB* iface_; - BinaryData magic_; - BinaryData ghash_; - BinaryData gentx_; - BinaryData zeros_; - - string blkdir_; - string homedir_; - string ldbdir_; - string blk0dat_;; - - BinaryData blkHash0; - BinaryData blkHash1; - BinaryData blkHash2; - BinaryData blkHash3; - BinaryData blkHash4; - BinaryData blkHash3A; - BinaryData blkHash4A; - BinaryData blkHash5A; - - BinaryData addrA_; - BinaryData addrB_; - BinaryData addrC_; - BinaryData addrD_; - BinaryData scrAddrA_; - BinaryData scrAddrB_; - BinaryData scrAddrC_; - BinaryData scrAddrD_; -}; - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockUtilsBare, BuildNoRegisterWlt) -{ - TheBDM.doInitialSyncOnLoad(); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockUtilsBare, Load5Blocks) -{ - BtcWallet wlt; - wlt.addScrAddress(scrAddrA_); - wlt.addScrAddress(scrAddrB_); - wlt.addScrAddress(scrAddrC_); - TheBDM.registerWallet(&wlt); - TheBDM.registerNewScrAddr(scrAddrD_); - - TheBDM.doInitialSyncOnLoad(); - TheBDM.scanBlockchainForTx(wlt); - - ScrAddrObj * scrobj; - scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); - EXPECT_EQ(scrobj->getFullBalance(),100*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); - EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); - EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrD_); - EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); // hasn't been scanned yet - - EXPECT_EQ(wlt.getFullBalance(), 150*COIN); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockUtilsBare, Load4Blocks_Plus1) -{ - BtcWallet wlt; - wlt.addScrAddress(scrAddrA_); - wlt.addScrAddress(scrAddrB_); - wlt.addScrAddress(scrAddrC_); - TheBDM.registerWallet(&wlt); - TheBDM.registerNewScrAddr(scrAddrD_); - - // Copy only the first four blocks. Will copy the full file next to test - // readBlkFileUpdate method on non-reorg blocks. - BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_, 1596); - TheBDM.doInitialSyncOnLoad(); - TheBDM.scanBlockchainForTx(wlt); - EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 3); - EXPECT_EQ(iface_->getTopBlockHash(HEADERS), blkHash3); - EXPECT_TRUE(TheBDM.getHeaderByHash(blkHash3)->isMainBranch()); - - BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_); - TheBDM.readBlkFileUpdate(); - TheBDM.scanBlockchainForTx(wlt); - EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 4); - EXPECT_EQ(iface_->getTopBlockHash(HEADERS), blkHash4); - EXPECT_TRUE(TheBDM.getHeaderByHash(blkHash4)->isMainBranch()); - - ScrAddrObj * scrobj; - scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); - EXPECT_EQ(scrobj->getFullBalance(),100*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); - EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); - EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrD_); - EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); // hasn't been scanned yet -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockUtilsBare, Load5Blocks_FullReorg) -{ - BtcWallet wlt; - wlt.addScrAddress(scrAddrA_); - wlt.addScrAddress(scrAddrB_); - wlt.addScrAddress(scrAddrC_); - TheBDM.registerWallet(&wlt); - TheBDM.registerNewScrAddr(scrAddrD_); - - BtcWallet wlt2; - wlt2.addScrAddress(scrAddrD_); - - TheBDM.doInitialSyncOnLoad(); - TheBDM.scanBlockchainForTx(wlt); - TheBDM.scanBlockchainForTx(wlt2); - - BtcUtils::copyFile("../reorgTest/blk_3A.dat", blk0dat_); - TheBDM.readBlkFileUpdate(); - BtcUtils::copyFile("../reorgTest/blk_4A.dat", blk0dat_); - TheBDM.readBlkFileUpdate(); - BtcUtils::copyFile("../reorgTest/blk_5A.dat", blk0dat_); - TheBDM.readBlkFileUpdate(); - - TheBDM.scanBlockchainForTx(wlt); - TheBDM.scanBlockchainForTx(wlt2); - - ScrAddrObj * scrobj; - scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); - EXPECT_EQ(scrobj->getFullBalance(),150*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); - EXPECT_EQ(scrobj->getFullBalance(), 10*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); - EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); - //scrobj = &wlt.getScrAddrObjByKey(scrAddrD_); - //EXPECT_EQ(scrobj->getFullBalance(),140*COIN); - - EXPECT_EQ(wlt.getFullBalance(), 160*COIN); -} - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockUtilsBare, Load5Blocks_RescanOps) -{ - ScrAddrObj * scrobj; - BtcWallet wlt; - wlt.addScrAddress(scrAddrA_); - wlt.addScrAddress(scrAddrB_); - wlt.addScrAddress(scrAddrC_); - TheBDM.registerWallet(&wlt); - - // Regular sync - TheBDM.doInitialSyncOnLoad(); - TheBDM.scanBlockchainForTx(wlt); - - scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); - EXPECT_EQ(scrobj->getFullBalance(),100*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); - EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); - EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); - - // Rebuild on-the-fly - TheBDM.doRebuildDatabases(); - TheBDM.scanBlockchainForTx(wlt); - - scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); - EXPECT_EQ(scrobj->getFullBalance(),100*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); - EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); - EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); - - // Rebuild on-the-fly - TheBDM.doFullRescanRegardlessOfSync(); - TheBDM.scanBlockchainForTx(wlt); - - scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); - EXPECT_EQ(scrobj->getFullBalance(),100*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); - EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); - EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); - - - TheBDM.doSyncIfNeeded(); - TheBDM.scanBlockchainForTx(wlt); - - scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); - EXPECT_EQ(scrobj->getFullBalance(),100*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); - EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); - EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); - - // Now add a new addr (with balance) and rescan - - //scrobj = &wlt.getScrAddrObjByKey(scrAddrD_); - //EXPECT_EQ(scrobj->getFullBalance(),100*COIN); // hasn't been scanned yet - -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockUtilsBare, Load5Blocks_RescanEmptyDB) -{ - ScrAddrObj * scrobj; - BtcWallet wlt; - wlt.addScrAddress(scrAddrA_); - wlt.addScrAddress(scrAddrB_); - wlt.addScrAddress(scrAddrC_); - TheBDM.registerWallet(&wlt); - - TheBDM.doInitialSyncOnLoad_Rescan(); - TheBDM.scanBlockchainForTx(wlt); - - scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); - EXPECT_EQ(scrobj->getFullBalance(),100*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); - EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); - EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockUtilsBare, Load5Blocks_RebuildEmptyDB) -{ - ScrAddrObj * scrobj; - BtcWallet wlt; - wlt.addScrAddress(scrAddrA_); - wlt.addScrAddress(scrAddrB_); - wlt.addScrAddress(scrAddrC_); - TheBDM.registerWallet(&wlt); - - /////////////////////////////////////////// - TheBDM.doInitialSyncOnLoad_Rebuild(); - /////////////////////////////////////////// - - TheBDM.scanBlockchainForTx(wlt); - - scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); - EXPECT_EQ(scrobj->getFullBalance(),100*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); - EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); - EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockUtilsBare, Load5Blocks_ForceFullRewhatever) -{ - ScrAddrObj * scrobj; - BtcWallet wlt; - wlt.addScrAddress(scrAddrA_); - wlt.addScrAddress(scrAddrB_); - wlt.addScrAddress(scrAddrC_); - TheBDM.registerWallet(&wlt); - - // This is just a regular load - TheBDM.doInitialSyncOnLoad(); - TheBDM.scanBlockchainForTx(wlt); - - wlt.addScrAddress(scrAddrD_); - - scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); - EXPECT_EQ(scrobj->getFullBalance(),100*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); - EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); - EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrD_); - EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); - - /////////////////////////////////////////// - TheBDM.doFullRescanRegardlessOfSync(); - /////////////////////////////////////////// - - TheBDM.scanBlockchainForTx(wlt); - - scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); - EXPECT_EQ(scrobj->getFullBalance(),100*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); - EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); - EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrD_); - EXPECT_EQ(scrobj->getFullBalance(),100*COIN); - - /////////////////////////////////////////// - TheBDM.doRebuildDatabases(); - /////////////////////////////////////////// - - TheBDM.scanBlockchainForTx(wlt); - - scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); - EXPECT_EQ(scrobj->getFullBalance(),100*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); - EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); - EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrD_); - EXPECT_EQ(scrobj->getFullBalance(),100*COIN); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockUtilsBare, Load5Blocks_ScanWhatIsNeeded) -{ - ScrAddrObj * scrobj; - BtcWallet wlt; - wlt.addScrAddress(scrAddrA_); - wlt.addScrAddress(scrAddrB_); - wlt.addScrAddress(scrAddrC_); - TheBDM.registerWallet(&wlt); - - // This is just a regular load - TheBDM.doInitialSyncOnLoad(); - TheBDM.scanBlockchainForTx(wlt); - - wlt.addScrAddress(scrAddrD_); - - scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); - EXPECT_EQ(scrobj->getFullBalance(),100*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); - EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); - EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrD_); - EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); - - /////////////////////////////////////////// - TheBDM.doSyncIfNeeded(); - /////////////////////////////////////////// - - TheBDM.scanBlockchainForTx(wlt); - - scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); - EXPECT_EQ(scrobj->getFullBalance(),100*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); - EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); - EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrD_); - EXPECT_EQ(scrobj->getFullBalance(),100*COIN); - - /////////////////////////////////////////// - TheBDM.doSyncIfNeeded(); - /////////////////////////////////////////// - - TheBDM.scanBlockchainForTx(wlt); - - scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); - EXPECT_EQ(scrobj->getFullBalance(),100*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); - EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); - EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); - scrobj = &wlt.getScrAddrObjByKey(scrAddrD_); - EXPECT_EQ(scrobj->getFullBalance(),100*COIN); -} - -class LoadTestnetBareTest : public ::testing::Test -{ -protected: - - ///////////////////////////////////////////////////////////////////////////// - virtual void TearDown(void) {} - - virtual void SetUp(void) - { - DBUtils.setArmoryDbType(ARMORY_DB_BARE); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - - blkdir_ = string("/home/alan/.bitcoin/testnet3/blocks"); - homedir_ = string("/home/alan/.armory/testnet3"); - ldbdir_ = string("/home/alan/.armory/testnet3/databases"); - - - mkdir(ldbdir_); - mkdir(homedir_); - - TheBDM.SelectNetwork("Test"); - TheBDM.SetBlkFileLocation(blkdir_); - TheBDM.SetHomeDirLocation(homedir_); - TheBDM.SetLevelDBLocation(ldbdir_); - } - - void mkdir(string newdir) - { - char* syscmd = new char[4096]; - sprintf(syscmd, "mkdir -p %s", newdir.c_str()); - system(syscmd); - delete[] syscmd; - } - - InterfaceToLDB* iface_; - BinaryData magic_; - BinaryData ghash_; - BinaryData gentx_; - BinaryData zeros_; - - string blkdir_; - string homedir_; - string ldbdir_; - string blk0dat_;; - - BinaryData addrA_; - BinaryData addrB_; - BinaryData addrC_; - BinaryData addrD_; - BinaryData scrAddrA_; - BinaryData scrAddrB_; - BinaryData scrAddrC_; - BinaryData scrAddrD_; -}; - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(LoadTestnetBareTest, DISABLED_StepThroughDebug_usually_disabled) -{ - // These aren't actually testnet addr, balances will be zero - BinaryData scrAddrA_ = READHEX("0062e907b15cbf27d5425399ebf6f0fb50ebb88f18"); - BinaryData scrAddrB_ = READHEX("00ee26c56fc1d942be8d7a24b2a1001dd894693980"); - BinaryData scrAddrC_ = READHEX("00cb2abde8bccacc32e893df3a054b9ef7f227a4ce"); - - BtcWallet wlt; - wlt.addScrAddress(scrAddrA_); - wlt.addScrAddress(scrAddrB_); - wlt.addScrAddress(scrAddrC_); - TheBDM.registerWallet(&wlt); - - TheBDM.doInitialSyncOnLoad(); - TheBDM.scanBlockchainForTx(wlt); - TheBDM.DestroyInstance(); -} - - -//////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -// THESE ARE ARMORY_DB_SUPER tests. Identical to above except for the mode. -//////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -class BlockUtilsSuper : public ::testing::Test -{ -protected: - - ///////////////////////////////////////////////////////////////////////////// - virtual void SetUp(void) - { - LOGDISABLESTDOUT(); - iface_ = LevelDBWrapper::GetInterfacePtr(); - magic_ = READHEX(MAINNET_MAGIC_BYTES); - ghash_ = READHEX(MAINNET_GENESIS_HASH_HEX); - gentx_ = READHEX(MAINNET_GENESIS_TX_HASH_HEX); - zeros_ = READHEX("00000000"); - - blkdir_ = string("./blkfiletest"); - homedir_ = string("./fakehomedir"); - ldbdir_ = string("./ldbtestdir"); - - - mkdir(blkdir_); - mkdir(homedir_); - - // Put the first 5 blocks into the blkdir - blk0dat_ = BtcUtils::getBlkFilename(blkdir_, 0); - BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_); - - TheBDM.SetDatabaseModes(ARMORY_DB_SUPER, DB_PRUNE_NONE); - TheBDM.SelectNetwork("Main"); - TheBDM.SetBlkFileLocation(blkdir_); - TheBDM.SetHomeDirLocation(homedir_); - TheBDM.SetLevelDBLocation(ldbdir_); - - iface_->openDatabases( ldbdir_, ghash_, gentx_, magic_, - ARMORY_DB_SUPER, DB_PRUNE_NONE); - if(!iface_->databasesAreOpen()) - LOGERR << "ERROR OPENING DATABASES FOR TESTING!"; - - blkHash0 = READHEX("6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000"); - blkHash1 = READHEX("1b5514b83257d924be7f10c65b95b1f3c0e50081e1dfd8943eece5eb00000000"); - blkHash2 = READHEX("979fc39616bf1dc6b1f88167f76383d44d65ccd0fc99b7f91bcb2c9500000000"); - blkHash3 = READHEX("50f8231e5fd476f470e1ba4937bc97cb304136c96c765339308935bc00000000"); - blkHash4 = READHEX("8e121ba0d275f49a21bbc171d7d49890de13c9b9733e0104654d262f00000000"); - blkHash3A= READHEX("dd63f62ef59d5b6a6da2a36407f76e4e28026a3fd3a46700d284424700000000"); - blkHash4A= READHEX("bfa204022816102169b4e1d4f78cdf77258048f6d14282144cc01d5500000000"); - blkHash5A= READHEX("4e049fd71ef7381a73e4f550d97812d1eb0fbd1489c1774e18855f1900000000"); - - addrA_ = READHEX("62e907b15cbf27d5425399ebf6f0fb50ebb88f18"); - addrB_ = READHEX("ee26c56fc1d942be8d7a24b2a1001dd894693980"); - addrC_ = READHEX("cb2abde8bccacc32e893df3a054b9ef7f227a4ce"); - addrD_ = READHEX("c522664fb0e55cdc5c0cea73b4aad97ec8343232"); - - scrAddrA_ = HASH160PREFIX + addrA_; - scrAddrB_ = HASH160PREFIX + addrB_; - scrAddrC_ = HASH160PREFIX + addrC_; - scrAddrD_ = HASH160PREFIX + addrD_; - - } - - - ///////////////////////////////////////////////////////////////////////////// - virtual void TearDown(void) - { - BlockDataManager_LevelDB::DestroyInstance(); - - rmdir(blkdir_); - rmdir(homedir_); - - char* delstr = new char[4096]; - sprintf(delstr, "%s/level*", ldbdir_.c_str()); - rmdir(delstr); - delete[] delstr; - - LOGENABLESTDOUT(); - } - - - -#if ! defined(_MSC_VER) && ! defined(__MINGW32__) - - ///////////////////////////////////////////////////////////////////////////// - void rmdir(string src) - { - char* syscmd = new char[4096]; - sprintf(syscmd, "rm -rf %s", src.c_str()); - system(syscmd); - delete[] syscmd; - } - - ///////////////////////////////////////////////////////////////////////////// - void mkdir(string newdir) - { - char* syscmd = new char[4096]; - sprintf(syscmd, "mkdir -p %s", newdir.c_str()); - system(syscmd); - delete[] syscmd; - } -#endif - - InterfaceToLDB* iface_; - BinaryData magic_; - BinaryData ghash_; - BinaryData gentx_; - BinaryData zeros_; - - string blkdir_; - string homedir_; - string ldbdir_; - string blk0dat_;; - - BinaryData blkHash0; - BinaryData blkHash1; - BinaryData blkHash2; - BinaryData blkHash3; - BinaryData blkHash4; - BinaryData blkHash3A; - BinaryData blkHash4A; - BinaryData blkHash5A; - - BinaryData addrA_; - BinaryData addrB_; - BinaryData addrC_; - BinaryData addrD_; - BinaryData scrAddrA_; - BinaryData scrAddrB_; - BinaryData scrAddrC_; - BinaryData scrAddrD_; -}; - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockUtilsSuper, HeadersOnly) -{ - EXPECT_EQ(TheBDM.getNumBlocks(), 0); - TheBDM.processNewHeadersInBlkFiles(0); - - EXPECT_EQ(TheBDM.getNumBlocks(), 5); - EXPECT_EQ(TheBDM.getTopBlockHeight(), 4); - EXPECT_EQ(TheBDM.getTopBlockHash(), blkHash4); - EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 4); - //iface_->printAllDatabaseEntries(HEADERS); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockUtilsSuper, HeadersOnly_Reorg) -{ - SETLOGLEVEL(LogLvlError); - EXPECT_EQ(TheBDM.getNumBlocks(), 0); - TheBDM.processNewHeadersInBlkFiles(0); - - EXPECT_EQ(TheBDM.getNumBlocks(), 5); - EXPECT_EQ(TheBDM.getTopBlockHeight(), 4); - - EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 4); - EXPECT_EQ(iface_->getTopBlockHash(HEADERS), blkHash4); - - BtcUtils::copyFile("../reorgTest/blk_3A.dat", BtcUtils::getBlkFilename(blkdir_, 1)); - TheBDM.processNewHeadersInBlkFiles(1); - EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 4); - EXPECT_EQ(iface_->getTopBlockHash(HEADERS), blkHash4); - EXPECT_FALSE(TheBDM.getHeaderByHash(blkHash3A)->isMainBranch()); - EXPECT_TRUE( TheBDM.getHeaderByHash(blkHash3 )->isMainBranch()); - - BtcUtils::copyFile("../reorgTest/blk_4A.dat", BtcUtils::getBlkFilename(blkdir_, 2)); - TheBDM.processNewHeadersInBlkFiles(2); - EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 4); - EXPECT_EQ(iface_->getTopBlockHash(HEADERS), blkHash4); - EXPECT_FALSE(TheBDM.getHeaderByHash(blkHash3A)->isMainBranch()); - EXPECT_TRUE( TheBDM.getHeaderByHash(blkHash3 )->isMainBranch()); - EXPECT_FALSE(TheBDM.getHeaderByHash(blkHash4A)->isMainBranch()); - EXPECT_TRUE( TheBDM.getHeaderByHash(blkHash4 )->isMainBranch()); - - BtcUtils::copyFile("../reorgTest/blk_5A.dat", BtcUtils::getBlkFilename(blkdir_, 3)); - TheBDM.processNewHeadersInBlkFiles(3); - EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 5); - EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 5); - EXPECT_EQ(iface_->getTopBlockHash(HEADERS), blkHash5A); - EXPECT_FALSE(TheBDM.getHeaderByHash(blkHash3 )->isMainBranch()); - EXPECT_TRUE( TheBDM.getHeaderByHash(blkHash3A)->isMainBranch()); - EXPECT_FALSE(TheBDM.getHeaderByHash(blkHash4 )->isMainBranch()); - EXPECT_TRUE( TheBDM.getHeaderByHash(blkHash4A)->isMainBranch()); - - SETLOGLEVEL(LogLvlDebug2); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockUtilsSuper, Load5Blocks) -{ - DBUtils.setArmoryDbType(ARMORY_DB_SUPER); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - TheBDM.doInitialSyncOnLoad(); - - StoredScriptHistory ssh; - - iface_->getStoredScriptHistory(ssh, scrAddrA_); - EXPECT_EQ(ssh.getScriptBalance(), 100*COIN); - EXPECT_EQ(ssh.getScriptReceived(), 100*COIN); - EXPECT_EQ(ssh.totalTxioCount_, 2); - - iface_->getStoredScriptHistory(ssh, scrAddrB_); - EXPECT_EQ(ssh.getScriptBalance(), 0*COIN); - EXPECT_EQ(ssh.getScriptReceived(), 140*COIN); - EXPECT_EQ(ssh.totalTxioCount_, 3); - - iface_->getStoredScriptHistory(ssh, scrAddrC_); - EXPECT_EQ(ssh.getScriptBalance(), 50*COIN); - EXPECT_EQ(ssh.getScriptReceived(), 60*COIN); - EXPECT_EQ(ssh.totalTxioCount_, 2); - - iface_->getStoredScriptHistory(ssh, scrAddrD_); - EXPECT_EQ(ssh.getScriptBalance(), 100*COIN); - EXPECT_EQ(ssh.getScriptReceived(), 100*COIN); - EXPECT_EQ(ssh.totalTxioCount_, 3); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockUtilsSuper, Load4BlocksPlus1) -{ - // Copy only the first four blocks. Will copy the full file next to test - // readBlkFileUpdate method on non-reorg blocks. - BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_, 1596); - TheBDM.doInitialSyncOnLoad(); - EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 3); - EXPECT_EQ(iface_->getTopBlockHash(HEADERS), blkHash3); - EXPECT_TRUE(TheBDM.getHeaderByHash(blkHash3)->isMainBranch()); - - BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_); - TheBDM.readBlkFileUpdate(); - EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 4); - EXPECT_EQ(iface_->getTopBlockHash(HEADERS), blkHash4); - EXPECT_TRUE(TheBDM.getHeaderByHash(blkHash4)->isMainBranch()); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockUtilsSuper, Load5Blocks_Plus2NoReorg) -{ - DBUtils.setArmoryDbType(ARMORY_DB_SUPER); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - TheBDM.doInitialSyncOnLoad(); - - - BtcUtils::copyFile("../reorgTest/blk_3A.dat", blk0dat_); - TheBDM.readBlkFileUpdate(); - EXPECT_EQ(TheBDM.getTopBlockHash(), blkHash4); - EXPECT_EQ(TheBDM.getTopBlockHeight(), 4); - - BtcUtils::copyFile("../reorgTest/blk_4A.dat", blk0dat_); - TheBDM.readBlkFileUpdate(); - EXPECT_EQ(TheBDM.getTopBlockHash(), blkHash4); - EXPECT_EQ(TheBDM.getTopBlockHeight(), 4); - - //BtcUtils::copyFile("../reorgTest/blk_5A.dat", blk0dat_); - //iface_->pprintBlkDataDB(BLKDATA); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockUtilsSuper, Load5Blocks_FullReorg) -{ - DBUtils.setArmoryDbType(ARMORY_DB_SUPER); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - TheBDM.doInitialSyncOnLoad(); - - BtcUtils::copyFile("../reorgTest/blk_3A.dat", blk0dat_); - TheBDM.readBlkFileUpdate(); - BtcUtils::copyFile("../reorgTest/blk_4A.dat", blk0dat_); - TheBDM.readBlkFileUpdate(); - BtcUtils::copyFile("../reorgTest/blk_5A.dat", blk0dat_); - TheBDM.readBlkFileUpdate(); - - //iface_->pprintBlkDataDB(BLKDATA); - StoredScriptHistory ssh; - - iface_->getStoredScriptHistory(ssh, scrAddrA_); - EXPECT_EQ(ssh.getScriptBalance(), 150*COIN); - EXPECT_EQ(ssh.getScriptReceived(), 150*COIN); - EXPECT_EQ(ssh.totalTxioCount_, 3); - - iface_->getStoredScriptHistory(ssh, scrAddrB_); - EXPECT_EQ(ssh.getScriptBalance(), 10*COIN); - EXPECT_EQ(ssh.getScriptReceived(), 150*COIN); - EXPECT_EQ(ssh.totalTxioCount_, 4); - - iface_->getStoredScriptHistory(ssh, scrAddrC_); - EXPECT_EQ(ssh.getScriptBalance(), 0*COIN); - EXPECT_EQ(ssh.getScriptReceived(), 10*COIN); - EXPECT_EQ(ssh.totalTxioCount_, 1); - - iface_->getStoredScriptHistory(ssh, scrAddrD_); - EXPECT_EQ(ssh.getScriptBalance(), 140*COIN); - EXPECT_EQ(ssh.getScriptReceived(), 140*COIN); - EXPECT_EQ(ssh.totalTxioCount_, 3); -} - -//////////////////////////////////////////////////////////////////////////////// -// These next two tests disabled because they broke after ARMORY_DB_BARE impl -TEST_F(BlockUtilsSuper, DISABLED_RestartDBAfterBuild) -{ - // Copy only the first four blocks. Will copy the full file next to test - // readBlkFileUpdate method on non-reorg blocks. - BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_, 926); - TheBDM.doInitialSyncOnLoad(); - EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 2); - EXPECT_EQ(iface_->getTopBlockHash(HEADERS), blkHash2); - EXPECT_TRUE(TheBDM.getHeaderByHash(blkHash2)->isMainBranch()); - TheBDM.DestroyInstance(); - - // Add two more blocks - BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_); - - // Now reinitialize the DB and hopefully detect the new blocks and update - TheBDM.SelectNetwork("Main"); - TheBDM.SetBlkFileLocation(blkdir_); - TheBDM.SetHomeDirLocation(homedir_); - TheBDM.SetLevelDBLocation(ldbdir_); - DBUtils.setArmoryDbType(ARMORY_DB_SUPER); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - - TheBDM.SetDatabaseModes(ARMORY_DB_SUPER, DB_PRUNE_NONE); - TheBDM.doInitialSyncOnLoad(); - - EXPECT_EQ(TheBDM.getTopBlockHeightInDB(HEADERS), 4); - EXPECT_EQ(TheBDM.getTopBlockHeightInDB(BLKDATA), 4); - EXPECT_TRUE(TheBDM.getHeaderByHash(blkHash4)->isMainBranch()); - - StoredScriptHistory ssh; - - iface_->getStoredScriptHistory(ssh, scrAddrA_); - EXPECT_EQ(ssh.getScriptBalance(), 100*COIN); - EXPECT_EQ(ssh.getScriptReceived(), 100*COIN); - EXPECT_EQ(ssh.totalTxioCount_, 2); - - iface_->getStoredScriptHistory(ssh, scrAddrB_); - EXPECT_EQ(ssh.getScriptBalance(), 0*COIN); - EXPECT_EQ(ssh.getScriptReceived(), 140*COIN); - EXPECT_EQ(ssh.totalTxioCount_, 3); - - iface_->getStoredScriptHistory(ssh, scrAddrC_); - EXPECT_EQ(ssh.getScriptBalance(), 50*COIN); - EXPECT_EQ(ssh.getScriptReceived(), 60*COIN); - EXPECT_EQ(ssh.totalTxioCount_, 2); - - iface_->getStoredScriptHistory(ssh, scrAddrD_); - EXPECT_EQ(ssh.getScriptBalance(), 100*COIN); - EXPECT_EQ(ssh.getScriptReceived(), 100*COIN); - EXPECT_EQ(ssh.totalTxioCount_, 3); - -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockUtilsSuper, DISABLED_RestartDBAfterBuild_withReplay) -{ - // Copy only the first four blocks. Will copy the full file next to test - // readBlkFileUpdate method on non-reorg blocks. - BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_, 926); - TheBDM.doInitialSyncOnLoad(); - EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 2); - EXPECT_EQ(iface_->getTopBlockHash(HEADERS), blkHash2); - EXPECT_TRUE(TheBDM.getHeaderByHash(blkHash2)->isMainBranch()); - TheBDM.DestroyInstance(); - - // Add two more blocks - BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_); - - // Now reinitialize the DB and hopefully detect the new blocks and update - TheBDM.SelectNetwork("Main"); - TheBDM.SetBlkFileLocation(blkdir_); - TheBDM.SetHomeDirLocation(homedir_); - TheBDM.SetLevelDBLocation(ldbdir_); - DBUtils.setArmoryDbType(ARMORY_DB_SUPER); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - - uint32_t replayRewind = 700; - TheBDM.SetDatabaseModes(ARMORY_DB_SUPER, DB_PRUNE_NONE); - TheBDM.doInitialSyncOnLoad(); - - - TheBDM.doInitialSyncOnLoad(); - - EXPECT_EQ(TheBDM.getTopBlockHeightInDB(HEADERS), 4); - EXPECT_EQ(TheBDM.getTopBlockHeightInDB(BLKDATA), 4); - EXPECT_TRUE(TheBDM.getHeaderByHash(blkHash4)->isMainBranch()); - - StoredScriptHistory ssh; - - iface_->getStoredScriptHistory(ssh, scrAddrA_); - EXPECT_EQ(ssh.getScriptBalance(), 100*COIN); - EXPECT_EQ(ssh.getScriptReceived(), 100*COIN); - EXPECT_EQ(ssh.totalTxioCount_, 2); - - iface_->getStoredScriptHistory(ssh, scrAddrB_); - EXPECT_EQ(ssh.getScriptBalance(), 0*COIN); - EXPECT_EQ(ssh.getScriptReceived(), 140*COIN); - EXPECT_EQ(ssh.totalTxioCount_, 3); - - iface_->getStoredScriptHistory(ssh, scrAddrC_); - EXPECT_EQ(ssh.getScriptBalance(), 50*COIN); - EXPECT_EQ(ssh.getScriptReceived(), 60*COIN); - EXPECT_EQ(ssh.totalTxioCount_, 2); - - iface_->getStoredScriptHistory(ssh, scrAddrD_); - EXPECT_EQ(ssh.getScriptBalance(), 100*COIN); - EXPECT_EQ(ssh.getScriptReceived(), 100*COIN); - EXPECT_EQ(ssh.totalTxioCount_, 3); - - // Random note (since I just spent 2 hours trying to figure out why - // I wasn't getting warnings about re-marking TxOuts spent that were - // already marked spent): We get three warnings about TxOuts that - // already marked unspent in the SSH objects when we replay blocks - // 1 and 2 (but not 0). This is expected. But, I also expected a - // warning about a TxOut already marked spent. Turns out that - // we are replaying the previous block first which calls "markUnspent" - // before we hit this mark-spent logic. So when we started the - // method, we actually did have a already-marked-spent TxOut, but - // it was marked unspent before we got the point of trying to mark - // it spent again. In other words, all expected behavior. -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockUtilsSuper, DISABLED_TimeAndSpaceTest_usuallydisabled) -{ - DBUtils.setArmoryDbType(ARMORY_DB_SUPER); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - - string oldblkdir = blkdir_; - //blkdir_ = string("/home/alan/.bitcoin/blks3"); - //blkdir_ = string("/home/alan/.bitcoin/blocks"); - //TheBDM.SelectNetwork("Main"); - blkdir_ = string("/home/alan/.bitcoin/testnet3/blocks"); - TheBDM.SelectNetwork("Test"); - TheBDM.SetBlkFileLocation(blkdir_); - TheBDM.SetHomeDirLocation(homedir_); - - StoredScriptHistory ssh; - TheBDM.doInitialSyncOnLoad(); - BinaryData scrAddr = READHEX("11b366edfc0a8b66feebae5c2e25a7b6a5d1cf31"); - BinaryData scrAddr2 = READHEX("39aa3d569e06a1d7926dc4be1193c99bf2eb9ee0"); - BinaryData scrAddr3 = READHEX("758e51b5e398a32c6abd091b3fde383291267cfa"); - BinaryData scrAddr4 = READHEX("6c22eb00e3f93acac5ae5d81a9db78a645dfc9c7"); - EXPECT_EQ(TheBDM.getDBBalanceForHash160(scrAddr), 18*COIN); - TheBDM.pprintSSHInfoAboutHash160(scrAddr); - TheBDM.pprintSSHInfoAboutHash160(scrAddr2); - TheBDM.pprintSSHInfoAboutHash160(scrAddr3); - TheBDM.pprintSSHInfoAboutHash160(scrAddr4); - blkdir_ = oldblkdir; - LOGINFO << "waiting... (please copy the DB dir...)"; - int pause; - cin >> pause; -} - - - - - - -//////////////////////////////////////////////////////////////////////////////// -// I thought I was going to do something different with this set of tests, -// but I ended up with an exact copy of the BlockUtilsSuper fixture. Oh well. -class BlockUtilsWithWalletTest: public ::testing::Test -{ -protected: - ///////////////////////////////////////////////////////////////////////////// - virtual void SetUp(void) - { - LOGDISABLESTDOUT(); - iface_ = LevelDBWrapper::GetInterfacePtr(); - magic_ = READHEX(MAINNET_MAGIC_BYTES); - ghash_ = READHEX(MAINNET_GENESIS_HASH_HEX); - gentx_ = READHEX(MAINNET_GENESIS_TX_HASH_HEX); - zeros_ = READHEX("00000000"); - - blkdir_ = string("./blkfiletest"); - homedir_ = string("./fakehomedir"); - ldbdir_ = string("./ldbtestdir"); - - mkdir(blkdir_); - mkdir(homedir_); - - // Put the first 5 blocks into the blkdir - blk0dat_ = BtcUtils::getBlkFilename(blkdir_, 0); - BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_); - - TheBDM.SetDatabaseModes(ARMORY_DB_SUPER, DB_PRUNE_NONE); - TheBDM.SelectNetwork("Main"); - TheBDM.SetBlkFileLocation(blkdir_); - TheBDM.SetHomeDirLocation(homedir_); - TheBDM.SetLevelDBLocation(ldbdir_); - - blkHash0 = READHEX("6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000"); - blkHash1 = READHEX("1b5514b83257d924be7f10c65b95b1f3c0e50081e1dfd8943eece5eb00000000"); - blkHash2 = READHEX("979fc39616bf1dc6b1f88167f76383d44d65ccd0fc99b7f91bcb2c9500000000"); - blkHash3 = READHEX("50f8231e5fd476f470e1ba4937bc97cb304136c96c765339308935bc00000000"); - blkHash4 = READHEX("8e121ba0d275f49a21bbc171d7d49890de13c9b9733e0104654d262f00000000"); - blkHash3A= READHEX("dd63f62ef59d5b6a6da2a36407f76e4e28026a3fd3a46700d284424700000000"); - blkHash4A= READHEX("bfa204022816102169b4e1d4f78cdf77258048f6d14282144cc01d5500000000"); - blkHash5A= READHEX("4e049fd71ef7381a73e4f550d97812d1eb0fbd1489c1774e18855f1900000000"); - - addrA_ = READHEX("62e907b15cbf27d5425399ebf6f0fb50ebb88f18"); - addrB_ = READHEX("ee26c56fc1d942be8d7a24b2a1001dd894693980"); - addrC_ = READHEX("cb2abde8bccacc32e893df3a054b9ef7f227a4ce"); - addrD_ = READHEX("c522664fb0e55cdc5c0cea73b4aad97ec8343232"); - - scrAddrA_ = HASH160PREFIX + addrA_; - scrAddrB_ = HASH160PREFIX + addrB_; - scrAddrC_ = HASH160PREFIX + addrC_; - scrAddrD_ = HASH160PREFIX + addrD_; - - } - - - ///////////////////////////////////////////////////////////////////////////// - virtual void TearDown(void) - { - BlockDataManager_LevelDB::DestroyInstance(); - - rmdir(blkdir_); - rmdir(homedir_); - - char* delstr = new char[4096]; - sprintf(delstr, "%s/level*", ldbdir_.c_str()); - rmdir(delstr); - delete[] delstr; - - LOGENABLESTDOUT(); - } - - - -#if ! defined(_MSC_VER) && ! defined(__MINGW32__) - - ///////////////////////////////////////////////////////////////////////////// - void rmdir(string src) - { - char* syscmd = new char[4096]; - sprintf(syscmd, "rm -rf %s", src.c_str()); - system(syscmd); - delete[] syscmd; - } - - ///////////////////////////////////////////////////////////////////////////// - void mkdir(string newdir) - { - char* syscmd = new char[4096]; - sprintf(syscmd, "mkdir -p %s", newdir.c_str()); - system(syscmd); - delete[] syscmd; - } -#endif - - InterfaceToLDB* iface_; - BinaryData magic_; - BinaryData ghash_; - BinaryData gentx_; - BinaryData zeros_; - - string blkdir_; - string homedir_; - string ldbdir_; - string blk0dat_;; - - BinaryData blkHash0; - BinaryData blkHash1; - BinaryData blkHash2; - BinaryData blkHash3; - BinaryData blkHash4; - BinaryData blkHash3A; - BinaryData blkHash4A; - BinaryData blkHash5A; - - BinaryData addrA_; - BinaryData addrB_; - BinaryData addrC_; - BinaryData addrD_; - - BinaryData scrAddrA_; - BinaryData scrAddrB_; - BinaryData scrAddrC_; - BinaryData scrAddrD_; -}; - - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockUtilsWithWalletTest, PreRegisterScrAddrs) -{ - BtcWallet wlt; - wlt.addScrAddress(scrAddrA_); - wlt.addScrAddress(scrAddrB_); - wlt.addScrAddress(scrAddrC_); - TheBDM.registerWallet(&wlt); - TheBDM.registerNewScrAddr(scrAddrD_); - - BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_); - TheBDM.doInitialSyncOnLoad(); - - TheBDM.fetchAllRegisteredScrAddrData(); - TheBDM.scanBlockchainForTx(wlt); - - uint64_t balanceWlt; - uint64_t balanceDB; - - balanceWlt = wlt.getScrAddrObjByKey(scrAddrA_).getFullBalance(); - balanceDB = iface_->getBalanceForScrAddr(scrAddrA_); - EXPECT_EQ(balanceWlt, 100*COIN); - EXPECT_EQ(balanceDB, 100*COIN); - - balanceWlt = wlt.getScrAddrObjByKey(scrAddrB_).getFullBalance(); - balanceDB = iface_->getBalanceForScrAddr(scrAddrB_); - EXPECT_EQ(balanceWlt, 0*COIN); - EXPECT_EQ(balanceDB, 0*COIN); - - balanceWlt = wlt.getScrAddrObjByKey(scrAddrC_).getFullBalance(); - balanceDB = iface_->getBalanceForScrAddr(scrAddrC_); - EXPECT_EQ(balanceWlt, 50*COIN); - EXPECT_EQ(balanceDB, 50*COIN); - - balanceWlt = wlt.getScrAddrObjByKey(scrAddrD_).getFullBalance(); - balanceDB = iface_->getBalanceForScrAddr(scrAddrD_); - EXPECT_EQ(balanceWlt, 0*COIN); // D is not part of the wallet - EXPECT_EQ(balanceDB, 100*COIN); -} - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockUtilsWithWalletTest, PostRegisterScrAddr) -{ - BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_); - TheBDM.doInitialSyncOnLoad(); - - // We do all the database stuff first, THEN load the addresses - BtcWallet wlt; - wlt.addScrAddress(scrAddrA_); - wlt.addScrAddress(scrAddrB_); - wlt.addScrAddress(scrAddrC_); - TheBDM.registerWallet(&wlt); - TheBDM.registerNewScrAddr(scrAddrD_); - TheBDM.fetchAllRegisteredScrAddrData(); - TheBDM.scanRegisteredTxForWallet(wlt); - - uint64_t balanceWlt; - uint64_t balanceDB; - - balanceWlt = wlt.getScrAddrObjByKey(scrAddrA_).getFullBalance(); - balanceDB = iface_->getBalanceForScrAddr(scrAddrA_); - EXPECT_EQ(balanceWlt, 100*COIN); - EXPECT_EQ(balanceDB, 100*COIN); - - balanceWlt = wlt.getScrAddrObjByKey(scrAddrB_).getFullBalance(); - balanceDB = iface_->getBalanceForScrAddr(scrAddrB_); - EXPECT_EQ(balanceWlt, 0*COIN); - EXPECT_EQ(balanceDB, 0*COIN); - - balanceWlt = wlt.getScrAddrObjByKey(scrAddrC_).getFullBalance(); - balanceDB = iface_->getBalanceForScrAddr(scrAddrC_); - EXPECT_EQ(balanceWlt, 50*COIN); - EXPECT_EQ(balanceDB, 50*COIN); - - balanceWlt = wlt.getScrAddrObjByKey(scrAddrD_).getFullBalance(); - balanceDB = iface_->getBalanceForScrAddr(scrAddrD_); - EXPECT_EQ(balanceWlt, 0*COIN); // D is not part of the wallet - EXPECT_EQ(balanceDB, 100*COIN); -} - -//////////////////////////////////////////////////////////////////////////////// -/* Never got around to finishing this... -class TestMainnetBlkchain: public ::testing::Test -{ -protected: - ///////////////////////////////////////////////////////////////////////////// - virtual void SetUp(void) - { - iface_ = LevelDBWrapper::GetInterfacePtr(); - magic_ = READHEX(MAINNET_MAGIC_BYTES); - ghash_ = READHEX(MAINNET_GENESIS_HASH_HEX); - gentx_ = READHEX(MAINNET_GENESIS_TX_HASH_HEX); - zeros_ = READHEX("00000000"); - DBUtils.setArmoryDbType(ARMORY_DB_FULL); - DBUtils.setDbPruneType(DB_PRUNE_NONE); - - blkdir_ = string("/home/alan/.bitcoin"); - homedir_ = string("./fakehomedir"); - ldbdir_ = string("/home/alan/ARMORY_DB_257k_BLKS"); - - iface_->openDatabases( ldbdir_, ghash_, gentx_, magic_, - ARMORY_DB_SUPER, DB_PRUNE_NONE); - if(!iface_->databasesAreOpen()) - LOGERR << "ERROR OPENING DATABASES FOR TESTING!"; - - mkdir(homedir_); - - TheBDM.SelectNetwork("Main"); - TheBDM.SetBlkFileLocation(blkdir_); - TheBDM.SetHomeDirLocation(homedir_); - TheBDM.SetLevelDBLocation(ldbdir_); - - addrA_ = READHEX("b077a2b5e8a53f1d3ef4100117125de6a5b15f6b"); - addrB_ = READHEX("4c765bca17f9881ad6d4336a1d4ec34a091e5a6f"); - - scrAddrA_ = HASH160PREFIX + addrA_; - scrAddrB_ = HASH160PREFIX + addrB_; - - LOGDISABLESTDOUT(); - } - - - ///////////////////////////////////////////////////////////////////////////// - virtual void TearDown(void) - { - rmdir(homedir_); - - BlockDataManager_LevelDB::DestroyInstance(); - LOGENABLESTDOUT(); - } - - - ///////////////////////////////////////////////////////////////////////////// - void rmdir(string src) - { - char* syscmd = new char[4096]; - sprintf(syscmd, "rm -rf %s", src.c_str()); - system(syscmd); - delete[] syscmd; - } - - ///////////////////////////////////////////////////////////////////////////// - void mkdir(string newdir) - { - char* syscmd = new char[4096]; - sprintf(syscmd, "mkdir -p %s", newdir.c_str()); - system(syscmd); - delete[] syscmd; - } -#endif - - InterfaceToLDB* iface_; - BinaryData magic_; - BinaryData ghash_; - BinaryData gentx_; - BinaryData zeros_; - - string blkdir_; - string homedir_; - string ldbdir_; - string blk0dat_;; - - BinaryData blkHash0; - BinaryData blkHash1; - BinaryData blkHash2; - BinaryData blkHash3; - BinaryData blkHash4; - BinaryData blkHash3A; - BinaryData blkHash4A; - BinaryData blkHash5A; - - BinaryData addrA_; - BinaryData addrB_; - BinaryData addrC_; - BinaryData addrD_; - - BinaryData scrAddrA_; - BinaryData scrAddrB_; - BinaryData scrAddrC_; - BinaryData scrAddrD_; -}; - -//////////////////////////////////////////////////////////////////////////////// -TEST_F(BlockUtilsWithWalletTest, TestBalanceMainnet_usuallydisabled) -{ - TheBDM.doInitialSyncOnLoad(); - - // We do all the database stuff first, THEN load the addresses - BtcWallet wlt; - wlt.addScrAddress(scrAddrA_); - wlt.addScrAddress(scrAddrB_); - TheBDM.registerWallet(&wlt); - TheBDM.registerNewScrAddr(scrAddrD_); - TheBDM.fetchAllRegisteredScrAddrData(); - TheBDM.scanRegisteredTxForWallet(wlt); - - uint64_t balanceWlt; - uint64_t balanceDB; - - balanceWlt = wlt.getScrAddrObjByKey(scrAddrA_).getFullBalance(); - balanceDB = iface_->getBalanceForScrAddr(scrAddrA_); - EXPECT_EQ(balanceWlt, 100*COIN); - EXPECT_EQ(balanceDB, 100*COIN); - - balanceWlt = wlt.getScrAddrObjByKey(scrAddrB_).getFullBalance(); - balanceDB = iface_->getBalanceForScrAddr(scrAddrB_); - EXPECT_EQ(balanceWlt, 0*COIN); - EXPECT_EQ(balanceDB, 0*COIN); - - balanceWlt = wlt.getScrAddrObjByKey(scrAddrC_).getFullBalance(); - balanceDB = iface_->getBalanceForScrAddr(scrAddrC_); - EXPECT_EQ(balanceWlt, 50*COIN); - EXPECT_EQ(balanceDB, 50*COIN); - - balanceWlt = wlt.getScrAddrObjByKey(scrAddrD_).getFullBalance(); - balanceDB = iface_->getBalanceForScrAddr(scrAddrD_); - EXPECT_EQ(balanceWlt, 0*COIN); // D is not part of the wallet - EXPECT_EQ(balanceDB, 100*COIN); -} -*/ - -//////////////////////////////////////////////////////////////////////////////// -/* TODO: Whoops, never finished this... -TEST_F(BlockUtilsWithWalletTest, ZeroConfUpdate) -{ - // Copy only the first four blocks - BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_, 1596); - TheBDM.doInitialSyncOnLoad(); - - // We do all the database stuff first, THEN load the addresses - BtcWallet wlt; - wlt.addScrAddress(scrAddrA_); - wlt.addScrAddress(scrAddrB_); - wlt.addScrAddress(scrAddrC_); - TheBDM.registerWallet(&wlt); - TheBDM.registerNewScrAddr(scrAddrD_); - TheBDM.fetchAllRegisteredScrAddrData(); - TheBDM.scanRegisteredTxForWallet(wlt); - - uint64_t balanceWlt; - uint64_t balanceDB; - - balanceWlt = wlt.getScrAddrObjByKey(scrAddrA_).getFullBalance(); - balanceDB = iface_->getBalanceForScrAddr(scrAddrA_); - EXPECT_EQ(balanceWlt, 50*COIN); - EXPECT_EQ(balanceDB, 50*COIN); - - balanceWlt = wlt.getScrAddrObjByKey(scrAddrB_).getFullBalance(); - balanceDB = iface_->getBalanceForScrAddr(scrAddrB_); - EXPECT_EQ(balanceWlt, 0*COIN); - EXPECT_EQ(balanceDB, 0*COIN); - - balanceWlt = wlt.getScrAddrObjByKey(scrAddrC_).getFullBalance(); - balanceDB = iface_->getBalanceForScrAddr(scrAddrC_); - EXPECT_EQ(balanceWlt, 100*COIN); - EXPECT_EQ(balanceDB, 100*COIN); - - balanceWlt = wlt.getScrAddrObjByKey(scrAddrD_).getFullBalance(); - balanceDB = iface_->getBalanceForScrAddr(scrAddrD_); - EXPECT_EQ(balanceWlt, 0*COIN); // D is not part of the wallet - EXPECT_EQ(balanceDB, 50*COIN); - - - BinaryData newTx = READHEX( - "01000000019b2468285fc191b7a033b2f32b3de8f0c39d1eac622f5132565f1e" - "a8ca74ec8d000000004a4930460221007a284fa21364d749389ff62328e837dd" - "2676cbe4e202c0766e3950cbd0a911e40221005ac1541e381b6d358df08cce6a" - "2869b76d5ffe05b6aaca5c03ebcba8559c4ede01ffffffff0100f2052a010000" - "001976a914c522664fb0e55cdc5c0cea73b4aad97ec834323288ac00000000"); - - TheBDM.addNewZeroConfTx(newTx, 1300000000, false); - - balanceWlt = wlt.getScrAddrObjByKey(scrAddrA_).getFullBalance(); - balanceDB = iface_->getBalanceForScrAddr(scrAddrA_); - EXPECT_EQ(balanceWlt, 50*COIN); - EXPECT_EQ(balanceDB, 50*COIN); - - balanceWlt = wlt.getScrAddrObjByKey(scrAddrB_).getFullBalance(); - balanceDB = iface_->getBalanceForScrAddr(scrAddrB_); - EXPECT_EQ(balanceWlt, 0*COIN); - EXPECT_EQ(balanceDB, 0*COIN); - - balanceWlt = wlt.getScrAddrObjByKey(scrAddrC_).getFullBalance(); - balanceDB = iface_->getBalanceForScrAddr(scrAddrC_); - EXPECT_EQ(balanceWlt, 100*COIN); - EXPECT_EQ(balanceDB, 100*COIN); - - balanceWlt = wlt.getScrAddrObjByKey(scrAddrD_).getFullBalance(); - balanceDB = iface_->getBalanceForScrAddr(scrAddrD_); - EXPECT_EQ(balanceWlt, 0*COIN); // D is not part of the wallet - EXPECT_EQ(balanceDB, 50*COIN); - -} -*/ - -// This was really just to time the logging to determine how much impact it -// has. It looks like writing to file is about 1,000,000 logs/sec, while -// writing to the null stream (below the threshold log level) is about -// 2,200,000/sec. As long as we use log messages sparingly (and timer -// calls which call the logger), there will be no problem leaving them -// on even in production code. -/* -TEST(TimeDebugging, WriteToLogNoStdOut) -{ - LOGDISABLESTDOUT(); - for(uint32_t i=0; i<1000000; i++) - LOGERR << "Testing writing out " << 3 << " diff things"; - LOGENABLESTDOUT(); -} - -TEST(TimeDebugging, WriteNull) -{ - for(uint32_t i=0; i<1000000; i++) - LOGDEBUG4 << "Testing writing out " << 3 << " diff things"; -} -*/ - - -//////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// -// Now actually execute all the tests -//////////////////////////////////////////////////////////////////////////////// -GTEST_API_ int main(int argc, char **argv) -{ - std::cout << "Running main() from gtest_main.cc\n"; - - // Setup the log file - STARTLOGGING("cppTestsLog.txt", LogLvlDebug2); - //LOGDISABLESTDOUT(); - - testing::InitGoogleTest(&argc, argv); - int exitCode = RUN_ALL_TESTS(); - - FLUSHLOG(); - - - return exitCode; -} - - +#include +#include +#include +#include "gtest.h" + +#include "../log.h" +#include "../BinaryData.h" +#include "../BtcUtils.h" +#include "../BlockObj.h" +#include "../StoredBlockObj.h" +#include "../PartialMerkle.h" +#include "../leveldb_wrapper.h" +#include "../BlockUtils.h" +#include "../EncryptionUtils.h" + + + +#ifdef _MSC_VER + #ifdef mlock + #undef mlock + #undef munlock + #endif + #include "win32_posix.h" + #undef close +#endif + +#define READHEX BinaryData::CreateFromHex +#define TheBDM BlockDataManager_LevelDB::GetInstance() + + +/* This didn't work at all +class BitcoinEnvironment : public ::testing::Environment +{ +public: + // Override this to define how to set up the environment. + virtual void SetUp() + { + rawHead_ = READHEX( + "01000000" + "1d8f4ec0443e1f19f305e488c1085c95de7cc3fd25e0d2c5bb5d000000000000" + "9762547903d36881a86751f3f5049e23050113f779735ef82734ebf0b4450081" + "d8c8c84d" + "b3936a1a" + "334b035b"); + headHashLE_ = READHEX( + "1195e67a7a6d0674bbd28ae096d602e1f038c8254b49dfe79d47000000000000"); + headHashBE_ = READHEX( + "000000000000479de7df494b25c838f0e102d696e08ad2bb74066d7a7ae69511"); + + rawTx0_ = READHEX( + "01000000016290dce984203b6a5032e543e9e272d8bce934c7de4d15fa0fe44d" + "d49ae4ece9010000008b48304502204f2fa458d439f957308bca264689aa175e" + "3b7c5f78a901cb450ebd20936b2c500221008ea3883a5b80128e55c9c6070aa6" + "264e1e0ce3d18b7cd7e85108ce3d18b7419a0141044202550a5a6d3bb81549c4" + "a7803b1ad59cdbba4770439a4923624a8acfc7d34900beb54a24188f7f0a4068" + "9d905d4847cc7d6c8d808a457d833c2d44ef83f76bffffffff0242582c0a0000" + "00001976a914c1b4695d53b6ee57a28647ce63e45665df6762c288ac80d1f008" + "000000001976a9140e0aec36fe2545fb31a41164fb6954adcd96b34288ac0000" + "0000"); + rawTx1_ = READHEX( + "0100000001f658dbc28e703d86ee17c9a2d3b167a8508b082fa0745f55be5144" + "a4369873aa010000008c49304602210041e1186ca9a41fdfe1569d5d807ca7ff" + "6c5ffd19d2ad1be42f7f2a20cdc8f1cc0221003366b5d64fe81e53910e156914" + "091d12646bc0d1d662b7a65ead3ebe4ab8f6c40141048d103d81ac9691cf13f3" + "fc94e44968ef67b27f58b27372c13108552d24a6ee04785838f34624b294afee" + "83749b64478bb8480c20b242c376e77eea2b3dc48b4bffffffff0200e1f50500" + "0000001976a9141b00a2f6899335366f04b277e19d777559c35bc888ac40aeeb" + "02000000001976a9140e0aec36fe2545fb31a41164fb6954adcd96b34288ac00" + "000000"); + + rawBlock_ = READHEX( + "01000000eb10c9a996a2340a4d74eaab41421ed8664aa49d18538bab59010000" + "000000005a2f06efa9f2bd804f17877537f2080030cadbfa1eb50e02338117cc" + "604d91b9b7541a4ecfbb0a1a64f1ade703010000000100000000000000000000" + "00000000000000000000000000000000000000000000ffffffff0804cfbb0a1a" + "02360affffffff0100f2052a01000000434104c2239c4eedb3beb26785753463" + "be3ec62b82f6acd62efb65f452f8806f2ede0b338e31d1f69b1ce449558d7061" + "aa1648ddc2bf680834d3986624006a272dc21cac000000000100000003e8caa1" + "2bcb2e7e86499c9de49c45c5a1c6167ea4b894c8c83aebba1b6100f343010000" + "008c493046022100e2f5af5329d1244807f8347a2c8d9acc55a21a5db769e927" + "4e7e7ba0bb605b26022100c34ca3350df5089f3415d8af82364d7f567a6a297f" + "cc2c1d2034865633238b8c014104129e422ac490ddfcb7b1c405ab9fb4244124" + "6c4bca578de4f27b230de08408c64cad03af71ee8a3140b40408a7058a1984a9" + "f246492386113764c1ac132990d1ffffffff5b55c18864e16c08ef9989d31c7a" + "343e34c27c30cd7caa759651b0e08cae0106000000008c4930460221009ec9aa" + "3e0caf7caa321723dea561e232603e00686d4bfadf46c5c7352b07eb00022100" + "a4f18d937d1e2354b2e69e02b18d11620a6a9332d563e9e2bbcb01cee559680a" + "014104411b35dd963028300e36e82ee8cf1b0c8d5bf1fc4273e970469f5cb931" + "ee07759a2de5fef638961726d04bd5eb4e5072330b9b371e479733c942964bb8" + "6e2b22ffffffff3de0c1e913e6271769d8c0172cea2f00d6d3240afc3a20f9fa" + "247ce58af30d2a010000008c493046022100b610e169fd15ac9f60fe2b507529" + "281cf2267673f4690ba428cbb2ba3c3811fd022100ffbe9e3d71b21977a8e97f" + "de4c3ba47b896d08bc09ecb9d086bb59175b5b9f03014104ff07a1833fd8098b" + "25f48c66dcf8fde34cbdbcc0f5f21a8c2005b160406cbf34cc432842c6b37b25" + "90d16b165b36a3efc9908d65fb0e605314c9b278f40f3e1affffffff0240420f" + "00000000001976a914adfa66f57ded1b655eb4ccd96ee07ca62bc1ddfd88ac00" + "7d6a7d040000001976a914981a0c9ae61fa8f8c96ae6f8e383d6e07e77133e88" + "ac00000000010000000138e7586e0784280df58bd3dc5e3d350c9036b1ec4107" + "951378f45881799c92a4000000008a47304402207c945ae0bbdaf9dadba07bdf" + "23faa676485a53817af975ddf85a104f764fb93b02201ac6af32ddf597e610b4" + "002e41f2de46664587a379a0161323a85389b4f82dda014104ec8883d3e4f7a3" + "9d75c9f5bb9fd581dc9fb1b7cdf7d6b5a665e4db1fdb09281a74ab138a2dba25" + "248b5be38bf80249601ae688c90c6e0ac8811cdb740fcec31dffffffff022f66" + "ac61050000001976a914964642290c194e3bfab661c1085e47d67786d2d388ac" + "2f77e200000000001976a9141486a7046affd935919a3cb4b50a8a0c233c286c" + "88ac00000000"); + + rawTxUnfrag_ = READHEX( + // Version + "01000000" + // NumTxIn + "02" + // Start TxIn0 + "0044fbc929d78e4203eed6f1d3d39c0157d8e5c100bbe0886779c0" + "ebf6a69324010000008a47304402206568144ed5e7064d6176c74738b04c08ca" + "19ca54ddeb480084b77f45eebfe57802207927d6975a5ac0e1bb36f5c05356dc" + "da1f521770511ee5e03239c8e1eecf3aed0141045d74feae58c4c36d7c35beac" + "05eddddc78b3ce4b02491a2eea72043978056a8bc439b99ddaad327207b09ef1" + "6a8910828e805b0cc8c11fba5caea2ee939346d7ffffffff" + // Start TxIn1 + "45c866b219b17695" + "2508f8e5aea728f950186554fc4a5807e2186a8e1c4009e5000000008c493046" + "022100bd5d41662f98cfddc46e86ea7e4a3bc8fe9f1dfc5c4836eaf7df582596" + "cfe0e9022100fc459ae4f59b8279d679003b88935896acd10021b6e2e4619377" + "e336b5296c5e014104c00bab76a708ba7064b2315420a1c533ca9945eeff9754" + "cdc574224589e9113469b4e71752146a10028079e04948ecdf70609bf1b9801f" + "6b73ab75947ac339e5ffffffff" + // NumTxOut + "02" + // Start TxOut0 + "ac4c8bd5000000001976a9148dce8946f1c7763bb60ea5cf16ef514cbed0633b88ac" + // Start TxOut1 + "002f6859000000001976a9146a59ac0e8f553f292dfe5e9f3aaa1da93499c15e88ac" + // Locktime + "00000000"); + + rawTxFragged_ = READHEX( + //"01000000020044fbc929d78e4203eed6f1d3d39c0157d8e5c100bbe0886779c0" + //"ebf6a69324010000008a47304402206568144ed5e7064d6176c74738b04c08ca" + //"19ca54ddeb480084b77f45eebfe57802207927d6975a5ac0e1bb36f5c05356dc" + //"da1f521770511ee5e03239c8e1eecf3aed0141045d74feae58c4c36d7c35beac" + //"05eddddc78b3ce4b02491a2eea72043978056a8bc439b99ddaad327207b09ef1" + //"6a8910828e805b0cc8c11fba5caea2ee939346d7ffffffff45c866b219b17695" + //"2508f8e5aea728f950186554fc4a5807e2186a8e1c4009e5000000008c493046" + //"022100bd5d41662f98cfddc46e86ea7e4a3bc8fe9f1dfc5c4836eaf7df582596" + //"cfe0e9022100fc459ae4f59b8279d679003b88935896acd10021b6e2e4619377" + //"e336b5296c5e014104c00bab76a708ba7064b2315420a1c533ca9945eeff9754" + //"cdc574224589e9113469b4e71752146a10028079e04948ecdf70609bf1b9801f" + //"6b73ab75947ac339e5ffffffff0200000000"); + // Version + "01000000" + // NumTxIn + "02" + // Start TxIn0 + "0044fbc929d78e4203eed6f1d3d39c0157d8e5c100bbe0886779c0" + "ebf6a69324010000008a47304402206568144ed5e7064d6176c74738b04c08ca" + "19ca54ddeb480084b77f45eebfe57802207927d6975a5ac0e1bb36f5c05356dc" + "da1f521770511ee5e03239c8e1eecf3aed0141045d74feae58c4c36d7c35beac" + "05eddddc78b3ce4b02491a2eea72043978056a8bc439b99ddaad327207b09ef1" + "6a8910828e805b0cc8c11fba5caea2ee939346d7ffffffff" + // Start TxIn1 + "45c866b219b17695" + "2508f8e5aea728f950186554fc4a5807e2186a8e1c4009e5000000008c493046" + "022100bd5d41662f98cfddc46e86ea7e4a3bc8fe9f1dfc5c4836eaf7df582596" + "cfe0e9022100fc459ae4f59b8279d679003b88935896acd10021b6e2e4619377" + "e336b5296c5e014104c00bab76a708ba7064b2315420a1c533ca9945eeff9754" + "cdc574224589e9113469b4e71752146a10028079e04948ecdf70609bf1b9801f" + "6b73ab75947ac339e5ffffffff" + // NumTxOut + "02" + // ... TxOuts fragged out + // Locktime + "00000000"); + + rawTxOut0_ = READHEX( + // Value + "ac4c8bd500000000" + // Script size (var_int) + "19" + // Script + "76""a9""14""8dce8946f1c7763bb60ea5cf16ef514cbed0633b""88""ac"); + rawTxOut1_ = READHEX( + // Value + "002f685900000000" + // Script size (var_int) + "19" + // Script + "76""a9""14""6a59ac0e8f553f292dfe5e9f3aaa1da93499c15e""88""ac"); + + bh_.unserialize(rawHead_); + tx1_.unserialize(rawTx0_); + tx2_.unserialize(rawTx1_); + + + sbh_.unserialize(rawHead_); + + // Make sure the global DB type and prune type are reset for each test + DBUtils.setArmoryDbType(ARMORY_DB_FULL); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + } + + BinaryData rawHead_; + BinaryData headHashLE_; + BinaryData headHashBE_; + + BinaryData rawBlock_; + + BinaryData rawTx0_; + BinaryData rawTx1_; + + BlockHeader bh_; + Tx tx1_; + Tx tx2_; + + BinaryData rawTxUnfrag_; + BinaryData rawTxFragged_; + BinaryData rawTxOut0_; + BinaryData rawTxOut1_; + + StoredHeader sbh_; + +}; + + +::testing::Environment* const btcenv = + ::testing::AddGlobalTestEnvironment(new BitcoinEnvironment); +*/ + + + + +//////////////////////////////////////////////////////////////////////////////// +class BinaryDataTest : public ::testing::Test +{ +protected: + virtual void SetUp(void) + { + str0_ = ""; + str4_ = "1234abcd"; + str5_ = "1234abcdef"; + + bd0_ = READHEX(str0_); + bd4_ = READHEX(str4_); + bd5_ = READHEX(str5_); + } + + string str0_; + string str4_; + string str5_; + + BinaryData bd0_; + BinaryData bd4_; + BinaryData bd5_; + +}; + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BinaryDataTest, Constructor) +{ + uint8_t* ptr = new uint8_t[4]; + + BinaryData a; + BinaryData b(4); + BinaryData c(ptr, 2); + BinaryData d(ptr, 4); + BinaryData e(b); + BinaryData f(string("xyza")); + + EXPECT_EQ(a.getSize(), 0); + EXPECT_EQ(b.getSize(), 4); + EXPECT_EQ(c.getSize(), 2); + EXPECT_EQ(d.getSize(), 4); + EXPECT_EQ(e.getSize(), 4); + EXPECT_EQ(f.getSize(), 4); + + EXPECT_TRUE( a.isNull()); + EXPECT_FALSE(b.isNull()); + EXPECT_FALSE(c.isNull()); + EXPECT_FALSE(d.isNull()); + EXPECT_FALSE(e.isNull()); + + BinaryDataRef g(f); + BinaryDataRef h(d); + BinaryData i(g); + + EXPECT_EQ( g.getSize(), 4); + EXPECT_EQ( i.getSize(), 4); + EXPECT_TRUE( g==f); + EXPECT_FALSE(g==h); + EXPECT_TRUE( i==g); + + delete[] ptr; +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BinaryDataTest, CopyFrom) +{ + BinaryData a,b,c,d,e,f; + a.copyFrom((uint8_t*)bd0_.getPtr(), bd0_.getSize()); + b.copyFrom((uint8_t*)bd4_.getPtr(), (uint8_t*)bd4_.getPtr()+4); + c.copyFrom((uint8_t*)bd4_.getPtr(), bd4_.getSize()); + d.copyFrom(str5_); + e.copyFrom(a); + + BinaryDataRef i(b); + f.copyFrom(i); + + EXPECT_EQ(a.getSize(), 0); + EXPECT_EQ(b.getSize(), 4); + EXPECT_EQ(c.getSize(), 4); + EXPECT_EQ(a,e); + EXPECT_EQ(b,c); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BinaryDataTest, CopyTo) +{ + BinaryData a,b,c,d,e,f,g,h; + bd0_.copyTo(a); + bd4_.copyTo(b); + + c.resize(bd5_.getSize()); + bd5_.copyTo(c.getPtr()); + + size_t sz = 2; + d.resize(sz); + e.resize(sz); + bd5_.copyTo(d.getPtr(), sz); + bd5_.copyTo(e.getPtr(), bd5_.getSize()-sz, sz); + + f.copyFrom(bd5_.getPtr(), bd5_.getPtr()+sz); + + EXPECT_TRUE(a==bd0_); + EXPECT_TRUE(b==bd4_); + EXPECT_TRUE(c==bd5_); + EXPECT_TRUE(bd5_.startsWith(d)); + EXPECT_TRUE(bd5_.endsWith(e)); + EXPECT_TRUE(d==f); + + EXPECT_EQ(a.getSize(), 0); + EXPECT_EQ(b.getSize(), 4); + EXPECT_EQ(c.getSize(), 5); + EXPECT_EQ(d.getSize(), 2); + EXPECT_NE(b,c); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BinaryDataTest, Fill) +{ + BinaryData a(0), b(1), c(4); + BinaryData aAns = READHEX(""); + BinaryData bAns = READHEX("aa"); + BinaryData cAns = READHEX("aaaaaaaa"); + + a.fill(0xaa); + b.fill(0xaa); + c.fill(0xaa); + + EXPECT_EQ(a, aAns); + EXPECT_EQ(b, bAns); + EXPECT_EQ(c, cAns); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BinaryDataTest, IndexOp) +{ + EXPECT_EQ(bd4_[0], 0x12); + EXPECT_EQ(bd4_[1], 0x34); + EXPECT_EQ(bd4_[2], 0xab); + EXPECT_EQ(bd4_[3], 0xcd); + + EXPECT_EQ(bd4_[-4], 0x12); + EXPECT_EQ(bd4_[-3], 0x34); + EXPECT_EQ(bd4_[-2], 0xab); + EXPECT_EQ(bd4_[-1], 0xcd); + + bd4_[1] = 0xff; + EXPECT_EQ(bd4_[0], 0x12); + EXPECT_EQ(bd4_[1], 0xff); + EXPECT_EQ(bd4_[2], 0xab); + EXPECT_EQ(bd4_[3], 0xcd); + + EXPECT_EQ(bd4_[-4], 0x12); + EXPECT_EQ(bd4_[-3], 0xff); + EXPECT_EQ(bd4_[-2], 0xab); + EXPECT_EQ(bd4_[-1], 0xcd); + + EXPECT_EQ(bd4_.toHexStr(), string("12ffabcd")); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BinaryDataTest, StartsEndsWith) +{ + BinaryData a = READHEX("abcd"); + EXPECT_TRUE( bd0_.startsWith(bd0_)); + EXPECT_TRUE( bd4_.startsWith(bd0_)); + EXPECT_TRUE( bd5_.startsWith(bd4_)); + EXPECT_TRUE( bd5_.startsWith(bd5_)); + EXPECT_FALSE(bd4_.startsWith(bd5_)); + EXPECT_TRUE( bd0_.startsWith(bd0_)); + EXPECT_FALSE(bd0_.startsWith(bd4_)); + EXPECT_FALSE(bd5_.endsWith(a)); + EXPECT_TRUE( bd4_.endsWith(a)); + EXPECT_FALSE(bd0_.endsWith(a)); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BinaryDataTest, Append) +{ + BinaryData a = READHEX("ef"); + + BinaryData static4 = bd4_; + + BinaryData b = bd4_ + a; + BinaryData c = bd4_.append(a); + + BinaryDataRef d(a); + bd4_.copyFrom(static4); + BinaryData e = bd4_.append(d); + bd4_.copyFrom(static4); + BinaryData f = bd4_.append(a.getPtr(), 1); + bd4_.copyFrom(static4); + BinaryData g = bd4_.append(0xef); + + BinaryData h = bd0_ + a; + BinaryData i = bd0_.append(a); + bd0_.resize(0); + BinaryData j = bd0_.append(a.getPtr(), 1); + bd0_.resize(0); + BinaryData k = bd0_.append(0xef); + + EXPECT_EQ(bd5_, b); + EXPECT_EQ(bd5_, c); + EXPECT_EQ(bd5_, e); + EXPECT_EQ(bd5_, f); + EXPECT_EQ(bd5_, g); + + EXPECT_NE(bd5_, h); + EXPECT_EQ(a, h); + EXPECT_EQ(a, i); + EXPECT_EQ(a, j); + EXPECT_EQ(a, k); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BinaryDataTest, Inequality) +{ + EXPECT_FALSE(bd0_ < bd0_); + EXPECT_TRUE( bd0_ < bd4_); + EXPECT_TRUE( bd0_ < bd5_); + + EXPECT_FALSE(bd4_ < bd0_); + EXPECT_FALSE(bd4_ < bd4_); + EXPECT_TRUE( bd4_ < bd5_); + + EXPECT_FALSE(bd5_ < bd0_); + EXPECT_FALSE(bd5_ < bd4_); + EXPECT_FALSE(bd5_ < bd5_); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BinaryDataTest, Equality) +{ + EXPECT_TRUE( bd0_==bd0_); + EXPECT_TRUE( bd4_==bd4_); + EXPECT_FALSE(bd4_==bd5_); + EXPECT_TRUE( bd0_!=bd4_); + EXPECT_TRUE( bd0_!=bd5_); + EXPECT_TRUE( bd4_!=bd5_); + EXPECT_FALSE(bd4_!=bd4_); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BinaryDataTest, ToString) +{ + EXPECT_EQ(bd0_.toHexStr(), str0_); + EXPECT_EQ(bd4_.toHexStr(), str4_); + EXPECT_EQ(bd4_.toHexStr(), str4_); + + string a,b; + bd0_.copyTo(a); + bd4_.copyTo(b); + EXPECT_EQ(bd0_.toBinStr(), a); + EXPECT_EQ(bd4_.toBinStr(), b); + + string stra("cdab3412"); + BinaryData bda = READHEX(stra); + + EXPECT_EQ(bd4_.toHexStr(true), stra); + EXPECT_EQ(bd4_.toBinStr(true), bda.toBinStr()); + +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BinaryDataTest, Endianness) +{ + BinaryData a = READHEX("cdab3412"); + BinaryData b = READHEX("1234cdab"); + + BinaryData static4 = bd4_; + + EXPECT_EQ( a.copySwapEndian(), bd4_); + EXPECT_EQ(bd4_.copySwapEndian(), a); + EXPECT_EQ(bd0_.copySwapEndian(), bd0_); + + + bd4_ = static4; + bd4_.swapEndian(); + EXPECT_EQ(bd4_, a); + + bd4_ = static4; + bd4_.swapEndian(2); + EXPECT_EQ(bd4_, b); + + bd4_ = static4; + bd4_.swapEndian(2,2); + EXPECT_EQ(bd4_, b); + + bd4_ = static4; + bd4_.swapEndian(2,4); + EXPECT_EQ(bd4_, b); +} + + +TEST_F(BinaryDataTest, IntToBinData) +{ + // 0x1234 in src code is always interpreted by the compiler as + // big-endian, regardless of the underlying architecture. So + // writing 0x1234 will be interpretted as an integer with value + // 4660 on all architectures. + BinaryData a,b; + + a = BinaryData::IntToStrLE(0xab); + b = BinaryData::IntToStrBE(0xab); + EXPECT_EQ(a, READHEX("ab")); + EXPECT_EQ(b, READHEX("ab")); + + a = BinaryData::IntToStrLE(0xabcd); + b = BinaryData::IntToStrBE(0xabcd); + EXPECT_EQ(a, READHEX("cdab")); + EXPECT_EQ(b, READHEX("abcd")); + + a = BinaryData::IntToStrLE((uint16_t)0xabcd); + b = BinaryData::IntToStrBE((uint16_t)0xabcd); + EXPECT_EQ(a, READHEX("cdab")); + EXPECT_EQ(b, READHEX("abcd")); + + // This fails b/c it auto "promotes" non-suffix literals to 4-byte ints + a = BinaryData::IntToStrLE(0xabcd); + b = BinaryData::IntToStrBE(0xabcd); + EXPECT_NE(a, READHEX("cdab")); + EXPECT_NE(b, READHEX("abcd")); + + a = BinaryData::IntToStrLE(0xfec38a11); + b = BinaryData::IntToStrBE(0xfec38a11); + EXPECT_EQ(a, READHEX("118ac3fe")); + EXPECT_EQ(b, READHEX("fec38a11")); + + a = BinaryData::IntToStrLE(0x00000000fec38a11ULL); + b = BinaryData::IntToStrBE(0x00000000fec38a11ULL); + EXPECT_EQ(a, READHEX("118ac3fe00000000")); + EXPECT_EQ(b, READHEX("00000000fec38a11")); + +} + +TEST_F(BinaryDataTest, BinDataToInt) +{ + uint8_t a8, b8; + uint16_t a16, b16; + uint32_t a32, b32; + uint64_t a64, b64; + + a8 = BinaryData::StrToIntBE(READHEX("ab")); + b8 = BinaryData::StrToIntLE(READHEX("ab")); + EXPECT_EQ(a8, 0xab); + EXPECT_EQ(b8, 0xab); + + a16 = BinaryData::StrToIntBE(READHEX("abcd")); + b16 = BinaryData::StrToIntLE(READHEX("abcd")); + EXPECT_EQ(a16, 0xabcd); + EXPECT_EQ(b16, 0xcdab); + + a32 = BinaryData::StrToIntBE(READHEX("fec38a11")); + b32 = BinaryData::StrToIntLE(READHEX("fec38a11")); + EXPECT_EQ(a32, 0xfec38a11); + EXPECT_EQ(b32, 0x118ac3fe); + + a64 = BinaryData::StrToIntBE(READHEX("00000000fec38a11")); + b64 = BinaryData::StrToIntLE(READHEX("00000000fec38a11")); + EXPECT_EQ(a64, 0x00000000fec38a11); + EXPECT_EQ(b64, 0x118ac3fe00000000); + + // These are really just identical tests, I have no idea whether it + // was worth spending the time to write these, and even this comment + // here explaining how it was probably a waste of time... + a8 = READ_UINT8_BE(READHEX("ab")); + b8 = READ_UINT8_LE(READHEX("ab")); + EXPECT_EQ(a8, 0xab); + EXPECT_EQ(b8, 0xab); + + a16 = READ_UINT16_BE(READHEX("abcd")); + b16 = READ_UINT16_LE(READHEX("abcd")); + EXPECT_EQ(a16, 0xabcd); + EXPECT_EQ(b16, 0xcdab); + + a32 = READ_UINT32_BE(READHEX("fec38a11")); + b32 = READ_UINT32_LE(READHEX("fec38a11")); + EXPECT_EQ(a32, 0xfec38a11); + EXPECT_EQ(b32, 0x118ac3fe); + + a64 = READ_UINT64_BE(READHEX("00000000fec38a11")); + b64 = READ_UINT64_LE(READHEX("00000000fec38a11")); + EXPECT_EQ(a64, 0x00000000fec38a11); + EXPECT_EQ(b64, 0x118ac3fe00000000); + + // Test the all-on-one read-int macros + a8 = READ_UINT8_HEX_BE("ab"); + b8 = READ_UINT8_HEX_LE("ab"); + EXPECT_EQ(a8, 0xab); + EXPECT_EQ(b8, 0xab); + + a16 = READ_UINT16_HEX_BE("abcd"); + b16 = READ_UINT16_HEX_LE("abcd"); + EXPECT_EQ(a16, 0xabcd); + EXPECT_EQ(b16, 0xcdab); + + a32 = READ_UINT32_HEX_BE("fec38a11"); + b32 = READ_UINT32_HEX_LE("fec38a11"); + EXPECT_EQ(a32, 0xfec38a11); + EXPECT_EQ(b32, 0x118ac3fe); + + a64 = READ_UINT64_HEX_BE("00000000fec38a11"); + b64 = READ_UINT64_HEX_LE("00000000fec38a11"); + EXPECT_EQ(a64, 0x00000000fec38a11); + EXPECT_EQ(b64, 0x118ac3fe00000000); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BinaryDataTest, Find) +{ + BinaryData a = READHEX("12"); + BinaryData b = READHEX("34"); + BinaryData c = READHEX("abcd"); + BinaryData d = READHEX("ff"); + + EXPECT_EQ(bd0_.find(bd0_), 0); + EXPECT_EQ(bd0_.find(bd4_), -1); + EXPECT_EQ(bd0_.find(bd4_, 2), -1); + EXPECT_EQ(bd4_.find(bd0_), 0); + EXPECT_EQ(bd4_.find(bd0_, 2), 2); + + EXPECT_EQ(bd4_.find(a), 0); + EXPECT_EQ(bd4_.find(b), 1); + EXPECT_EQ(bd4_.find(c), 2); + EXPECT_EQ(bd4_.find(d), -1); + + EXPECT_EQ(bd4_.find(a, 0), 0); + EXPECT_EQ(bd4_.find(b, 0), 1); + EXPECT_EQ(bd4_.find(c, 0), 2); + EXPECT_EQ(bd4_.find(d, 0), -1); + + EXPECT_EQ(bd4_.find(a, 1), -1); + EXPECT_EQ(bd4_.find(b, 1), 1); + EXPECT_EQ(bd4_.find(c, 1), 2); + EXPECT_EQ(bd4_.find(d, 1), -1); + + EXPECT_EQ(bd4_.find(a, 4), -1); + EXPECT_EQ(bd4_.find(b, 4), -1); + EXPECT_EQ(bd4_.find(c, 4), -1); + EXPECT_EQ(bd4_.find(d, 4), -1); + + EXPECT_EQ(bd4_.find(a, 8), -1); + EXPECT_EQ(bd4_.find(b, 8), -1); + EXPECT_EQ(bd4_.find(c, 8), -1); + EXPECT_EQ(bd4_.find(d, 8), -1); +} + + +TEST_F(BinaryDataTest, Contains) +{ + BinaryData a = READHEX("12"); + BinaryData b = READHEX("34"); + BinaryData c = READHEX("abcd"); + BinaryData d = READHEX("ff"); + + EXPECT_TRUE( bd0_.contains(bd0_)); + EXPECT_FALSE(bd0_.contains(bd4_)); + EXPECT_FALSE(bd0_.contains(bd4_, 2)); + + EXPECT_TRUE( bd4_.contains(a)); + EXPECT_TRUE( bd4_.contains(b)); + EXPECT_TRUE( bd4_.contains(c)); + EXPECT_FALSE(bd4_.contains(d)); + + EXPECT_TRUE( bd4_.contains(a, 0)); + EXPECT_TRUE( bd4_.contains(b, 0)); + EXPECT_TRUE( bd4_.contains(c, 0)); + EXPECT_FALSE(bd4_.contains(d, 0)); + + EXPECT_FALSE(bd4_.contains(a, 1)); + EXPECT_TRUE( bd4_.contains(b, 1)); + EXPECT_TRUE( bd4_.contains(c, 1)); + EXPECT_FALSE(bd4_.contains(d, 1)); + + EXPECT_FALSE(bd4_.contains(a, 4)); + EXPECT_FALSE(bd4_.contains(b, 4)); + EXPECT_FALSE(bd4_.contains(c, 4)); + EXPECT_FALSE(bd4_.contains(d, 4)); + + EXPECT_FALSE(bd4_.contains(a, 8)); + EXPECT_FALSE(bd4_.contains(b, 8)); + EXPECT_FALSE(bd4_.contains(c, 8)); + EXPECT_FALSE(bd4_.contains(d, 8)); +} + +//////////////////////////////////////////////////////////////////////////////// +//TEST_F(BinaryDataTest, GenerateRandom) +//{ + // Yeah, this would be a fun one to try to test... +//} + + +//////////////////////////////////////////////////////////////////////////////// +//TEST_F(BinaryDataTest, ReadFile) +//{ + //ofstream os("test +//} + + + +//////////////////////////////////////////////////////////////////////////////// +class BinaryDataRefTest : public ::testing::Test +{ +protected: + virtual void SetUp(void) + { + str0_ = ""; + str4_ = "1234abcd"; + str5_ = "1234abcdef"; + + bd0_ = READHEX(str0_); + bd4_ = READHEX(str4_); + bd5_ = READHEX(str5_); + + bdr__ = BinaryDataRef(); + bdr0_ = BinaryDataRef(bd0_); + bdr4_ = BinaryDataRef(bd4_); + bdr5_ = BinaryDataRef(bd5_); + } + + string str0_; + string str4_; + string str5_; + + BinaryData bd0_; + BinaryData bd4_; + BinaryData bd5_; + + BinaryDataRef bdr__; + BinaryDataRef bdr0_; + BinaryDataRef bdr4_; + BinaryDataRef bdr5_; +}; + + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BinaryDataRefTest, Constructor) +{ + BinaryDataRef a; + BinaryDataRef b((uint8_t*)bd0_.getPtr(), bd0_.getSize()); + BinaryDataRef c((uint8_t*)bd0_.getPtr(), (uint8_t*)bd0_.getPtr()); + BinaryDataRef d((uint8_t*)bd4_.getPtr(), bd4_.getSize()); + BinaryDataRef e((uint8_t*)bd4_.getPtr(), (uint8_t*)bd4_.getPtr()+4); + BinaryDataRef f(bd0_); + BinaryDataRef g(bd4_); + BinaryDataRef h(str0_); + BinaryDataRef i(str4_); + + EXPECT_TRUE(a.getPtr()==NULL); + EXPECT_EQ(a.getSize(), 0); + + EXPECT_TRUE(b.getPtr()==NULL); + EXPECT_EQ(b.getSize(), 0); + + EXPECT_TRUE(c.getPtr()==NULL); + EXPECT_EQ(c.getSize(), 0); + + EXPECT_FALSE(d.getPtr()==NULL); + EXPECT_EQ(d.getSize(), 4); + + EXPECT_FALSE(e.getPtr()==NULL); + EXPECT_EQ(e.getSize(), 4); + + EXPECT_TRUE(f.getPtr()==NULL); + EXPECT_EQ(f.getSize(), 0); + + EXPECT_FALSE(g.getPtr()==NULL); + EXPECT_EQ(g.getSize(), 4); + + EXPECT_TRUE(h.getPtr()==NULL); + EXPECT_EQ(h.getSize(), 0); + + EXPECT_FALSE(i.getPtr()==NULL); + EXPECT_EQ(i.getSize(), 8); + + EXPECT_TRUE( a.isNull()); + EXPECT_TRUE( b.isNull()); + EXPECT_TRUE( c.isNull()); + EXPECT_FALSE(d.isNull()); + EXPECT_FALSE(e.isNull()); + EXPECT_TRUE( f.isNull()); + EXPECT_FALSE(g.isNull()); + EXPECT_TRUE( h.isNull()); + EXPECT_FALSE(i.isNull()); +} + + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BinaryDataRefTest, PostConstruct) +{ + BinaryDataRef a,b,c,d,e,f,g,h,i; + + b.setRef((uint8_t*)bd0_.getPtr(), bd0_.getSize()); + c.setRef((uint8_t*)bd0_.getPtr(), (uint8_t*)bd0_.getPtr()); + d.setRef((uint8_t*)bd4_.getPtr(), bd4_.getSize()); + e.setRef((uint8_t*)bd4_.getPtr(), (uint8_t*)bd4_.getPtr()+4); + f.setRef(bd0_); + g.setRef(bd4_); + h.setRef(str0_); + i.setRef(str4_); + + EXPECT_TRUE(a.getPtr()==NULL); + EXPECT_EQ(a.getSize(), 0); + + EXPECT_TRUE(b.getPtr()==NULL); + EXPECT_EQ(b.getSize(), 0); + + EXPECT_TRUE(c.getPtr()==NULL); + EXPECT_EQ(c.getSize(), 0); + + EXPECT_FALSE(d.getPtr()==NULL); + EXPECT_EQ(d.getSize(), 4); + + EXPECT_FALSE(e.getPtr()==NULL); + EXPECT_EQ(e.getSize(), 4); + + EXPECT_TRUE(f.getPtr()==NULL); + EXPECT_EQ(f.getSize(), 0); + + EXPECT_FALSE(g.getPtr()==NULL); + EXPECT_EQ(g.getSize(), 4); + + EXPECT_FALSE(h.getPtr()==NULL); + EXPECT_EQ(h.getSize(), 0); + + EXPECT_FALSE(i.getPtr()==NULL); + EXPECT_EQ(i.getSize(), 8); + + EXPECT_TRUE( a.isNull()); + EXPECT_TRUE( b.isNull()); + EXPECT_TRUE( c.isNull()); + EXPECT_FALSE(d.isNull()); + EXPECT_FALSE(e.isNull()); + EXPECT_TRUE( f.isNull()); + EXPECT_FALSE(g.isNull()); + EXPECT_FALSE(h.isNull()); + EXPECT_FALSE(i.isNull()); +} + + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BinaryDataRefTest, CopyTo) +{ + BinaryData a,b,c,d,e,f,g,h; + bdr0_.copyTo(a); + bdr4_.copyTo(b); + + c.resize(bdr5_.getSize()); + bdr5_.copyTo(c.getPtr()); + + size_t sz = 2; + d.resize(sz); + e.resize(sz); + bdr5_.copyTo(d.getPtr(), sz); + bdr5_.copyTo(e.getPtr(), bdr5_.getSize()-sz, sz); + + f.copyFrom(bdr5_.getPtr(), bdr5_.getPtr()+sz); + + EXPECT_TRUE(a==bdr0_); + EXPECT_TRUE(b==bdr4_); + EXPECT_TRUE(c==bdr5_); + EXPECT_TRUE(bdr5_.startsWith(d)); + EXPECT_TRUE(bdr5_.endsWith(e)); + EXPECT_TRUE(d==f); + + EXPECT_EQ(a.getSize(), 0); + EXPECT_EQ(b.getSize(), 4); + EXPECT_EQ(c.getSize(), 5); + EXPECT_EQ(d.getSize(), 2); + EXPECT_NE(b,c); + + g = bdr0_.copy(); + h = bdr4_.copy(); + + EXPECT_EQ(g, bdr0_); + EXPECT_EQ(h, bdr4_); + EXPECT_EQ(g, bdr0_.copy()); + EXPECT_EQ(h, bdr4_.copy()); + + EXPECT_EQ(bdr0_, g); + EXPECT_EQ(bdr4_, h); + EXPECT_EQ(bdr0_.copy(), g); + EXPECT_EQ(bdr4_.copy(), h); +} + + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BinaryDataRefTest, ToString) +{ + EXPECT_EQ(bdr0_.toHexStr(), str0_); + EXPECT_EQ(bdr4_.toHexStr(), str4_); + EXPECT_EQ(bdr4_.toHexStr(), str4_); + + string a,b; + bdr0_.copyTo(a); + bdr4_.copyTo(b); + EXPECT_EQ(bd0_.toBinStr(), a); + EXPECT_EQ(bd4_.toBinStr(), b); + + string stra("cdab3412"); + BinaryData bda = READHEX(stra); + + EXPECT_EQ(bdr4_.toHexStr(true), stra); + EXPECT_EQ(bdr4_.toBinStr(true), bda.toBinStr()); + +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BinaryDataRefTest, Find) +{ + BinaryData a = READHEX("12"); + BinaryData b = READHEX("34"); + BinaryData c = READHEX("abcd"); + BinaryData d = READHEX("ff"); + + EXPECT_EQ(bdr0_.find(bdr0_), 0); + EXPECT_EQ(bdr0_.find(bdr4_), -1); + EXPECT_EQ(bdr0_.find(bdr4_, 2), -1); + EXPECT_EQ(bdr4_.find(bdr0_), 0); + EXPECT_EQ(bdr4_.find(bdr0_, 2), 2); + + EXPECT_EQ(bdr4_.find(a), 0); + EXPECT_EQ(bdr4_.find(b), 1); + EXPECT_EQ(bdr4_.find(c), 2); + EXPECT_EQ(bdr4_.find(d), -1); + + EXPECT_EQ(bdr4_.find(a, 0), 0); + EXPECT_EQ(bdr4_.find(b, 0), 1); + EXPECT_EQ(bdr4_.find(c, 0), 2); + EXPECT_EQ(bdr4_.find(d, 0), -1); + + EXPECT_EQ(bdr4_.find(a, 1), -1); + EXPECT_EQ(bdr4_.find(b, 1), 1); + EXPECT_EQ(bdr4_.find(c, 1), 2); + EXPECT_EQ(bdr4_.find(d, 1), -1); + + EXPECT_EQ(bdr4_.find(a, 4), -1); + EXPECT_EQ(bdr4_.find(b, 4), -1); + EXPECT_EQ(bdr4_.find(c, 4), -1); + EXPECT_EQ(bdr4_.find(d, 4), -1); + + EXPECT_EQ(bdr4_.find(a, 8), -1); + EXPECT_EQ(bdr4_.find(b, 8), -1); + EXPECT_EQ(bdr4_.find(c, 8), -1); + EXPECT_EQ(bdr4_.find(d, 8), -1); + + EXPECT_EQ(bdr4_.find(a.getRef(), 0), 0); + EXPECT_EQ(bdr4_.find(b.getRef(), 0), 1); + EXPECT_EQ(bdr4_.find(c.getRef(), 0), 2); + EXPECT_EQ(bdr4_.find(d.getRef(), 0), -1); +} + + +TEST_F(BinaryDataRefTest, Contains) +{ + BinaryData a = READHEX("12"); + BinaryData b = READHEX("34"); + BinaryData c = READHEX("abcd"); + BinaryData d = READHEX("ff"); + + EXPECT_TRUE( bdr0_.contains(bdr0_)); + EXPECT_FALSE(bdr0_.contains(bdr4_)); + EXPECT_FALSE(bdr0_.contains(bdr4_, 2)); + + EXPECT_TRUE( bdr4_.contains(a)); + EXPECT_TRUE( bdr4_.contains(b)); + EXPECT_TRUE( bdr4_.contains(c)); + EXPECT_FALSE(bdr4_.contains(d)); + + EXPECT_TRUE( bdr4_.contains(a, 0)); + EXPECT_TRUE( bdr4_.contains(b, 0)); + EXPECT_TRUE( bdr4_.contains(c, 0)); + EXPECT_FALSE(bdr4_.contains(d, 0)); + + EXPECT_FALSE(bdr4_.contains(a, 1)); + EXPECT_TRUE( bdr4_.contains(b, 1)); + EXPECT_TRUE( bdr4_.contains(c, 1)); + EXPECT_FALSE(bdr4_.contains(d, 1)); + + EXPECT_FALSE(bdr4_.contains(a, 4)); + EXPECT_FALSE(bdr4_.contains(b, 4)); + EXPECT_FALSE(bdr4_.contains(c, 4)); + EXPECT_FALSE(bdr4_.contains(d, 4)); + + EXPECT_FALSE(bdr4_.contains(a, 8)); + EXPECT_FALSE(bdr4_.contains(b, 8)); + EXPECT_FALSE(bdr4_.contains(c, 8)); + EXPECT_FALSE(bdr4_.contains(d, 8)); + + EXPECT_TRUE( bdr4_.contains(a.getRef(), 0)); + EXPECT_TRUE( bdr4_.contains(b.getRef(), 0)); + EXPECT_TRUE( bdr4_.contains(c.getRef(), 0)); + EXPECT_FALSE(bdr4_.contains(d.getRef(), 0)); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BinaryDataRefTest, StartsEndsWith) +{ + BinaryData a = READHEX("abcd"); + EXPECT_TRUE( bdr0_.startsWith(bdr0_)); + EXPECT_TRUE( bdr4_.startsWith(bdr0_)); + EXPECT_TRUE( bdr5_.startsWith(bdr4_)); + EXPECT_TRUE( bdr5_.startsWith(bdr5_)); + EXPECT_FALSE(bdr4_.startsWith(bdr5_)); + EXPECT_TRUE( bdr0_.startsWith(bdr0_)); + EXPECT_FALSE(bdr0_.startsWith(bdr4_)); + + EXPECT_TRUE( bdr0_.startsWith(bd0_)); + EXPECT_TRUE( bdr4_.startsWith(bd0_)); + EXPECT_TRUE( bdr5_.startsWith(bd4_)); + EXPECT_TRUE( bdr5_.startsWith(bd5_)); + EXPECT_FALSE(bdr4_.startsWith(bd5_)); + EXPECT_TRUE( bdr0_.startsWith(bd0_)); + EXPECT_FALSE(bdr0_.startsWith(bd4_)); + EXPECT_FALSE(bdr5_.endsWith(a)); + EXPECT_TRUE( bdr4_.endsWith(a)); + EXPECT_FALSE(bdr0_.endsWith(a)); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BinaryDataRefTest, Inequality) +{ + EXPECT_FALSE(bdr0_ < bdr0_); + EXPECT_TRUE( bdr0_ < bdr4_); + EXPECT_TRUE( bdr0_ < bdr5_); + + EXPECT_FALSE(bdr4_ < bdr0_); + EXPECT_FALSE(bdr4_ < bdr4_); + EXPECT_TRUE( bdr4_ < bdr5_); + + EXPECT_FALSE(bdr5_ < bdr0_); + EXPECT_FALSE(bdr5_ < bdr4_); + EXPECT_FALSE(bdr5_ < bdr5_); + + EXPECT_FALSE(bdr0_ < bd0_); + EXPECT_TRUE( bdr0_ < bd4_); + EXPECT_TRUE( bdr0_ < bd5_); + + EXPECT_FALSE(bdr4_ < bd0_); + EXPECT_FALSE(bdr4_ < bd4_); + EXPECT_TRUE( bdr4_ < bd5_); + + EXPECT_FALSE(bdr5_ < bd0_); + EXPECT_FALSE(bdr5_ < bd4_); + EXPECT_FALSE(bdr5_ < bd5_); + + EXPECT_FALSE(bdr0_ > bdr0_); + EXPECT_TRUE( bdr4_ > bdr0_); + EXPECT_TRUE( bdr5_ > bdr0_); + + EXPECT_FALSE(bdr0_ > bdr4_); + EXPECT_FALSE(bdr4_ > bdr4_); + EXPECT_TRUE( bdr5_ > bdr4_); + + EXPECT_FALSE(bdr0_ > bdr5_); + EXPECT_FALSE(bdr4_ > bdr5_); + EXPECT_FALSE(bdr5_ > bdr5_); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BinaryDataRefTest, Equality) +{ + EXPECT_TRUE( bdr0_==bdr0_); + EXPECT_TRUE( bdr4_==bdr4_); + EXPECT_FALSE(bdr4_==bdr5_); + EXPECT_TRUE( bdr0_!=bdr4_); + EXPECT_TRUE( bdr0_!=bdr5_); + EXPECT_TRUE( bdr4_!=bdr5_); + EXPECT_FALSE(bdr4_!=bdr4_); + + EXPECT_TRUE( bdr0_==bd0_); + EXPECT_TRUE( bdr4_==bd4_); + EXPECT_FALSE(bdr4_==bd5_); + EXPECT_TRUE( bdr0_!=bd4_); + EXPECT_TRUE( bdr0_!=bd5_); + EXPECT_TRUE( bdr4_!=bd5_); + EXPECT_FALSE(bdr4_!=bd4_); +} + + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +TEST(BitReadWriteTest, Writer8) +{ + BitPacker bitp; + + //EXPECT_EQ( bitp.getValue(), 0); + EXPECT_EQ( bitp.getBitsUsed(), 0); + EXPECT_EQ( bitp.getBinaryData(), READHEX("00")); + + bitp.putBit(true); + //EXPECT_EQ( bitp.getValue(), 128); + EXPECT_EQ( bitp.getBitsUsed(), 1); + EXPECT_EQ( bitp.getBinaryData(), READHEX("80")); + + bitp.putBit(false); + //EXPECT_EQ( bitp.getValue(), 128); + EXPECT_EQ( bitp.getBitsUsed(), 2); + EXPECT_EQ( bitp.getBinaryData(), READHEX("80")); + + bitp.putBit(true); + //EXPECT_EQ( bitp.getValue(), 160); + EXPECT_EQ( bitp.getBitsUsed(), 3); + EXPECT_EQ( bitp.getBinaryData(), READHEX("a0")); + + bitp.putBits(0, 2); + //EXPECT_EQ( bitp.getValue(), 160); + EXPECT_EQ( bitp.getBitsUsed(), 5); + EXPECT_EQ( bitp.getBinaryData(), READHEX("a0")); + + bitp.putBits(3, 3); + //EXPECT_EQ( bitp.getValue(), 163); + EXPECT_EQ( bitp.getBitsUsed(), 8); + EXPECT_EQ( bitp.getBinaryData(), READHEX("a3")); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST(BitReadWriteTest, Writer16) +{ + BitPacker bitp; + + //EXPECT_EQ( bitp.getValue(), 0); + EXPECT_EQ( bitp.getBitsUsed(), 0); + EXPECT_EQ( bitp.getBinaryData(), READHEX("0000")); + + bitp.putBit(true); + //EXPECT_EQ( bitp.getValue(), 0x8000); + EXPECT_EQ( bitp.getBitsUsed(), 1); + EXPECT_EQ( bitp.getBinaryData(), READHEX("8000")); + + bitp.putBit(false); + //EXPECT_EQ( bitp.getValue(), 0x8000); + EXPECT_EQ( bitp.getBitsUsed(), 2); + EXPECT_EQ( bitp.getBinaryData(), READHEX("8000")); + + bitp.putBit(true); + //EXPECT_EQ( bitp.getValue(), 0xa000); + EXPECT_EQ( bitp.getBitsUsed(), 3); + EXPECT_EQ( bitp.getBinaryData(), READHEX("a000")); + + bitp.putBits(0, 2); + //EXPECT_EQ( bitp.getValue(), 0xa000); + EXPECT_EQ( bitp.getBitsUsed(), 5); + EXPECT_EQ( bitp.getBinaryData(), READHEX("a000")); + + bitp.putBits(3, 3); + //EXPECT_EQ( bitp.getValue(), 0xa300); + EXPECT_EQ( bitp.getBitsUsed(), 8); + EXPECT_EQ( bitp.getBinaryData(), READHEX("a300")); + + bitp.putBits(3, 8); + //EXPECT_EQ( bitp.getValue(), 0xa303); + EXPECT_EQ( bitp.getBitsUsed(), 16); + EXPECT_EQ( bitp.getBinaryData(), READHEX("a303")); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST(BitReadWriteTest, Writer32) +{ + BitPacker bitp; + + bitp.putBits(0xffffff00, 32); + //EXPECT_EQ( bitp.getValue(), 0xffffff00); + EXPECT_EQ( bitp.getBitsUsed(), 32); + EXPECT_EQ( bitp.getBinaryData(), READHEX("ffffff00")); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST(BitReadWriteTest, Writer64) +{ + BitPacker bitp; + + bitp.putBits(0xffffff00ffffffaaULL, 64); + //EXPECT_EQ( bitp.getValue(), 0xffffff00ffffffaaULL); + EXPECT_EQ( bitp.getBitsUsed(), 64); + EXPECT_EQ( bitp.getBinaryData(), READHEX("ffffff00ffffffaa")); + + BitPacker bitp2; + bitp2.putBits(0xff, 32); + bitp2.putBits(0xff, 32); + //EXPECT_EQ( bitp2.getValue(), 0x000000ff000000ffULL); + EXPECT_EQ( bitp2.getBitsUsed(), 64); + EXPECT_EQ( bitp2.getBinaryData(), READHEX("000000ff000000ff")); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST(BitReadWriteTest, Reader8) +{ + BitUnpacker bitu; + + bitu.setValue(0xa3); + EXPECT_TRUE( bitu.getBit()); + EXPECT_FALSE(bitu.getBit()); + EXPECT_TRUE( bitu.getBit()); + EXPECT_EQ( bitu.getBits(2), 0); + EXPECT_EQ( bitu.getBits(3), 3); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST(BitReadWriteTest, Reader16) +{ + BitUnpacker bitu; + + bitu.setValue(0xa303); + + EXPECT_TRUE( bitu.getBit()); + EXPECT_FALSE(bitu.getBit()); + EXPECT_TRUE( bitu.getBit()); + EXPECT_EQ( bitu.getBits(2), 0); + EXPECT_EQ( bitu.getBits(3), 3); + EXPECT_EQ( bitu.getBits(8), 3); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST(BitReadWriteTest, Reader32) +{ + BitUnpacker bitu(0xffffff00); + EXPECT_EQ(bitu.getBits(32), 0xffffff00); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST(BitReadWriteTest, Reader64) +{ + BitUnpacker bitu(0xffffff00ffffffaaULL); + EXPECT_EQ( bitu.getBits(64), 0xffffff00ffffffaaULL); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST(BinaryReadWriteTest, Writer) +{ + BinaryData out = READHEX("01""0100""013200aa""ff00ff00ff00ff00" + "ab""fdffff""fe013200aa""ffff00ff00ff00ff00"); + + BinaryWriter bw; + bw.put_uint8_t(1); EXPECT_EQ(bw.getSize(), 1); + bw.put_uint16_t(1); EXPECT_EQ(bw.getSize(), 3); + bw.put_uint32_t(0xaa003201); EXPECT_EQ(bw.getSize(), 7); + bw.put_uint64_t(0x00ff00ff00ff00ffULL); EXPECT_EQ(bw.getSize(), 15); + bw.put_var_int(0xab); EXPECT_EQ(bw.getSize(), 16); + bw.put_var_int(0xffff); EXPECT_EQ(bw.getSize(), 19); + bw.put_var_int(0xaa003201); EXPECT_EQ(bw.getSize(), 24); + bw.put_var_int(0x00ff00ff00ff00ffULL); EXPECT_EQ(bw.getSize(), 33); + + EXPECT_EQ(bw.getData(), out); + EXPECT_EQ(bw.getDataRef(), out.getRef()); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST(BinaryReadWriteTest, WriterEndian) +{ + BinaryData out = READHEX("01""0100""013200aa""ff00ff00ff00ff00" + "ab""fdffff""fe013200aa""ffff00ff00ff00ff00"); + + BinaryWriter bw; + bw.put_uint8_t(1); EXPECT_EQ(bw.getSize(), 1); + bw.put_uint16_t(0x0100, BE); EXPECT_EQ(bw.getSize(), 3); + bw.put_uint32_t(0x013200aa, BE); EXPECT_EQ(bw.getSize(), 7); + bw.put_uint64_t(0xff00ff00ff00ff00ULL, BE); EXPECT_EQ(bw.getSize(), 15); + bw.put_var_int(0xab); EXPECT_EQ(bw.getSize(), 16); + bw.put_var_int(0xffff); EXPECT_EQ(bw.getSize(), 19); + bw.put_var_int(0xaa003201); EXPECT_EQ(bw.getSize(), 24); + bw.put_var_int(0x00ff00ff00ff00ffULL); EXPECT_EQ(bw.getSize(), 33); + EXPECT_EQ(bw.getData(), out); + EXPECT_EQ(bw.getDataRef(), out.getRef()); + + BinaryWriter bw2; + bw2.put_uint8_t(1); EXPECT_EQ(bw2.getSize(), 1); + bw2.put_uint16_t(0x0001, LE); EXPECT_EQ(bw2.getSize(), 3); + bw2.put_uint32_t(0xaa003201, LE); EXPECT_EQ(bw2.getSize(), 7); + bw2.put_uint64_t(0x00ff00ff00ff00ffULL, LE); EXPECT_EQ(bw2.getSize(), 15); + bw2.put_var_int(0xab); EXPECT_EQ(bw2.getSize(), 16); + bw2.put_var_int(0xffff); EXPECT_EQ(bw2.getSize(), 19); + bw2.put_var_int(0xaa003201); EXPECT_EQ(bw2.getSize(), 24); + bw2.put_var_int(0x00ff00ff00ff00ffULL); EXPECT_EQ(bw2.getSize(), 33); + EXPECT_EQ(bw2.getData(), out); + EXPECT_EQ(bw2.getDataRef(), out.getRef()); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST(BinaryReadWriteTest, Reader) +{ + BinaryData in = READHEX("01""0100""013200aa""ff00ff00ff00ff00" + "ab""fdffff""fe013200aa""ffff00ff00ff00ff00"); + + BinaryReader br(in); + EXPECT_EQ(br.get_uint8_t(), 1); + EXPECT_EQ(br.get_uint16_t(), 1); + EXPECT_EQ(br.get_uint32_t(), 0xaa003201); + EXPECT_EQ(br.get_uint64_t(), 0x00ff00ff00ff00ffULL); + EXPECT_EQ(br.get_var_int(), 0xab); + EXPECT_EQ(br.get_var_int(), 0xffff); + EXPECT_EQ(br.get_var_int(), 0xaa003201); + EXPECT_EQ(br.get_var_int(), 0x00ff00ff00ff00ffULL); + + BinaryRefReader brr(in); + EXPECT_EQ(brr.get_uint8_t(), 1); + EXPECT_EQ(brr.get_uint16_t(), 1); + EXPECT_EQ(brr.get_uint32_t(), 0xaa003201); + EXPECT_EQ(brr.get_uint64_t(), 0x00ff00ff00ff00ffULL); + EXPECT_EQ(brr.get_var_int(), 0xab); + EXPECT_EQ(brr.get_var_int(), 0xffff); + EXPECT_EQ(brr.get_var_int(), 0xaa003201); + EXPECT_EQ(brr.get_var_int(), 0x00ff00ff00ff00ffULL); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST(BinaryReadWriteTest, ReaderEndian) +{ + BinaryData in = READHEX("01""0100""013200aa""ff00ff00ff00ff00" + "ab""fdffff""fe013200aa""ffff00ff00ff00ff00"); + + BinaryReader br(in); + EXPECT_EQ(br.get_uint8_t(LE), 1); + EXPECT_EQ(br.get_uint16_t(LE), 1); + EXPECT_EQ(br.get_uint32_t(LE), 0xaa003201); + EXPECT_EQ(br.get_uint64_t(LE), 0x00ff00ff00ff00ffULL); + EXPECT_EQ(br.get_var_int(), 0xab); + EXPECT_EQ(br.get_var_int(), 0xffff); + EXPECT_EQ(br.get_var_int(), 0xaa003201); + EXPECT_EQ(br.get_var_int(), 0x00ff00ff00ff00ffULL); + + BinaryRefReader brr(in); + EXPECT_EQ(brr.get_uint8_t(LE), 1); + EXPECT_EQ(brr.get_uint16_t(LE), 1); + EXPECT_EQ(brr.get_uint32_t(LE), 0xaa003201); + EXPECT_EQ(brr.get_uint64_t(LE), 0x00ff00ff00ff00ffULL); + EXPECT_EQ(brr.get_var_int(), 0xab); + EXPECT_EQ(brr.get_var_int(), 0xffff); + EXPECT_EQ(brr.get_var_int(), 0xaa003201); + EXPECT_EQ(brr.get_var_int(), 0x00ff00ff00ff00ffULL); + + BinaryReader br2(in); + EXPECT_EQ(br2.get_uint8_t(LITTLEENDIAN), 1); + EXPECT_EQ(br2.get_uint16_t(LITTLEENDIAN), 1); + EXPECT_EQ(br2.get_uint32_t(LITTLEENDIAN), 0xaa003201); + EXPECT_EQ(br2.get_uint64_t(LITTLEENDIAN), 0x00ff00ff00ff00ffULL); + EXPECT_EQ(br2.get_var_int(), 0xab); + EXPECT_EQ(br2.get_var_int(), 0xffff); + EXPECT_EQ(br2.get_var_int(), 0xaa003201); + EXPECT_EQ(br2.get_var_int(), 0x00ff00ff00ff00ffULL); + + BinaryRefReader brr2(in); + EXPECT_EQ(brr2.get_uint8_t(LITTLEENDIAN), 1); + EXPECT_EQ(brr2.get_uint16_t(LITTLEENDIAN), 1); + EXPECT_EQ(brr2.get_uint32_t(LITTLEENDIAN), 0xaa003201); + EXPECT_EQ(brr2.get_uint64_t(LITTLEENDIAN), 0x00ff00ff00ff00ffULL); + EXPECT_EQ(brr2.get_var_int(), 0xab); + EXPECT_EQ(brr2.get_var_int(), 0xffff); + EXPECT_EQ(brr2.get_var_int(), 0xaa003201); + EXPECT_EQ(brr2.get_var_int(), 0x00ff00ff00ff00ffULL); + + BinaryReader brBE(in); + EXPECT_EQ(brBE.get_uint8_t(BE), 1); + EXPECT_EQ(brBE.get_uint16_t(BE), 0x0100); + EXPECT_EQ(brBE.get_uint32_t(BE), 0x013200aa); + EXPECT_EQ(brBE.get_uint64_t(BE), 0xff00ff00ff00ff00ULL); + EXPECT_EQ(brBE.get_var_int(), 0xab); + EXPECT_EQ(brBE.get_var_int(), 0xffff); + EXPECT_EQ(brBE.get_var_int(), 0xaa003201); + EXPECT_EQ(brBE.get_var_int(), 0x00ff00ff00ff00ffULL); + + BinaryRefReader brrBE(in); + EXPECT_EQ(brrBE.get_uint8_t(BE), 1); + EXPECT_EQ(brrBE.get_uint16_t(BE), 0x0100); + EXPECT_EQ(brrBE.get_uint32_t(BE), 0x013200aa); + EXPECT_EQ(brrBE.get_uint64_t(BE), 0xff00ff00ff00ff00ULL); + EXPECT_EQ(brrBE.get_var_int(), 0xab); + EXPECT_EQ(brrBE.get_var_int(), 0xffff); + EXPECT_EQ(brrBE.get_var_int(), 0xaa003201); + EXPECT_EQ(brrBE.get_var_int(), 0x00ff00ff00ff00ffULL); + + BinaryReader brBE2(in); + EXPECT_EQ(brBE2.get_uint8_t(BIGENDIAN), 1); + EXPECT_EQ(brBE2.get_uint16_t(BIGENDIAN), 0x0100); + EXPECT_EQ(brBE2.get_uint32_t(BIGENDIAN), 0x013200aa); + EXPECT_EQ(brBE2.get_uint64_t(BIGENDIAN), 0xff00ff00ff00ff00ULL); + EXPECT_EQ(brBE2.get_var_int(), 0xab); + EXPECT_EQ(brBE2.get_var_int(), 0xffff); + EXPECT_EQ(brBE2.get_var_int(), 0xaa003201); + EXPECT_EQ(brBE2.get_var_int(), 0x00ff00ff00ff00ffULL); + + BinaryRefReader brrBE2(in); + EXPECT_EQ(brrBE2.get_uint8_t(BIGENDIAN), 1); + EXPECT_EQ(brrBE2.get_uint16_t(BIGENDIAN), 0x0100); + EXPECT_EQ(brrBE2.get_uint32_t(BIGENDIAN), 0x013200aa); + EXPECT_EQ(brrBE2.get_uint64_t(BIGENDIAN), 0xff00ff00ff00ff00ULL); + EXPECT_EQ(brrBE2.get_var_int(), 0xab); + EXPECT_EQ(brrBE2.get_var_int(), 0xffff); + EXPECT_EQ(brrBE2.get_var_int(), 0xaa003201); + EXPECT_EQ(brrBE2.get_var_int(), 0x00ff00ff00ff00ffULL); +} + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +class BtcUtilsTest : public ::testing::Test +{ +protected: + virtual void SetUp(void) + { + rawHead_ = READHEX( + "010000001d8f4ec0443e1f19f305e488c1085c95de7cc3fd25e0d2c5bb5d0000" + "000000009762547903d36881a86751f3f5049e23050113f779735ef82734ebf0" + "b4450081d8c8c84db3936a1a334b035b"); + headHashLE_ = READHEX( + "1195e67a7a6d0674bbd28ae096d602e1f038c8254b49dfe79d47000000000000"); + headHashBE_ = READHEX( + "000000000000479de7df494b25c838f0e102d696e08ad2bb74066d7a7ae69511"); + + satoshiPubKey_ = READHEX( "04" + "fc9702847840aaf195de8442ebecedf5b095cdbb9bc716bda9110971b28a49e0" + "ead8564ff0db22209e0374782c093bb899692d524e9d6a6956e7c5ecbcd68284"); + satoshiHash160_ = READHEX("65a4358f4691660849d9f235eb05f11fabbd69fa"); + + prevHashCB_ = READHEX( + "0000000000000000000000000000000000000000000000000000000000000000"); + prevHashReg_ = READHEX( + "894862e362905c6075074d9ec4b4e2dc34720089b1e9ef4738ee1b13f3bdcdb7"); + } + + BinaryData rawHead_; + BinaryData headHashLE_; + BinaryData headHashBE_; + + BinaryData satoshiPubKey_; + BinaryData satoshiHash160_; + + BinaryData prevHashCB_; + BinaryData prevHashReg_; +}; + + + + +TEST_F(BtcUtilsTest, ReadVarInt) +{ + BinaryData vi0 = READHEX("00"); + BinaryData vi1 = READHEX("21"); + BinaryData vi3 = READHEX("fdff00"); + BinaryData vi5 = READHEX("fe00000100"); + BinaryData vi9 = READHEX("ff0010a5d4e8000000"); + + uint64_t v = 0; + uint64_t w = 33; + uint64_t x = 255; + uint64_t y = 65536; + uint64_t z = 1000000000000ULL; + + BinaryRefReader brr; + pair a; + + brr.setNewData(vi0); + a = BtcUtils::readVarInt(brr); + EXPECT_EQ(a.first, v); + EXPECT_EQ(a.second, 1); + + brr.setNewData(vi1); + a = BtcUtils::readVarInt(brr); + EXPECT_EQ(a.first, w); + EXPECT_EQ(a.second, 1); + + brr.setNewData(vi3); + a = BtcUtils::readVarInt(brr); + EXPECT_EQ(a.first, x); + EXPECT_EQ(a.second, 3); + + brr.setNewData(vi5); + a = BtcUtils::readVarInt(brr); + EXPECT_EQ(a.first, y); + EXPECT_EQ(a.second, 5); + + brr.setNewData(vi9); + a = BtcUtils::readVarInt(brr); + EXPECT_EQ(a.first, z); + EXPECT_EQ(a.second, 9); + + // Just the length + EXPECT_EQ(BtcUtils::readVarIntLength(vi0.getPtr()), 1); + EXPECT_EQ(BtcUtils::readVarIntLength(vi1.getPtr()), 1); + EXPECT_EQ(BtcUtils::readVarIntLength(vi3.getPtr()), 3); + EXPECT_EQ(BtcUtils::readVarIntLength(vi5.getPtr()), 5); + EXPECT_EQ(BtcUtils::readVarIntLength(vi9.getPtr()), 9); + + EXPECT_EQ(BtcUtils::calcVarIntSize(v), 1); + EXPECT_EQ(BtcUtils::calcVarIntSize(w), 1); + EXPECT_EQ(BtcUtils::calcVarIntSize(x), 3); + EXPECT_EQ(BtcUtils::calcVarIntSize(y), 5); + EXPECT_EQ(BtcUtils::calcVarIntSize(z), 9); +} + + +TEST_F(BtcUtilsTest, Num2Str) +{ + EXPECT_EQ(BtcUtils::numToStrWCommas(0), string("0")); + EXPECT_EQ(BtcUtils::numToStrWCommas(100), string("100")); + EXPECT_EQ(BtcUtils::numToStrWCommas(-100), string("-100")); + EXPECT_EQ(BtcUtils::numToStrWCommas(999), string("999")); + EXPECT_EQ(BtcUtils::numToStrWCommas(1234), string("1,234")); + EXPECT_EQ(BtcUtils::numToStrWCommas(-1234), string("-1,234")); + EXPECT_EQ(BtcUtils::numToStrWCommas(12345678), string("12,345,678")); + EXPECT_EQ(BtcUtils::numToStrWCommas(-12345678), string("-12,345,678")); +} + + + +TEST_F(BtcUtilsTest, PackBits) +{ + list::iterator iter, iter2; + list bitList; + + bitList = BtcUtils::UnpackBits( READHEX("00"), 0); + EXPECT_EQ(bitList.size(), 0); + + bitList = BtcUtils::UnpackBits( READHEX("00"), 3); + EXPECT_EQ(bitList.size(), 3); + iter = bitList.begin(); + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + + + bitList = BtcUtils::UnpackBits( READHEX("00"), 8); + EXPECT_EQ(bitList.size(), 8); + iter = bitList.begin(); + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + + bitList = BtcUtils::UnpackBits( READHEX("017f"), 8); + EXPECT_EQ(bitList.size(), 8); + iter = bitList.begin(); + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_TRUE( *iter); iter++; + + + bitList = BtcUtils::UnpackBits( READHEX("017f"), 12); + EXPECT_EQ(bitList.size(), 12); + iter = bitList.begin(); + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_TRUE( *iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_TRUE( *iter); iter++; + EXPECT_TRUE( *iter); iter++; + EXPECT_TRUE( *iter); iter++; + + bitList = BtcUtils::UnpackBits( READHEX("017f"), 16); + EXPECT_EQ(bitList.size(), 16); + iter = bitList.begin(); + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_TRUE( *iter); iter++; + EXPECT_FALSE(*iter); iter++; + EXPECT_TRUE( *iter); iter++; + EXPECT_TRUE( *iter); iter++; + EXPECT_TRUE( *iter); iter++; + EXPECT_TRUE( *iter); iter++; + EXPECT_TRUE( *iter); iter++; + EXPECT_TRUE( *iter); iter++; + EXPECT_TRUE( *iter); iter++; + + + BinaryData packed; + packed = BtcUtils::PackBits(bitList); + EXPECT_EQ(packed, READHEX("017f")); + + bitList = BtcUtils::UnpackBits( READHEX("017f"), 12); + packed = BtcUtils::PackBits(bitList); + EXPECT_EQ(packed, READHEX("0170")); +} + + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BtcUtilsTest, SimpleHash) +{ + BinaryData hashOut; + + // sha256(sha256(X)); + BtcUtils::getHash256(rawHead_.getPtr(), rawHead_.getSize(), hashOut); + EXPECT_EQ(hashOut, headHashLE_); + EXPECT_EQ(hashOut, headHashBE_.copySwapEndian()); + + BtcUtils::getHash256_NoSafetyCheck(rawHead_.getPtr(), rawHead_.getSize(), hashOut); + EXPECT_EQ(hashOut, headHashLE_); + EXPECT_EQ(hashOut, headHashBE_.copySwapEndian()); + + hashOut = BtcUtils::getHash256(rawHead_.getPtr(), rawHead_.getSize()); + EXPECT_EQ(hashOut, headHashLE_); + + BtcUtils::getHash256(rawHead_, hashOut); + EXPECT_EQ(hashOut, headHashLE_); + + BtcUtils::getHash256(rawHead_.getRef(), hashOut); + EXPECT_EQ(hashOut, headHashLE_); + + hashOut = BtcUtils::getHash256(rawHead_); + EXPECT_EQ(hashOut, headHashLE_); + + + // ripemd160(sha256(X)); + BtcUtils::getHash160(satoshiPubKey_.getPtr(), satoshiPubKey_.getSize(), hashOut); + EXPECT_EQ(hashOut, satoshiHash160_); + + BtcUtils::getHash160(satoshiPubKey_.getPtr(), satoshiPubKey_.getSize(), hashOut); + EXPECT_EQ(hashOut, satoshiHash160_); + + hashOut = BtcUtils::getHash160(satoshiPubKey_.getPtr(), satoshiPubKey_.getSize()); + EXPECT_EQ(hashOut, satoshiHash160_); + + BtcUtils::getHash160(satoshiPubKey_, hashOut); + EXPECT_EQ(hashOut, satoshiHash160_); + + BtcUtils::getHash160(satoshiPubKey_.getRef(), hashOut); + EXPECT_EQ(hashOut, satoshiHash160_); + + hashOut = BtcUtils::getHash160(satoshiPubKey_); + EXPECT_EQ(hashOut, satoshiHash160_); +} + + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BtcUtilsTest, TxOutScriptID_Hash160) +{ + //TXOUT_SCRIPT_STDHASH160, + //TXOUT_SCRIPT_STDPUBKEY65, + //TXOUT_SCRIPT_STDPUBKEY33, + //TXOUT_SCRIPT_MULTISIG, + //TXOUT_SCRIPT_P2SH, + //TXOUT_SCRIPT_NONSTANDARD, + + BinaryData script = READHEX("76a914a134408afa258a50ed7a1d9817f26b63cc9002cc88ac"); + BinaryData a160 = READHEX( "a134408afa258a50ed7a1d9817f26b63cc9002cc"); + BinaryData unique = READHEX("00a134408afa258a50ed7a1d9817f26b63cc9002cc"); + TXOUT_SCRIPT_TYPE scrType = BtcUtils::getTxOutScriptType(script); + EXPECT_EQ(scrType, TXOUT_SCRIPT_STDHASH160 ); + EXPECT_EQ(BtcUtils::getTxOutRecipientAddr(script), a160 ); + EXPECT_EQ(BtcUtils::getTxOutRecipientAddr(script, scrType), a160 ); + EXPECT_EQ(BtcUtils::getTxOutScrAddr(script), unique ); + EXPECT_EQ(BtcUtils::getTxOutScrAddr(script, scrType), unique ); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BtcUtilsTest, TxOutScriptID_PubKey65) +{ + BinaryData script = READHEX( + "4104b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb1" + "6e6537a576782eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7bac"); + BinaryData a160 = READHEX( "e24b86bff5112623ba67c63b6380636cbdf1a66d"); + BinaryData unique = READHEX("00e24b86bff5112623ba67c63b6380636cbdf1a66d"); + TXOUT_SCRIPT_TYPE scrType = BtcUtils::getTxOutScriptType(script); + EXPECT_EQ(scrType, TXOUT_SCRIPT_STDPUBKEY65 ); + EXPECT_EQ(BtcUtils::getTxOutRecipientAddr(script), a160 ); + EXPECT_EQ(BtcUtils::getTxOutRecipientAddr(script, scrType), a160 ); + EXPECT_EQ(BtcUtils::getTxOutScrAddr(script), unique ); + EXPECT_EQ(BtcUtils::getTxOutScrAddr(script, scrType), unique ); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BtcUtilsTest, TxOutScriptID_PubKey33) +{ + BinaryData script = READHEX( + "21024005c945d86ac6b01fb04258345abea7a845bd25689edb723d5ad4068ddd3036ac"); + BinaryData a160 = READHEX( "0c1b83d01d0ffb2bccae606963376cca3863a7ce"); + BinaryData unique = READHEX("000c1b83d01d0ffb2bccae606963376cca3863a7ce"); + TXOUT_SCRIPT_TYPE scrType = BtcUtils::getTxOutScriptType(script); + EXPECT_EQ(scrType, TXOUT_SCRIPT_STDPUBKEY33 ); + EXPECT_EQ(BtcUtils::getTxOutRecipientAddr(script), a160 ); + EXPECT_EQ(BtcUtils::getTxOutRecipientAddr(script, scrType), a160 ); + EXPECT_EQ(BtcUtils::getTxOutScrAddr(script), unique ); + EXPECT_EQ(BtcUtils::getTxOutScrAddr(script, scrType), unique ); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BtcUtilsTest, TxOutScriptID_NonStd) +{ + // This was from block 150951 which was erroneously produced by MagicalTux + // This is not only non-standard, it's non-spendable + BinaryData script = READHEX("76a90088ac"); + BinaryData a160 = BtcUtils::BadAddress_; + BinaryData unique = READHEX("ff") + BtcUtils::getHash160(READHEX("76a90088ac")); + TXOUT_SCRIPT_TYPE scrType = BtcUtils::getTxOutScriptType(script); + EXPECT_EQ(scrType, TXOUT_SCRIPT_NONSTANDARD ); + EXPECT_EQ(BtcUtils::getTxOutRecipientAddr(script), a160 ); + EXPECT_EQ(BtcUtils::getTxOutRecipientAddr(script, scrType), a160 ); + EXPECT_EQ(BtcUtils::getTxOutScrAddr(script), unique ); + EXPECT_EQ(BtcUtils::getTxOutScrAddr(script, scrType), unique ); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BtcUtilsTest, TxOutScriptID_P2SH) +{ + // P2SH script from tx: 4ac04b4830d115eb9a08f320ef30159cc107dfb72b29bbc2f370093f962397b4 (TxOut: 1) + // Spent in tx: fd16d6bbf1a3498ca9777b9d31ceae883eb8cb6ede1fafbdd218bae107de66fe (TxIn: 1) + // P2SH address: 3Lip6sxQymNr9LD2cAVp6wLrw8xdKBdYFG + // Hash160: d0c15a7d41500976056b3345f542d8c944077c8a + BinaryData script = READHEX("a914d0c15a7d41500976056b3345f542d8c944077c8a87"); // send to P2SH + BinaryData a160 = READHEX( "d0c15a7d41500976056b3345f542d8c944077c8a"); + BinaryData unique = READHEX("05d0c15a7d41500976056b3345f542d8c944077c8a"); + TXOUT_SCRIPT_TYPE scrType = BtcUtils::getTxOutScriptType(script); + EXPECT_EQ(scrType, TXOUT_SCRIPT_P2SH); + EXPECT_EQ(BtcUtils::getTxOutRecipientAddr(script), a160 ); + EXPECT_EQ(BtcUtils::getTxOutRecipientAddr(script, scrType), a160 ); + EXPECT_EQ(BtcUtils::getTxOutScrAddr(script), unique ); + EXPECT_EQ(BtcUtils::getTxOutScrAddr(script, scrType), unique ); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BtcUtilsTest, TxOutScriptID_Multisig) +{ + BinaryData script = READHEX( + "5221034758cefcb75e16e4dfafb32383b709fa632086ea5ca982712de6add93" + "060b17a2103fe96237629128a0ae8c3825af8a4be8fe3109b16f62af19cec0b1" + "eb93b8717e252ae"); + BinaryData pub1 = READHEX( + "034758cefcb75e16e4dfafb32383b709fa632086ea5ca982712de6add93060b17a"); + BinaryData pub2 = READHEX( + "03fe96237629128a0ae8c3825af8a4be8fe3109b16f62af19cec0b1eb93b8717e2"); + BinaryData addr1 = READHEX("b3348abf9dd2d1491359f937e2af64b1bb6d525a"); + BinaryData addr2 = READHEX("785652a6b8e721e80ffa353e5dfd84f0658284a9"); + BinaryData a160 = BtcUtils::BadAddress_; + BinaryData unique = READHEX( + "fe0202785652a6b8e721e80ffa353e5dfd84f0658284a9b3348abf9dd2d14913" + "59f937e2af64b1bb6d525a"); + + TXOUT_SCRIPT_TYPE scrType = BtcUtils::getTxOutScriptType(script); + EXPECT_EQ(scrType, TXOUT_SCRIPT_MULTISIG); + EXPECT_EQ(BtcUtils::getTxOutRecipientAddr(script), a160 ); + EXPECT_EQ(BtcUtils::getTxOutRecipientAddr(script, scrType), a160 ); + EXPECT_EQ(BtcUtils::getTxOutScrAddr(script), unique ); + EXPECT_EQ(BtcUtils::getTxOutScrAddr(script, scrType), unique ); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BtcUtilsTest, TxOutScriptID_MultiList) +{ + BinaryData script = READHEX( + "5221034758cefcb75e16e4dfafb32383b709fa632086ea5ca982712de6add930" + "60b17a2103fe96237629128a0ae8c3825af8a4be8fe3109b16f62af19cec0b1e" + "b93b8717e252ae"); + BinaryData addr0 = READHEX("785652a6b8e721e80ffa353e5dfd84f0658284a9"); + BinaryData addr1 = READHEX("b3348abf9dd2d1491359f937e2af64b1bb6d525a"); + BinaryData a160 = BtcUtils::BadAddress_; + BinaryData unique = READHEX( + "fe0202785652a6b8e721e80ffa353e5dfd84f0658284a9b3348abf9dd2d14913" + "59f937e2af64b1bb6d525a"); + + BinaryData pub0 = READHEX( + "034758cefcb75e16e4dfafb32383b709fa632086ea5ca982712de6add93060b17a"); + BinaryData pub1 = READHEX( + "03fe96237629128a0ae8c3825af8a4be8fe3109b16f62af19cec0b1eb93b8717e2"); + + vector a160List; + uint32_t M; + + M = BtcUtils::getMultisigAddrList(script, a160List); + EXPECT_EQ(M, 2); + EXPECT_EQ(a160List.size(), 2); // N + + EXPECT_EQ(a160List[0], addr0); + EXPECT_EQ(a160List[1], addr1); + + vector pkList; + M = BtcUtils::getMultisigPubKeyList(script, pkList); + EXPECT_EQ(M, 2); + EXPECT_EQ(pkList.size(), 2); // N + + EXPECT_EQ(pkList[0], pub0); + EXPECT_EQ(pkList[1], pub1); +} + + +//TEST_F(BtcUtilsTest, TxInScriptID) +//{ + //TXIN_SCRIPT_STDUNCOMPR, + //TXIN_SCRIPT_STDCOMPR, + //TXIN_SCRIPT_COINBASE, + //TXIN_SCRIPT_SPENDPUBKEY, + //TXIN_SCRIPT_SPENDMULTI, + //TXIN_SCRIPT_SPENDP2SH, + //TXIN_SCRIPT_NONSTANDARD +//} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BtcUtilsTest, TxInScriptID_StdUncompr) +{ + BinaryData script = READHEX( + "493046022100b9daf2733055be73ae00ee0c5d78ca639d554fe779f163396c1a" + "39b7913e7eac02210091f0deeb2e510c74354afb30cc7d8fbac81b1ca8b39406" + "13379adc41a6ffd226014104b1537fa5bc2242d25ebf54f31e76ebabe0b3de4a" + "4dccd9004f058d6c2caa5d31164252e1e04e5df627fae7adec27fa9d40c271fc" + "4d30ff375ef6b26eba192bac"); + BinaryData a160 = READHEX("c42a8290196b2c5bcb35471b45aa0dc096baed5e"); + BinaryData prevHash = prevHashReg_; + + TXIN_SCRIPT_TYPE scrType = BtcUtils::getTxInScriptType( script, prevHash); + EXPECT_EQ(scrType, TXIN_SCRIPT_STDUNCOMPR); + EXPECT_EQ(BtcUtils::getTxInAddr(script, prevHash), a160); + EXPECT_EQ(BtcUtils::getTxInAddr(script, prevHash, scrType), a160); + EXPECT_EQ(BtcUtils::getTxInAddrFromType(script, scrType), a160); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BtcUtilsTest, TxInScriptID_StdCompr) +{ + BinaryData script = READHEX( + "47304402205299224886e5e3402b0e9fa3527bcfe1d73c4e2040f18de8dd17f1" + "16e3365a1102202590dcc16c4b711daae6c37977ba579ca65bcaa8fba2bd7168" + "a984be727ccf7a01210315122ff4d41d9fe3538a0a8c6c7f813cf12a901069a4" + "3d6478917246dc92a782"); + BinaryData a160 = READHEX("03214fc1433a287e964d6c4242093c34e4ed0001"); + BinaryData prevHash = prevHashReg_; + + TXIN_SCRIPT_TYPE scrType = BtcUtils::getTxInScriptType(script, prevHash); + EXPECT_EQ(scrType, TXIN_SCRIPT_STDCOMPR); + EXPECT_EQ(BtcUtils::getTxInAddr(script, prevHash), a160); + EXPECT_EQ(BtcUtils::getTxInAddr(script, prevHash, scrType), a160); + EXPECT_EQ(BtcUtils::getTxInAddrFromType(script, scrType), a160); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BtcUtilsTest, TxInScriptID_Coinbase) +{ + BinaryData script = READHEX( + "0310920304000071c3124d696e656420627920425443204775696c640800b75f950e000000"); + BinaryData a160 = BtcUtils::BadAddress_; + BinaryData prevHash = prevHashCB_; + + TXIN_SCRIPT_TYPE scrType = BtcUtils::getTxInScriptType(script, prevHash); + EXPECT_EQ(scrType, TXIN_SCRIPT_COINBASE); + EXPECT_EQ(BtcUtils::getTxInAddr(script, prevHash), a160); + EXPECT_EQ(BtcUtils::getTxInAddr(script, prevHash, scrType), a160); + EXPECT_EQ(BtcUtils::getTxInAddrFromType(script, scrType), a160); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BtcUtilsTest, TxInScriptID_SpendPubKey) +{ + BinaryData script = READHEX( + "47304402201ffc44394e5a3dd9c8b55bdc12147e18574ac945d15dac026793bf" + "3b8ff732af022035fd832549b5176126f735d87089c8c1c1319447a458a09818" + "e173eaf0c2eef101"); + BinaryData a160 = BtcUtils::BadAddress_; + BinaryData prevHash = prevHashReg_; + + TXIN_SCRIPT_TYPE scrType = BtcUtils::getTxInScriptType(script, prevHash); + EXPECT_EQ(scrType, TXIN_SCRIPT_SPENDPUBKEY); + EXPECT_EQ(BtcUtils::getTxInAddr(script, prevHash), a160); + EXPECT_EQ(BtcUtils::getTxInAddr(script, prevHash, scrType), a160); + EXPECT_EQ(BtcUtils::getTxInAddrFromType(script, scrType), a160); + //txInHash160s.push_back( READHEX("957efec6af757ccbbcf9a436f0083c5ddaa3bf1d")); // this one can't be determined +} + + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BtcUtilsTest, TxInScriptID_SpendMultisig) +{ + + BinaryData script = READHEX( + "004830450221009254113fa46918f299b1d18ec918613e56cffbeba0960db05f" + "66b51496e5bf3802201e229de334bd753a2b08b36cc3f38f5263a23e9714a737" + "520db45494ec095ce80148304502206ee62f539d5cd94f990b7abfda77750f58" + "ff91043c3f002501e5448ef6dba2520221009d29229cdfedda1dd02a1a90bb71" + "b30b77e9c3fc28d1353f054c86371f6c2a8101"); + BinaryData a160 = BtcUtils::BadAddress_; + BinaryData prevHash = prevHashReg_; + TXIN_SCRIPT_TYPE scrType = BtcUtils::getTxInScriptType(script, prevHash); + EXPECT_EQ(scrType, TXIN_SCRIPT_SPENDMULTI); + EXPECT_EQ(BtcUtils::getTxInAddr(script, prevHash), a160); + EXPECT_EQ(BtcUtils::getTxInAddr(script, prevHash, scrType), a160); + EXPECT_EQ(BtcUtils::getTxInAddrFromType(script, scrType), a160); + + + vector scrParts = BtcUtils::splitPushOnlyScriptRefs(script); + BinaryData zero = READHEX("00"); + BinaryData sig1 = READHEX( + "30450221009254113fa46918f299b1d18ec918613e56cffbeba0960db05f66b5" + "1496e5bf3802201e229de334bd753a2b08b36cc3f38f5263a23e9714a737520d" + "b45494ec095ce801"); + BinaryData sig2 = READHEX( + "304502206ee62f539d5cd94f990b7abfda77750f58ff91043c3f002501e5448e" + "f6dba2520221009d29229cdfedda1dd02a1a90bb71b30b77e9c3fc28d1353f05" + "4c86371f6c2a8101"); + + EXPECT_EQ(scrParts.size(), 3); + EXPECT_EQ(scrParts[0], zero); + EXPECT_EQ(scrParts[1], sig1); + EXPECT_EQ(scrParts[2], sig2); + + //BinaryData p2sh = READHEX("5221034758cefcb75e16e4dfafb32383b709fa632086ea5ca982712de6add93060b17a2103fe96237629128a0ae8c3825af8a4be8fe3109b16f62af19cec0b1eb93b8717e252ae"); + //BinaryData pub1 = READHEX("034758cefcb75e16e4dfafb32383b709fa632086ea5ca982712de6add93060b17a"); + //BinaryData pub1 = READHEX("03fe96237629128a0ae8c3825af8a4be8fe3109b16f62af19cec0b1eb93b8717e2"); + + +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BtcUtilsTest, TxInScriptID_SpendP2SH) +{ + + // Spending P2SH output as above: fd16d6bbf1a3498ca9777b9d31ceae883eb8cb6ede1fafbdd218bae107de66fe (TxIn: 1, 219 B) + // Leading 0x00 byte is required due to a bug in OP_CHECKMULTISIG + BinaryData script = READHEX( + "004830450221009254113fa46918f299b1d18ec918613e56cffbeba0960db05f" + "66b51496e5bf3802201e229de334bd753a2b08b36cc3f38f5263a23e9714a737" + "520db45494ec095ce80148304502206ee62f539d5cd94f990b7abfda77750f58" + "ff91043c3f002501e5448ef6dba2520221009d29229cdfedda1dd02a1a90bb71" + "b30b77e9c3fc28d1353f054c86371f6c2a8101475221034758cefcb75e16e4df" + "afb32383b709fa632086ea5ca982712de6add93060b17a2103fe96237629128a" + "0ae8c3825af8a4be8fe3109b16f62af19cec0b1eb93b8717e252ae"); + BinaryData a160 = READHEX("d0c15a7d41500976056b3345f542d8c944077c8a"); + BinaryData prevHash = prevHashReg_; + TXIN_SCRIPT_TYPE scrType = BtcUtils::getTxInScriptType(script, prevHash); + EXPECT_EQ(scrType, TXIN_SCRIPT_SPENDP2SH); + EXPECT_EQ(BtcUtils::getTxInAddr(script, prevHash), a160); + EXPECT_EQ(BtcUtils::getTxInAddr(script, prevHash, scrType), a160); + EXPECT_EQ(BtcUtils::getTxInAddrFromType(script, scrType), a160); +} + + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BtcUtilsTest, BitsToDifficulty) +{ + + double a = BtcUtils::convertDiffBitsToDouble(READHEX("ffff001d")); + double b = BtcUtils::convertDiffBitsToDouble(READHEX("be2f021a")); + double c = BtcUtils::convertDiffBitsToDouble(READHEX("3daa011a")); + + EXPECT_DOUBLE_EQ(a, 1.0); + EXPECT_DOUBLE_EQ(b, 7672999.920164138); + EXPECT_DOUBLE_EQ(c, 10076292.883418716); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BtcUtilsTest, ScriptToOpCodes) +{ + BinaryData complexScript = READHEX( + "526b006b7dac7ca9143cd1def404e12a85ead2b4d3f5f9f817fb0d46ef879a6c" + "936b7dac7ca9146a4e7d5f798e90e84db9244d4805459f87275943879a6c936b" + "7dac7ca914486efdd300987a054510b4ce1148d4ad290d911e879a6c936b6c6ca2"); + + vector opstr; + opstr.reserve(40); + opstr.push_back(string("OP_2")); + opstr.push_back(string("OP_TOALTSTACK")); + opstr.push_back(string("OP_0")); + opstr.push_back(string("OP_TOALTSTACK")); + opstr.push_back(string("OP_TUCK")); + opstr.push_back(string("OP_CHECKSIG")); + opstr.push_back(string("OP_SWAP")); + opstr.push_back(string("OP_HASH160")); + opstr.push_back(string("[PUSHDATA -- 20 BYTES:]")); + opstr.push_back(string("3cd1def404e12a85ead2b4d3f5f9f817fb0d46ef")); + opstr.push_back(string("OP_EQUAL")); + opstr.push_back(string("OP_BOOLAND")); + opstr.push_back(string("OP_FROMALTSTACK")); + opstr.push_back(string("OP_ADD")); + opstr.push_back(string("OP_TOALTSTACK")); + opstr.push_back(string("OP_TUCK")); + opstr.push_back(string("OP_CHECKSIG")); + opstr.push_back(string("OP_SWAP")); + opstr.push_back(string("OP_HASH160")); + opstr.push_back(string("[PUSHDATA -- 20 BYTES:]")); + opstr.push_back(string("6a4e7d5f798e90e84db9244d4805459f87275943")); + opstr.push_back(string("OP_EQUAL")); + opstr.push_back(string("OP_BOOLAND")); + opstr.push_back(string("OP_FROMALTSTACK")); + opstr.push_back(string("OP_ADD")); + opstr.push_back(string("OP_TOALTSTACK")); + opstr.push_back(string("OP_TUCK")); + opstr.push_back(string("OP_CHECKSIG")); + opstr.push_back(string("OP_SWAP")); + opstr.push_back(string("OP_HASH160")); + opstr.push_back(string("[PUSHDATA -- 20 BYTES:]")); + opstr.push_back(string("486efdd300987a054510b4ce1148d4ad290d911e")); + opstr.push_back(string("OP_EQUAL")); + opstr.push_back(string("OP_BOOLAND")); + opstr.push_back(string("OP_FROMALTSTACK")); + opstr.push_back(string("OP_ADD")); + opstr.push_back(string("OP_TOALTSTACK")); + opstr.push_back(string("OP_FROMALTSTACK")); + opstr.push_back(string("OP_FROMALTSTACK")); + opstr.push_back(string("OP_GREATERTHANOREQUAL")); + + vector output = BtcUtils::convertScriptToOpStrings(complexScript); + ASSERT_EQ(output.size(), opstr.size()); + for(uint32_t i=0; i txins(6); + txins[0].unserialize_checked(rawTxIn_.getPtr(), len); + txins[1].unserialize_checked(rawTxIn_.getPtr(), len, len); + txins[2].unserialize_checked(rawTxIn_.getPtr(), len, len, TxRef(), 12); + txins[3].unserialize(rawTxIn_.getRef()); + txins[4].unserialize(brr); + txins[5].unserialize_swigsafe_(rawTxIn_); + + for(uint32_t i=0; i<6; i++) + { + EXPECT_TRUE( txins[i].isInitialized()); + EXPECT_EQ( txins[i].serialize().getSize(), len); + EXPECT_EQ( txins[i].getScriptType(), TXIN_SCRIPT_STDUNCOMPR); + EXPECT_EQ( txins[i].getScriptSize(), len-(36+1+4)); + EXPECT_TRUE( txins[i].isStandard()); + EXPECT_FALSE(txins[i].isCoinbase()); + EXPECT_EQ( txins[i].getSequence(), UINT32_MAX); + EXPECT_EQ( txins[i].getSenderScrAddrIfAvail(), srcAddr); + EXPECT_EQ( txins[i].getOutPoint().serialize(), rawOP); + + EXPECT_FALSE(txins[i].getParentTxRef().isInitialized()); + EXPECT_EQ( txins[i].getParentHeight(), UINT32_MAX); + EXPECT_EQ( txins[i].getParentHash(), BinaryData(0)); + EXPECT_EQ( txins[i].serialize(), rawTxIn_); + if(i==2) + EXPECT_EQ(txins[i].getIndex(), 12); + else + EXPECT_EQ(txins[i].getIndex(), UINT32_MAX); + } +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockObjTest, TxOutUnserialize) +{ + BinaryRefReader brr(rawTxOut_); + uint32_t len = rawTxOut_.getSize(); + BinaryData dstAddr = READHEX("8dce8946f1c7763bb60ea5cf16ef514cbed0633b"); + + vector txouts(6); + txouts[0].unserialize_checked(rawTxOut_.getPtr(), len); + txouts[1].unserialize_checked(rawTxOut_.getPtr(), len, len); + txouts[2].unserialize_checked(rawTxOut_.getPtr(), len, len, TxRef(), 12); + txouts[3].unserialize(rawTxOut_.getRef()); + txouts[4].unserialize(brr); + txouts[5].unserialize_swigsafe_(rawTxOut_); + + for(uint32_t i=0; i<6; i++) + { + EXPECT_TRUE( txouts[i].isInitialized()); + EXPECT_EQ( txouts[i].getSize(), len); + EXPECT_EQ( txouts[i].getScriptType(), TXOUT_SCRIPT_STDHASH160); + EXPECT_EQ( txouts[i].getScriptSize(), 25); + EXPECT_TRUE( txouts[i].isStandard()); + EXPECT_EQ( txouts[i].getValue(), 0x00000000d58b4cac); + EXPECT_EQ( txouts[i].getScrAddressStr(), HASH160PREFIX+dstAddr); + + EXPECT_TRUE( txouts[i].isScriptStandard()); + EXPECT_TRUE( txouts[i].isScriptStdHash160()); + EXPECT_FALSE(txouts[i].isScriptStdPubKey65()); + EXPECT_FALSE(txouts[i].isScriptStdPubKey33()); + EXPECT_FALSE(txouts[i].isScriptP2SH()); + EXPECT_FALSE(txouts[i].isScriptNonStd()); + + EXPECT_FALSE(txouts[i].getParentTxRef().isInitialized()); + EXPECT_EQ( txouts[i].getParentHeight(), UINT32_MAX); + EXPECT_EQ( txouts[i].getParentHash(), BinaryData(0)); + EXPECT_EQ( txouts[i].serialize(), rawTxOut_); + if(i==2) + EXPECT_EQ(txouts[i].getIndex(), 12); + else + EXPECT_EQ(txouts[i].getIndex(), UINT32_MAX); + } +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockObjTest, TxNoInit) +{ + Tx tx; + + EXPECT_FALSE(tx.isInitialized()); + + // Actually, why even bother with all these no-init tests? We should always + // check whether the tx is initialized before using it. If you don't, you + // deserve to seg fault :) + //EXPECT_EQ( tx.getSize(), UINT32_MAX); + //EXPECT_TRUE( tx.isStandard()); + //EXPECT_EQ( tx.getValue(), 0x00000000d58b4cac); + //EXPECT_EQ( tx.getRecipientAddr(), dstAddr); + + //EXPECT_TRUE( tx.isScriptStandard()); + //EXPECT_TRUE( tx.isScriptStdHash160()); + //EXPECT_FALSE(tx.isScriptStdPubKey65()); + //EXPECT_FALSE(tx.isScriptStdPubKey33()); + //EXPECT_FALSE(tx.isScriptP2SH()); + //EXPECT_FALSE(tx.isScriptNonStd()); + +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockObjTest, TxUnserialize) +{ + uint32_t len = rawTx0_.getSize(); + BinaryData tx0hash = READHEX( + "aa739836a44451be555f74a02f088b50a867b1d3a2c917ee863d708ec2db58f6"); + + BinaryData tx0_In0 = READHEX("aff189b24a36a1b93de2ea4d157c13d18251270a"); + BinaryData tx0_Out0 = READHEX("c1b4695d53b6ee57a28647ce63e45665df6762c2"); + BinaryData tx0_Out1 = READHEX("0e0aec36fe2545fb31a41164fb6954adcd96b342"); + BinaryData tx0_Val0 = READHEX("42582c0a00000000"); + BinaryData tx0_Val1 = READHEX("80d1f00800000000"); + BinaryRefReader brr(rawTx0_); + + uint64_t v0 = *(uint64_t*)tx0_Val0.getPtr(); + uint64_t v1 = *(uint64_t*)tx0_Val1.getPtr(); + + Tx tx; + vector txs(10); + txs[0] = Tx(rawTx0_.getPtr(), len); + txs[1] = Tx(brr); brr.resetPosition(); + txs[2] = Tx(rawTx0_); + txs[3] = Tx(rawTx0_.getRef()); + txs[4].unserialize(rawTx0_.getPtr(), len); + txs[5].unserialize(rawTx0_); + txs[6].unserialize(rawTx0_.getRef()); + txs[7].unserialize(brr); brr.resetPosition(); + txs[8].unserialize_swigsafe_(rawTx0_); + txs[9] = Tx::createFromStr(rawTx0_); + + for(uint32_t i=0; i<10; i++) + { + EXPECT_TRUE( txs[i].isInitialized()); + EXPECT_EQ( txs[i].getSize(), len); + + EXPECT_EQ( txs[i].getVersion(), 1); + EXPECT_EQ( txs[i].getNumTxIn(), 1); + EXPECT_EQ( txs[i].getNumTxOut(), 2); + EXPECT_EQ( txs[i].getThisHash(), tx0hash.copySwapEndian()); + //EXPECT_FALSE(txs[i].isMainBranch()); + + EXPECT_EQ( txs[i].getTxInOffset(0), 5); + EXPECT_EQ( txs[i].getTxInOffset(1), 185); + EXPECT_EQ( txs[i].getTxOutOffset(0), 186); + EXPECT_EQ( txs[i].getTxOutOffset(1), 220); + EXPECT_EQ( txs[i].getTxOutOffset(2), 254); + + EXPECT_EQ( txs[i].getLockTime(), 0); + + EXPECT_EQ( txs[i].serialize(), rawTx0_); + EXPECT_EQ( txs[0].getTxInCopy(0).getSenderScrAddrIfAvail(), tx0_In0); + EXPECT_EQ( txs[i].getTxOutCopy(0).getScrAddressStr(), HASH160PREFIX+tx0_Out0); + EXPECT_EQ( txs[i].getTxOutCopy(1).getScrAddressStr(), HASH160PREFIX+tx0_Out1); + EXPECT_EQ( txs[i].getScrAddrForTxOut(0), HASH160PREFIX+tx0_Out0); + EXPECT_EQ( txs[i].getScrAddrForTxOut(1), HASH160PREFIX+tx0_Out1); + EXPECT_EQ( txs[i].getTxOutCopy(0).getValue(), v0); + EXPECT_EQ( txs[i].getTxOutCopy(1).getValue(), v1); + EXPECT_EQ( txs[i].getSumOfOutputs(), v0+v1); + + EXPECT_EQ( txs[i].getBlockTxIndex(), UINT16_MAX); + } +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockObjTest, DISABLED_FullBlock) +{ + EXPECT_TRUE(false); + + BinaryRefReader brr(rawBlock_); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockObjTest, DISABLED_TxIOPairStuff) +{ + EXPECT_TRUE(false); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockObjTest, DISABLED_RegisteredTxStuff) +{ + EXPECT_TRUE(false); +} + + + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +class StoredBlockObjTest : public ::testing::Test +{ +protected: + virtual void SetUp(void) + { + rawHead_ = READHEX( + "01000000" + "1d8f4ec0443e1f19f305e488c1085c95de7cc3fd25e0d2c5bb5d000000000000" + "9762547903d36881a86751f3f5049e23050113f779735ef82734ebf0b4450081" + "d8c8c84d" + "b3936a1a" + "334b035b"); + headHashLE_ = READHEX( + "1195e67a7a6d0674bbd28ae096d602e1f038c8254b49dfe79d47000000000000"); + headHashBE_ = READHEX( + "000000000000479de7df494b25c838f0e102d696e08ad2bb74066d7a7ae69511"); + + rawTx0_ = READHEX( + "01000000016290dce984203b6a5032e543e9e272d8bce934c7de4d15fa0fe44d" + "d49ae4ece9010000008b48304502204f2fa458d439f957308bca264689aa175e" + "3b7c5f78a901cb450ebd20936b2c500221008ea3883a5b80128e55c9c6070aa6" + "264e1e0ce3d18b7cd7e85108ce3d18b7419a0141044202550a5a6d3bb81549c4" + "a7803b1ad59cdbba4770439a4923624a8acfc7d34900beb54a24188f7f0a4068" + "9d905d4847cc7d6c8d808a457d833c2d44ef83f76bffffffff0242582c0a0000" + "00001976a914c1b4695d53b6ee57a28647ce63e45665df6762c288ac80d1f008" + "000000001976a9140e0aec36fe2545fb31a41164fb6954adcd96b34288ac0000" + "0000"); + rawTx1_ = READHEX( + "0100000001f658dbc28e703d86ee17c9a2d3b167a8508b082fa0745f55be5144" + "a4369873aa010000008c49304602210041e1186ca9a41fdfe1569d5d807ca7ff" + "6c5ffd19d2ad1be42f7f2a20cdc8f1cc0221003366b5d64fe81e53910e156914" + "091d12646bc0d1d662b7a65ead3ebe4ab8f6c40141048d103d81ac9691cf13f3" + "fc94e44968ef67b27f58b27372c13108552d24a6ee04785838f34624b294afee" + "83749b64478bb8480c20b242c376e77eea2b3dc48b4bffffffff0200e1f50500" + "0000001976a9141b00a2f6899335366f04b277e19d777559c35bc888ac40aeeb" + "02000000001976a9140e0aec36fe2545fb31a41164fb6954adcd96b34288ac00" + "000000"); + + rawBlock_ = READHEX( + "01000000eb10c9a996a2340a4d74eaab41421ed8664aa49d18538bab59010000" + "000000005a2f06efa9f2bd804f17877537f2080030cadbfa1eb50e02338117cc" + "604d91b9b7541a4ecfbb0a1a64f1ade703010000000100000000000000000000" + "00000000000000000000000000000000000000000000ffffffff0804cfbb0a1a" + "02360affffffff0100f2052a01000000434104c2239c4eedb3beb26785753463" + "be3ec62b82f6acd62efb65f452f8806f2ede0b338e31d1f69b1ce449558d7061" + "aa1648ddc2bf680834d3986624006a272dc21cac000000000100000003e8caa1" + "2bcb2e7e86499c9de49c45c5a1c6167ea4b894c8c83aebba1b6100f343010000" + "008c493046022100e2f5af5329d1244807f8347a2c8d9acc55a21a5db769e927" + "4e7e7ba0bb605b26022100c34ca3350df5089f3415d8af82364d7f567a6a297f" + "cc2c1d2034865633238b8c014104129e422ac490ddfcb7b1c405ab9fb4244124" + "6c4bca578de4f27b230de08408c64cad03af71ee8a3140b40408a7058a1984a9" + "f246492386113764c1ac132990d1ffffffff5b55c18864e16c08ef9989d31c7a" + "343e34c27c30cd7caa759651b0e08cae0106000000008c4930460221009ec9aa" + "3e0caf7caa321723dea561e232603e00686d4bfadf46c5c7352b07eb00022100" + "a4f18d937d1e2354b2e69e02b18d11620a6a9332d563e9e2bbcb01cee559680a" + "014104411b35dd963028300e36e82ee8cf1b0c8d5bf1fc4273e970469f5cb931" + "ee07759a2de5fef638961726d04bd5eb4e5072330b9b371e479733c942964bb8" + "6e2b22ffffffff3de0c1e913e6271769d8c0172cea2f00d6d3240afc3a20f9fa" + "247ce58af30d2a010000008c493046022100b610e169fd15ac9f60fe2b507529" + "281cf2267673f4690ba428cbb2ba3c3811fd022100ffbe9e3d71b21977a8e97f" + "de4c3ba47b896d08bc09ecb9d086bb59175b5b9f03014104ff07a1833fd8098b" + "25f48c66dcf8fde34cbdbcc0f5f21a8c2005b160406cbf34cc432842c6b37b25" + "90d16b165b36a3efc9908d65fb0e605314c9b278f40f3e1affffffff0240420f" + "00000000001976a914adfa66f57ded1b655eb4ccd96ee07ca62bc1ddfd88ac00" + "7d6a7d040000001976a914981a0c9ae61fa8f8c96ae6f8e383d6e07e77133e88" + "ac00000000010000000138e7586e0784280df58bd3dc5e3d350c9036b1ec4107" + "951378f45881799c92a4000000008a47304402207c945ae0bbdaf9dadba07bdf" + "23faa676485a53817af975ddf85a104f764fb93b02201ac6af32ddf597e610b4" + "002e41f2de46664587a379a0161323a85389b4f82dda014104ec8883d3e4f7a3" + "9d75c9f5bb9fd581dc9fb1b7cdf7d6b5a665e4db1fdb09281a74ab138a2dba25" + "248b5be38bf80249601ae688c90c6e0ac8811cdb740fcec31dffffffff022f66" + "ac61050000001976a914964642290c194e3bfab661c1085e47d67786d2d388ac" + "2f77e200000000001976a9141486a7046affd935919a3cb4b50a8a0c233c286c" + "88ac00000000"); + + rawTxUnfrag_ = READHEX( + // Version + "01000000" + // NumTxIn + "02" + // Start TxIn0 + "0044fbc929d78e4203eed6f1d3d39c0157d8e5c100bbe0886779c0" + "ebf6a69324010000008a47304402206568144ed5e7064d6176c74738b04c08ca" + "19ca54ddeb480084b77f45eebfe57802207927d6975a5ac0e1bb36f5c05356dc" + "da1f521770511ee5e03239c8e1eecf3aed0141045d74feae58c4c36d7c35beac" + "05eddddc78b3ce4b02491a2eea72043978056a8bc439b99ddaad327207b09ef1" + "6a8910828e805b0cc8c11fba5caea2ee939346d7ffffffff" + // Start TxIn1 + "45c866b219b17695" + "2508f8e5aea728f950186554fc4a5807e2186a8e1c4009e5000000008c493046" + "022100bd5d41662f98cfddc46e86ea7e4a3bc8fe9f1dfc5c4836eaf7df582596" + "cfe0e9022100fc459ae4f59b8279d679003b88935896acd10021b6e2e4619377" + "e336b5296c5e014104c00bab76a708ba7064b2315420a1c533ca9945eeff9754" + "cdc574224589e9113469b4e71752146a10028079e04948ecdf70609bf1b9801f" + "6b73ab75947ac339e5ffffffff" + // NumTxOut + "02" + // Start TxOut0 + "ac4c8bd5000000001976a9148dce8946f1c7763bb60ea5cf16ef514cbed0633b88ac" + // Start TxOut1 + "002f6859000000001976a9146a59ac0e8f553f292dfe5e9f3aaa1da93499c15e88ac" + // Locktime + "00000000"); + + rawTxFragged_ = READHEX( + //"01000000020044fbc929d78e4203eed6f1d3d39c0157d8e5c100bbe0886779c0" + //"ebf6a69324010000008a47304402206568144ed5e7064d6176c74738b04c08ca" + //"19ca54ddeb480084b77f45eebfe57802207927d6975a5ac0e1bb36f5c05356dc" + //"da1f521770511ee5e03239c8e1eecf3aed0141045d74feae58c4c36d7c35beac" + //"05eddddc78b3ce4b02491a2eea72043978056a8bc439b99ddaad327207b09ef1" + //"6a8910828e805b0cc8c11fba5caea2ee939346d7ffffffff45c866b219b17695" + //"2508f8e5aea728f950186554fc4a5807e2186a8e1c4009e5000000008c493046" + //"022100bd5d41662f98cfddc46e86ea7e4a3bc8fe9f1dfc5c4836eaf7df582596" + //"cfe0e9022100fc459ae4f59b8279d679003b88935896acd10021b6e2e4619377" + //"e336b5296c5e014104c00bab76a708ba7064b2315420a1c533ca9945eeff9754" + //"cdc574224589e9113469b4e71752146a10028079e04948ecdf70609bf1b9801f" + //"6b73ab75947ac339e5ffffffff0200000000"); + // Version + "01000000" + // NumTxIn + "02" + // Start TxIn0 + "0044fbc929d78e4203eed6f1d3d39c0157d8e5c100bbe0886779c0" + "ebf6a69324010000008a47304402206568144ed5e7064d6176c74738b04c08ca" + "19ca54ddeb480084b77f45eebfe57802207927d6975a5ac0e1bb36f5c05356dc" + "da1f521770511ee5e03239c8e1eecf3aed0141045d74feae58c4c36d7c35beac" + "05eddddc78b3ce4b02491a2eea72043978056a8bc439b99ddaad327207b09ef1" + "6a8910828e805b0cc8c11fba5caea2ee939346d7ffffffff" + // Start TxIn1 + "45c866b219b17695" + "2508f8e5aea728f950186554fc4a5807e2186a8e1c4009e5000000008c493046" + "022100bd5d41662f98cfddc46e86ea7e4a3bc8fe9f1dfc5c4836eaf7df582596" + "cfe0e9022100fc459ae4f59b8279d679003b88935896acd10021b6e2e4619377" + "e336b5296c5e014104c00bab76a708ba7064b2315420a1c533ca9945eeff9754" + "cdc574224589e9113469b4e71752146a10028079e04948ecdf70609bf1b9801f" + "6b73ab75947ac339e5ffffffff" + // NumTxOut + "02" + // ... TxOuts fragged out + // Locktime + "00000000"); + + rawTxOut0_ = READHEX( + // Value + "ac4c8bd500000000" + // Script size (var_int) + "19" + // Script + "76""a9""14""8dce8946f1c7763bb60ea5cf16ef514cbed0633b""88""ac"); + rawTxOut1_ = READHEX( + // Value + "002f685900000000" + // Script size (var_int) + "19" + // Script + "76""a9""14""6a59ac0e8f553f292dfe5e9f3aaa1da93499c15e""88""ac"); + + bh_.unserialize(rawHead_); + tx1_.unserialize(rawTx0_); + tx2_.unserialize(rawTx1_); + + + sbh_.unserialize(rawHead_); + + // Make sure the global DB type and prune type are reset for each test + DBUtils.setArmoryDbType(ARMORY_DB_FULL); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + } + + BinaryData PREFBYTE(DB_PREFIX pref) + { + BinaryWriter bw; + bw.put_uint8_t((uint8_t)pref); + return bw.getData(); + } + + BinaryData rawHead_; + BinaryData headHashLE_; + BinaryData headHashBE_; + + BinaryData rawBlock_; + + BinaryData rawTx0_; + BinaryData rawTx1_; + + BlockHeader bh_; + Tx tx1_; + Tx tx2_; + + BinaryData rawTxUnfrag_; + BinaryData rawTxFragged_; + BinaryData rawTxOut0_; + BinaryData rawTxOut1_; + + + + StoredHeader sbh_; +}; + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, StoredObjNoInit) +{ + StoredHeader sbh; + StoredTx stx; + StoredTxOut stxo; + StoredScriptHistory ssh; + StoredUndoData sud; + StoredHeadHgtList hhl; + StoredTxHints sths; + + EXPECT_FALSE( sbh.isInitialized() ); + EXPECT_FALSE( stx.isInitialized() ); + EXPECT_FALSE( stxo.isInitialized() ); + EXPECT_FALSE( ssh.isInitialized() ); + EXPECT_FALSE( sud.isInitialized() ); + EXPECT_FALSE( hhl.isInitialized() ); + EXPECT_FALSE( sths.isInitialized() ); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, GetDBKeys) +{ + StoredHeader sbh; + StoredTx stx; + StoredTxOut stxo; + StoredScriptHistory ssh1; + StoredScriptHistory ssh2; + StoredUndoData sud; + StoredHeadHgtList hhl; + StoredTxHints sths; + + BinaryData key = READHEX("aaaaffff"); + uint32_t hgt = 123000; + uint8_t dup = 15; + uint8_t txi = 7; + uint8_t txo = 1; + BinaryData hgtx = READHEX("01e0780f"); + BinaryData txidx = WRITE_UINT16_BE(txi); + BinaryData txoidx = WRITE_UINT16_BE(txo); + + sbh.blockHeight_ = hgt; + sbh.duplicateID_ = dup; + + stx.blockHeight_ = hgt; + stx.duplicateID_ = dup; + stx.txIndex_ = txi; + + stxo.blockHeight_ = hgt; + stxo.duplicateID_ = dup; + stxo.txIndex_ = txi; + stxo.txOutIndex_ = txo; + + ssh1.uniqueKey_ = key; + ssh2.uniqueKey_ = key; + sud.blockHeight_ = hgt; + sud.duplicateID_ = dup; + hhl.height_ = hgt; + sths.txHashPrefix_= key; + + BinaryData TXB = PREFBYTE(DB_PREFIX_TXDATA); + BinaryData SSB = PREFBYTE(DB_PREFIX_SCRIPT); + BinaryData UDB = PREFBYTE(DB_PREFIX_UNDODATA); + BinaryData HHB = PREFBYTE(DB_PREFIX_HEADHGT); + BinaryData THB = PREFBYTE(DB_PREFIX_TXHINTS); + EXPECT_EQ(sbh.getDBKey( true ), TXB + hgtx); + EXPECT_EQ(stx.getDBKey( true ), TXB + hgtx + txidx); + EXPECT_EQ(stxo.getDBKey( true ), TXB + hgtx + txidx + txoidx); + EXPECT_EQ(ssh1.getDBKey( true ), SSB + key); + EXPECT_EQ(ssh2.getDBKey( true ), SSB + key); + EXPECT_EQ(sud.getDBKey( true ), UDB + hgtx); + EXPECT_EQ(hhl.getDBKey( true ), HHB + WRITE_UINT32_BE(hgt)); + EXPECT_EQ(sths.getDBKey( true ), THB + key); + + EXPECT_EQ(sbh.getDBKey( false ), hgtx); + EXPECT_EQ(stx.getDBKey( false ), hgtx + txidx); + EXPECT_EQ(stxo.getDBKey( false ), hgtx + txidx + txoidx); + EXPECT_EQ(ssh1.getDBKey( false ), key); + EXPECT_EQ(ssh2.getDBKey( false ), key); + EXPECT_EQ(sud.getDBKey( false ), hgtx); + EXPECT_EQ(hhl.getDBKey( false ), WRITE_UINT32_BE(hgt)); + EXPECT_EQ(sths.getDBKey( false ), key); +} + + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, LengthUnfrag) +{ + StoredTx tx; + vector offin, offout; + + uint32_t lenUnfrag = BtcUtils::StoredTxCalcLength( rawTxUnfrag_.getPtr(), + false, + &offin, + &offout); + ASSERT_EQ(lenUnfrag, 438); + + ASSERT_EQ(offin.size(), 3); + EXPECT_EQ(offin[0], 5); + EXPECT_EQ(offin[1], 184); + EXPECT_EQ(offin[2], 365); + + ASSERT_EQ(offout.size(), 3); + EXPECT_EQ(offout[0], 366); + EXPECT_EQ(offout[1], 400); + EXPECT_EQ(offout[2], 434); +} + + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, LengthFragged) +{ + vector offin, offout; + + uint32_t lenFragged = BtcUtils::StoredTxCalcLength( rawTxFragged_.getPtr(), + true, + &offin, + &offout); + ASSERT_EQ(lenFragged, 370); + + ASSERT_EQ(offin.size(), 3); + EXPECT_EQ(offin[0], 5); + EXPECT_EQ(offin[1], 184); + EXPECT_EQ(offin[2], 365); + + ASSERT_EQ(offout.size(), 3); + EXPECT_EQ(offout[0], 366); + EXPECT_EQ(offout[1], 366); + EXPECT_EQ(offout[2], 366); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, BlkDataKeys) +{ + uint32_t hgt = 0x001a332b; + uint8_t dup = 0x01; + uint16_t tix = 0x0102; + uint16_t tox = 0x0021; + + EXPECT_EQ(DBUtils.getBlkDataKey(hgt, dup), + READHEX("031a332b01")); + EXPECT_EQ(DBUtils.getBlkDataKey(hgt, dup, tix), + READHEX("031a332b010102")); + EXPECT_EQ(DBUtils.getBlkDataKey(hgt, dup, tix, tox), + READHEX("031a332b0101020021")); + + EXPECT_EQ(DBUtils.getBlkDataKeyNoPrefix(hgt, dup), + READHEX("1a332b01")); + EXPECT_EQ(DBUtils.getBlkDataKeyNoPrefix(hgt, dup, tix), + READHEX("1a332b010102")); + EXPECT_EQ(DBUtils.getBlkDataKeyNoPrefix(hgt, dup, tix, tox), + READHEX("1a332b0101020021")); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, ReadBlkKeyData) +{ + BinaryData TXP = WRITE_UINT8_BE((uint8_t)DB_PREFIX_TXDATA); + BinaryData key5p = TXP + READHEX("01e078""0f"); + BinaryData key7p = TXP + READHEX("01e078""0f""0007"); + BinaryData key9p = TXP + READHEX("01e078""0f""0007""0001"); + BinaryData key5 = READHEX("01e078""0f"); + BinaryData key7 = READHEX("01e078""0f""0007"); + BinaryData key9 = READHEX("01e078""0f""0007""0001"); + BinaryRefReader brr; + + uint32_t hgt; + uint8_t dup; + uint16_t txi; + uint16_t txo; + + BLKDATA_TYPE bdtype; + + ///////////////////////////////////////////////////////////////////////////// + // 5 bytes, with prefix + brr.setNewData(key5p); + bdtype = DBUtils.readBlkDataKey(brr, hgt, dup); + EXPECT_EQ( hgt, 123000); + EXPECT_EQ( dup, 15); + EXPECT_EQ( brr.getSizeRemaining(), 0); + EXPECT_EQ( bdtype, BLKDATA_HEADER); + + brr.setNewData(key5p); + bdtype = DBUtils.readBlkDataKey(brr, hgt, dup, txi); + EXPECT_EQ( hgt, 123000); + EXPECT_EQ( dup, 15); + EXPECT_EQ( txi, UINT16_MAX); + EXPECT_EQ( brr.getSizeRemaining(), 0); + EXPECT_EQ( bdtype, BLKDATA_HEADER); + + brr.setNewData(key5p); + bdtype = DBUtils.readBlkDataKey(brr, hgt, dup, txi, txo); + EXPECT_EQ( hgt, 123000); + EXPECT_EQ( dup, 15); + EXPECT_EQ( txi, UINT16_MAX); + EXPECT_EQ( txo, UINT16_MAX); + EXPECT_EQ( brr.getSizeRemaining(), 0); + EXPECT_EQ( bdtype, BLKDATA_HEADER); + + + ///////////////////////////////////////////////////////////////////////////// + // 7 bytes, with prefix + brr.setNewData(key7p); + bdtype = DBUtils.readBlkDataKey(brr, hgt, dup); + EXPECT_EQ( hgt, 123000); + EXPECT_EQ( dup, 15); + EXPECT_EQ( brr.getSizeRemaining(), 0); + EXPECT_EQ( bdtype, BLKDATA_TX); + + brr.setNewData(key7p); + bdtype = DBUtils.readBlkDataKey(brr, hgt, dup, txi); + EXPECT_EQ( hgt, 123000); + EXPECT_EQ( dup, 15); + EXPECT_EQ( txi, 7); + EXPECT_EQ( brr.getSizeRemaining(), 0); + EXPECT_EQ( bdtype, BLKDATA_TX); + + brr.setNewData(key7p); + bdtype = DBUtils.readBlkDataKey(brr, hgt, dup, txi, txo); + EXPECT_EQ( hgt, 123000); + EXPECT_EQ( dup, 15); + EXPECT_EQ( txi, 7); + EXPECT_EQ( txo, UINT16_MAX); + EXPECT_EQ( brr.getSizeRemaining(), 0); + EXPECT_EQ( bdtype, BLKDATA_TX); + + + ///////////////////////////////////////////////////////////////////////////// + // 9 bytes, with prefix + brr.setNewData(key9p); + bdtype = DBUtils.readBlkDataKey(brr, hgt, dup); + EXPECT_EQ( hgt, 123000); + EXPECT_EQ( dup, 15); + EXPECT_EQ( brr.getSizeRemaining(), 0); + EXPECT_EQ( bdtype, BLKDATA_TXOUT); + + brr.setNewData(key9p); + bdtype = DBUtils.readBlkDataKey(brr, hgt, dup, txi); + EXPECT_EQ( hgt, 123000); + EXPECT_EQ( dup, 15); + EXPECT_EQ( txi, 7); + EXPECT_EQ( brr.getSizeRemaining(), 0); + EXPECT_EQ( bdtype, BLKDATA_TXOUT); + + brr.setNewData(key9p); + bdtype = DBUtils.readBlkDataKey(brr, hgt, dup, txi, txo); + EXPECT_EQ( hgt, 123000); + EXPECT_EQ( dup, 15); + EXPECT_EQ( txi, 7); + EXPECT_EQ( txo, 1); + EXPECT_EQ( brr.getSizeRemaining(), 0); + EXPECT_EQ( bdtype, BLKDATA_TXOUT); + + + ///////////////////////////////////////////////////////////////////////////// + // 5 bytes, no prefix + brr.setNewData(key5); + bdtype = DBUtils.readBlkDataKeyNoPrefix(brr, hgt, dup); + EXPECT_EQ( hgt, 123000); + EXPECT_EQ( dup, 15); + EXPECT_EQ( brr.getSizeRemaining(), 0); + EXPECT_EQ( bdtype, BLKDATA_HEADER); + + brr.setNewData(key5); + bdtype = DBUtils.readBlkDataKeyNoPrefix(brr, hgt, dup, txi); + EXPECT_EQ( hgt, 123000); + EXPECT_EQ( dup, 15); + EXPECT_EQ( txi, UINT16_MAX); + EXPECT_EQ( brr.getSizeRemaining(), 0); + EXPECT_EQ( bdtype, BLKDATA_HEADER); + + brr.setNewData(key5); + bdtype = DBUtils.readBlkDataKeyNoPrefix(brr, hgt, dup, txi, txo); + EXPECT_EQ( hgt, 123000); + EXPECT_EQ( dup, 15); + EXPECT_EQ( txi, UINT16_MAX); + EXPECT_EQ( txo, UINT16_MAX); + EXPECT_EQ( brr.getSizeRemaining(), 0); + EXPECT_EQ( bdtype, BLKDATA_HEADER); + + + ///////////////////////////////////////////////////////////////////////////// + // 7 bytes, no prefix + brr.setNewData(key7); + bdtype = DBUtils.readBlkDataKeyNoPrefix(brr, hgt, dup); + EXPECT_EQ( hgt, 123000); + EXPECT_EQ( dup, 15); + EXPECT_EQ( brr.getSizeRemaining(), 0); + EXPECT_EQ( bdtype, BLKDATA_TX); + + brr.setNewData(key7); + bdtype = DBUtils.readBlkDataKeyNoPrefix(brr, hgt, dup, txi); + EXPECT_EQ( hgt, 123000); + EXPECT_EQ( dup, 15); + EXPECT_EQ( txi, 7); + EXPECT_EQ( brr.getSizeRemaining(), 0); + EXPECT_EQ( bdtype, BLKDATA_TX); + + brr.setNewData(key7); + bdtype = DBUtils.readBlkDataKeyNoPrefix(brr, hgt, dup, txi, txo); + EXPECT_EQ( hgt, 123000); + EXPECT_EQ( dup, 15); + EXPECT_EQ( txi, 7); + EXPECT_EQ( txo, UINT16_MAX); + EXPECT_EQ( brr.getSizeRemaining(), 0); + EXPECT_EQ( bdtype, BLKDATA_TX); + + + ///////////////////////////////////////////////////////////////////////////// + // 9 bytes, no prefix + brr.setNewData(key9); + bdtype = DBUtils.readBlkDataKeyNoPrefix(brr, hgt, dup); + EXPECT_EQ( hgt, 123000); + EXPECT_EQ( dup, 15); + EXPECT_EQ( brr.getSizeRemaining(), 0); + EXPECT_EQ( bdtype, BLKDATA_TXOUT); + + brr.setNewData(key9); + bdtype = DBUtils.readBlkDataKeyNoPrefix(brr, hgt, dup, txi); + EXPECT_EQ( hgt, 123000); + EXPECT_EQ( dup, 15); + EXPECT_EQ( txi, 7); + EXPECT_EQ( brr.getSizeRemaining(), 0); + EXPECT_EQ( bdtype, BLKDATA_TXOUT); + + brr.setNewData(key9); + bdtype = DBUtils.readBlkDataKeyNoPrefix(brr, hgt, dup, txi, txo); + EXPECT_EQ( hgt, 123000); + EXPECT_EQ( dup, 15); + EXPECT_EQ( txi, 7); + EXPECT_EQ( txo, 1); + EXPECT_EQ( brr.getSizeRemaining(), 0); + EXPECT_EQ( bdtype, BLKDATA_TXOUT); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, SHeaderUnserialize) +{ + // SetUp already contains sbh_.unserialize(rawHead_); + EXPECT_TRUE( sbh_.isInitialized()); + EXPECT_FALSE(sbh_.isMainBranch_); + EXPECT_FALSE(sbh_.haveFullBlock()); + EXPECT_FALSE(sbh_.isMerkleCreated()); + EXPECT_EQ( sbh_.numTx_, UINT32_MAX); + EXPECT_EQ( sbh_.numBytes_, UINT32_MAX); + EXPECT_EQ( sbh_.blockHeight_, UINT32_MAX); + EXPECT_EQ( sbh_.duplicateID_, UINT8_MAX); + EXPECT_EQ( sbh_.merkle_.getSize(), 0); + EXPECT_EQ( sbh_.stxMap_.size(), 0); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, SHeaderDBSerFull_H) +{ + DBUtils.setArmoryDbType(ARMORY_DB_FULL); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + + sbh_.blockHeight_ = 65535; + sbh_.duplicateID_ = 1; + sbh_.merkle_ = READHEX("deadbeef"); + sbh_.merkleIsPartial_ = false; + sbh_.isMainBranch_ = true; + sbh_.numTx_ = 15; + sbh_.numBytes_ = 65535; + + // SetUp already contains sbh_.unserialize(rawHead_); + BinaryData last4 = READHEX("00ffff01"); + EXPECT_EQ(sbh_.serializeDBValue(HEADERS), rawHead_ + last4); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, SHeaderDBSerFull_B1) +{ + // ARMORY_DB_FULL means no merkle string (cause all Tx are in the DB + // so the merkle tree would be redundant. + DBUtils.setArmoryDbType(ARMORY_DB_FULL); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + + sbh_.blockHeight_ = 65535; + sbh_.duplicateID_ = 1; + sbh_.merkle_ = READHEX("deadbeef"); + sbh_.merkleIsPartial_ = false; + sbh_.isMainBranch_ = true; + sbh_.numTx_ = 15; + sbh_.numBytes_ = 65535; + + // SetUp already contains sbh_.unserialize(rawHead_); + BinaryData flags = READHEX("01340000"); + BinaryData ntx = READHEX("0f000000"); + BinaryData nbyte = READHEX("ffff0000"); + + BinaryData headBlkData = flags + rawHead_ + ntx + nbyte; + EXPECT_EQ(sbh_.serializeDBValue(BLKDATA), headBlkData); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, SHeaderDBSerFull_B2) +{ + // With merkle string + DBUtils.setArmoryDbType(ARMORY_DB_PARTIAL); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + + BinaryWriter bw; + + sbh_.blockHeight_ = 65535; + sbh_.duplicateID_ = 1; + sbh_.merkle_ = READHEX("deadbeef"); + sbh_.merkleIsPartial_ = false; + sbh_.isMainBranch_ = true; + sbh_.numTx_ = 15; + sbh_.numBytes_ = 65535; + + // SetUp already contains sbh_.unserialize(rawHead_); + BinaryData flags = READHEX("01260000"); + BinaryData ntx = READHEX("0f000000"); + BinaryData nbyte = READHEX("ffff0000"); + + BinaryData headBlkData = flags + rawHead_ + ntx + nbyte + sbh_.merkle_; + EXPECT_EQ(sbh_.serializeDBValue(BLKDATA), headBlkData); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, SHeaderDBSerFull_B3) +{ + DBUtils.setArmoryDbType(ARMORY_DB_LITE); + DBUtils.setDbPruneType(DB_PRUNE_ALL); + + BinaryWriter bw; + + sbh_.blockHeight_ = 65535; + sbh_.duplicateID_ = 1; + sbh_.merkle_ = BinaryData(0); + sbh_.merkleIsPartial_ = false; + sbh_.isMainBranch_ = true; + sbh_.numTx_ = 15; + sbh_.numBytes_ = 65535; + + // SetUp already contains sbh_.unserialize(rawHead_); + BinaryData flags = READHEX("01100000"); + BinaryData ntx = READHEX("0f000000"); + BinaryData nbyte = READHEX("ffff0000"); + + BinaryData headBlkData = flags + rawHead_ + ntx + nbyte; + EXPECT_EQ(sbh_.serializeDBValue(BLKDATA), headBlkData); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, SHeaderDBUnserFull_H) +{ + BinaryData dbval = READHEX( + "010000001d8f4ec0443e1f19f305e488c1085c95de7cc3fd25e0d2c5bb5d0000" + "000000009762547903d36881a86751f3f5049e23050113f779735ef82734ebf0" + "b4450081d8c8c84db3936a1a334b035b00ffff01"); + + BinaryRefReader brr(dbval); + sbh_.unserializeDBValue(HEADERS, brr); + + EXPECT_EQ(sbh_.blockHeight_, 65535); + EXPECT_EQ(sbh_.duplicateID_, 1); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, SHeaderDBUnserFull_B1) +{ + BinaryData dbval = READHEX( + "01340000010000001d8f4ec0443e1f19f305e488c1085c95de7cc3fd25e0d2c5" + "bb5d0000000000009762547903d36881a86751f3f5049e23050113f779735ef8" + "2734ebf0b4450081d8c8c84db3936a1a334b035b0f000000ffff0000"); + + BinaryRefReader brr(dbval); + sbh_.unserializeDBValue(BLKDATA, brr); + sbh_.setHeightAndDup(65535, 1); + + EXPECT_EQ(sbh_.blockHeight_, 65535); + EXPECT_EQ(sbh_.duplicateID_, 1); + EXPECT_EQ(sbh_.merkle_ , READHEX("")); + EXPECT_EQ(sbh_.numTx_ , 15); + EXPECT_EQ(sbh_.numBytes_ , 65535); + EXPECT_EQ(sbh_.unserArmVer_, 0x00); + EXPECT_EQ(sbh_.unserBlkVer_, 1); + EXPECT_EQ(sbh_.unserDbType_, ARMORY_DB_FULL); + EXPECT_EQ(sbh_.unserPrType_, DB_PRUNE_NONE); + EXPECT_EQ(sbh_.unserMkType_, MERKLE_SER_NONE); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, SHeaderDBUnserFull_B2) +{ + BinaryData dbval = READHEX( + "01260000010000001d8f4ec0443e1f19f305e488c1085c95de7cc3fd25e0d2c5" + "bb5d0000000000009762547903d36881a86751f3f5049e23050113f779735ef8" + "2734ebf0b4450081d8c8c84db3936a1a334b035b0f000000ffff0000deadbeef"); + + BinaryRefReader brr(dbval); + sbh_.unserializeDBValue(BLKDATA, brr); + sbh_.setHeightAndDup(65535, 1); + + EXPECT_EQ(sbh_.blockHeight_ , 65535); + EXPECT_EQ(sbh_.duplicateID_ , 1); + EXPECT_EQ(sbh_.merkle_ , READHEX("deadbeef")); + EXPECT_EQ(sbh_.numTx_ , 15); + EXPECT_EQ(sbh_.numBytes_ , 65535); + EXPECT_EQ(sbh_.unserArmVer_, 0x00); + EXPECT_EQ(sbh_.unserBlkVer_, 1); + EXPECT_EQ(sbh_.unserDbType_, ARMORY_DB_PARTIAL); + EXPECT_EQ(sbh_.unserPrType_, DB_PRUNE_NONE); + EXPECT_EQ(sbh_.unserMkType_, MERKLE_SER_FULL); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, SHeaderDBUnserFull_B3) +{ + BinaryData dbval = READHEX( + "01100000010000001d8f4ec0443e1f19f305e488c1085c95de7cc3fd25e0d2c5" + "bb5d0000000000009762547903d36881a86751f3f5049e23050113f779735ef8" + "2734ebf0b4450081d8c8c84db3936a1a334b035b0f000000ffff0000"); + + BinaryRefReader brr(dbval); + sbh_.unserializeDBValue(BLKDATA, brr); + sbh_.setHeightAndDup(65535, 1); + + EXPECT_EQ(sbh_.blockHeight_, 65535); + EXPECT_EQ(sbh_.duplicateID_, 1); + EXPECT_EQ(sbh_.merkle_ , READHEX("")); + EXPECT_EQ(sbh_.numTx_ , 15); + EXPECT_EQ(sbh_.numBytes_ , 65535); + EXPECT_EQ(sbh_.unserArmVer_, 0x00); + EXPECT_EQ(sbh_.unserBlkVer_, 1); + EXPECT_EQ(sbh_.unserDbType_, ARMORY_DB_LITE); + EXPECT_EQ(sbh_.unserPrType_, DB_PRUNE_ALL); + EXPECT_EQ(sbh_.unserMkType_, MERKLE_SER_NONE); +} + + + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, STxUnserUnfrag) +{ + Tx regTx(rawTx0_); + + StoredTx stx; + stx.createFromTx(regTx, false); + + EXPECT_TRUE( stx.isInitialized()); + EXPECT_TRUE( stx.haveAllTxOut()); + EXPECT_FALSE(stx.isFragged_); + EXPECT_EQ( stx.version_, 1); + EXPECT_EQ( stx.blockHeight_, UINT32_MAX); + EXPECT_EQ( stx.duplicateID_, UINT8_MAX); + EXPECT_EQ( stx.txIndex_, UINT16_MAX); + EXPECT_EQ( stx.dataCopy_.getSize(), 258); + EXPECT_EQ( stx.numBytes_, 258); + EXPECT_EQ( stx.fragBytes_, 190); + + ASSERT_EQ( stx.stxoMap_.size(), 2); + EXPECT_TRUE( stx.stxoMap_[0].isInitialized()); + EXPECT_TRUE( stx.stxoMap_[1].isInitialized()); + EXPECT_EQ( stx.stxoMap_[0].txIndex_, UINT16_MAX); + EXPECT_EQ( stx.stxoMap_[1].txIndex_, UINT16_MAX); + EXPECT_EQ( stx.stxoMap_[0].txOutIndex_, 0); + EXPECT_EQ( stx.stxoMap_[1].txOutIndex_, 1); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, STxUnserFragged) +{ + Tx regTx(rawTx0_); + + StoredTx stx; + stx.createFromTx(regTx, true); + + EXPECT_TRUE( stx.isInitialized()); + EXPECT_TRUE( stx.haveAllTxOut()); + EXPECT_TRUE( stx.isFragged_); + EXPECT_EQ( stx.version_, 1); + EXPECT_EQ( stx.blockHeight_, UINT32_MAX); + EXPECT_EQ( stx.duplicateID_, UINT8_MAX); + EXPECT_EQ( stx.txIndex_, UINT16_MAX); + EXPECT_EQ( stx.dataCopy_.getSize(), 190); + + ASSERT_EQ( stx.stxoMap_.size(), 2); + EXPECT_TRUE( stx.stxoMap_[0].isInitialized()); + EXPECT_TRUE( stx.stxoMap_[1].isInitialized()); + EXPECT_EQ( stx.stxoMap_[0].txIndex_, UINT16_MAX); + EXPECT_EQ( stx.stxoMap_[1].txIndex_, UINT16_MAX); + EXPECT_EQ( stx.stxoMap_[0].txOutIndex_, 0); + EXPECT_EQ( stx.stxoMap_[1].txOutIndex_, 1); +} + + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, STxReconstruct) +{ + Tx regTx, reconTx; + StoredTx stx; + + // Reconstruct an unfragged tx + regTx.unserialize(rawTx0_); + stx.createFromTx(regTx, false); + + reconTx = stx.getTxCopy(); + EXPECT_EQ(reconTx.serialize(), rawTx0_); + EXPECT_EQ(stx.getSerializedTx(), rawTx0_); + + // Reconstruct an fragged tx + regTx.unserialize(rawTx0_); + stx.createFromTx(regTx, true); + + reconTx = stx.getTxCopy(); + EXPECT_EQ(reconTx.serialize(), rawTx0_); + EXPECT_EQ(stx.getSerializedTx(), rawTx0_); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, STxSerUnfragToFrag) +{ + StoredTx stx; + stx.unserialize(rawTxUnfrag_); + + EXPECT_EQ(stx.getSerializedTx(), rawTxUnfrag_); + EXPECT_EQ(stx.getSerializedTxFragged(), rawTxFragged_); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, STxSerDBValue_1) +{ + DBUtils.setArmoryDbType(ARMORY_DB_FULL); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + + Tx origTx(rawTxUnfrag_); + + StoredTx stx; + stx.unserialize(rawTxUnfrag_); + + // 0123 45 67 01 23 4567 + // |----| |--| |-- --| + // DBVer TxVer TxSer + // + // For this example: DBVer=0, TxVer=1, TxSer=FRAGGED[1] + // 0000 01 00 01 -- ---- + BinaryData first2 = READHEX("0440"); // little-endian, of course + BinaryData txHash = origTx.getThisHash(); + BinaryData fragged = stx.getSerializedTxFragged(); + BinaryData output = first2 + txHash + fragged; + EXPECT_EQ(stx.serializeDBValue(), output); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, DISABLED_STxSerDBValue_2) +{ + // I modified the ARMORY_DB_SUPER code to frag, as well. There's no + // mode that doesn't frag, now. + DBUtils.setArmoryDbType(ARMORY_DB_SUPER); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + + Tx origTx(rawTxUnfrag_); + + StoredTx stx; + stx.unserialize(rawTxUnfrag_); + + // 0123 45 67 01 23 4567 + // |----| |--| |-- --| + // DBVer TxVer TxSer + // + // For this example: DBVer=0, TxVer=1, TxSer=FRAGGED[1] + // 0000 01 00 00 -- ---- + BinaryData first2 = READHEX("0400"); // little-endian, of course + BinaryData txHash = origTx.getThisHash(); + BinaryData fragged = stx.getSerializedTx(); // Full Tx this time + BinaryData output = first2 + txHash + fragged; + EXPECT_EQ(stx.serializeDBValue(), output); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, STxUnserDBValue_1) +{ + Tx origTx(rawTxUnfrag_); + + BinaryData toUnser = READHEX( + "0440e471262336aa67391e57c8c6fe03bae29734079e06ff75c7fa4d0a873c83" + "f03c01000000020044fbc929d78e4203eed6f1d3d39c0157d8e5c100bbe08867" + "79c0ebf6a69324010000008a47304402206568144ed5e7064d6176c74738b04c" + "08ca19ca54ddeb480084b77f45eebfe57802207927d6975a5ac0e1bb36f5c053" + "56dcda1f521770511ee5e03239c8e1eecf3aed0141045d74feae58c4c36d7c35" + "beac05eddddc78b3ce4b02491a2eea72043978056a8bc439b99ddaad327207b0" + "9ef16a8910828e805b0cc8c11fba5caea2ee939346d7ffffffff45c866b219b1" + "76952508f8e5aea728f950186554fc4a5807e2186a8e1c4009e5000000008c49" + "3046022100bd5d41662f98cfddc46e86ea7e4a3bc8fe9f1dfc5c4836eaf7df58" + "2596cfe0e9022100fc459ae4f59b8279d679003b88935896acd10021b6e2e461" + "9377e336b5296c5e014104c00bab76a708ba7064b2315420a1c533ca9945eeff" + "9754cdc574224589e9113469b4e71752146a10028079e04948ecdf70609bf1b9" + "801f6b73ab75947ac339e5ffffffff0200000000"); + + BinaryRefReader brr(toUnser); + + StoredTx stx; + stx.unserializeDBValue(brr); + + EXPECT_TRUE( stx.isInitialized()); + EXPECT_EQ( stx.thisHash_, origTx.getThisHash()); + EXPECT_EQ( stx.lockTime_, origTx.getLockTime()); + EXPECT_EQ( stx.dataCopy_, rawTxFragged_); + EXPECT_TRUE( stx.isFragged_); + EXPECT_EQ( stx.version_, 1); + EXPECT_EQ( stx.blockHeight_, UINT32_MAX); + EXPECT_EQ( stx.duplicateID_, UINT8_MAX); + EXPECT_EQ( stx.txIndex_, UINT16_MAX); + EXPECT_EQ( stx.numTxOut_, origTx.getNumTxOut()); + EXPECT_EQ( stx.numBytes_, UINT32_MAX); + EXPECT_EQ( stx.fragBytes_, 370); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, STxUnserDBValue_2) +{ + Tx origTx(rawTxUnfrag_); + + BinaryData toUnser = READHEX( + "0004e471262336aa67391e57c8c6fe03bae29734079e06ff75c7fa4d0a873c83" + "f03c01000000020044fbc929d78e4203eed6f1d3d39c0157d8e5c100bbe08867" + "79c0ebf6a69324010000008a47304402206568144ed5e7064d6176c74738b04c" + "08ca19ca54ddeb480084b77f45eebfe57802207927d6975a5ac0e1bb36f5c053" + "56dcda1f521770511ee5e03239c8e1eecf3aed0141045d74feae58c4c36d7c35" + "beac05eddddc78b3ce4b02491a2eea72043978056a8bc439b99ddaad327207b0" + "9ef16a8910828e805b0cc8c11fba5caea2ee939346d7ffffffff45c866b219b1" + "76952508f8e5aea728f950186554fc4a5807e2186a8e1c4009e5000000008c49" + "3046022100bd5d41662f98cfddc46e86ea7e4a3bc8fe9f1dfc5c4836eaf7df58" + "2596cfe0e9022100fc459ae4f59b8279d679003b88935896acd10021b6e2e461" + "9377e336b5296c5e014104c00bab76a708ba7064b2315420a1c533ca9945eeff" + "9754cdc574224589e9113469b4e71752146a10028079e04948ecdf70609bf1b9" + "801f6b73ab75947ac339e5ffffffff02ac4c8bd5000000001976a9148dce8946" + "f1c7763bb60ea5cf16ef514cbed0633b88ac002f6859000000001976a9146a59" + "ac0e8f553f292dfe5e9f3aaa1da93499c15e88ac00000000"); + + BinaryRefReader brr(toUnser); + + StoredTx stx; + stx.unserializeDBValue(brr); + + EXPECT_TRUE( stx.isInitialized()); + EXPECT_EQ( stx.thisHash_, origTx.getThisHash()); + EXPECT_EQ( stx.lockTime_, origTx.getLockTime()); + EXPECT_EQ( stx.dataCopy_, rawTxUnfrag_); + EXPECT_FALSE(stx.isFragged_); + EXPECT_EQ( stx.version_, 1); + EXPECT_EQ( stx.blockHeight_, UINT32_MAX); + EXPECT_EQ( stx.duplicateID_, UINT8_MAX); + EXPECT_EQ( stx.txIndex_, UINT16_MAX); + EXPECT_EQ( stx.numTxOut_, origTx.getNumTxOut()); + EXPECT_EQ( stx.numBytes_, origTx.getSize()); + EXPECT_EQ( stx.fragBytes_, 370); +} + + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, STxOutUnserialize) +{ + TxOut txo0, txo1; + StoredTxOut stxo0, stxo1; + + stxo0.unserialize(rawTxOut0_); + stxo1.unserialize(rawTxOut1_); + txo0.unserialize(rawTxOut0_); + txo1.unserialize(rawTxOut1_); + + uint64_t val0 = READ_UINT64_HEX_LE("ac4c8bd500000000"); + uint64_t val1 = READ_UINT64_HEX_LE("002f685900000000"); + + EXPECT_EQ(stxo0.getSerializedTxOut(), rawTxOut0_); + EXPECT_EQ(stxo0.getSerializedTxOut(), txo0.serialize()); + EXPECT_EQ(stxo1.getSerializedTxOut(), rawTxOut1_); + EXPECT_EQ(stxo1.getSerializedTxOut(), txo1.serialize()); + + EXPECT_EQ(stxo0.getValue(), val0); + EXPECT_EQ(stxo1.getValue(), val1); + + TxOut txoRecon = stxo0.getTxOutCopy(); + EXPECT_EQ(txoRecon.serialize(), rawTxOut0_); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, STxOutSerDBValue_1) +{ + DBUtils.setArmoryDbType(ARMORY_DB_FULL); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + + StoredTxOut stxo0; + + stxo0.unserialize(rawTxOut0_); + + stxo0.txVersion_ = 1; + stxo0.spentness_ = TXOUT_UNSPENT; + + // 0123 45 67 0 123 4567 + // |----| |--| |--| |-| + // DBVer TxVer Spnt CB + // + // For this example: DBVer=0, TxVer=1, TxSer=FRAGGED[1] + // 0000 01 00 0 --- ---- + EXPECT_EQ(stxo0.serializeDBValue(), READHEX("0400") + rawTxOut0_); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, STxOutSerDBValue_2) +{ + DBUtils.setArmoryDbType(ARMORY_DB_FULL); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + + StoredTxOut stxo0; + stxo0.unserialize(rawTxOut0_); + stxo0.txVersion_ = 1; + stxo0.spentness_ = TXOUT_UNSPENT; + + // Test a spent TxOut + // 0000 01 01 0 --- ---- + BinaryData spentStr = DBUtils.getBlkDataKeyNoPrefix( 100000, 1, 127, 15); + stxo0.spentness_ = TXOUT_SPENT; + stxo0.spentByTxInKey_ = spentStr; + EXPECT_EQ(stxo0.serializeDBValue(), READHEX("0500")+rawTxOut0_+spentStr); +} + + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, STxOutSerDBValue_3) +{ + DBUtils.setArmoryDbType(ARMORY_DB_FULL); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + + StoredTxOut stxo0; + stxo0.unserialize(rawTxOut0_); + stxo0.txVersion_ = 1; + stxo0.isCoinbase_ = true; + + // Test a spent TxOut but in lite mode where we don't record spentness + // 0000 01 01 1 --- ---- + DBUtils.setArmoryDbType(ARMORY_DB_LITE); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + BinaryData spentStr = DBUtils.getBlkDataKeyNoPrefix( 100000, 1, 127, 15); + stxo0.spentness_ = TXOUT_SPENT; + stxo0.spentByTxInKey_ = spentStr; + EXPECT_EQ(stxo0.serializeDBValue(), READHEX("0680")+rawTxOut0_); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, STxOutUnserDBValue_1) +{ + BinaryData input = READHEX( "0400ac4c8bd5000000001976a9148dce8946f1c7763b" + "b60ea5cf16ef514cbed0633b88ac"); + StoredTxOut stxo; + stxo.unserializeDBValue(input); + + EXPECT_TRUE( stxo.isInitialized()); + EXPECT_EQ( stxo.txVersion_, 1); + EXPECT_EQ( stxo.dataCopy_, rawTxOut0_); + EXPECT_EQ( stxo.blockHeight_, UINT32_MAX); + EXPECT_EQ( stxo.duplicateID_, UINT8_MAX); + EXPECT_EQ( stxo.txIndex_, UINT16_MAX); + EXPECT_EQ( stxo.txOutIndex_, UINT16_MAX); + EXPECT_EQ( stxo.spentness_, TXOUT_UNSPENT); + EXPECT_EQ( stxo.spentByTxInKey_.getSize(), 0); + EXPECT_FALSE(stxo.isCoinbase_); + EXPECT_EQ( stxo.unserArmVer_, 0); +} +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, STxOutUnserDBValue_2) +{ + BinaryData input = READHEX( "0500ac4c8bd5000000001976a9148dce8946f1c7763b" + "b60ea5cf16ef514cbed0633b88ac01a086017f000f00"); + StoredTxOut stxo; + stxo.unserializeDBValue(input); + + EXPECT_TRUE( stxo.isInitialized()); + EXPECT_EQ( stxo.txVersion_, 1); + EXPECT_EQ( stxo.dataCopy_, rawTxOut0_); + EXPECT_EQ( stxo.blockHeight_, UINT32_MAX); + EXPECT_EQ( stxo.duplicateID_, UINT8_MAX); + EXPECT_EQ( stxo.txIndex_, UINT16_MAX); + EXPECT_EQ( stxo.txOutIndex_, UINT16_MAX); + EXPECT_EQ( stxo.spentness_, TXOUT_SPENT); + EXPECT_FALSE(stxo.isCoinbase_); + EXPECT_EQ( stxo.spentByTxInKey_, READHEX("01a086017f000f00")); + EXPECT_EQ( stxo.unserArmVer_, 0); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, STxOutUnserDBValue_3) +{ + BinaryData input = READHEX( "0680ac4c8bd5000000001976a9148dce8946f1c7763b" + "b60ea5cf16ef514cbed0633b88ac"); + StoredTxOut stxo; + stxo.unserializeDBValue(input); + + EXPECT_TRUE( stxo.isInitialized()); + EXPECT_EQ( stxo.txVersion_, 1); + EXPECT_EQ( stxo.dataCopy_, rawTxOut0_); + EXPECT_EQ( stxo.blockHeight_, UINT32_MAX); + EXPECT_EQ( stxo.duplicateID_, UINT8_MAX); + EXPECT_EQ( stxo.txIndex_, UINT16_MAX); + EXPECT_EQ( stxo.txOutIndex_, UINT16_MAX); + EXPECT_EQ( stxo.spentness_, TXOUT_SPENTUNK); + EXPECT_TRUE( stxo.isCoinbase_); + EXPECT_EQ( stxo.spentByTxInKey_.getSize(), 0); + EXPECT_EQ( stxo.unserArmVer_, 0); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, SHeaderFullBlock) +{ + // I'll make this more robust later... kind of tired of writing tests... + StoredHeader sbh; + sbh.unserializeFullBlock(rawBlock_.getRef()); + + BinaryWriter bw; + sbh.serializeFullBlock(bw); + + EXPECT_EQ(bw.getDataRef(), rawBlock_.getRef()); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, SUndoDataSer) +{ + DBUtils.setArmoryDbType(ARMORY_DB_FULL); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + + BinaryData arbHash = READHEX("11112221111222111122222211112222" + "11112221111222111122211112221111"); + BinaryData op0_str = READHEX("aaaabbbbaaaabbbbaaaabbbbaaaabbbb" + "aaaabbbbaaaabbbbaaaabbbbaaaabbbb"); + BinaryData op1_str = READHEX("ffffbbbbffffbbbbffffbbbbffffbbbb" + "ffffbbbbffffbbbbffffbbbbffffbbbb"); + + + StoredUndoData sud; + OutPoint op0(op0_str, 1); + OutPoint op1(op1_str, 2); + + StoredTxOut stxo0, stxo1; + stxo0.unserialize(rawTxOut0_); + stxo1.unserialize(rawTxOut1_); + + stxo0.txVersion_ = 1; + stxo1.txVersion_ = 1; + stxo0.blockHeight_ = 100000; + stxo1.blockHeight_ = 100000; + stxo0.duplicateID_ = 2; + stxo1.duplicateID_ = 2; + stxo0.txIndex_ = 17; + stxo1.txIndex_ = 17; + stxo0.parentHash_ = arbHash; + stxo1.parentHash_ = arbHash; + stxo0.txOutIndex_ = 5; + stxo1.txOutIndex_ = 5; + + sud.stxOutsRemovedByBlock_.clear(); + sud.stxOutsRemovedByBlock_.push_back(stxo0); + sud.stxOutsRemovedByBlock_.push_back(stxo1); + sud.outPointsAddedByBlock_.clear(); + sud.outPointsAddedByBlock_.push_back(op0); + sud.outPointsAddedByBlock_.push_back(op1); + + sud.blockHash_ = arbHash; + sud.blockHeight_ = 123000; // unused for this test + sud.duplicateID_ = 15; // unused for this test + + BinaryData flags = READHEX("34"); + BinaryData str2 = WRITE_UINT32_LE(2); + BinaryData str5 = WRITE_UINT32_LE(5); + BinaryData answer = + arbHash + + str2 + + flags + stxo0.getDBKey(false) + arbHash + str5 + rawTxOut0_ + + flags + stxo1.getDBKey(false) + arbHash + str5 + rawTxOut1_ + + str2 + + op0.serialize() + + op1.serialize(); + + EXPECT_EQ(sud.serializeDBValue(), answer); +} + + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, SUndoDataUnser) +{ + DBUtils.setArmoryDbType(ARMORY_DB_FULL); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + + BinaryData arbHash = READHEX("11112221111222111122222211112222" + "11112221111222111122211112221111"); + BinaryData op0_str = READHEX("aaaabbbbaaaabbbbaaaabbbbaaaabbbb" + "aaaabbbbaaaabbbbaaaabbbbaaaabbbb"); + BinaryData op1_str = READHEX("ffffbbbbffffbbbbffffbbbbffffbbbb" + "ffffbbbbffffbbbbffffbbbbffffbbbb"); + OutPoint op0(op0_str, 1); + OutPoint op1(op1_str, 2); + + //BinaryData sudToUnser = READHEX( + //"1111222111122211112222221111222211112221111222111122211112221111" + //"0200000024111122211112221111222222111122221111222111122211112221" + //"111222111105000000ac4c8bd5000000001976a9148dce8946f1c7763bb60ea5" + //"cf16ef514cbed0633b88ac241111222111122211112222221111222211112221" + //"11122211112221111222111105000000002f6859000000001976a9146a59ac0e" + //"8f553f292dfe5e9f3aaa1da93499c15e88ac02000000aaaabbbbaaaabbbbaaaa" + //"bbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbb01000000ffffbbbbffff" + //"bbbbffffbbbbffffbbbbffffbbbbffffbbbbffffbbbbffffbbbb02000000"); + + BinaryData sudToUnser = READHEX( + "1111222111122211112222221111222211112221111222111122211112221111" + "02000000240186a0020011000511112221111222111122222211112222111122" + "2111122211112221111222111105000000ac4c8bd5000000001976a9148dce89" + "46f1c7763bb60ea5cf16ef514cbed0633b88ac240186a0020011000511112221" + "1112221111222222111122221111222111122211112221111222111105000000" + "002f6859000000001976a9146a59ac0e8f553f292dfe5e9f3aaa1da93499c15e" + "88ac02000000aaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaa" + "bbbbaaaabbbb01000000ffffbbbbffffbbbbffffbbbbffffbbbbffffbbbbffff" + "bbbbffffbbbbffffbbbb02000000"); + + StoredUndoData sud; + sud.unserializeDBValue(sudToUnser); + + ASSERT_EQ(sud.outPointsAddedByBlock_.size(), 2); + ASSERT_EQ(sud.stxOutsRemovedByBlock_.size(), 2); + + EXPECT_EQ(sud.outPointsAddedByBlock_[0].serialize(), op0.serialize()); + EXPECT_EQ(sud.outPointsAddedByBlock_[1].serialize(), op1.serialize()); + EXPECT_EQ(sud.stxOutsRemovedByBlock_[0].getSerializedTxOut(), rawTxOut0_); + EXPECT_EQ(sud.stxOutsRemovedByBlock_[1].getSerializedTxOut(), rawTxOut1_); + + EXPECT_EQ(sud.stxOutsRemovedByBlock_[0].parentHash_, arbHash); + EXPECT_EQ(sud.stxOutsRemovedByBlock_[1].parentHash_, arbHash); + + EXPECT_EQ(sud.stxOutsRemovedByBlock_[0].blockHeight_, 100000); + EXPECT_EQ(sud.stxOutsRemovedByBlock_[1].blockHeight_, 100000); + EXPECT_EQ(sud.stxOutsRemovedByBlock_[0].duplicateID_, 2); + EXPECT_EQ(sud.stxOutsRemovedByBlock_[1].duplicateID_, 2); + EXPECT_EQ(sud.stxOutsRemovedByBlock_[0].txIndex_, 17); + EXPECT_EQ(sud.stxOutsRemovedByBlock_[1].txIndex_, 17); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, STxHintsSer) +{ + BinaryData hint0 = DBUtils.getBlkDataKeyNoPrefix(123000, 7, 255); + BinaryData hint1 = DBUtils.getBlkDataKeyNoPrefix(123000, 15, 127); + BinaryData hint2 = DBUtils.getBlkDataKeyNoPrefix(183922, 15, 3); + + StoredTxHints sths; + sths.txHashPrefix_ = READHEX("aaaaffff"); + sths.dbKeyList_.clear(); + + ///// + BinaryWriter ans0; + ans0.put_var_int(0); + EXPECT_EQ(sths.serializeDBValue(), ans0.getData()); + + ///// + sths.dbKeyList_.push_back(hint0); + sths.preferredDBKey_ = hint0; + BinaryWriter ans1; + ans1.put_var_int(1); + ans1.put_BinaryData(hint0); + EXPECT_EQ(sths.dbKeyList_.size(), 1); + EXPECT_EQ(sths.preferredDBKey_, hint0); + EXPECT_EQ(sths.serializeDBValue(), ans1.getData()); + + ///// + sths.dbKeyList_.push_back(hint1); + sths.dbKeyList_.push_back(hint2); + BinaryWriter ans3; + ans3.put_var_int(3); + ans3.put_BinaryData(hint0); + ans3.put_BinaryData(hint1); + ans3.put_BinaryData(hint2); + EXPECT_EQ(sths.dbKeyList_.size(), 3); + EXPECT_EQ(sths.preferredDBKey_, hint0); + EXPECT_EQ(sths.serializeDBValue(), ans3.getData()); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, STxHintsReorder) +{ + BinaryData hint0 = DBUtils.getBlkDataKeyNoPrefix(123000, 7, 255); + BinaryData hint1 = DBUtils.getBlkDataKeyNoPrefix(123000, 15, 127); + BinaryData hint2 = DBUtils.getBlkDataKeyNoPrefix(183922, 15, 3); + + StoredTxHints sths; + sths.txHashPrefix_ = READHEX("aaaaffff"); + sths.dbKeyList_.clear(); + sths.dbKeyList_.push_back(hint0); + sths.dbKeyList_.push_back(hint1); + sths.dbKeyList_.push_back(hint2); + sths.preferredDBKey_ = hint1; + + BinaryWriter expectedOut; + expectedOut.put_var_int(3); + expectedOut.put_BinaryData(hint1); + expectedOut.put_BinaryData(hint0); + expectedOut.put_BinaryData(hint2); + + EXPECT_EQ(sths.serializeDBValue(), expectedOut.getData()); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, STxHintsUnser) +{ + BinaryData hint0 = DBUtils.getBlkDataKeyNoPrefix(123000, 7, 255); + BinaryData hint1 = DBUtils.getBlkDataKeyNoPrefix(123000, 15, 127); + BinaryData hint2 = DBUtils.getBlkDataKeyNoPrefix(183922, 15, 3); + + BinaryData in0 = READHEX("00"); + BinaryData in1 = READHEX("01""01e0780700ff"); + BinaryData in3 = READHEX("03""01e0780700ff""01e0780f007f""02ce720f0003"); + + StoredTxHints sths0, sths1, sths3; + + sths0.unserializeDBValue(in0); + + EXPECT_EQ(sths0.dbKeyList_.size(), 0); + EXPECT_EQ(sths0.preferredDBKey_.getSize(), 0); + + sths1.unserializeDBValue(in1); + + EXPECT_EQ(sths1.dbKeyList_.size(), 1); + EXPECT_EQ(sths1.dbKeyList_[0], hint0); + EXPECT_EQ(sths1.preferredDBKey_, hint0); + + sths3.unserializeDBValue(in3); + EXPECT_EQ(sths3.dbKeyList_.size(), 3); + EXPECT_EQ(sths3.dbKeyList_[0], hint0); + EXPECT_EQ(sths3.dbKeyList_[1], hint1); + EXPECT_EQ(sths3.dbKeyList_[2], hint2); + EXPECT_EQ(sths3.preferredDBKey_, hint0); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, SHeadHgtListSer) +{ + StoredHeadHgtList baseHHL, testHHL; + baseHHL.height_ = 123000; + baseHHL.dupAndHashList_.resize(0); + BinaryData hash0 = READHEX("aaaabbbbaaaabbbbaaaabbbbaaaabbbb" + "aaaabbbbaaaabbbbaaaabbbbaaaabbbb"); + BinaryData hash1 = READHEX("2222bbbb2222bbbb2222bbbb2222bbbb" + "2222bbbb2222bbbb2222bbbb2222bbbb"); + BinaryData hash2 = READHEX("2222ffff2222ffff2222ffff2222ffff" + "2222ffff2222ffff2222ffff2222ffff"); + + uint8_t dup0 = 0; + uint8_t dup1 = 1; + uint8_t dup2 = 7; + + BinaryWriter expectOut; + + // Test writing empty list + expectOut.reset(); + expectOut.put_uint8_t(0); + EXPECT_EQ(testHHL.serializeDBValue(), expectOut.getData()); + + + // Test writing list with one entry but no preferred dupID + expectOut.reset(); + testHHL = baseHHL; + testHHL.dupAndHashList_.push_back(pair(dup0, hash0)); + expectOut.put_uint8_t(1); + expectOut.put_uint8_t(dup0); + expectOut.put_BinaryData(hash0); + EXPECT_EQ(testHHL.serializeDBValue(), expectOut.getData()); + + // Test writing list with one entry which is a preferred dupID + expectOut.reset(); + testHHL = baseHHL; + testHHL.preferredDup_ = 0; + testHHL.dupAndHashList_.push_back(pair(dup0, hash0)); + expectOut.put_uint8_t(1); + expectOut.put_uint8_t(dup0 | 0x80); + expectOut.put_BinaryData(hash0); + EXPECT_EQ(testHHL.serializeDBValue(), expectOut.getData()); + + // Test writing list with one entry preferred dupID but that dup isn't avail + expectOut.reset(); + testHHL = baseHHL; + testHHL.preferredDup_ = 1; + testHHL.dupAndHashList_.push_back(pair(dup0, hash0)); + expectOut.put_uint8_t(1); + expectOut.put_uint8_t(dup0); + expectOut.put_BinaryData(hash0); + EXPECT_EQ(testHHL.serializeDBValue(), expectOut.getData()); + + // Test writing with three entries, no preferred + expectOut.reset(); + testHHL = baseHHL; + testHHL.dupAndHashList_.push_back(pair(dup0, hash0)); + testHHL.dupAndHashList_.push_back(pair(dup1, hash1)); + testHHL.dupAndHashList_.push_back(pair(dup2, hash2)); + expectOut.put_uint8_t(3); + expectOut.put_uint8_t(dup0); expectOut.put_BinaryData(hash0); + expectOut.put_uint8_t(dup1); expectOut.put_BinaryData(hash1); + expectOut.put_uint8_t(dup2); expectOut.put_BinaryData(hash2); + EXPECT_EQ(testHHL.serializeDBValue(), expectOut.getData()); + + + // Test writing with three entries, with preferred + expectOut.reset(); + testHHL = baseHHL; + testHHL.dupAndHashList_.push_back(pair(dup0, hash0)); + testHHL.dupAndHashList_.push_back(pair(dup1, hash1)); + testHHL.dupAndHashList_.push_back(pair(dup2, hash2)); + testHHL.preferredDup_ = 1; + expectOut.put_uint8_t(3); + expectOut.put_uint8_t(dup1 | 0x80); expectOut.put_BinaryData(hash1); + expectOut.put_uint8_t(dup0); expectOut.put_BinaryData(hash0); + expectOut.put_uint8_t(dup2); expectOut.put_BinaryData(hash2); + EXPECT_EQ(testHHL.serializeDBValue(), expectOut.getData()); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(StoredBlockObjTest, SHeadHgtListUnser) +{ + BinaryData hash0 = READHEX("aaaabbbbaaaabbbbaaaabbbbaaaabbbb" + "aaaabbbbaaaabbbbaaaabbbbaaaabbbb"); + BinaryData hash1 = READHEX("2222bbbb2222bbbb2222bbbb2222bbbb" + "2222bbbb2222bbbb2222bbbb2222bbbb"); + BinaryData hash2 = READHEX("2222ffff2222ffff2222ffff2222ffff" + "2222ffff2222ffff2222ffff2222ffff"); + + vector tests; + tests.push_back( READHEX( + "0100aaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbb")); + tests.push_back( READHEX( + "0180aaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbb")); + tests.push_back( READHEX( + "0300aaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaa" + "bbbb012222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb22" + "22bbbb072222ffff2222ffff2222ffff2222ffff2222ffff2222ffff2222ffff" + "2222ffff")); + tests.push_back( READHEX( + "03812222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222" + "bbbb00aaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaaaabbbbaa" + "aabbbb072222ffff2222ffff2222ffff2222ffff2222ffff2222ffff2222ffff" + "2222ffff")); + + uint8_t dup0 = 0; + uint8_t dup1 = 1; + uint8_t dup2 = 7; + + for(uint32_t i=0; isetValidDupIDForHeight(hgt,dup); + + ssh.uniqueKey_ = HASH160PREFIX + a160; + ssh.version_ = 1; + ssh.alreadyScannedUpToBlk_ = UINT32_MAX; + + ssh.markTxOutUnspent(txio0); + + // Check the initial state matches expectations + EXPECT_EQ(ssh.serializeDBValue(), expectSSH_orig); + + ssh.insertTxio(txio1); + // Mark the first output spent (second one was already marked spent) + ssh.markTxOutSpent( dbKey0, dbKey5); + EXPECT_EQ(ssh.serializeDBValue(), expectSSH_bothspent); + + // Undo the last operation + ssh.markTxOutUnspent(dbKey0); + EXPECT_EQ(ssh.serializeDBValue(), expectSSH_orig); + + + ssh.markTxOutUnspent(dbKey1); + EXPECT_EQ(ssh.serializeDBValue(), expectSSH_bothunspent); + + ssh.markTxOutSpent( dbKey1, dbKey2); + EXPECT_EQ(ssh.serializeDBValue(), expectSSH_orig); + + + ssh.eraseTxio(dbKey1); + EXPECT_EQ(ssh.serializeDBValue(), expectSSH_afterrm); + +} +*/ + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +class LevelDBTest : public ::testing::Test +{ +protected: + virtual void SetUp(void) + { + iface_ = LevelDBWrapper::GetInterfacePtr(); + magic_ = READHEX(MAINNET_MAGIC_BYTES); + ghash_ = READHEX(MAINNET_GENESIS_HASH_HEX); + gentx_ = READHEX(MAINNET_GENESIS_TX_HASH_HEX); + zeros_ = READHEX("00000000"); + DBUtils.setArmoryDbType(ARMORY_DB_FULL); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + + rawHead_ = READHEX( + "01000000" + "1d8f4ec0443e1f19f305e488c1085c95de7cc3fd25e0d2c5bb5d000000000000" + "9762547903d36881a86751f3f5049e23050113f779735ef82734ebf0b4450081" + "d8c8c84d" + "b3936a1a" + "334b035b"); + headHashLE_ = READHEX( + "1195e67a7a6d0674bbd28ae096d602e1f038c8254b49dfe79d47000000000000"); + headHashBE_ = READHEX( + "000000000000479de7df494b25c838f0e102d696e08ad2bb74066d7a7ae69511"); + + rawTx0_ = READHEX( + "01000000016290dce984203b6a5032e543e9e272d8bce934c7de4d15fa0fe44d" + "d49ae4ece9010000008b48304502204f2fa458d439f957308bca264689aa175e" + "3b7c5f78a901cb450ebd20936b2c500221008ea3883a5b80128e55c9c6070aa6" + "264e1e0ce3d18b7cd7e85108ce3d18b7419a0141044202550a5a6d3bb81549c4" + "a7803b1ad59cdbba4770439a4923624a8acfc7d34900beb54a24188f7f0a4068" + "9d905d4847cc7d6c8d808a457d833c2d44ef83f76bffffffff0242582c0a0000" + "00001976a914c1b4695d53b6ee57a28647ce63e45665df6762c288ac80d1f008" + "000000001976a9140e0aec36fe2545fb31a41164fb6954adcd96b34288ac0000" + "0000"); + rawTx1_ = READHEX( + "0100000001f658dbc28e703d86ee17c9a2d3b167a8508b082fa0745f55be5144" + "a4369873aa010000008c49304602210041e1186ca9a41fdfe1569d5d807ca7ff" + "6c5ffd19d2ad1be42f7f2a20cdc8f1cc0221003366b5d64fe81e53910e156914" + "091d12646bc0d1d662b7a65ead3ebe4ab8f6c40141048d103d81ac9691cf13f3" + "fc94e44968ef67b27f58b27372c13108552d24a6ee04785838f34624b294afee" + "83749b64478bb8480c20b242c376e77eea2b3dc48b4bffffffff0200e1f50500" + "0000001976a9141b00a2f6899335366f04b277e19d777559c35bc888ac40aeeb" + "02000000001976a9140e0aec36fe2545fb31a41164fb6954adcd96b34288ac00" + "000000"); + + rawBlock_ = READHEX( + // Header + "01000000eb10c9a996a2340a4d74eaab41421ed8664aa49d18538bab59010000" + "000000005a2f06efa9f2bd804f17877537f2080030cadbfa1eb50e02338117cc" + "604d91b9b7541a4ecfbb0a1a64f1ade7" + // 3 transactions + "03" + ///// Tx0, version + "01000000" + "01" + // Tx0, Txin0 + "0000000000000000000000000000000000000000000000000000000000000000" + "ffffffff" + "08""04cfbb0a1a02360a""ffffffff" + // Tx0, 1 TxOut + "01" + // Tx0, TxOut0 + "00f2052a01000000" + "434104c2239c4eedb3beb26785753463be3ec62b82f6acd62efb65f452f8806f" + "2ede0b338e31d1f69b1ce449558d7061aa1648ddc2bf680834d3986624006a27" + "2dc21cac" + // Tx0, Locktime + "00000000" + ///// Tx1, Version + "01000000" + // Tx1, 3 txins + "03" + // Tx1, TxIn0 + "e8caa12bcb2e7e86499c9de49c45c5a1c6167ea4b894c8c83aebba1b6100f343" + "01000000" + "8c493046022100e2f5af5329d1244807f8347a2c8d9acc55a21a5db769e9274e" + "7e7ba0bb605b26022100c34ca3350df5089f3415d8af82364d7f567a6a297fcc" + "2c1d2034865633238b8c014104129e422ac490ddfcb7b1c405ab9fb42441246c" + "4bca578de4f27b230de08408c64cad03af71ee8a3140b40408a7058a1984a9f2" + "46492386113764c1ac132990d1""ffffffff" + // Tx1, TxIn1 + "5b55c18864e16c08ef9989d31c7a343e34c27c30cd7caa759651b0e08cae0106" + "00000000" + "8c4930460221009ec9aa3e0caf7caa321723dea561e232603e00686d4bfadf46" + "c5c7352b07eb00022100a4f18d937d1e2354b2e69e02b18d11620a6a9332d563" + "e9e2bbcb01cee559680a014104411b35dd963028300e36e82ee8cf1b0c8d5bf1" + "fc4273e970469f5cb931ee07759a2de5fef638961726d04bd5eb4e5072330b9b" + "371e479733c942964bb86e2b22""ffffffff" + // Tx1, TxIn2 + "3de0c1e913e6271769d8c0172cea2f00d6d3240afc3a20f9fa247ce58af30d2a" + "01000000" + "8c493046022100b610e169fd15ac9f60fe2b507529281cf2267673f4690ba428" + "cbb2ba3c3811fd022100ffbe9e3d71b21977a8e97fde4c3ba47b896d08bc09ec" + "b9d086bb59175b5b9f03014104ff07a1833fd8098b25f48c66dcf8fde34cbdbc" + "c0f5f21a8c2005b160406cbf34cc432842c6b37b2590d16b165b36a3efc9908d" + "65fb0e605314c9b278f40f3e1a""ffffffff" + // Tx1, 2 TxOuts + "02" + // Tx1, TxOut0 + "40420f0000000000""19""76a914adfa66f57ded1b655eb4ccd96ee07ca62bc1ddfd88ac" + // Tx1, TxOut1 + "007d6a7d04000000""19""76a914981a0c9ae61fa8f8c96ae6f8e383d6e07e77133e88ac" + // Tx1 Locktime + "00000000" + ///// Tx2 Version + "01000000" + // Tx2 1 TxIn + "01" + "38e7586e0784280df58bd3dc5e3d350c9036b1ec4107951378f45881799c92a4" + "00000000" + "8a47304402207c945ae0bbdaf9dadba07bdf23faa676485a53817af975ddf85a" + "104f764fb93b02201ac6af32ddf597e610b4002e41f2de46664587a379a01613" + "23a85389b4f82dda014104ec8883d3e4f7a39d75c9f5bb9fd581dc9fb1b7cdf7" + "d6b5a665e4db1fdb09281a74ab138a2dba25248b5be38bf80249601ae688c90c" + "6e0ac8811cdb740fcec31d""ffffffff" + // Tx2, 2 TxOuts + "02" + // Tx2, TxOut0 + "2f66ac6105000000""19""76a914964642290c194e3bfab661c1085e47d67786d2d388ac" + // Tx2, TxOut1 + "2f77e20000000000""19""76a9141486a7046affd935919a3cb4b50a8a0c233c286c88ac" + // Tx2 Locktime + "00000000"); + + rawTxUnfrag_ = READHEX( + // Version + "01000000" + // NumTxIn + "02" + // Start TxIn0 + "0044fbc929d78e4203eed6f1d3d39c0157d8e5c100bbe0886779c0" + "ebf6a69324010000008a47304402206568144ed5e7064d6176c74738b04c08ca" + "19ca54ddeb480084b77f45eebfe57802207927d6975a5ac0e1bb36f5c05356dc" + "da1f521770511ee5e03239c8e1eecf3aed0141045d74feae58c4c36d7c35beac" + "05eddddc78b3ce4b02491a2eea72043978056a8bc439b99ddaad327207b09ef1" + "6a8910828e805b0cc8c11fba5caea2ee939346d7ffffffff" + // Start TxIn1 + "45c866b219b17695" + "2508f8e5aea728f950186554fc4a5807e2186a8e1c4009e5000000008c493046" + "022100bd5d41662f98cfddc46e86ea7e4a3bc8fe9f1dfc5c4836eaf7df582596" + "cfe0e9022100fc459ae4f59b8279d679003b88935896acd10021b6e2e4619377" + "e336b5296c5e014104c00bab76a708ba7064b2315420a1c533ca9945eeff9754" + "cdc574224589e9113469b4e71752146a10028079e04948ecdf70609bf1b9801f" + "6b73ab75947ac339e5ffffffff" + // NumTxOut + "02" + // Start TxOut0 + "ac4c8bd5000000001976a9148dce8946f1c7763bb60ea5cf16ef514cbed0633b88ac" + // Start TxOut1 + "002f6859000000001976a9146a59ac0e8f553f292dfe5e9f3aaa1da93499c15e88ac" + // Locktime + "00000000"); + + rawTxFragged_ = READHEX( + // Version + "01000000" + // NumTxIn + "02" + // Start TxIn0 + "0044fbc929d78e4203eed6f1d3d39c0157d8e5c100bbe0886779c0" + "ebf6a69324010000008a47304402206568144ed5e7064d6176c74738b04c08ca" + "19ca54ddeb480084b77f45eebfe57802207927d6975a5ac0e1bb36f5c05356dc" + "da1f521770511ee5e03239c8e1eecf3aed0141045d74feae58c4c36d7c35beac" + "05eddddc78b3ce4b02491a2eea72043978056a8bc439b99ddaad327207b09ef1" + "6a8910828e805b0cc8c11fba5caea2ee939346d7ffffffff" + // Start TxIn1 + "45c866b219b17695" + "2508f8e5aea728f950186554fc4a5807e2186a8e1c4009e5000000008c493046" + "022100bd5d41662f98cfddc46e86ea7e4a3bc8fe9f1dfc5c4836eaf7df582596" + "cfe0e9022100fc459ae4f59b8279d679003b88935896acd10021b6e2e4619377" + "e336b5296c5e014104c00bab76a708ba7064b2315420a1c533ca9945eeff9754" + "cdc574224589e9113469b4e71752146a10028079e04948ecdf70609bf1b9801f" + "6b73ab75947ac339e5ffffffff" + // NumTxOut + "02" + // ... TxOuts fragged out + // Locktime + "00000000"); + + rawTxOut0_ = READHEX( + // Value + "ac4c8bd500000000" + // Script size (var_int) + "19" + // Script + "76""a9""14""8dce8946f1c7763bb60ea5cf16ef514cbed0633b""88""ac"); + rawTxOut1_ = READHEX( + // Value + "002f685900000000" + // Script size (var_int) + "19" + // Script + "76""a9""14""6a59ac0e8f553f292dfe5e9f3aaa1da93499c15e""88""ac"); + + bh_.unserialize(rawHead_); + tx1_.unserialize(rawTx0_); + tx2_.unserialize(rawTx1_); + sbh_.unserialize(rawHead_); + + // Make sure the global DB type and prune type are reset for each test + DBUtils.setArmoryDbType(ARMORY_DB_FULL); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + + LOGDISABLESTDOUT(); + } + + ///// + virtual void TearDown(void) + { + // This seem to be the best way to remove a dir tree in C++ (in Linux) + iface_->closeDatabases(); + iface_ = NULL; + + #ifdef _MSC_VER + rmdir("./ldbtestdir/level*"); + #else + system("rm -rf ./ldbtestdir/level*"); + #endif + } + + ///// + void addOutPairH(BinaryData key, BinaryData val) + { + expectOutH_.push_back( pair(key,val)); + } + + ///// + void addOutPairB(BinaryData key, BinaryData val) + { + expectOutB_.push_back( pair(key,val)); + } + + ///// + void replaceTopOutPairB(BinaryData key, BinaryData val) + { + uint32_t last = expectOutB_.size() -1; + expectOutB_[last] = pair(key,val); + } + + ///// + void printOutPairs(void) + { + cout << "Num Houts: " << expectOutH_.size() << endl; + for(uint32_t i=0; igetAllDatabaseEntries(HEADERS); + + if(fromDB.size() < endplus1H || expectOutH_.size() < endplus1H) + { + LOGERR << "Headers DB not the correct size"; + LOGERR << "DB size: " << (int)fromDB.size(); + LOGERR << "Expected: " << (int)expectOutH_.size(); + return false; + } + + for(uint32_t i=startH; igetAllDatabaseEntries(BLKDATA); + if(fromDB.size() < endplus1B || expectOutB_.size() < endplus1B) + { + LOGERR << "BLKDATA DB not the correct size"; + LOGERR << "DB size: " << (int)fromDB.size(); + LOGERR << "Expected: " << (int)expectOutB_.size(); + return false; + } + + for(uint32_t i=startB; iopenDatabases( string("ldbtestdir"), + ghash_, gentx_, magic_, + ARMORY_DB_FULL, DB_PRUNE_NONE); + + BinaryData DBINFO = StoredDBInfo().getDBKey(); + BinaryData flags = READHEX("03100000"); + BinaryData val0 = magic_+flags+zeros_+zeros_+ghash_; + addOutPairH(DBINFO, val0); + addOutPairB(DBINFO, val0); + + return iface_->databasesAreOpen(); + } + + + InterfaceToLDB* iface_; + vector > expectOutH_; + vector > expectOutB_; + + BinaryData magic_; + BinaryData ghash_; + BinaryData gentx_; + BinaryData zeros_; + + BinaryData rawHead_; + BinaryData headHashLE_; + BinaryData headHashBE_; + BinaryData rawBlock_; + BinaryData rawTx0_; + BinaryData rawTx1_; + BlockHeader bh_; + Tx tx1_; + Tx tx2_; + StoredHeader sbh_; + BinaryData rawTxUnfrag_; + BinaryData rawTxFragged_; + BinaryData rawTxOut0_; + BinaryData rawTxOut1_; + +}; + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(LevelDBTest, OpenClose) +{ + iface_->openDatabases( string("ldbtestdir"), + ghash_, + gentx_, + magic_, + ARMORY_DB_FULL, + DB_PRUNE_NONE); + + ASSERT_TRUE(iface_->databasesAreOpen()); + + EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 0); + EXPECT_EQ(iface_->getTopBlockHash(HEADERS), READHEX(MAINNET_GENESIS_HASH_HEX)); + + KVLIST HList = iface_->getAllDatabaseEntries(HEADERS); + KVLIST BList = iface_->getAllDatabaseEntries(BLKDATA); + + // 0123 4567 0123 4567 + // 0000 0010 0001 ---- ---- ---- ---- ---- + BinaryData flags = READHEX("03100000"); + + for(uint32_t i=0; icloseDatabases(); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(LevelDBTest, OpenCloseOpenNominal) +{ + // 0123 4567 0123 4567 + // 0000 0010 0001 ---- ---- ---- ---- ---- + BinaryData flags = READHEX("03100000"); + + iface_->openDatabases( string("ldbtestdir"), + ghash_, + gentx_, + magic_, + ARMORY_DB_FULL, + DB_PRUNE_NONE); + + + iface_->closeDatabases(); + + iface_->openDatabases( string("ldbtestdir"), + ghash_, + gentx_, + magic_, + ARMORY_DB_FULL, + DB_PRUNE_NONE); + ASSERT_TRUE(iface_->databasesAreOpen()); + + KVLIST HList = iface_->getAllDatabaseEntries(HEADERS); + KVLIST BList = iface_->getAllDatabaseEntries(BLKDATA); + + for(uint32_t i=0; icloseDatabases(); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(LevelDBTest, DISABLED_OpenCloseOpenMismatch) +{ + LOGERR << "Expecting four error messages here: this is normal"; + iface_->openDatabases( string("ldbtestdir"), + ghash_, + gentx_, + magic_, + ARMORY_DB_FULL, + DB_PRUNE_NONE); + EXPECT_TRUE(iface_->databasesAreOpen()); + iface_->closeDatabases(); + + iface_->openDatabases( string("ldbtestdir"), + ghash_.getSliceCopy(0, 31) + READHEX("00"), + gentx_, + magic_, + ARMORY_DB_FULL, + DB_PRUNE_NONE); + EXPECT_TRUE(iface_->databasesAreOpen()); + iface_->closeDatabases(); + + iface_->openDatabases( string("ldbtestdir"), + ghash_, + gentx_, + magic_.getSliceCopy(0,3) + READHEX("00"), + ARMORY_DB_FULL, + DB_PRUNE_NONE); + EXPECT_FALSE(iface_->databasesAreOpen()); + + iface_->openDatabases( string("ldbtestdir"), + ghash_, + gentx_, + magic_, + ARMORY_DB_SUPER, + DB_PRUNE_WHATEVER); + EXPECT_FALSE(iface_->databasesAreOpen()); + + iface_->openDatabases( string("ldbtestdir"), + ghash_, + gentx_, + magic_); + ASSERT_TRUE( iface_->databasesAreOpen()); + + EXPECT_EQ( DBUtils.getArmoryDbType(), ARMORY_DB_FULL); + EXPECT_EQ( DBUtils.getDbPruneType(), DB_PRUNE_NONE); + + KVLIST HList = iface_->getAllDatabaseEntries(HEADERS); + KVLIST BList = iface_->getAllDatabaseEntries(BLKDATA); + + EXPECT_EQ(HList.begin()->first, READHEX("00")); + EXPECT_EQ(BList.begin()->first, READHEX("00")); + + iface_->closeDatabases(); + + +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(LevelDBTest, PutGetDelete) +{ + BinaryData flags = READHEX("03100000"); + + iface_->openDatabases( string("ldbtestdir"), + ghash_, + gentx_, + magic_, + ARMORY_DB_FULL, + DB_PRUNE_NONE); + ASSERT_TRUE(iface_->databasesAreOpen()); + + DB_PREFIX TXDATA = DB_PREFIX_TXDATA; + BinaryData DBINFO = StoredDBInfo().getDBKey(); + BinaryData PREFIX = WRITE_UINT8_BE((uint8_t)TXDATA); + BinaryData val0 = magic_+flags+zeros_+zeros_+ghash_; + BinaryData commonValue = READHEX("abcd1234"); + BinaryData keyAB = READHEX("0000"); + BinaryData nothing = BinaryData(0); + + addOutPairH(DBINFO, val0); + + addOutPairB(DBINFO, val0); + addOutPairB( keyAB, commonValue); + addOutPairB(PREFIX + keyAB, commonValue); + + ASSERT_TRUE( compareKVListRange(0,1, 0,1)); + + iface_->putValue(BLKDATA, keyAB, commonValue); + ASSERT_TRUE( compareKVListRange(0,1, 0,2)); + + iface_->putValue(BLKDATA, DB_PREFIX_TXDATA, keyAB, commonValue); + ASSERT_TRUE( compareKVListRange(0,1, 0,3)); + + // Now test a bunch of get* methods + ASSERT_EQ( iface_->getValue( BLKDATA, PREFIX+keyAB), commonValue); + ASSERT_EQ( iface_->getValue( BLKDATA, DB_PREFIX_DBINFO, nothing),val0); + ASSERT_EQ( iface_->getValue( BLKDATA, DBINFO), val0); + ASSERT_EQ( iface_->getValueRef( BLKDATA, PREFIX+keyAB), commonValue); + ASSERT_EQ( iface_->getValueRef( BLKDATA, TXDATA, keyAB), commonValue); + ASSERT_EQ( iface_->getValueReader(BLKDATA, PREFIX+keyAB).getRawRef(), commonValue); + ASSERT_EQ( iface_->getValueReader(BLKDATA, TXDATA, keyAB).getRawRef(),commonValue); + + iface_->deleteValue(BLKDATA, DB_PREFIX_TXDATA, keyAB); + ASSERT_TRUE( compareKVListRange(0,1, 0,2)); + + iface_->deleteValue(BLKDATA, PREFIX+ keyAB); + ASSERT_TRUE( compareKVListRange(0,1, 0,1)); + + iface_->deleteValue(BLKDATA, PREFIX+ keyAB); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(LevelDBTest, STxOutPutGet) +{ + BinaryData TXP = WRITE_UINT8_BE((uint8_t)DB_PREFIX_TXDATA); + BinaryData stxoVal = READHEX("0400") + rawTxOut0_; + BinaryData stxoKey = TXP + READHEX("01e078""0f""0007""0001"); + + ASSERT_TRUE(standardOpenDBs()); + + StoredTxOut stxo0; + stxo0.txVersion_ = 1; + stxo0.spentness_ = TXOUT_UNSPENT; + stxo0.blockHeight_ = 123000; + stxo0.duplicateID_ = 15; + stxo0.txIndex_ = 7; + stxo0.txOutIndex_ = 1; + stxo0.unserialize(rawTxOut0_); + iface_->putStoredTxOut(stxo0); + + // Construct expected output + addOutPairB(stxoKey, stxoVal); + ASSERT_TRUE(compareKVListRange(0,1, 0,2)); + + StoredTxOut stxoGet; + iface_->getStoredTxOut(stxoGet, 123000, 15, 7, 1); + EXPECT_EQ(stxoGet.serializeDBValue(), stxo0.serializeDBValue()); + + //iface_->validDupByHeight_[123000] = 15; + //iface_->getStoredTxOut(stxoGet, 123000, 7, 1); + //EXPECT_EQ(stxoGet.serializeDBValue(), stxo0.serializeDBValue()); + + StoredTxOut stxo1; + stxo1.txVersion_ = 1; + stxo1.spentness_ = TXOUT_UNSPENT; + stxo1.blockHeight_ = 200333; + stxo1.duplicateID_ = 3; + stxo1.txIndex_ = 7; + stxo1.txOutIndex_ = 1; + stxo1.unserialize(rawTxOut1_); + stxoVal = READHEX("0400") + rawTxOut1_; + stxoKey = TXP + READHEX("030e8d""03""00070001"); + iface_->putStoredTxOut(stxo1); + + iface_->getStoredTxOut(stxoGet, 123000, 15, 7, 1); + EXPECT_EQ(stxoGet.serializeDBValue(), stxo0.serializeDBValue()); + iface_->getStoredTxOut(stxoGet, 200333, 3, 7, 1); + EXPECT_EQ(stxoGet.serializeDBValue(), stxo1.serializeDBValue()); + + addOutPairB(stxoKey, stxoVal); + ASSERT_TRUE(compareKVListRange(0,1, 0,3)); + +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(LevelDBTest, PutFullTxNoOuts) +{ + DBUtils.setArmoryDbType(ARMORY_DB_FULL); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + + ASSERT_TRUE(standardOpenDBs()); + + StoredTx stx; + stx.createFromTx(rawTxUnfrag_); + stx.setKeyData(123000, 15, 7); + + BinaryData TXP = WRITE_UINT8_BE((uint8_t)DB_PREFIX_TXDATA); + BinaryData stxKey = TXP + READHEX("01e078""0f""0007"); + BinaryData stxVal = READHEX("0440") + stx.thisHash_ + rawTxFragged_; + + iface_->putStoredTx(stx, false); + addOutPairB(stxKey, stxVal); + EXPECT_TRUE(compareKVListRange(0,1, 0,2)); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(LevelDBTest, PutFullTx) +{ + DBUtils.setArmoryDbType(ARMORY_DB_FULL); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + + BinaryData TXP = WRITE_UINT8_BE((uint8_t)DB_PREFIX_TXDATA); + BinaryData stxoVal = READHEX("0400") + rawTxOut0_; + BinaryData stxKey = TXP + READHEX("01e078""0f""0007"); + BinaryData stxo0Key = TXP + READHEX("01e078""0f""0007""0000"); + BinaryData stxo1Key = TXP + READHEX("01e078""0f""0007""0001"); + BinaryData stxo0raw = READHEX( + "ac4c8bd5000000001976a9148dce8946f1c7763bb60ea5cf16ef514cbed0633b88ac"); + BinaryData stxo1raw = READHEX( + "002f6859000000001976a9146a59ac0e8f553f292dfe5e9f3aaa1da93499c15e88ac"); + + ASSERT_TRUE(standardOpenDBs()); + + StoredTx stx; + stx.createFromTx(rawTxUnfrag_); + stx.setKeyData(123000, 15, 7); + + ASSERT_EQ(stx.stxoMap_.size(), 2); + for(uint32_t i=0; i<2; i++) + { + stx.stxoMap_[i].spentness_ = TXOUT_UNSPENT; + stx.stxoMap_[i].isCoinbase_ = false; + + ASSERT_EQ(stx.stxoMap_[i].blockHeight_, 123000); + ASSERT_EQ(stx.stxoMap_[i].duplicateID_, 15); + ASSERT_EQ(stx.stxoMap_[i].txIndex_, 7); + ASSERT_EQ(stx.stxoMap_[i].txOutIndex_, i); + ASSERT_EQ(stx.stxoMap_[i].isCoinbase_, false); + } + + BinaryData stxVal = READHEX("0440") + stx.thisHash_ + rawTxFragged_; + BinaryData stxo0Val = READHEX("0400") + stxo0raw; + BinaryData stxo1Val = READHEX("0400") + stxo1raw; + + iface_->putStoredTx(stx); + addOutPairB(stxKey, stxVal); + addOutPairB(stxo0Key, stxo0Val); + addOutPairB(stxo1Key, stxo1Val); + EXPECT_TRUE(compareKVListRange(0,1, 0,4)); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(LevelDBTest, PutFullBlockNoTx) +{ + DBUtils.setArmoryDbType(ARMORY_DB_FULL); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + + StoredHeader sbh; + BinaryRefReader brr(rawBlock_); + sbh.unserializeFullBlock(brr); + sbh.setKeyData(123000, UINT8_MAX); + + StoredHeadHgtList hhl; + hhl.height_ = 123000; + hhl.dupAndHashList_.push_back( pair(0, sbh.thisHash_)); + hhl.preferredDup_ = UINT8_MAX; + + BinaryData TXP = WRITE_UINT8_BE((uint8_t)DB_PREFIX_TXDATA); + BinaryData HHP = WRITE_UINT8_BE((uint8_t)DB_PREFIX_HEADHASH); + BinaryData HGP = WRITE_UINT8_BE((uint8_t)DB_PREFIX_HEADHGT); + BinaryData hgtx = READHEX("01e078""00"); + BinaryData sbh_HH_key = HHP + sbh.thisHash_; + BinaryData sbh_HH_val = sbh.dataCopy_ + hgtx; + BinaryData sbh_HG_key = hhl.getDBKey(); + BinaryData sbh_HG_val = hhl.serializeDBValue(); + + ASSERT_TRUE(standardOpenDBs()); + + addOutPairH( sbh_HH_key, sbh_HH_val); + addOutPairH( sbh_HG_key, sbh_HG_val); + + uint8_t sdup = iface_->putStoredHeader(sbh, false); + EXPECT_TRUE(compareKVListRange(0,3, 0,1)); + EXPECT_EQ(sdup, 0); + + // Try adding it again and see if get the correct dup again, and no touch DB + sdup = iface_->putStoredHeader(sbh, false); + EXPECT_TRUE(compareKVListRange(0,3, 0,1)); + EXPECT_EQ(sdup, 0); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(LevelDBTest, PutGetBareHeader) +{ + DBUtils.setArmoryDbType(ARMORY_DB_FULL); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + + StoredHeader sbh; + BinaryRefReader brr(rawBlock_); + sbh.unserializeFullBlock(brr); + sbh.setKeyData(123000, UINT8_MAX); + BinaryData header0 = sbh.thisHash_; + + ASSERT_TRUE(standardOpenDBs()); + + uint8_t sdup = iface_->putBareHeader(sbh); + EXPECT_EQ(sdup, 0); + EXPECT_EQ(sbh.duplicateID_, 0); + + // Try adding it again and see if get the correct dup again, and no touch DB + sdup = iface_->putStoredHeader(sbh, false); + EXPECT_EQ(sdup, 0); + EXPECT_EQ(sbh.duplicateID_, 0); + + // Add a new header and make sure duplicate ID is done correctly + BinaryData newHeader = READHEX( + "0000000105d3571220ef5f87c6ac0bc8bf5b33c02a9e6edf83c84d840109592c" + "0000000027523728e15f5fe1ac507bff92499eada4af8a0c485d5178e3f96568" + "c18f84994e0e4efc1c0175d646a91ad4"); + BinaryData header1 = BtcUtils::getHash256(newHeader); + + StoredHeader sbh2; + sbh2.unserialize(newHeader); + sbh2.setKeyData(123000, UINT8_MAX); + + uint8_t newDup = iface_->putBareHeader(sbh2); + EXPECT_EQ(newDup, 1); + EXPECT_EQ(sbh2.duplicateID_, 1); + + // Now add a new, isMainBranch_ header + StoredHeader sbh3; + BinaryData anotherHead = READHEX( + "010000001d8f4ec0443e1f19f305e488c1085c95de7cc3fd25e0d2c5bb5d0000" + "000000009762547903d36881a86751f3f5049e23050113f779735ef82734ebf0" + "b4450081d8c8c84db3936a1a334b035b"); + BinaryData header2 = BtcUtils::getHash256(anotherHead); + + sbh3.unserialize(anotherHead); + sbh3.setKeyData(123000, UINT8_MAX); + sbh3.isMainBranch_ = true; + uint8_t anotherDup = iface_->putBareHeader(sbh3); + EXPECT_EQ(anotherDup, 2); + EXPECT_EQ(sbh3.duplicateID_, 2); + EXPECT_EQ(iface_->getValidDupIDForHeight(123000), 2); + + // Now test getting bare headers + StoredHeader sbh4; + iface_->getBareHeader(sbh4, 123000); + EXPECT_EQ(sbh4.thisHash_, header2); + EXPECT_EQ(sbh4.duplicateID_, 2); + + iface_->getBareHeader(sbh4, 123000, 1); + EXPECT_EQ(sbh4.thisHash_, header1); + EXPECT_EQ(sbh4.duplicateID_, 1); + + // Re-add the same SBH3, make sure nothing changes + iface_->putBareHeader(sbh3); + EXPECT_EQ(sbh3.duplicateID_, 2); + EXPECT_EQ(iface_->getValidDupIDForHeight(123000), 2); + +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(LevelDBTest, PutFullBlock) +{ + DBUtils.setArmoryDbType(ARMORY_DB_FULL); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + ASSERT_TRUE(standardOpenDBs()); + + StoredHeader sbh; + BinaryRefReader brr(rawBlock_); + sbh.unserializeFullBlock(brr); + sbh.setKeyData(123000); + + BinaryData rawHeader = READHEX( + "01000000eb10c9a996a2340a4d74eaab41421ed8664aa49d18538bab59010000" + "000000005a2f06efa9f2bd804f17877537f2080030cadbfa1eb50e02338117cc" + "604d91b9b7541a4ecfbb0a1a64f1ade7"); + + // Compute and write the headers to the expected-output + StoredHeadHgtList hhl; + hhl.height_ = 123000; + hhl.dupAndHashList_.push_back( pair(0, sbh.thisHash_)); + hhl.preferredDup_ = UINT8_MAX; + BinaryData HHP = WRITE_UINT8_BE((uint8_t)DB_PREFIX_HEADHASH); + BinaryData HGP = WRITE_UINT8_BE((uint8_t)DB_PREFIX_HEADHGT); + BinaryData sbh_HH_key = HHP + sbh.thisHash_; + BinaryData sbh_HH_val = rawHeader + READHEX("01e078""00"); + BinaryData sbh_HG_key = hhl.getDBKey(); + BinaryData sbh_HG_val = hhl.serializeDBValue(); + // Only HEADHASH and HEADHGT entries get written + addOutPairH( sbh_HH_key, sbh_HH_val); + addOutPairH( sbh_HG_key, sbh_HG_val); + + // Now compute and write the BLKDATA + BinaryData TXP = WRITE_UINT8_BE((uint8_t)DB_PREFIX_TXDATA); + BinaryData sbhKey = TXP + READHEX("01e078""00"); + BinaryData stx0Key = sbhKey + READHEX("0000"); + BinaryData stx1Key = sbhKey + READHEX("0001"); + BinaryData stx2Key = sbhKey + READHEX("0002"); + BinaryData stxo00Key = stx0Key + READHEX("0000"); + BinaryData stxo10Key = stx1Key + READHEX("0000"); + BinaryData stxo11Key = stx1Key + READHEX("0001"); + BinaryData stxo20Key = stx2Key + READHEX("0000"); + BinaryData stxo21Key = stx2Key + READHEX("0001"); + BinaryData stxo00Raw = READHEX( "00f2052a01000000""434104" + "c2239c4eedb3beb26785753463be3ec62b82f6acd62efb65f452f8806f2ede0b" + "338e31d1f69b1ce449558d7061aa1648ddc2bf680834d3986624006a272dc21cac"); + BinaryData stxo10Raw = READHEX( "40420f0000000000" + "19""76a914adfa66f57ded1b655eb4ccd96ee07ca62bc1ddfd88ac"); + BinaryData stxo11Raw = READHEX( "007d6a7d04000000" + "19""76a914981a0c9ae61fa8f8c96ae6f8e383d6e07e77133e88ac"); + BinaryData stxo20Raw = READHEX( "2f66ac6105000000" + "19""76a914964642290c194e3bfab661c1085e47d67786d2d388ac"); + BinaryData stxo21Raw = READHEX( "2f77e20000000000" + "19""76a9141486a7046affd935919a3cb4b50a8a0c233c286c88ac"); + + StoredTx & stx0 = sbh.stxMap_[0]; + StoredTx & stx1 = sbh.stxMap_[1]; + StoredTx & stx2 = sbh.stxMap_[2]; + BinaryData hflags = READHEX("01340000"); + BinaryData ntx = READHEX("03000000"); + BinaryData nbyte = READHEX("46040000"); + + // Add header to BLKDATA + addOutPairB(sbhKey, hflags + rawHeader + ntx + nbyte); + + // Add Tx0 to BLKDATA + addOutPairB(stx0Key, READHEX("0440") + stx0.thisHash_ + stx0.getSerializedTxFragged()); + addOutPairB(stxo00Key, READHEX("0480") + stxo00Raw); // is coinbase + + // Add Tx1 to BLKDATA + addOutPairB(stx1Key, READHEX("0440") + stx1.thisHash_ + stx1.getSerializedTxFragged()); + addOutPairB(stxo10Key, READHEX("0400") + stxo10Raw); + addOutPairB(stxo11Key, READHEX("0400") + stxo11Raw); + + // Add Tx2 to BLKDATA + addOutPairB(stx2Key, READHEX("0440") + stx2.thisHash_ + stx2.getSerializedTxFragged()); + addOutPairB(stxo20Key, READHEX("0400") + stxo20Raw); + addOutPairB(stxo21Key, READHEX("0400") + stxo21Raw); + + // DuplicateID values get set when we putStoredHeader since we don't know + // what dupIDs have been taken until we try to put it in the database. + ASSERT_EQ(sbh.stxMap_.size(), 3); + for(uint32_t i=0; i<3; i++) + { + ASSERT_EQ(sbh.stxMap_[i].blockHeight_, 123000); + ASSERT_EQ(sbh.stxMap_[i].duplicateID_, UINT8_MAX); + ASSERT_EQ(sbh.stxMap_[i].txIndex_, i); + for(uint32_t j=0; jputStoredHeader(sbh); + + ASSERT_TRUE(compareKVListRange(0,3, 0,9)); +} + + + +//////////////////////////////////////////////////////////////////////////////// +// Of course, this test only works if the previous test passes (but doesn't +// require a saved state, it just re-puts the full block into the DB). I +// did it this way, because I wasn't comfortable committing the pre-filled +// DB to the repo. +TEST_F(LevelDBTest, GetFullBlock) +{ + DBUtils.setArmoryDbType(ARMORY_DB_FULL); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + ASSERT_TRUE(standardOpenDBs()); + + StoredHeader sbh; + BinaryRefReader brr(rawBlock_); + sbh.unserializeFullBlock(brr); + sbh.setKeyData(123000); + sbh.isMainBranch_ = true; + + // Check the DBInfo before and after putting the valid header + StoredDBInfo sdbi; + iface_->getStoredDBInfo(HEADERS, sdbi); + EXPECT_EQ(sdbi.topBlkHgt_, 0); + EXPECT_EQ(sdbi.topBlkHash_, iface_->getGenesisBlockHash()); + + iface_->putStoredHeader(sbh); + + // Since we marked this block main-branch, it should have non-MAX validDup + EXPECT_EQ(iface_->getValidDupIDForHeight(123000), 0); + BinaryData headerHash = READHEX( + "7d97d862654e03d6c43b77820a40df894e3d6890784528e9cd05000000000000"); + iface_->getStoredDBInfo(HEADERS, sdbi); + EXPECT_EQ(sdbi.topBlkHgt_, 123000); + EXPECT_EQ(sdbi.topBlkHash_, headerHash); + + BinaryData rawHeader = READHEX( + "01000000eb10c9a996a2340a4d74eaab41421ed8664aa49d18538bab59010000" + "000000005a2f06efa9f2bd804f17877537f2080030cadbfa1eb50e02338117cc" + "604d91b9b7541a4ecfbb0a1a64f1ade7"); + + //////////////////// + // Now test the get methods + + for(uint32_t i=0; i<4; i++) + { + StoredHeader sbhGet; + + if(i==0) + EXPECT_TRUE(iface_->getStoredHeader(sbhGet, 123000, 0, false)); + else if(i==1) + EXPECT_TRUE(iface_->getStoredHeader(sbhGet, headerHash, false)); + else if(i==2) + EXPECT_TRUE(iface_->getStoredHeader(sbhGet, 123000, 0, true)); + else if(i==3) + EXPECT_TRUE(iface_->getStoredHeader(sbhGet, headerHash, true)); + + EXPECT_TRUE( sbhGet.isInitialized()); + EXPECT_EQ( sbhGet.dataCopy_, rawHeader); + EXPECT_EQ( sbhGet.thisHash_, headerHash); + EXPECT_EQ( sbhGet.numTx_, 3); + EXPECT_EQ( sbhGet.numBytes_, 1094); + EXPECT_EQ( sbhGet.blockHeight_, 123000); + EXPECT_EQ( sbhGet.duplicateID_, 0); + EXPECT_EQ( sbhGet.merkle_.getSize(), 0); + EXPECT_FALSE(sbhGet.merkleIsPartial_); + EXPECT_EQ( sbhGet.unserArmVer_, 0); + EXPECT_EQ( sbhGet.unserBlkVer_, 1); + EXPECT_EQ( sbhGet.unserDbType_, ARMORY_DB_FULL); + EXPECT_EQ( sbhGet.unserPrType_, DB_PRUNE_NONE); + + if(i<2) + EXPECT_EQ( sbhGet.stxMap_.size(), 0); + else + { + EXPECT_EQ( sbhGet.stxMap_.size(), 3); + EXPECT_EQ( sbhGet.stxMap_[1].blockHeight_, 123000); + EXPECT_EQ( sbhGet.stxMap_[1].duplicateID_, 0); + EXPECT_EQ( sbhGet.stxMap_[1].txIndex_, 1); + EXPECT_EQ( sbhGet.stxMap_[1].numTxOut_, 2); + EXPECT_EQ( sbhGet.stxMap_[1].numBytes_, 621); + EXPECT_EQ( sbhGet.stxMap_[0].stxoMap_[0].isCoinbase_, true); + EXPECT_EQ( sbhGet.stxMap_[0].stxoMap_[0].spentness_, TXOUT_SPENTUNK); + EXPECT_EQ( sbhGet.stxMap_[1].stxoMap_[0].isCoinbase_, false); + EXPECT_EQ( sbhGet.stxMap_[1].stxoMap_[0].blockHeight_, 123000); + EXPECT_EQ( sbhGet.stxMap_[1].stxoMap_[0].duplicateID_, 0); + EXPECT_EQ( sbhGet.stxMap_[1].stxoMap_[0].txIndex_, 1); + EXPECT_EQ( sbhGet.stxMap_[1].stxoMap_[0].txOutIndex_, 0); + } + } + +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(LevelDBTest, PutGetStoredTxHints) +{ + ASSERT_TRUE(standardOpenDBs()); + + BinaryData prefix = READHEX("aabbccdd"); + + StoredTxHints sths; + EXPECT_FALSE(iface_->getStoredTxHints(sths, prefix)); + + sths.txHashPrefix_ = prefix; + + ASSERT_TRUE(iface_->putStoredTxHints(sths)); + + BinaryData THP = WRITE_UINT8_BE((uint8_t)DB_PREFIX_TXHINTS); + addOutPairB(THP + prefix, READHEX("00")); + + compareKVListRange(0,1, 0,2); + + ///// + sths.dbKeyList_.push_back(READHEX("abcd1234ffff")); + replaceTopOutPairB(THP + prefix, READHEX("01""abcd1234ffff")); + EXPECT_TRUE(iface_->putStoredTxHints(sths)); + compareKVListRange(0,1, 0,2); + + ///// + sths.dbKeyList_.push_back(READHEX("00002222aaaa")); + replaceTopOutPairB(THP + prefix, READHEX("02""abcd1234ffff""00002222aaaa")); + EXPECT_TRUE(iface_->putStoredTxHints(sths)); + compareKVListRange(0,1, 0,2); + + ///// + sths.preferredDBKey_ = READHEX("00002222aaaa"); + replaceTopOutPairB(THP + prefix, READHEX("02""00002222aaaa""abcd1234ffff")); + EXPECT_TRUE(iface_->putStoredTxHints(sths)); + compareKVListRange(0,1, 0,2); + + // Now test the get methods + EXPECT_TRUE( iface_->getStoredTxHints(sths, prefix)); + EXPECT_EQ( sths.txHashPrefix_, prefix); + EXPECT_EQ( sths.dbKeyList_.size(), 2); + EXPECT_EQ( sths.preferredDBKey_, READHEX("00002222aaaa")); + + // + sths.dbKeyList_.resize(0); + sths.preferredDBKey_.resize(0); + EXPECT_TRUE( iface_->putStoredTxHints(sths)); + EXPECT_TRUE( iface_->getStoredTxHints(sths, prefix)); + EXPECT_EQ( sths.txHashPrefix_, prefix); + EXPECT_EQ( sths.dbKeyList_.size(), 0); + EXPECT_EQ( sths.preferredDBKey_.getSize(), 0); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(LevelDBTest, PutGetStoredScriptHistory) +{ + ASSERT_TRUE(standardOpenDBs()); + + /////////////////////////////////////////////////////////////////////////// + // A whole bunch of setup stuff we need for SSH operations to work right + InterfaceToLDB * iface = LevelDBWrapper::GetInterfacePtr(); + iface->setValidDupIDForHeight(255,0); + iface->setValidDupIDForHeight(256,0); + + BinaryData dbkey0 = READHEX("0000ff00""0001""0001"); + BinaryData dbkey1 = READHEX("0000ff00""0002""0002"); + BinaryData dbkey2 = READHEX("00010000""0004""0004"); + BinaryData dbkey3 = READHEX("00010000""0006""0006"); + uint64_t val0 = READ_UINT64_HEX_LE("0100000000000000"); + uint64_t val1 = READ_UINT64_HEX_LE("0002000000000000"); + uint64_t val2 = READ_UINT64_HEX_LE("0000030000000000"); + uint64_t val3 = READ_UINT64_HEX_LE("0000000400000000"); + + BinaryData PREFIX = READHEX("03"); + BinaryData RAWTX = READHEX( + "01000000016290dce984203b6a5032e543e9e272d8bce934c7de4d15fa0fe44d" + "d49ae4ece9010000008b48304502204f2fa458d439f957308bca264689aa175e" + "3b7c5f78a901cb450ebd20936b2c500221008ea3883a5b80128e55c9c6070aa6" + "264e1e0ce3d18b7cd7e85108ce3d18b7419a0141044202550a5a6d3bb81549c4" + "a7803b1ad59cdbba4770439a4923624a8acfc7d34900beb54a24188f7f0a4068" + "9d905d4847cc7d6c8d808a457d833c2d44ef83f76bffffffff0242582c0a0000" + "00001976a914c1b4695d53b6ee57a28647ce63e45665df6762c288ac80d1f008" + "000000001976a9140e0aec36fe2545fb31a41164fb6954adcd96b34288ac0000" + "0000"); + iface->putValue(BLKDATA, DB_PREFIX_TXDATA, dbkey0, RAWTX); + iface->putValue(BLKDATA, DB_PREFIX_TXDATA, dbkey1, RAWTX); + iface->putValue(BLKDATA, DB_PREFIX_TXDATA, dbkey2, RAWTX); + iface->putValue(BLKDATA, DB_PREFIX_TXDATA, dbkey3, RAWTX); + + TxIOPair txio0(dbkey0, val0); + TxIOPair txio1(dbkey1, val1); + TxIOPair txio2(dbkey2, val2); + TxIOPair txio3(dbkey3, val3); + txio3.setMultisig(true); + /////////////////////////////////////////////////////////////////////////// + + StoredSubHistory * subptr; + TxIOPair * txioptr; + + StoredScriptHistory ssh, sshorig, sshtemp; + BinaryData hgtX0 = READHEX("0000ff00"); + BinaryData hgtX1 = READHEX("00010000"); + BinaryData uniq = READHEX("00""0000ffff0000ffff0000ffff0000ffff0000ffff"); + sshorig.uniqueKey_ = uniq; + uint32_t blk = READ_UINT32_HEX_LE("ffff0000"); + sshorig.alreadyScannedUpToBlk_ = blk; + sshorig.version_ = 1; + + ///////////////////////////////////////////////////////////////////////////// + // Haven't actually done anything yet... + ssh = sshorig; + EXPECT_EQ(ssh.uniqueKey_, uniq); + EXPECT_EQ(ssh.alreadyScannedUpToBlk_, blk); + EXPECT_EQ(ssh.subHistMap_.size(), 0); + + ///////////////////////////////////////////////////////////////////////////// + // An empty SSH -- this shouldn't happen in production, but test it anyway + iface_->putStoredScriptHistory(ssh); + iface_->getStoredScriptHistory(sshtemp, uniq); + + EXPECT_EQ(sshtemp.uniqueKey_, uniq); + EXPECT_EQ(sshtemp.alreadyScannedUpToBlk_, blk); + EXPECT_EQ(sshtemp.subHistMap_.size(), 0); + + ///////////////////////////////////////////////////////////////////////////// + // A single txio + ssh = sshorig; + ssh.insertTxio(txio0); + + iface_->putStoredScriptHistory(ssh); + iface_->getStoredScriptHistory(sshtemp, uniq); + + EXPECT_EQ(sshtemp.uniqueKey_, uniq); + EXPECT_EQ(sshtemp.alreadyScannedUpToBlk_, blk); + EXPECT_EQ(sshtemp.totalTxioCount_, 1); + EXPECT_EQ(sshtemp.totalUnspent_, val0); + EXPECT_EQ(sshtemp.subHistMap_.size(), 1); + ASSERT_NE(sshtemp.subHistMap_.find(hgtX0), sshtemp.subHistMap_.end()); + subptr = &sshtemp.subHistMap_[hgtX0]; + EXPECT_EQ(subptr->uniqueKey_, uniq); + EXPECT_EQ(subptr->hgtX_, hgtX0); + ASSERT_EQ(subptr->txioSet_.size(), 1); + ASSERT_NE(subptr->txioSet_.find(dbkey0), subptr->txioSet_.end()); + txioptr = &subptr->txioSet_[dbkey0]; + EXPECT_EQ(txioptr->getDBKeyOfOutput(), dbkey0); + EXPECT_EQ(txioptr->getValue(), val0); + EXPECT_FALSE(txioptr->isMultisig()); + + ///////////////////////////////////////////////////////////////////////////// + // Two TxIOPairs in one sub history + ssh = sshorig; + sshtemp = StoredScriptHistory(); + ssh.insertTxio(txio0); + ssh.insertTxio(txio1); + + iface_->putStoredScriptHistory(ssh); + iface_->getStoredScriptHistory(sshtemp, uniq); + + EXPECT_EQ(sshtemp.uniqueKey_, uniq); + EXPECT_EQ(sshtemp.alreadyScannedUpToBlk_, blk); + EXPECT_EQ(sshtemp.totalTxioCount_, 2); + EXPECT_EQ(sshtemp.totalUnspent_, val0+val1); + EXPECT_EQ(sshtemp.subHistMap_.size(), 1); + ASSERT_NE(sshtemp.subHistMap_.find(hgtX0), sshtemp.subHistMap_.end()); + subptr = &sshtemp.subHistMap_[hgtX0]; + EXPECT_EQ(subptr->uniqueKey_, uniq); + EXPECT_EQ(subptr->hgtX_, hgtX0); + ASSERT_EQ(subptr->txioSet_.size(), 2); + ASSERT_NE(subptr->txioSet_.find(dbkey0), subptr->txioSet_.end()); + txioptr = &subptr->txioSet_[dbkey0]; + EXPECT_EQ(txioptr->getDBKeyOfOutput(), dbkey0); + EXPECT_EQ(txioptr->getValue(), val0); + EXPECT_FALSE(txioptr->isMultisig()); + txioptr = &subptr->txioSet_[dbkey1]; + EXPECT_EQ(txioptr->getDBKeyOfOutput(), dbkey1); + EXPECT_EQ(txioptr->getValue(), val1); + EXPECT_FALSE(txioptr->isMultisig()); + + + ///////////////////////////////////////////////////////////////////////////// + // Add new sub-history with multisig + ssh = sshorig; + ssh.insertTxio(txio0); + ssh.insertTxio(txio1); + ssh.insertTxio(txio3); + + iface_->putStoredScriptHistory(ssh); + iface_->getStoredScriptHistory(sshtemp, uniq); + + EXPECT_EQ(sshtemp.uniqueKey_, uniq); + EXPECT_EQ(sshtemp.alreadyScannedUpToBlk_, blk); + EXPECT_EQ(sshtemp.totalTxioCount_, 3); + EXPECT_EQ(sshtemp.totalUnspent_, val0+val1); + EXPECT_EQ(sshtemp.subHistMap_.size(), 2); + + ASSERT_NE(sshtemp.subHistMap_.find(hgtX0), sshtemp.subHistMap_.end()); + subptr = &sshtemp.subHistMap_[hgtX0]; + EXPECT_EQ(subptr->uniqueKey_, uniq); + EXPECT_EQ(subptr->hgtX_, hgtX0); + ASSERT_EQ(subptr->txioSet_.size(), 2); + ASSERT_NE(subptr->txioSet_.find(dbkey0), subptr->txioSet_.end()); + txioptr = &subptr->txioSet_[dbkey0]; + EXPECT_EQ(txioptr->getDBKeyOfOutput(), dbkey0); + EXPECT_EQ(txioptr->getValue(), val0); + txioptr = &subptr->txioSet_[dbkey1]; + EXPECT_EQ(txioptr->getDBKeyOfOutput(), dbkey1); + EXPECT_EQ(txioptr->getValue(), val1); + EXPECT_FALSE(txioptr->isMultisig()); + + ASSERT_NE(sshtemp.subHistMap_.find(hgtX1), sshtemp.subHistMap_.end()); + subptr = &sshtemp.subHistMap_[hgtX1]; + EXPECT_EQ(subptr->uniqueKey_, uniq); + EXPECT_EQ(subptr->hgtX_, hgtX1); + ASSERT_EQ(subptr->txioSet_.size(), 1); + ASSERT_NE(subptr->txioSet_.find(dbkey3), subptr->txioSet_.end()); + txioptr = &subptr->txioSet_[dbkey3]; + EXPECT_EQ(txioptr->getDBKeyOfOutput(), dbkey3); + EXPECT_EQ(txioptr->getValue(), val3); + EXPECT_TRUE(txioptr->isMultisig()); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(LevelDBTest, DISABLED_PutGetStoredUndoData) +{ + // We don't actually use undo data at all yet, so I'll skip the tests for now +} + + +TEST_F(LevelDBTest, HeaderDump) +{ + // We don't actually use undo data at all yet, so I'll skip the tests for now +} +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +class TxRefTest : public ::testing::Test +{ +protected: +}; + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(TxRefTest, TxRefNoInit) +{ + TxRef txr; + EXPECT_FALSE(txr.isInitialized()); + //EXPECT_FALSE(txr.isBound()); + + EXPECT_EQ(txr.getDBKey(), BinaryData(0)); + EXPECT_EQ(txr.getDBKeyRef(), BinaryDataRef()); + //EXPECT_EQ(txr.getBlockTimestamp(), UINT32_MAX); + EXPECT_EQ(txr.getBlockHeight(), UINT32_MAX); + EXPECT_EQ(txr.getDuplicateID(), UINT8_MAX ); + EXPECT_EQ(txr.getBlockTxIndex(), UINT16_MAX); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(TxRefTest, TxRefKeyParts) +{ + TxRef txr; + BinaryData newKey = READHEX("e3c4027f000f"); + BinaryDataRef newRef(newKey); + + + txr.setDBKey(newKey); + EXPECT_EQ(txr.getDBKey(), newKey); + EXPECT_EQ(txr.getDBKeyRef(), newRef); + + EXPECT_EQ(txr.getBlockHeight(), 0xe3c402); + EXPECT_EQ(txr.getDuplicateID(), 127); + EXPECT_EQ(txr.getBlockTxIndex(), 15); +} + + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +// TODO: These tests were taken directly from the BlockUtilsSuper.cpp where +// they previously ran without issue. After bringing them over to here, +// they now seg-fault. Disabled for now, since the PartialMerkleTrees +// are not actually in use anywhere yet. +class DISABLED_PartialMerkleTest : public ::testing::Test +{ +protected: + + virtual void SetUp(void) + { + vector txList_(7); + // The "abcd" quartets are to trigger endianness errors -- without them, + // these hashes are palindromes that work regardless of your endian-handling + txList_[0] = READHEX("00000000000000000000000000000000" + "000000000000000000000000abcd0000"); + txList_[1] = READHEX("11111111111111111111111111111111" + "111111111111111111111111abcd1111"); + txList_[2] = READHEX("22222222222222222222222222222222" + "222222222222222222222222abcd2222"); + txList_[3] = READHEX("33333333333333333333333333333333" + "333333333333333333333333abcd3333"); + txList_[4] = READHEX("44444444444444444444444444444444" + "444444444444444444444444abcd4444"); + txList_[5] = READHEX("55555555555555555555555555555555" + "555555555555555555555555abcd5555"); + txList_[6] = READHEX("66666666666666666666666666666666" + "666666666666666666666666abcd6666"); + + vector merkleTree_ = BtcUtils::calculateMerkleTree(txList_); + + /* + cout << "Merkle Tree looks like the following (7 tx): " << endl; + cout << "The ** indicates the nodes we care about for partial tree test" << endl; + cout << " \n"; + cout << " _____0a10_____ \n"; + cout << " / \\ \n"; + cout << " _/ \\_ \n"; + cout << " 65df b4d6 \n"; + cout << " / \\ / \\ \n"; + cout << " 6971 22dc 5675 d0b6 \n"; + cout << " / \\ / \\ / \\ / \n"; + cout << " 0000 1111 2222 3333 4444 5555 6666 \n"; + cout << " ** ** \n"; + cout << " " << endl; + cout << endl; + + cout << "Full Merkle Tree (this one has been unit tested before):" << endl; + for(uint32_t i=0; i txList_; + vector merkleTree_; +}; + + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(DISABLED_PartialMerkleTest, FullTree) +{ + vector isOurs(7); + isOurs[0] = true; + isOurs[1] = true; + isOurs[2] = true; + isOurs[3] = true; + isOurs[4] = true; + isOurs[5] = true; + isOurs[6] = true; + + //cout << "Start serializing a full tree" << endl; + PartialMerkleTree pmtFull(7, &isOurs, &txList_); + BinaryData pmtSerFull = pmtFull.serialize(); + + //cout << "Finished serializing (full)" << endl; + //cout << "Merkle Root: " << pmtFull.getMerkleRoot().toHexStr() << endl; + + //cout << "Starting unserialize (full):" << endl; + //cout << "Serialized: " << pmtSerFull.toHexStr() << endl; + PartialMerkleTree pmtFull2(7); + pmtFull2.unserialize(pmtSerFull); + BinaryData pmtSerFull2 = pmtFull2.serialize(); + //cout << "Reserializ: " << pmtSerFull2.toHexStr() << endl; + //cout << "Equal? " << (pmtSerFull==pmtSerFull2 ? "True" : "False") << endl; + + //cout << "Print Tree:" << endl; + //pmtFull2.pprintTree(); + EXPECT_EQ(pmtSerFull, pmtSerFull2); +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(DISABLED_PartialMerkleTest, SingleLeaf) +{ + vector isOurs(7); + ///////////////////////////////////////////////////////////////////////////// + // Test all 7 single-flagged trees + for(uint32_t i=0; i<7; i++) + { + for(uint32_t j=0; j<7; j++) + isOurs[j] = i==j; + + PartialMerkleTree pmt(7, &isOurs, &txList_); + //cout << "Serializing (partial)" << endl; + BinaryData pmtSer = pmt.serialize(); + PartialMerkleTree pmt2(7); + //cout << "Unserializing (partial)" << endl; + pmt2.unserialize(pmtSer); + //cout << "Reserializing (partial)" << endl; + BinaryData pmtSer2 = pmt2.serialize(); + //cout << "Serialized (Partial): " << pmtSer.toHexStr() << endl; + //cout << "Reserializ (Partial): " << pmtSer.toHexStr() << endl; + //cout << "Equal? " << (pmtSer==pmtSer2 ? "True" : "False") << endl; + + //cout << "Print Tree:" << endl; + //pmt2.pprintTree(); + EXPECT_EQ(pmtSer, pmtSer2); + } +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(DISABLED_PartialMerkleTest, MultiLeaf) +{ + // Use deterministic seed + srand(0); + + vector isOurs(7); + + ///////////////////////////////////////////////////////////////////////////// + // Test a variety of 3-flagged trees + for(uint32_t i=0; i<512; i++) + { + if(i<256) + { + // 2/3 of leaves will be selected + for(uint32_t j=0; j<7; j++) + isOurs[j] = (rand() % 3 < 2); + } + else + { + // 1/3 of leaves will be selected + for(uint32_t j=0; j<7; j++) + isOurs[j] = (rand() % 3 < 1); + } + + PartialMerkleTree pmt(7, &isOurs, &txList_); + //cout << "Serializing (partial)" << endl; + BinaryData pmtSer = pmt.serialize(); + PartialMerkleTree pmt2(7); + //cout << "Unserializing (partial)" << endl; + pmt2.unserialize(pmtSer); + //cout << "Reserializing (partial)" << endl; + BinaryData pmtSer2 = pmt2.serialize(); + //cout << "Serialized (Partial): " << pmtSer.toHexStr() << endl; + //cout << "Reserializ (Partial): " << pmtSer.toHexStr() << endl; + cout << "Equal? " << (pmtSer==pmtSer2 ? "True" : "False") << endl; + + //cout << "Print Tree:" << endl; + //pmt2.pprintTree(); + EXPECT_EQ(pmtSer, pmtSer2); + } +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(DISABLED_PartialMerkleTest, EmptyTree) +{ + vector isOurs(7); + isOurs[0] = false; + isOurs[1] = false; + isOurs[2] = false; + isOurs[3] = false; + isOurs[4] = false; + isOurs[5] = false; + isOurs[6] = false; + + //cout << "Start serializing a full tree" << endl; + PartialMerkleTree pmtFull(7, &isOurs, &txList_); + BinaryData pmtSerFull = pmtFull.serialize(); + + //cout << "Finished serializing (full)" << endl; + //cout << "Merkle Root: " << pmtFull.getMerkleRoot().toHexStr() << endl; + + //cout << "Starting unserialize (full):" << endl; + //cout << "Serialized: " << pmtSerFull.toHexStr() << endl; + PartialMerkleTree pmtFull2(7); + pmtFull2.unserialize(pmtSerFull); + BinaryData pmtSerFull2 = pmtFull2.serialize(); + //cout << "Reserializ: " << pmtSerFull2.toHexStr() << endl; + //cout << "Equal? " << (pmtSerFull==pmtSerFull2 ? "True" : "False") << endl; + + //cout << "Print Tree:" << endl; + //pmtFull2.pprintTree(); + EXPECT_EQ(pmtSerFull, pmtSerFull2); + +} + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +// THESE ARE ARMORY_DB_BARE tests. Identical to above except for the mode. +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +class BlockUtilsBare : public ::testing::Test +{ +protected: + + ///////////////////////////////////////////////////////////////////////////// + virtual void SetUp(void) + { + LOGDISABLESTDOUT(); + iface_ = LevelDBWrapper::GetInterfacePtr(); + magic_ = READHEX(MAINNET_MAGIC_BYTES); + ghash_ = READHEX(MAINNET_GENESIS_HASH_HEX); + gentx_ = READHEX(MAINNET_GENESIS_TX_HASH_HEX); + zeros_ = READHEX("00000000"); + DBUtils.setArmoryDbType(ARMORY_DB_BARE); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + + blkdir_ = string("./blkfiletest"); + homedir_ = string("./fakehomedir"); + ldbdir_ = string("./ldbtestdir"); + + iface_->openDatabases( ldbdir_, ghash_, gentx_, magic_, + ARMORY_DB_BARE, DB_PRUNE_NONE); + if(!iface_->databasesAreOpen()) + LOGERR << "ERROR OPENING DATABASES FOR TESTING!"; + + mkdir(blkdir_); + mkdir(homedir_); + + // Put the first 5 blocks into the blkdir + blk0dat_ = BtcUtils::getBlkFilename(blkdir_, 0); + BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_); + + TheBDM.SelectNetwork("Main"); + TheBDM.SetBlkFileLocation(blkdir_); + TheBDM.SetHomeDirLocation(homedir_); + TheBDM.SetLevelDBLocation(ldbdir_); + + blkHash0 = READHEX("6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000"); + blkHash1 = READHEX("1b5514b83257d924be7f10c65b95b1f3c0e50081e1dfd8943eece5eb00000000"); + blkHash2 = READHEX("979fc39616bf1dc6b1f88167f76383d44d65ccd0fc99b7f91bcb2c9500000000"); + blkHash3 = READHEX("50f8231e5fd476f470e1ba4937bc97cb304136c96c765339308935bc00000000"); + blkHash4 = READHEX("8e121ba0d275f49a21bbc171d7d49890de13c9b9733e0104654d262f00000000"); + blkHash3A= READHEX("dd63f62ef59d5b6a6da2a36407f76e4e28026a3fd3a46700d284424700000000"); + blkHash4A= READHEX("bfa204022816102169b4e1d4f78cdf77258048f6d14282144cc01d5500000000"); + blkHash5A= READHEX("4e049fd71ef7381a73e4f550d97812d1eb0fbd1489c1774e18855f1900000000"); + + addrA_ = READHEX("62e907b15cbf27d5425399ebf6f0fb50ebb88f18"); + addrB_ = READHEX("ee26c56fc1d942be8d7a24b2a1001dd894693980"); + addrC_ = READHEX("cb2abde8bccacc32e893df3a054b9ef7f227a4ce"); + addrD_ = READHEX("c522664fb0e55cdc5c0cea73b4aad97ec8343232"); + + scrAddrA_ = HASH160PREFIX + addrA_; + scrAddrB_ = HASH160PREFIX + addrB_; + scrAddrC_ = HASH160PREFIX + addrC_; + scrAddrD_ = HASH160PREFIX + addrD_; + + } + + + ///////////////////////////////////////////////////////////////////////////// + virtual void TearDown(void) + { + BlockDataManager_LevelDB::DestroyInstance(); + + rmdir(blkdir_); + rmdir(homedir_); + + char* delstr = new char[4096]; + sprintf(delstr, "%s/level*", ldbdir_.c_str()); + rmdir(delstr); + delete[] delstr; + + LOGENABLESTDOUT(); + } + + + +#if ! defined(_MSC_VER) && ! defined(__MINGW32__) + + ///////////////////////////////////////////////////////////////////////////// + void rmdir(string src) + { + char* syscmd = new char[4096]; + sprintf(syscmd, "rm -rf %s", src.c_str()); + system(syscmd); + delete[] syscmd; + } + + ///////////////////////////////////////////////////////////////////////////// + void mkdir(string newdir) + { + char* syscmd = new char[4096]; + sprintf(syscmd, "mkdir -p %s", newdir.c_str()); + system(syscmd); + delete[] syscmd; + } +#endif + + InterfaceToLDB* iface_; + BinaryData magic_; + BinaryData ghash_; + BinaryData gentx_; + BinaryData zeros_; + + string blkdir_; + string homedir_; + string ldbdir_; + string blk0dat_;; + + BinaryData blkHash0; + BinaryData blkHash1; + BinaryData blkHash2; + BinaryData blkHash3; + BinaryData blkHash4; + BinaryData blkHash3A; + BinaryData blkHash4A; + BinaryData blkHash5A; + + BinaryData addrA_; + BinaryData addrB_; + BinaryData addrC_; + BinaryData addrD_; + BinaryData scrAddrA_; + BinaryData scrAddrB_; + BinaryData scrAddrC_; + BinaryData scrAddrD_; +}; + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockUtilsBare, BuildNoRegisterWlt) +{ + TheBDM.doInitialSyncOnLoad(); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockUtilsBare, Load5Blocks) +{ + BtcWallet wlt; + wlt.addScrAddress(scrAddrA_); + wlt.addScrAddress(scrAddrB_); + wlt.addScrAddress(scrAddrC_); + TheBDM.registerWallet(&wlt); + TheBDM.registerNewScrAddr(scrAddrD_); + + TheBDM.doInitialSyncOnLoad(); + TheBDM.scanBlockchainForTx(wlt); + + ScrAddrObj * scrobj; + scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); + EXPECT_EQ(scrobj->getFullBalance(),100*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); + EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); + EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrD_); + EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); // hasn't been scanned yet + + EXPECT_EQ(wlt.getFullBalance(), 150*COIN); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockUtilsBare, Load4Blocks_Plus1) +{ + BtcWallet wlt; + wlt.addScrAddress(scrAddrA_); + wlt.addScrAddress(scrAddrB_); + wlt.addScrAddress(scrAddrC_); + TheBDM.registerWallet(&wlt); + TheBDM.registerNewScrAddr(scrAddrD_); + + // Copy only the first four blocks. Will copy the full file next to test + // readBlkFileUpdate method on non-reorg blocks. + BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_, 1596); + TheBDM.doInitialSyncOnLoad(); + TheBDM.scanBlockchainForTx(wlt); + EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 3); + EXPECT_EQ(iface_->getTopBlockHash(HEADERS), blkHash3); + EXPECT_TRUE(TheBDM.getHeaderByHash(blkHash3)->isMainBranch()); + + BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_); + TheBDM.readBlkFileUpdate(); + TheBDM.scanBlockchainForTx(wlt); + EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 4); + EXPECT_EQ(iface_->getTopBlockHash(HEADERS), blkHash4); + EXPECT_TRUE(TheBDM.getHeaderByHash(blkHash4)->isMainBranch()); + + ScrAddrObj * scrobj; + scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); + EXPECT_EQ(scrobj->getFullBalance(),100*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); + EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); + EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrD_); + EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); // hasn't been scanned yet +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockUtilsBare, Load5Blocks_FullReorg) +{ + BtcWallet wlt; + wlt.addScrAddress(scrAddrA_); + wlt.addScrAddress(scrAddrB_); + wlt.addScrAddress(scrAddrC_); + TheBDM.registerWallet(&wlt); + TheBDM.registerNewScrAddr(scrAddrD_); + + BtcWallet wlt2; + wlt2.addScrAddress(scrAddrD_); + + TheBDM.doInitialSyncOnLoad(); + TheBDM.scanBlockchainForTx(wlt); + TheBDM.scanBlockchainForTx(wlt2); + + BtcUtils::copyFile("../reorgTest/blk_3A.dat", blk0dat_); + TheBDM.readBlkFileUpdate(); + BtcUtils::copyFile("../reorgTest/blk_4A.dat", blk0dat_); + TheBDM.readBlkFileUpdate(); + BtcUtils::copyFile("../reorgTest/blk_5A.dat", blk0dat_); + TheBDM.readBlkFileUpdate(); + + TheBDM.scanBlockchainForTx(wlt); + TheBDM.scanBlockchainForTx(wlt2); + + ScrAddrObj * scrobj; + scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); + EXPECT_EQ(scrobj->getFullBalance(),150*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); + EXPECT_EQ(scrobj->getFullBalance(), 10*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); + EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); + //scrobj = &wlt.getScrAddrObjByKey(scrAddrD_); + //EXPECT_EQ(scrobj->getFullBalance(),140*COIN); + + EXPECT_EQ(wlt.getFullBalance(), 160*COIN); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockUtilsBare, CorruptedBlock) +{ + BtcWallet wlt; + wlt.addScrAddress(scrAddrA_); + wlt.addScrAddress(scrAddrB_); + wlt.addScrAddress(scrAddrC_); + TheBDM.registerWallet(&wlt); + TheBDM.registerNewScrAddr(scrAddrD_); + + BtcWallet wlt2; + wlt2.addScrAddress(scrAddrD_); + + TheBDM.doInitialSyncOnLoad(); + TheBDM.scanBlockchainForTx(wlt); + TheBDM.scanBlockchainForTx(wlt2); + + // corrupt blk_5A + { + const std::string src = "../reorgTest/blk_5A.dat"; + const std::string dst = blk0dat_; + + const uint64_t srcsz = BtcUtils::GetFileSize(src); + + BinaryData temp((size_t)srcsz); + ifstream is(src.c_str(), ios::in | ios::binary); + is.read((char*)temp.getPtr(), srcsz); + is.close(); + + ofstream os(dst.c_str(), ios::out | ios::binary); + os.write((char*)temp.getPtr(), 100); + os.write((char*)temp.getPtr()+120, srcsz-100-20); // erase 20 bytes + os.close(); + } + + TheBDM.readBlkFileUpdate(); + + + TheBDM.scanBlockchainForTx(wlt); + TheBDM.scanBlockchainForTx(wlt2); + + ScrAddrObj * scrobj; + scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); + EXPECT_EQ(scrobj->getFullBalance(),100*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); + EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); + //scrobj = &wlt.getScrAddrObjByKey(scrAddrD_); + //EXPECT_EQ(scrobj->getFullBalance(),140*COIN); + + EXPECT_EQ(wlt.getFullBalance(), 150*COIN); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockUtilsBare, Load5Blocks_RescanOps) +{ + ScrAddrObj * scrobj; + BtcWallet wlt; + wlt.addScrAddress(scrAddrA_); + wlt.addScrAddress(scrAddrB_); + wlt.addScrAddress(scrAddrC_); + TheBDM.registerWallet(&wlt); + + // Regular sync + TheBDM.doInitialSyncOnLoad(); + TheBDM.scanBlockchainForTx(wlt); + + scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); + EXPECT_EQ(scrobj->getFullBalance(),100*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); + EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); + EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); + + // Rebuild on-the-fly + TheBDM.doRebuildDatabases(); + TheBDM.scanBlockchainForTx(wlt); + + scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); + EXPECT_EQ(scrobj->getFullBalance(),100*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); + EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); + EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); + + // Rebuild on-the-fly + TheBDM.doFullRescanRegardlessOfSync(); + TheBDM.scanBlockchainForTx(wlt); + + scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); + EXPECT_EQ(scrobj->getFullBalance(),100*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); + EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); + EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); + + + TheBDM.doSyncIfNeeded(); + TheBDM.scanBlockchainForTx(wlt); + + scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); + EXPECT_EQ(scrobj->getFullBalance(),100*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); + EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); + EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); + + // Now add a new addr (with balance) and rescan + + //scrobj = &wlt.getScrAddrObjByKey(scrAddrD_); + //EXPECT_EQ(scrobj->getFullBalance(),100*COIN); // hasn't been scanned yet + +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockUtilsBare, Load5Blocks_RescanEmptyDB) +{ + ScrAddrObj * scrobj; + BtcWallet wlt; + wlt.addScrAddress(scrAddrA_); + wlt.addScrAddress(scrAddrB_); + wlt.addScrAddress(scrAddrC_); + TheBDM.registerWallet(&wlt); + + TheBDM.doInitialSyncOnLoad_Rescan(); + TheBDM.scanBlockchainForTx(wlt); + + scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); + EXPECT_EQ(scrobj->getFullBalance(),100*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); + EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); + EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockUtilsBare, Load5Blocks_RebuildEmptyDB) +{ + ScrAddrObj * scrobj; + BtcWallet wlt; + wlt.addScrAddress(scrAddrA_); + wlt.addScrAddress(scrAddrB_); + wlt.addScrAddress(scrAddrC_); + TheBDM.registerWallet(&wlt); + + /////////////////////////////////////////// + TheBDM.doInitialSyncOnLoad_Rebuild(); + /////////////////////////////////////////// + + TheBDM.scanBlockchainForTx(wlt); + + scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); + EXPECT_EQ(scrobj->getFullBalance(),100*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); + EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); + EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockUtilsBare, Load5Blocks_ForceFullRewhatever) +{ + ScrAddrObj * scrobj; + BtcWallet wlt; + wlt.addScrAddress(scrAddrA_); + wlt.addScrAddress(scrAddrB_); + wlt.addScrAddress(scrAddrC_); + TheBDM.registerWallet(&wlt); + + // This is just a regular load + TheBDM.doInitialSyncOnLoad(); + TheBDM.scanBlockchainForTx(wlt); + + wlt.addScrAddress(scrAddrD_); + + scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); + EXPECT_EQ(scrobj->getFullBalance(),100*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); + EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); + EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrD_); + EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); + + /////////////////////////////////////////// + TheBDM.doFullRescanRegardlessOfSync(); + /////////////////////////////////////////// + + TheBDM.scanBlockchainForTx(wlt); + + scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); + EXPECT_EQ(scrobj->getFullBalance(),100*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); + EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); + EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrD_); + EXPECT_EQ(scrobj->getFullBalance(),100*COIN); + + /////////////////////////////////////////// + TheBDM.doRebuildDatabases(); + /////////////////////////////////////////// + + TheBDM.scanBlockchainForTx(wlt); + + scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); + EXPECT_EQ(scrobj->getFullBalance(),100*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); + EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); + EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrD_); + EXPECT_EQ(scrobj->getFullBalance(),100*COIN); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockUtilsBare, Load5Blocks_ScanWhatIsNeeded) +{ + ScrAddrObj * scrobj; + BtcWallet wlt; + wlt.addScrAddress(scrAddrA_); + wlt.addScrAddress(scrAddrB_); + wlt.addScrAddress(scrAddrC_); + TheBDM.registerWallet(&wlt); + + // This is just a regular load + TheBDM.doInitialSyncOnLoad(); + TheBDM.scanBlockchainForTx(wlt); + + wlt.addScrAddress(scrAddrD_); + + scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); + EXPECT_EQ(scrobj->getFullBalance(),100*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); + EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); + EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrD_); + EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); + + /////////////////////////////////////////// + TheBDM.doSyncIfNeeded(); + /////////////////////////////////////////// + + TheBDM.scanBlockchainForTx(wlt); + + scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); + EXPECT_EQ(scrobj->getFullBalance(),100*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); + EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); + EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrD_); + EXPECT_EQ(scrobj->getFullBalance(),100*COIN); + + /////////////////////////////////////////// + TheBDM.doSyncIfNeeded(); + /////////////////////////////////////////// + + TheBDM.scanBlockchainForTx(wlt); + + scrobj = &wlt.getScrAddrObjByKey(scrAddrA_); + EXPECT_EQ(scrobj->getFullBalance(),100*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrB_); + EXPECT_EQ(scrobj->getFullBalance(), 0*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrC_); + EXPECT_EQ(scrobj->getFullBalance(), 50*COIN); + scrobj = &wlt.getScrAddrObjByKey(scrAddrD_); + EXPECT_EQ(scrobj->getFullBalance(),100*COIN); +} + + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +/* +class FullScanTest : public ::testing::Test +{ +protected: + + ///////////////////////////////////////////////////////////////////////////// + virtual void SetUp(void) + { + LOGDISABLESTDOUT(); + //iface_ = LevelDBWrapper::GetInterfacePtr(); + magic_ = READHEX(MAINNET_MAGIC_BYTES); + ghash_ = READHEX(MAINNET_GENESIS_HASH_HEX); + gentx_ = READHEX(MAINNET_GENESIS_TX_HASH_HEX); + zeros_ = READHEX("00000000"); + //DBUtils.setArmoryDbType(ARMORY_DB_BARE); + //DBUtils.setDbPruneType(DB_PRUNE_NONE); + + blkdatadir_ = string("./databases/leveldb_blkdata"); + } + + ///// + virtual void TearDown(void) + { + // This seem to be the best way to remove a dir tree in C++ (in Linux) + //iface_->closeDatabases(); + iface_ = NULL; + + } + + leveldb::Slice binaryDataToSlice(BinaryData const & bd) + {return leveldb::Slice((char*)bd.getPtr(), bd.getSize());} + leveldb::Slice binaryDataRefToSlice(BinaryDataRef const & bdr) + {return leveldb::Slice((char*)bdr.getPtr(), bdr.getSize());} + + BinaryRefReader sliceToRefReader(leveldb::Slice slice) + { return BinaryRefReader( (uint8_t*)(slice.data()), slice.size()); } + + InterfaceToLDB* iface_; + BinaryData magic_; + BinaryData ghash_; + BinaryData gentx_; + BinaryData zeros_; + string blkdatadir_; + BinaryRefReader currKey_; + BinaryRefReader currVal_; + +}; + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(FullScanTest, DISABLED_TestSuperRaw) +{ + + + leveldb::Options opts; + opts.compression = leveldb::kNoCompression; + //opts.block_cache = leveldb::NewLRUCache(2048 * 1024 * 1024); + leveldb::DB* dbptr; + leveldb::Status stat = leveldb::DB::Open(opts, blkdatadir_, &dbptr); + + if(!stat.ok()) + cout << "Error opening DB: " << stat.ToString() << endl; + + leveldb::ReadOptions readopts; + readopts.fill_cache = false; + //cout << "fill_cache = true (bad)" << endl; + cout << "fill_cache = false (good)" << endl; + leveldb::Iterator* iter = dbptr->NewIterator(readopts); + + + BinaryData firstKeyBD = DBUtils.getBlkDataKey(0,0); + leveldb::Slice firstKey = binaryDataToSlice(firstKeyBD); + iter->Seek(firstKey); + + uint32_t hgt; + uint8_t dup; + uint16_t txi; + uint16_t txo; + BLKDATA_TYPE bdtype; + BinaryData pref = WRITE_UINT8_LE((uint8_t)DB_PREFIX_TXDATA); + + // This scraddr should have 8 tx + BinaryData scraddr = WRITE_UINT8_LE((uint8_t)TXOUT_SCRIPT_STDHASH160); + scraddr = scraddr + READHEX("33da2736b16558c9569cc226d4b349ce1aacf649"); + + + vector opList; + + BinaryData currTxHash(32); + while(iter->Valid()) + { + currKey_.setNewData((uint8_t*)iter->key().data(), iter->key().size()); + currVal_.setNewData((uint8_t*)iter->value().data(), iter->value().size()); + + if(!currKey_.getRawRef().startsWith(pref)) + break; + + bdtype = DBUtils.readBlkDataKey(currKey_, hgt, dup, txi, txo); + + if(bdtype==BLKDATA_HEADER) + { + // Need to do a little reading&processing to make sure it is actually pulling dat afrom disk + currVal_.advance(4); + BinaryData hhash = currVal_.get_BinaryData(80); + if(hhash.startsWith(zeros_)) + cout << "Starts with four zeros!" << endl; + } + else if(bdtype==BLKDATA_TX) + { + currVal_.advance(2); + currTxHash = currVal_.get_BinaryData(32); + } + else if(bdtype==BLKDATA_TXOUT) + { + currVal_.advance(2); + + TxOut txout; + txout.unserialize(currVal_); + if(txout.getScrAddressStr() == scraddr) + { + cout << "Found a txout!" << endl; + opList.push_back(OutPoint(currTxHash, txo)); + } + } + else if(bdtype==NOT_BLKDATA) + break; + + iter->Next(); + } + + for(uint32_t o=0; oNewIterator(leveldb::ReadOptions()); + uint32_t i=0; + uint32_t nblk=0; + uint64_t lastWrite=0; + uint64_t writeBytes=0; + string fname = BtcUtils::getBlkFilename("/home/alan/.bitcoin/blocks", i); + + BinaryData magic(4), nbytes(4); + BinaryData expectedMagicBytes = READHEX(MAINNET_MAGIC_BYTES); + + leveldb::WriteBatch* writebatch = new leveldb::WriteBatch; + + while(BtcUtils::GetFileSize(fname) != FILE_DOES_NOT_EXIST) + { + cout << "Opening: " << fname.c_str() << "(" << BtcUtils::GetFileSize(fname) << ")" << endl; + ifstream is(fname.c_str(), ios::in | ios::binary); + + while(!is.eof()) + { + // Magic bytes + is.read((char*)magic.getPtr(), 4); + if(magic != expectedMagicBytes) + break; + + // Number of bytes in block + is.read((char*)nbytes.getPtr(), 4); + uint32_t blkBytes = READ_UINT32_LE(nbytes.getPtr()); + + // Reading + cout << nblk << " Reading " << blkBytes << " bytes in block; "; + BinaryReader br(blkBytes); + is.read((char*)br.exposeDataPtr(), blkBytes); + + br.advance(80); + uint32_t numTx = (uint32_t)br.get_var_int(); + cout << "Number of tx in block: " << numTx << endl; + for(uint32_t tx=0; txPut(ldbKey, ldbVal); + + } + + if(writeBytes - lastWrite > 1024*1024*1024) + { + lastWrite = writeBytes; + dbptr->Write(leveldb::WriteOptions(), writebatch); + writebatch->Clear(); + } + nblk++; + + } + + + i++; + fname = BtcUtils::getBlkFilename("/home/alan/.bitcoin/blocks", i); + + } + + dbptr->Write(leveldb::WriteOptions(), writebatch); + writebatch->Clear(); + delete writebatch; + delete dbptr; +} + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(FullScanTest, ReadSuperRawDB) +{ + string dbdir("./rawdbdir"); + BinaryData scraddr = WRITE_UINT8_LE((uint8_t)TXOUT_SCRIPT_STDHASH160); + scraddr = scraddr + READHEX("33da2736b16558c9569cc226d4b349ce1aacf649"); + + leveldb::Options opts; + opts.compression = leveldb::kNoCompression; + opts.block_cache = leveldb::NewLRUCache(2048 * 1024 * 1024); + opts.block_size = 8*1024*1024; + leveldb::DB* dbptr; + leveldb::Status stat = leveldb::DB::Open(opts, dbdir, &dbptr); + + if(!stat.ok()) + cout << "Error opening DB: " << stat.ToString() << endl; + + leveldb::ReadOptions readopts; + readopts.fill_cache = false; + leveldb::Iterator* iter = dbptr->NewIterator(readopts); + iter->SeekToFirst(); + + vector txinOffsets, txoutOffsets; + while(iter->Valid()) + { + currKey_.setNewData((uint8_t*)iter->key().data(), iter->key().size()); + currVal_.setNewData((uint8_t*)iter->value().data(), iter->value().size()); + + uint8_t const * txPtr = currVal_.exposeDataPtr(); + BtcUtils::TxCalcLength(txPtr, &txinOffsets, &txoutOffsets); + + for(uint32_t txo=0; txoNext(); + } + + cout << "Done!" << endl; + +} +*/ + + + + + +class LoadTestnetBareTest : public ::testing::Test +{ +protected: + + ///////////////////////////////////////////////////////////////////////////// + virtual void TearDown(void) {} + + virtual void SetUp(void) + { + DBUtils.setArmoryDbType(ARMORY_DB_BARE); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + + blkdir_ = string("/home/alan/.bitcoin/testnet3/blocks"); + homedir_ = string("/home/alan/.armory/testnet3"); + ldbdir_ = string("/home/alan/.armory/testnet3/databases"); + + + mkdir(ldbdir_); + mkdir(homedir_); + + TheBDM.SelectNetwork("Test"); + TheBDM.SetBlkFileLocation(blkdir_); + TheBDM.SetHomeDirLocation(homedir_); + TheBDM.SetLevelDBLocation(ldbdir_); + } + + void mkdir(string newdir) + { + char* syscmd = new char[4096]; + sprintf(syscmd, "mkdir -p %s", newdir.c_str()); + system(syscmd); + delete[] syscmd; + } + + InterfaceToLDB* iface_; + BinaryData magic_; + BinaryData ghash_; + BinaryData gentx_; + BinaryData zeros_; + + string blkdir_; + string homedir_; + string ldbdir_; + string blk0dat_;; + + BinaryData addrA_; + BinaryData addrB_; + BinaryData addrC_; + BinaryData addrD_; + BinaryData scrAddrA_; + BinaryData scrAddrB_; + BinaryData scrAddrC_; + BinaryData scrAddrD_; +}; + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(LoadTestnetBareTest, DISABLED_StepThroughDebug_usually_disabled) +{ + // These aren't actually testnet addr, balances will be zero + BinaryData scrAddrA_ = READHEX("0062e907b15cbf27d5425399ebf6f0fb50ebb88f18"); + BinaryData scrAddrB_ = READHEX("00ee26c56fc1d942be8d7a24b2a1001dd894693980"); + BinaryData scrAddrC_ = READHEX("00cb2abde8bccacc32e893df3a054b9ef7f227a4ce"); + + BtcWallet wlt; + wlt.addScrAddress(scrAddrA_); + wlt.addScrAddress(scrAddrB_); + wlt.addScrAddress(scrAddrC_); + TheBDM.registerWallet(&wlt); + + TheBDM.doInitialSyncOnLoad(); + TheBDM.scanBlockchainForTx(wlt); + TheBDM.DestroyInstance(); +} + + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +// THESE ARE ARMORY_DB_SUPER tests. Identical to above except for the mode. +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +class BlockUtilsSuper : public ::testing::Test +{ +protected: + + ///////////////////////////////////////////////////////////////////////////// + virtual void SetUp(void) + { + LOGDISABLESTDOUT(); + iface_ = LevelDBWrapper::GetInterfacePtr(); + magic_ = READHEX(MAINNET_MAGIC_BYTES); + ghash_ = READHEX(MAINNET_GENESIS_HASH_HEX); + gentx_ = READHEX(MAINNET_GENESIS_TX_HASH_HEX); + zeros_ = READHEX("00000000"); + + blkdir_ = string("./blkfiletest"); + homedir_ = string("./fakehomedir"); + ldbdir_ = string("./ldbtestdir"); + + + mkdir(blkdir_); + mkdir(homedir_); + + // Put the first 5 blocks into the blkdir + blk0dat_ = BtcUtils::getBlkFilename(blkdir_, 0); + BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_); + + TheBDM.SetDatabaseModes(ARMORY_DB_SUPER, DB_PRUNE_NONE); + TheBDM.SelectNetwork("Main"); + TheBDM.SetBlkFileLocation(blkdir_); + TheBDM.SetHomeDirLocation(homedir_); + TheBDM.SetLevelDBLocation(ldbdir_); + + iface_->openDatabases( ldbdir_, ghash_, gentx_, magic_, + ARMORY_DB_SUPER, DB_PRUNE_NONE); + if(!iface_->databasesAreOpen()) + LOGERR << "ERROR OPENING DATABASES FOR TESTING!"; + + blkHash0 = READHEX("6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000"); + blkHash1 = READHEX("1b5514b83257d924be7f10c65b95b1f3c0e50081e1dfd8943eece5eb00000000"); + blkHash2 = READHEX("979fc39616bf1dc6b1f88167f76383d44d65ccd0fc99b7f91bcb2c9500000000"); + blkHash3 = READHEX("50f8231e5fd476f470e1ba4937bc97cb304136c96c765339308935bc00000000"); + blkHash4 = READHEX("8e121ba0d275f49a21bbc171d7d49890de13c9b9733e0104654d262f00000000"); + blkHash3A= READHEX("dd63f62ef59d5b6a6da2a36407f76e4e28026a3fd3a46700d284424700000000"); + blkHash4A= READHEX("bfa204022816102169b4e1d4f78cdf77258048f6d14282144cc01d5500000000"); + blkHash5A= READHEX("4e049fd71ef7381a73e4f550d97812d1eb0fbd1489c1774e18855f1900000000"); + + addrA_ = READHEX("62e907b15cbf27d5425399ebf6f0fb50ebb88f18"); + addrB_ = READHEX("ee26c56fc1d942be8d7a24b2a1001dd894693980"); + addrC_ = READHEX("cb2abde8bccacc32e893df3a054b9ef7f227a4ce"); + addrD_ = READHEX("c522664fb0e55cdc5c0cea73b4aad97ec8343232"); + + scrAddrA_ = HASH160PREFIX + addrA_; + scrAddrB_ = HASH160PREFIX + addrB_; + scrAddrC_ = HASH160PREFIX + addrC_; + scrAddrD_ = HASH160PREFIX + addrD_; + + } + + + ///////////////////////////////////////////////////////////////////////////// + virtual void TearDown(void) + { + BlockDataManager_LevelDB::DestroyInstance(); + + rmdir(blkdir_); + rmdir(homedir_); + + char* delstr = new char[4096]; + sprintf(delstr, "%s/level*", ldbdir_.c_str()); + rmdir(delstr); + delete[] delstr; + + LOGENABLESTDOUT(); + } + + + +#if ! defined(_MSC_VER) && ! defined(__MINGW32__) + + ///////////////////////////////////////////////////////////////////////////// + void rmdir(string src) + { + char* syscmd = new char[4096]; + sprintf(syscmd, "rm -rf %s", src.c_str()); + system(syscmd); + delete[] syscmd; + } + + ///////////////////////////////////////////////////////////////////////////// + void mkdir(string newdir) + { + char* syscmd = new char[4096]; + sprintf(syscmd, "mkdir -p %s", newdir.c_str()); + system(syscmd); + delete[] syscmd; + } +#endif + + InterfaceToLDB* iface_; + BinaryData magic_; + BinaryData ghash_; + BinaryData gentx_; + BinaryData zeros_; + + string blkdir_; + string homedir_; + string ldbdir_; + string blk0dat_;; + + BinaryData blkHash0; + BinaryData blkHash1; + BinaryData blkHash2; + BinaryData blkHash3; + BinaryData blkHash4; + BinaryData blkHash3A; + BinaryData blkHash4A; + BinaryData blkHash5A; + + BinaryData addrA_; + BinaryData addrB_; + BinaryData addrC_; + BinaryData addrD_; + BinaryData scrAddrA_; + BinaryData scrAddrB_; + BinaryData scrAddrC_; + BinaryData scrAddrD_; +}; + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockUtilsSuper, HeadersOnly) +{ + EXPECT_EQ(TheBDM.getNumBlocks(), 0); + TheBDM.processNewHeadersInBlkFiles(0); + + EXPECT_EQ(TheBDM.getNumBlocks(), 5); + EXPECT_EQ(TheBDM.getTopBlockHeight(), 4); + EXPECT_EQ(TheBDM.getTopBlockHash(), blkHash4); + EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 4); + //iface_->printAllDatabaseEntries(HEADERS); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockUtilsSuper, HeadersOnly_Reorg) +{ + SETLOGLEVEL(LogLvlError); + EXPECT_EQ(TheBDM.getNumBlocks(), 0); + TheBDM.processNewHeadersInBlkFiles(0); + + EXPECT_EQ(TheBDM.getNumBlocks(), 5); + EXPECT_EQ(TheBDM.getTopBlockHeight(), 4); + + EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 4); + EXPECT_EQ(iface_->getTopBlockHash(HEADERS), blkHash4); + + BtcUtils::copyFile("../reorgTest/blk_3A.dat", BtcUtils::getBlkFilename(blkdir_, 1)); + TheBDM.processNewHeadersInBlkFiles(1); + EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 4); + EXPECT_EQ(iface_->getTopBlockHash(HEADERS), blkHash4); + EXPECT_FALSE(TheBDM.getHeaderByHash(blkHash3A)->isMainBranch()); + EXPECT_TRUE( TheBDM.getHeaderByHash(blkHash3 )->isMainBranch()); + + BtcUtils::copyFile("../reorgTest/blk_4A.dat", BtcUtils::getBlkFilename(blkdir_, 2)); + TheBDM.processNewHeadersInBlkFiles(2); + EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 4); + EXPECT_EQ(iface_->getTopBlockHash(HEADERS), blkHash4); + EXPECT_FALSE(TheBDM.getHeaderByHash(blkHash3A)->isMainBranch()); + EXPECT_TRUE( TheBDM.getHeaderByHash(blkHash3 )->isMainBranch()); + EXPECT_FALSE(TheBDM.getHeaderByHash(blkHash4A)->isMainBranch()); + EXPECT_TRUE( TheBDM.getHeaderByHash(blkHash4 )->isMainBranch()); + + BtcUtils::copyFile("../reorgTest/blk_5A.dat", BtcUtils::getBlkFilename(blkdir_, 3)); + TheBDM.processNewHeadersInBlkFiles(3); + EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 5); + EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 5); + EXPECT_EQ(iface_->getTopBlockHash(HEADERS), blkHash5A); + EXPECT_FALSE(TheBDM.getHeaderByHash(blkHash3 )->isMainBranch()); + EXPECT_TRUE( TheBDM.getHeaderByHash(blkHash3A)->isMainBranch()); + EXPECT_FALSE(TheBDM.getHeaderByHash(blkHash4 )->isMainBranch()); + EXPECT_TRUE( TheBDM.getHeaderByHash(blkHash4A)->isMainBranch()); + + SETLOGLEVEL(LogLvlDebug2); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockUtilsSuper, Load5Blocks) +{ + DBUtils.setArmoryDbType(ARMORY_DB_SUPER); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + TheBDM.doInitialSyncOnLoad(); + + StoredScriptHistory ssh; + + iface_->getStoredScriptHistory(ssh, scrAddrA_); + EXPECT_EQ(ssh.getScriptBalance(), 100*COIN); + EXPECT_EQ(ssh.getScriptReceived(), 100*COIN); + EXPECT_EQ(ssh.totalTxioCount_, 2); + + iface_->getStoredScriptHistory(ssh, scrAddrB_); + EXPECT_EQ(ssh.getScriptBalance(), 0*COIN); + EXPECT_EQ(ssh.getScriptReceived(), 140*COIN); + EXPECT_EQ(ssh.totalTxioCount_, 3); + + iface_->getStoredScriptHistory(ssh, scrAddrC_); + EXPECT_EQ(ssh.getScriptBalance(), 50*COIN); + EXPECT_EQ(ssh.getScriptReceived(), 60*COIN); + EXPECT_EQ(ssh.totalTxioCount_, 2); + + iface_->getStoredScriptHistory(ssh, scrAddrD_); + EXPECT_EQ(ssh.getScriptBalance(), 100*COIN); + EXPECT_EQ(ssh.getScriptReceived(), 100*COIN); + EXPECT_EQ(ssh.totalTxioCount_, 3); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockUtilsSuper, Load4BlocksPlus1) +{ + // Copy only the first four blocks. Will copy the full file next to test + // readBlkFileUpdate method on non-reorg blocks. + BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_, 1596); + TheBDM.doInitialSyncOnLoad(); + EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 3); + EXPECT_EQ(iface_->getTopBlockHash(HEADERS), blkHash3); + EXPECT_TRUE(TheBDM.getHeaderByHash(blkHash3)->isMainBranch()); + + BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_); + TheBDM.readBlkFileUpdate(); + EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 4); + EXPECT_EQ(iface_->getTopBlockHash(HEADERS), blkHash4); + EXPECT_TRUE(TheBDM.getHeaderByHash(blkHash4)->isMainBranch()); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockUtilsSuper, Load5Blocks_Plus2NoReorg) +{ + DBUtils.setArmoryDbType(ARMORY_DB_SUPER); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + TheBDM.doInitialSyncOnLoad(); + + + BtcUtils::copyFile("../reorgTest/blk_3A.dat", blk0dat_); + TheBDM.readBlkFileUpdate(); + EXPECT_EQ(TheBDM.getTopBlockHash(), blkHash4); + EXPECT_EQ(TheBDM.getTopBlockHeight(), 4); + + BtcUtils::copyFile("../reorgTest/blk_4A.dat", blk0dat_); + TheBDM.readBlkFileUpdate(); + EXPECT_EQ(TheBDM.getTopBlockHash(), blkHash4); + EXPECT_EQ(TheBDM.getTopBlockHeight(), 4); + + //BtcUtils::copyFile("../reorgTest/blk_5A.dat", blk0dat_); + //iface_->pprintBlkDataDB(BLKDATA); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockUtilsSuper, Load5Blocks_FullReorg) +{ + DBUtils.setArmoryDbType(ARMORY_DB_SUPER); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + TheBDM.doInitialSyncOnLoad(); + + BtcUtils::copyFile("../reorgTest/blk_3A.dat", blk0dat_); + TheBDM.readBlkFileUpdate(); + BtcUtils::copyFile("../reorgTest/blk_4A.dat", blk0dat_); + TheBDM.readBlkFileUpdate(); + BtcUtils::copyFile("../reorgTest/blk_5A.dat", blk0dat_); + TheBDM.readBlkFileUpdate(); + + //iface_->pprintBlkDataDB(BLKDATA); + StoredScriptHistory ssh; + + iface_->getStoredScriptHistory(ssh, scrAddrA_); + EXPECT_EQ(ssh.getScriptBalance(), 150*COIN); + EXPECT_EQ(ssh.getScriptReceived(), 150*COIN); + EXPECT_EQ(ssh.totalTxioCount_, 3); + + iface_->getStoredScriptHistory(ssh, scrAddrB_); + EXPECT_EQ(ssh.getScriptBalance(), 10*COIN); + EXPECT_EQ(ssh.getScriptReceived(), 150*COIN); + EXPECT_EQ(ssh.totalTxioCount_, 4); + + iface_->getStoredScriptHistory(ssh, scrAddrC_); + EXPECT_EQ(ssh.getScriptBalance(), 0*COIN); + EXPECT_EQ(ssh.getScriptReceived(), 10*COIN); + EXPECT_EQ(ssh.totalTxioCount_, 1); + + iface_->getStoredScriptHistory(ssh, scrAddrD_); + EXPECT_EQ(ssh.getScriptBalance(), 140*COIN); + EXPECT_EQ(ssh.getScriptReceived(), 140*COIN); + EXPECT_EQ(ssh.totalTxioCount_, 3); +} + +//////////////////////////////////////////////////////////////////////////////// +// These next two tests disabled because they broke after ARMORY_DB_BARE impl +TEST_F(BlockUtilsSuper, DISABLED_RestartDBAfterBuild) +{ + // Copy only the first four blocks. Will copy the full file next to test + // readBlkFileUpdate method on non-reorg blocks. + BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_, 926); + TheBDM.doInitialSyncOnLoad(); + EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 2); + EXPECT_EQ(iface_->getTopBlockHash(HEADERS), blkHash2); + EXPECT_TRUE(TheBDM.getHeaderByHash(blkHash2)->isMainBranch()); + TheBDM.DestroyInstance(); + + // Add two more blocks + BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_); + + // Now reinitialize the DB and hopefully detect the new blocks and update + TheBDM.SelectNetwork("Main"); + TheBDM.SetBlkFileLocation(blkdir_); + TheBDM.SetHomeDirLocation(homedir_); + TheBDM.SetLevelDBLocation(ldbdir_); + DBUtils.setArmoryDbType(ARMORY_DB_SUPER); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + + TheBDM.SetDatabaseModes(ARMORY_DB_SUPER, DB_PRUNE_NONE); + TheBDM.doInitialSyncOnLoad(); + + EXPECT_EQ(TheBDM.getTopBlockHeightInDB(HEADERS), 4); + EXPECT_EQ(TheBDM.getTopBlockHeightInDB(BLKDATA), 4); + EXPECT_TRUE(TheBDM.getHeaderByHash(blkHash4)->isMainBranch()); + + StoredScriptHistory ssh; + + iface_->getStoredScriptHistory(ssh, scrAddrA_); + EXPECT_EQ(ssh.getScriptBalance(), 100*COIN); + EXPECT_EQ(ssh.getScriptReceived(), 100*COIN); + EXPECT_EQ(ssh.totalTxioCount_, 2); + + iface_->getStoredScriptHistory(ssh, scrAddrB_); + EXPECT_EQ(ssh.getScriptBalance(), 0*COIN); + EXPECT_EQ(ssh.getScriptReceived(), 140*COIN); + EXPECT_EQ(ssh.totalTxioCount_, 3); + + iface_->getStoredScriptHistory(ssh, scrAddrC_); + EXPECT_EQ(ssh.getScriptBalance(), 50*COIN); + EXPECT_EQ(ssh.getScriptReceived(), 60*COIN); + EXPECT_EQ(ssh.totalTxioCount_, 2); + + iface_->getStoredScriptHistory(ssh, scrAddrD_); + EXPECT_EQ(ssh.getScriptBalance(), 100*COIN); + EXPECT_EQ(ssh.getScriptReceived(), 100*COIN); + EXPECT_EQ(ssh.totalTxioCount_, 3); + +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockUtilsSuper, DISABLED_RestartDBAfterBuild_withReplay) +{ + // Copy only the first four blocks. Will copy the full file next to test + // readBlkFileUpdate method on non-reorg blocks. + BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_, 926); + TheBDM.doInitialSyncOnLoad(); + EXPECT_EQ(iface_->getTopBlockHeight(HEADERS), 2); + EXPECT_EQ(iface_->getTopBlockHash(HEADERS), blkHash2); + EXPECT_TRUE(TheBDM.getHeaderByHash(blkHash2)->isMainBranch()); + TheBDM.DestroyInstance(); + + // Add two more blocks + BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_); + + // Now reinitialize the DB and hopefully detect the new blocks and update + TheBDM.SelectNetwork("Main"); + TheBDM.SetBlkFileLocation(blkdir_); + TheBDM.SetHomeDirLocation(homedir_); + TheBDM.SetLevelDBLocation(ldbdir_); + DBUtils.setArmoryDbType(ARMORY_DB_SUPER); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + + uint32_t replayRewind = 700; + TheBDM.SetDatabaseModes(ARMORY_DB_SUPER, DB_PRUNE_NONE); + TheBDM.doInitialSyncOnLoad(); + + + TheBDM.doInitialSyncOnLoad(); + + EXPECT_EQ(TheBDM.getTopBlockHeightInDB(HEADERS), 4); + EXPECT_EQ(TheBDM.getTopBlockHeightInDB(BLKDATA), 4); + EXPECT_TRUE(TheBDM.getHeaderByHash(blkHash4)->isMainBranch()); + + StoredScriptHistory ssh; + + iface_->getStoredScriptHistory(ssh, scrAddrA_); + EXPECT_EQ(ssh.getScriptBalance(), 100*COIN); + EXPECT_EQ(ssh.getScriptReceived(), 100*COIN); + EXPECT_EQ(ssh.totalTxioCount_, 2); + + iface_->getStoredScriptHistory(ssh, scrAddrB_); + EXPECT_EQ(ssh.getScriptBalance(), 0*COIN); + EXPECT_EQ(ssh.getScriptReceived(), 140*COIN); + EXPECT_EQ(ssh.totalTxioCount_, 3); + + iface_->getStoredScriptHistory(ssh, scrAddrC_); + EXPECT_EQ(ssh.getScriptBalance(), 50*COIN); + EXPECT_EQ(ssh.getScriptReceived(), 60*COIN); + EXPECT_EQ(ssh.totalTxioCount_, 2); + + iface_->getStoredScriptHistory(ssh, scrAddrD_); + EXPECT_EQ(ssh.getScriptBalance(), 100*COIN); + EXPECT_EQ(ssh.getScriptReceived(), 100*COIN); + EXPECT_EQ(ssh.totalTxioCount_, 3); + + // Random note (since I just spent 2 hours trying to figure out why + // I wasn't getting warnings about re-marking TxOuts spent that were + // already marked spent): We get three warnings about TxOuts that + // already marked unspent in the SSH objects when we replay blocks + // 1 and 2 (but not 0). This is expected. But, I also expected a + // warning about a TxOut already marked spent. Turns out that + // we are replaying the previous block first which calls "markUnspent" + // before we hit this mark-spent logic. So when we started the + // method, we actually did have a already-marked-spent TxOut, but + // it was marked unspent before we got the point of trying to mark + // it spent again. In other words, all expected behavior. +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockUtilsSuper, DISABLED_TimeAndSpaceTest_usuallydisabled) +{ + DBUtils.setArmoryDbType(ARMORY_DB_SUPER); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + + string oldblkdir = blkdir_; + //blkdir_ = string("/home/alan/.bitcoin/blks3"); + //blkdir_ = string("/home/alan/.bitcoin/blocks"); + //TheBDM.SelectNetwork("Main"); + blkdir_ = string("/home/alan/.bitcoin/testnet3/blocks"); + TheBDM.SelectNetwork("Test"); + TheBDM.SetBlkFileLocation(blkdir_); + TheBDM.SetHomeDirLocation(homedir_); + + StoredScriptHistory ssh; + TheBDM.doInitialSyncOnLoad(); + BinaryData scrAddr = READHEX("11b366edfc0a8b66feebae5c2e25a7b6a5d1cf31"); + BinaryData scrAddr2 = READHEX("39aa3d569e06a1d7926dc4be1193c99bf2eb9ee0"); + BinaryData scrAddr3 = READHEX("758e51b5e398a32c6abd091b3fde383291267cfa"); + BinaryData scrAddr4 = READHEX("6c22eb00e3f93acac5ae5d81a9db78a645dfc9c7"); + EXPECT_EQ(TheBDM.getDBBalanceForHash160(scrAddr), 18*COIN); + TheBDM.pprintSSHInfoAboutHash160(scrAddr); + TheBDM.pprintSSHInfoAboutHash160(scrAddr2); + TheBDM.pprintSSHInfoAboutHash160(scrAddr3); + TheBDM.pprintSSHInfoAboutHash160(scrAddr4); + blkdir_ = oldblkdir; + LOGINFO << "waiting... (please copy the DB dir...)"; + int pause; + cin >> pause; +} + + + + + + +//////////////////////////////////////////////////////////////////////////////// +// I thought I was going to do something different with this set of tests, +// but I ended up with an exact copy of the BlockUtilsSuper fixture. Oh well. +class BlockUtilsWithWalletTest: public ::testing::Test +{ +protected: + ///////////////////////////////////////////////////////////////////////////// + virtual void SetUp(void) + { + LOGDISABLESTDOUT(); + iface_ = LevelDBWrapper::GetInterfacePtr(); + magic_ = READHEX(MAINNET_MAGIC_BYTES); + ghash_ = READHEX(MAINNET_GENESIS_HASH_HEX); + gentx_ = READHEX(MAINNET_GENESIS_TX_HASH_HEX); + zeros_ = READHEX("00000000"); + + blkdir_ = string("./blkfiletest"); + homedir_ = string("./fakehomedir"); + ldbdir_ = string("./ldbtestdir"); + + mkdir(blkdir_); + mkdir(homedir_); + + // Put the first 5 blocks into the blkdir + blk0dat_ = BtcUtils::getBlkFilename(blkdir_, 0); + BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_); + + TheBDM.SetDatabaseModes(ARMORY_DB_SUPER, DB_PRUNE_NONE); + TheBDM.SelectNetwork("Main"); + TheBDM.SetBlkFileLocation(blkdir_); + TheBDM.SetHomeDirLocation(homedir_); + TheBDM.SetLevelDBLocation(ldbdir_); + + blkHash0 = READHEX("6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000"); + blkHash1 = READHEX("1b5514b83257d924be7f10c65b95b1f3c0e50081e1dfd8943eece5eb00000000"); + blkHash2 = READHEX("979fc39616bf1dc6b1f88167f76383d44d65ccd0fc99b7f91bcb2c9500000000"); + blkHash3 = READHEX("50f8231e5fd476f470e1ba4937bc97cb304136c96c765339308935bc00000000"); + blkHash4 = READHEX("8e121ba0d275f49a21bbc171d7d49890de13c9b9733e0104654d262f00000000"); + blkHash3A= READHEX("dd63f62ef59d5b6a6da2a36407f76e4e28026a3fd3a46700d284424700000000"); + blkHash4A= READHEX("bfa204022816102169b4e1d4f78cdf77258048f6d14282144cc01d5500000000"); + blkHash5A= READHEX("4e049fd71ef7381a73e4f550d97812d1eb0fbd1489c1774e18855f1900000000"); + + addrA_ = READHEX("62e907b15cbf27d5425399ebf6f0fb50ebb88f18"); + addrB_ = READHEX("ee26c56fc1d942be8d7a24b2a1001dd894693980"); + addrC_ = READHEX("cb2abde8bccacc32e893df3a054b9ef7f227a4ce"); + addrD_ = READHEX("c522664fb0e55cdc5c0cea73b4aad97ec8343232"); + + scrAddrA_ = HASH160PREFIX + addrA_; + scrAddrB_ = HASH160PREFIX + addrB_; + scrAddrC_ = HASH160PREFIX + addrC_; + scrAddrD_ = HASH160PREFIX + addrD_; + + } + + + ///////////////////////////////////////////////////////////////////////////// + virtual void TearDown(void) + { + BlockDataManager_LevelDB::DestroyInstance(); + + rmdir(blkdir_); + rmdir(homedir_); + + char* delstr = new char[4096]; + sprintf(delstr, "%s/level*", ldbdir_.c_str()); + rmdir(delstr); + delete[] delstr; + + LOGENABLESTDOUT(); + } + + +#if ! defined(_MSC_VER) && ! defined(__MINGW32__) + + ///////////////////////////////////////////////////////////////////////////// + void rmdir(string src) + { + char* syscmd = new char[4096]; + sprintf(syscmd, "rm -rf %s", src.c_str()); + system(syscmd); + delete[] syscmd; + } + + ///////////////////////////////////////////////////////////////////////////// + void mkdir(string newdir) + { + char* syscmd = new char[4096]; + sprintf(syscmd, "mkdir -p %s", newdir.c_str()); + system(syscmd); + delete[] syscmd; + } +#endif + + InterfaceToLDB* iface_; + BinaryData magic_; + BinaryData ghash_; + BinaryData gentx_; + BinaryData zeros_; + + string blkdir_; + string homedir_; + string ldbdir_; + string blk0dat_;; + + BinaryData blkHash0; + BinaryData blkHash1; + BinaryData blkHash2; + BinaryData blkHash3; + BinaryData blkHash4; + BinaryData blkHash3A; + BinaryData blkHash4A; + BinaryData blkHash5A; + + BinaryData addrA_; + BinaryData addrB_; + BinaryData addrC_; + BinaryData addrD_; + + BinaryData scrAddrA_; + BinaryData scrAddrB_; + BinaryData scrAddrC_; + BinaryData scrAddrD_; +}; + + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockUtilsWithWalletTest, PreRegisterScrAddrs) +{ + BtcWallet wlt; + wlt.addScrAddress(scrAddrA_); + wlt.addScrAddress(scrAddrB_); + wlt.addScrAddress(scrAddrC_); + TheBDM.registerWallet(&wlt); + TheBDM.registerNewScrAddr(scrAddrD_); + + BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_); + TheBDM.doInitialSyncOnLoad(); + + TheBDM.fetchAllRegisteredScrAddrData(); + TheBDM.scanBlockchainForTx(wlt); + + uint64_t balanceWlt; + uint64_t balanceDB; + + balanceWlt = wlt.getScrAddrObjByKey(scrAddrA_).getFullBalance(); + balanceDB = iface_->getBalanceForScrAddr(scrAddrA_); + EXPECT_EQ(balanceWlt, 100*COIN); + EXPECT_EQ(balanceDB, 100*COIN); + + balanceWlt = wlt.getScrAddrObjByKey(scrAddrB_).getFullBalance(); + balanceDB = iface_->getBalanceForScrAddr(scrAddrB_); + EXPECT_EQ(balanceWlt, 0*COIN); + EXPECT_EQ(balanceDB, 0*COIN); + + balanceWlt = wlt.getScrAddrObjByKey(scrAddrC_).getFullBalance(); + balanceDB = iface_->getBalanceForScrAddr(scrAddrC_); + EXPECT_EQ(balanceWlt, 50*COIN); + EXPECT_EQ(balanceDB, 50*COIN); + + balanceWlt = wlt.getScrAddrObjByKey(scrAddrD_).getFullBalance(); + balanceDB = iface_->getBalanceForScrAddr(scrAddrD_); + EXPECT_EQ(balanceWlt, 0*COIN); // D is not part of the wallet + EXPECT_EQ(balanceDB, 100*COIN); +} + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockUtilsWithWalletTest, PostRegisterScrAddr) +{ + BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_); + TheBDM.doInitialSyncOnLoad(); + + // We do all the database stuff first, THEN load the addresses + BtcWallet wlt; + wlt.addScrAddress(scrAddrA_); + wlt.addScrAddress(scrAddrB_); + wlt.addScrAddress(scrAddrC_); + TheBDM.registerWallet(&wlt); + TheBDM.registerNewScrAddr(scrAddrD_); + TheBDM.fetchAllRegisteredScrAddrData(); + TheBDM.scanRegisteredTxForWallet(wlt); + + uint64_t balanceWlt; + uint64_t balanceDB; + + balanceWlt = wlt.getScrAddrObjByKey(scrAddrA_).getFullBalance(); + balanceDB = iface_->getBalanceForScrAddr(scrAddrA_); + EXPECT_EQ(balanceWlt, 100*COIN); + EXPECT_EQ(balanceDB, 100*COIN); + + balanceWlt = wlt.getScrAddrObjByKey(scrAddrB_).getFullBalance(); + balanceDB = iface_->getBalanceForScrAddr(scrAddrB_); + EXPECT_EQ(balanceWlt, 0*COIN); + EXPECT_EQ(balanceDB, 0*COIN); + + balanceWlt = wlt.getScrAddrObjByKey(scrAddrC_).getFullBalance(); + balanceDB = iface_->getBalanceForScrAddr(scrAddrC_); + EXPECT_EQ(balanceWlt, 50*COIN); + EXPECT_EQ(balanceDB, 50*COIN); + + balanceWlt = wlt.getScrAddrObjByKey(scrAddrD_).getFullBalance(); + balanceDB = iface_->getBalanceForScrAddr(scrAddrD_); + EXPECT_EQ(balanceWlt, 0*COIN); // D is not part of the wallet + EXPECT_EQ(balanceDB, 100*COIN); +} + + +// Comments need to be added.... +// Most of this data is from the BIP32 test vectors. +class TestCryptoECDSA : public ::testing::Test +{ +protected: + ///////////////////////////////////////////////////////////////////////////// + virtual void SetUp(void) + { + verifyX = READHEX("39a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2"); + verifyY = READHEX("3cbe7ded0e7ce6a594896b8f62888fdbc5c8821305e2ea42bf01e37300116281"); + + multScalarA = READHEX("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"); + multScalarB = READHEX("483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8"); + multRes = READHEX("805714a252d0c0b58910907e85b5b801fff610a36bdf46847a4bf5d9ae2d10ed"); + + multScalar = READHEX("04bfb2dd60fa8921c2a4085ec15507a921f49cdc839f27f0f280e9c1495d44b5"); + multPointX = READHEX("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"); + multPointY = READHEX("483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8"); + multPointRes = READHEX("7f8bd85f90169a606b0b4323c70e5a12e8a89cbc76647b6ed6a39b4b53825214c590a32f111f857573cf8f2c85d969815e4dd35ae0dc9c7e868195c309b8bada"); + + addAX = READHEX("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"); + addAY = READHEX("483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8"); + addBX = READHEX("5a784662a4a20a65bf6aab9ae98a6c068a81c52e4b032c0fb5400c706cfccc56"); + addBY = READHEX("7f717885be239daadce76b568958305183ad616ff74ed4dc219a74c26d35f839"); + addRes = READHEX("fe2f7c8109d9ae628856d51a02ab25300a8757e088fc336d75cb8dc4cc2ce3339013be71e57c3abeee6ad158646df81d92f8c0778f88100eeb61535f9ff9776d"); + + invAX = READHEX("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"); + invAY = READHEX("483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8"); + invRes = READHEX("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798b7c52588d95c3b9aa25b0403f1eef75702e84bb7597aabe663b82f6f04ef2777"); + + compPointPrv1 = READHEX("000f479245fb19a38a1954c5c7c0ebab2f9bdfd96a17563ef28a6a4b1a2a764ef4"); + compPointPub1 = READHEX("02e8445082a72f29b75ca48748a914df60622a609cacfce8ed0e35804560741d29"); + uncompPointPub1 = READHEX("04e8445082a72f29b75ca48748a914df60622a609cacfce8ed0e35804560741d292728ad8d58a140050c1016e21f285636a580f4d2711b7fac3957a594ddf416a0"); + + compPointPrv2 = READHEX("00e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35"); + compPointPub2 = READHEX("0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2"); + uncompPointPub2 = READHEX("0439a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c23cbe7ded0e7ce6a594896b8f62888fdbc5c8821305e2ea42bf01e37300116281"); + + invModRes = READHEX("000000000000000000000000000000000000000000000000000000000000006b"); + + LOGDISABLESTDOUT(); + } + + + ///////////////////////////////////////////////////////////////////////////// + virtual void TearDown(void) + { + } + + + SecureBinaryData verifyX; + SecureBinaryData verifyY; + + SecureBinaryData multScalarA; + SecureBinaryData multScalarB; + SecureBinaryData multRes; + + SecureBinaryData multScalar; + SecureBinaryData multPointX; + SecureBinaryData multPointY; + SecureBinaryData multPointRes; + + SecureBinaryData addAX; + SecureBinaryData addAY; + SecureBinaryData addBX; + SecureBinaryData addBY; + SecureBinaryData addRes; + + SecureBinaryData invAX; + SecureBinaryData invAY; + SecureBinaryData invRes; + + SecureBinaryData compPointPrv1; + SecureBinaryData uncompPointPub1; + SecureBinaryData compPointPub1; + SecureBinaryData compPointPrv2; + SecureBinaryData uncompPointPub2; + SecureBinaryData compPointPub2; + + SecureBinaryData invModRes; +}; + +// Verify that a point known to be on the secp256k1 curve is recognized as such. +//////////////////////////////////////////////////////////////////////////////// +TEST_F(TestCryptoECDSA, VerifySECP256K1Point) +{ + EXPECT_TRUE(CryptoECDSA().ECVerifyPoint(verifyX, verifyY)); +} + +// Multiply two scalars and check the result. +//////////////////////////////////////////////////////////////////////////////// +TEST_F(TestCryptoECDSA, SECP256K1MultScalars) +{ + SecureBinaryData testRes = CryptoECDSA().ECMultiplyScalars(multScalarA, + multScalarB); + EXPECT_EQ(multRes, testRes); +} + +//////////////////////////////////////////////////////////////////////////////// +/* Never got around to finishing this... +class TestMainnetBlkchain: public ::testing::Test +{ +protected: + ///////////////////////////////////////////////////////////////////////////// + virtual void SetUp(void) + { + iface_ = LevelDBWrapper::GetInterfacePtr(); + magic_ = READHEX(MAINNET_MAGIC_BYTES); + ghash_ = READHEX(MAINNET_GENESIS_HASH_HEX); + gentx_ = READHEX(MAINNET_GENESIS_TX_HASH_HEX); + zeros_ = READHEX("00000000"); + DBUtils.setArmoryDbType(ARMORY_DB_FULL); + DBUtils.setDbPruneType(DB_PRUNE_NONE); + + blkdir_ = string("/home/alan/.bitcoin"); + homedir_ = string("./fakehomedir"); + ldbdir_ = string("/home/alan/ARMORY_DB_257k_BLKS"); + + iface_->openDatabases( ldbdir_, ghash_, gentx_, magic_, + ARMORY_DB_SUPER, DB_PRUNE_NONE); + if(!iface_->databasesAreOpen()) + LOGERR << "ERROR OPENING DATABASES FOR TESTING!"; + + mkdir(homedir_); + + TheBDM.SelectNetwork("Main"); + TheBDM.SetBlkFileLocation(blkdir_); + TheBDM.SetHomeDirLocation(homedir_); + TheBDM.SetLevelDBLocation(ldbdir_); + + addrA_ = READHEX("b077a2b5e8a53f1d3ef4100117125de6a5b15f6b"); + addrB_ = READHEX("4c765bca17f9881ad6d4336a1d4ec34a091e5a6f"); + + scrAddrA_ = HASH160PREFIX + addrA_; + scrAddrB_ = HASH160PREFIX + addrB_; + + LOGDISABLESTDOUT(); + } + + + ///////////////////////////////////////////////////////////////////////////// + virtual void TearDown(void) + { + rmdir(homedir_); + + BlockDataManager_LevelDB::DestroyInstance(); + LOGENABLESTDOUT(); + } + + + ///////////////////////////////////////////////////////////////////////////// + void rmdir(string src) + { + char* syscmd = new char[4096]; + sprintf(syscmd, "rm -rf %s", src.c_str()); + system(syscmd); + delete[] syscmd; + } + + ///////////////////////////////////////////////////////////////////////////// + void mkdir(string newdir) + { + char* syscmd = new char[4096]; + sprintf(syscmd, "mkdir -p %s", newdir.c_str()); + system(syscmd); + delete[] syscmd; + } +#endif + + InterfaceToLDB* iface_; + BinaryData magic_; + BinaryData ghash_; + BinaryData gentx_; + BinaryData zeros_; + + string blkdir_; + string homedir_; + string ldbdir_; + string blk0dat_;; + + BinaryData blkHash0; + BinaryData blkHash1; + BinaryData blkHash2; + BinaryData blkHash3; + BinaryData blkHash4; + BinaryData blkHash3A; + BinaryData blkHash4A; + BinaryData blkHash5A; + + BinaryData addrA_; + BinaryData addrB_; + BinaryData addrC_; + BinaryData addrD_; + + BinaryData scrAddrA_; + BinaryData scrAddrB_; + BinaryData scrAddrC_; + BinaryData scrAddrD_; +}; + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockUtilsWithWalletTest, TestBalanceMainnet_usuallydisabled) +{ + TheBDM.doInitialSyncOnLoad(); + + // We do all the database stuff first, THEN load the addresses + BtcWallet wlt; + wlt.addScrAddress(scrAddrA_); + wlt.addScrAddress(scrAddrB_); + TheBDM.registerWallet(&wlt); + TheBDM.registerNewScrAddr(scrAddrD_); + TheBDM.fetchAllRegisteredScrAddrData(); + TheBDM.scanRegisteredTxForWallet(wlt); + + uint64_t balanceWlt; + uint64_t balanceDB; + + balanceWlt = wlt.getScrAddrObjByKey(scrAddrA_).getFullBalance(); + balanceDB = iface_->getBalanceForScrAddr(scrAddrA_); + EXPECT_EQ(balanceWlt, 100*COIN); + EXPECT_EQ(balanceDB, 100*COIN); + + balanceWlt = wlt.getScrAddrObjByKey(scrAddrB_).getFullBalance(); + balanceDB = iface_->getBalanceForScrAddr(scrAddrB_); + EXPECT_EQ(balanceWlt, 0*COIN); + EXPECT_EQ(balanceDB, 0*COIN); + + balanceWlt = wlt.getScrAddrObjByKey(scrAddrC_).getFullBalance(); + balanceDB = iface_->getBalanceForScrAddr(scrAddrC_); + EXPECT_EQ(balanceWlt, 50*COIN); + EXPECT_EQ(balanceDB, 50*COIN); + + balanceWlt = wlt.getScrAddrObjByKey(scrAddrD_).getFullBalance(); + balanceDB = iface_->getBalanceForScrAddr(scrAddrD_); + EXPECT_EQ(balanceWlt, 0*COIN); // D is not part of the wallet + EXPECT_EQ(balanceDB, 100*COIN); +} +*/ + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(BlockUtilsWithWalletTest, ZeroConfUpdate) +{ + // Copy only the first four blocks + BtcUtils::copyFile("../reorgTest/blk_0_to_4.dat", blk0dat_, 513); + + BtcWallet wlt; + wlt.addScrAddress(scrAddrA_); + wlt.addScrAddress(scrAddrB_); + wlt.addScrAddress(scrAddrC_); + wlt.addScrAddress(scrAddrD_); + + TheBDM.registerWallet(&wlt); + + TheBDM.doInitialSyncOnLoad(); + TheBDM.fetchAllRegisteredScrAddrData(); + TheBDM.scanRegisteredTxForWallet(wlt); + + uint64_t balanceWlt; + uint64_t balanceDB; + + EXPECT_EQ(wlt.getScrAddrObjByKey(scrAddrA_).getFullBalance(), 50*COIN); + EXPECT_EQ(wlt.getScrAddrObjByKey(scrAddrB_).getFullBalance(), 50*COIN); + EXPECT_EQ(wlt.getScrAddrObjByKey(scrAddrC_).getFullBalance(), 0*COIN); + EXPECT_EQ(wlt.getScrAddrObjByKey(scrAddrD_).getFullBalance(), 0*COIN); + + BinaryData txWithChangeHash = READHEX( + "7f47caaade4bd25b1dc8639411600fd5c279e402bd01c0a0b3c703caf05cc229"); + BinaryData txWithChange = READHEX( + "0100000001aee7e7fc832d028f454d4fa1ca60ba2f1760d35a80570cb63fe0d6" + "dd4755087a000000004a49304602210038fcc428e8f28ebea2e8682a611ac301" + "2aedf5289535f3776c3b3acf5fbcff74022100c51c373fab30abd0e9a594be13" + "8bdd99a21cdcdb2258cf9795c3d569ac25c3aa01ffffffff0200ca9a3b000000" + "001976a914cb2abde8bccacc32e893df3a054b9ef7f227a4ce88ac00286bee00" + "0000001976a914ee26c56fc1d942be8d7a24b2a1001dd89469398088ac000000" + "00"); + + ///// + TheBDM.addNewZeroConfTx(txWithChange, 1300000000, false); + TheBDM.rescanWalletZeroConf(wlt); + + EXPECT_EQ(wlt.getScrAddrObjByKey(scrAddrA_).getFullBalance(), 50*COIN); + EXPECT_EQ(wlt.getScrAddrObjByKey(scrAddrB_).getFullBalance(), 40*COIN); + EXPECT_EQ(wlt.getScrAddrObjByKey(scrAddrC_).getFullBalance(), 10*COIN); + EXPECT_EQ(wlt.getScrAddrObjByKey(scrAddrD_).getFullBalance(), 0*COIN); + + +} + +// This was really just to time the logging to determine how much impact it +// has. It looks like writing to file is about 1,000,000 logs/sec, while +// writing to the null stream (below the threshold log level) is about +// 2,200,000/sec. As long as we use log messages sparingly (and timer +// calls which call the logger), there will be no problem leaving them +// on even in production code. +/* +TEST(TimeDebugging, WriteToLogNoStdOut) +{ + LOGDISABLESTDOUT(); + for(uint32_t i=0; i<1000000; i++) + LOGERR << "Testing writing out " << 3 << " diff things"; + LOGENABLESTDOUT(); +} + +TEST(TimeDebugging, WriteNull) +{ + for(uint32_t i=0; i<1000000; i++) + LOGDEBUG4 << "Testing writing out " << 3 << " diff things"; +} +*/ + + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +// Now actually execute all the tests +//////////////////////////////////////////////////////////////////////////////// +GTEST_API_ int main(int argc, char **argv) +{ + std::cout << "Running main() from gtest_main.cc\n"; + + // Setup the log file + STARTLOGGING("cppTestsLog.txt", LogLvlDebug2); + //LOGDISABLESTDOUT(); + + testing::InitGoogleTest(&argc, argv); + int exitCode = RUN_ALL_TESTS(); + + FLUSHLOG(); + + + return exitCode; +} diff --git a/cppForSwig/gtest/Makefile b/cppForSwig/gtest/Makefile index 00247b47c..fb4a7be88 100644 --- a/cppForSwig/gtest/Makefile +++ b/cppForSwig/gtest/Makefile @@ -28,8 +28,8 @@ CPPFLAGS += -I$(GTEST_DIR) \ -L$(USER_DIR)/cryptopp \ -L$(USER_DIR)/leveldb \ -D__STDC_LIMIT_MACROS \ - -g \ - #-D_DEBUG \ + -D_DEBUG \ + -g #-O2 \ HEADERS += $(USER_DIR)/BinaryData.h \ @@ -54,7 +54,7 @@ OBJECTS += BinaryData.o \ # Flags passed to the C++ compiler. CXXFLAGS += -g -#CXXFLAGS += -O2 +#CXXFLAGS += -O2 -DNDEBUG -pipe -fPIC # All tests produced by this Makefile. Remember to add new tests you # created to the list. diff --git a/cppForSwig/guardian/guardian.cpp b/cppForSwig/guardian/guardian.cpp index a773333e0..c674e778d 100644 --- a/cppForSwig/guardian/guardian.cpp +++ b/cppForSwig/guardian/guardian.cpp @@ -1,6 +1,6 @@ //////////////////////////////////////////////////////////////////////////////// // // -// Copyright (C) 2011-2013, Alan C. Reiner // +// Copyright (C) 2011-2014, Armory Technologies, Inc. // // Distributed under the GNU Affero General Public License (AGPL v3) // // See LICENSE or http://www.gnu.org/licenses/agpl.html // // // diff --git a/cppForSwig/guardian/guardian.vcxproj b/cppForSwig/guardian/guardian.vcxproj index 063f49178..5a58c146e 100644 --- a/cppForSwig/guardian/guardian.vcxproj +++ b/cppForSwig/guardian/guardian.vcxproj @@ -17,14 +17,6 @@ Release x64 - - WinXP Release - Win32 - - - WinXP Release - x64 - WinXP_32 Win32 @@ -33,22 +25,6 @@ WinXP_32 x64 - - WinXP_release_x86 - Win32 - - - WinXP_release_x86 - x64 - - - Win_XP_32 - Win32 - - - Win_XP_32 - x64 - {D35F732D-55D7-4037-9C6D-E141F740F802} @@ -171,6 +147,7 @@ false + $(SolutionDir)\..\ArmoryStandalone\ false diff --git a/cppForSwig/leveldb/.gitignore b/cppForSwig/leveldb/.gitignore index f03043056..063025144 100644 --- a/cppForSwig/leveldb/.gitignore +++ b/cppForSwig/leveldb/.gitignore @@ -6,3 +6,4 @@ build_config.mk *.so.* *_test db_bench +leveldbutil diff --git a/cppForSwig/leveldb/AUTHORS b/cppForSwig/leveldb/AUTHORS index fc40194ab..2439d7a45 100644 --- a/cppForSwig/leveldb/AUTHORS +++ b/cppForSwig/leveldb/AUTHORS @@ -9,3 +9,4 @@ Sanjay Ghemawat # Partial list of contributors: Kevin Regan +Johan Bilien diff --git a/cppForSwig/leveldb/Makefile b/cppForSwig/leveldb/Makefile index ccb9a1af0..1b9a703f4 100644 --- a/cppForSwig/leveldb/Makefile +++ b/cppForSwig/leveldb/Makefile @@ -6,8 +6,7 @@ # Uncomment exactly one of the lines labelled (A), (B), and (C) below # to switch between compilation modes. -#OPT ?= -O2 -DNDEBUG # (A) Production use (optimized mode) -OPT ?= -fPIC -O2 -DNDEBUG # (A) Production use (optimized mode) +OPT ?= -O2 -fPIC -DNDEBUG # (A) Production use (optimized mode) # OPT ?= -g2 # (B) Debug mode, w/ full line-level debugging symbols # OPT ?= -O2 -g2 -DNDEBUG # (C) Profiling mode: opt, but w/debugging symbols #----------------------------------------------- @@ -32,6 +31,7 @@ TESTHARNESS = ./util/testharness.o $(TESTUTIL) TESTS = \ arena_test \ + autocompact_test \ bloom_test \ c_test \ cache_test \ @@ -44,6 +44,7 @@ TESTS = \ filename_test \ filter_block_test \ issue178_test \ + issue200_test \ log_test \ memenv_test \ skiplist_test \ @@ -71,7 +72,7 @@ SHARED = $(SHARED1) else # Update db.h if you change these. SHARED_MAJOR = 1 -SHARED_MINOR = 12 +SHARED_MINOR = 15 SHARED1 = libleveldb.$(PLATFORM_SHARED_EXT) SHARED2 = $(SHARED1).$(SHARED_MAJOR) SHARED3 = $(SHARED1).$(SHARED_MAJOR).$(SHARED_MINOR) @@ -115,6 +116,9 @@ leveldbutil: db/leveldb_main.o $(LIBOBJECTS) arena_test: util/arena_test.o $(LIBOBJECTS) $(TESTHARNESS) $(CXX) $(LDFLAGS) util/arena_test.o $(LIBOBJECTS) $(TESTHARNESS) -o $@ $(LIBS) +autocompact_test: db/autocompact_test.o $(LIBOBJECTS) $(TESTHARNESS) + $(CXX) $(LDFLAGS) db/autocompact_test.o $(LIBOBJECTS) $(TESTHARNESS) -o $@ $(LIBS) + bloom_test: util/bloom_test.o $(LIBOBJECTS) $(TESTHARNESS) $(CXX) $(LDFLAGS) util/bloom_test.o $(LIBOBJECTS) $(TESTHARNESS) -o $@ $(LIBS) @@ -151,6 +155,9 @@ filter_block_test: table/filter_block_test.o $(LIBOBJECTS) $(TESTHARNESS) issue178_test: issues/issue178_test.o $(LIBOBJECTS) $(TESTHARNESS) $(CXX) $(LDFLAGS) issues/issue178_test.o $(LIBOBJECTS) $(TESTHARNESS) -o $@ $(LIBS) +issue200_test: issues/issue200_test.o $(LIBOBJECTS) $(TESTHARNESS) + $(CXX) $(LDFLAGS) issues/issue200_test.o $(LIBOBJECTS) $(TESTHARNESS) -o $@ $(LIBS) + log_test: db/log_test.o $(LIBOBJECTS) $(TESTHARNESS) $(CXX) $(LDFLAGS) db/log_test.o $(LIBOBJECTS) $(TESTHARNESS) -o $@ $(LIBS) @@ -188,14 +195,14 @@ IOSVERSION=$(shell defaults read $(PLATFORMSROOT)/iPhoneOS.platform/version CFBu mkdir -p ios-x86/$(dir $@) $(CXX) $(CXXFLAGS) -isysroot $(SIMULATORROOT)/SDKs/iPhoneSimulator$(IOSVERSION).sdk -arch i686 -c $< -o ios-x86/$@ mkdir -p ios-arm/$(dir $@) - $(DEVICEROOT)/usr/bin/$(CXX) $(CXXFLAGS) -isysroot $(DEVICEROOT)/SDKs/iPhoneOS$(IOSVERSION).sdk -arch armv6 -arch armv7 -c $< -o ios-arm/$@ + xcrun -sdk iphoneos $(CXX) $(CXXFLAGS) -isysroot $(DEVICEROOT)/SDKs/iPhoneOS$(IOSVERSION).sdk -arch armv6 -arch armv7 -c $< -o ios-arm/$@ lipo ios-x86/$@ ios-arm/$@ -create -output $@ .c.o: mkdir -p ios-x86/$(dir $@) $(CC) $(CFLAGS) -isysroot $(SIMULATORROOT)/SDKs/iPhoneSimulator$(IOSVERSION).sdk -arch i686 -c $< -o ios-x86/$@ mkdir -p ios-arm/$(dir $@) - $(DEVICEROOT)/usr/bin/$(CC) $(CFLAGS) -isysroot $(DEVICEROOT)/SDKs/iPhoneOS$(IOSVERSION).sdk -arch armv6 -arch armv7 -c $< -o ios-arm/$@ + xcrun -sdk iphoneos $(CC) $(CFLAGS) -isysroot $(DEVICEROOT)/SDKs/iPhoneOS$(IOSVERSION).sdk -arch armv6 -arch armv7 -c $< -o ios-arm/$@ lipo ios-x86/$@ ios-arm/$@ -create -output $@ else diff --git a/cppForSwig/leveldb/build_detect_platform b/cppForSwig/leveldb/build_detect_platform index 71574b963..6e59c6f8f 100755 --- a/cppForSwig/leveldb/build_detect_platform +++ b/cppForSwig/leveldb/build_detect_platform @@ -131,6 +131,16 @@ case "$TARGET_OS" in # man ld: +h internal_name PLATFORM_SHARED_LDFLAGS="-shared -Wl,+h -Wl," ;; + IOS) + PLATFORM=IOS + COMMON_FLAGS="$MEMCMP_FLAG -DOS_MACOSX" + [ -z "$INSTALL_PATH" ] && INSTALL_PATH=`pwd` + PORT_FILE=port/port_posix.cc + PLATFORM_SHARED_EXT= + PLATFORM_SHARED_LDFLAGS= + PLATFORM_SHARED_CFLAGS= + PLATFORM_SHARED_VERSIONED= + ;; *) echo "Unknown platform!" >&2 exit 1 @@ -173,6 +183,16 @@ EOF COMMON_FLAGS="$COMMON_FLAGS -DLEVELDB_PLATFORM_POSIX" fi + # Test whether Snappy library is installed + # http://code.google.com/p/snappy/ + $CXX $CXXFLAGS -x c++ - -o $CXXOUTPUT 2>/dev/null < + int main() {} +EOF + if [ "$?" = 0 ]; then + COMMON_FLAGS="$COMMON_FLAGS -DSNAPPY" + PLATFORM_LIBS="$PLATFORM_LIBS -lsnappy" + fi # Test whether tcmalloc is available $CXX $CXXFLAGS -x c++ - -o $CXXOUTPUT -ltcmalloc 2>/dev/null <Write(WriteOptions(), &batch)); + WriteOptions options; + // Corrupt() doesn't work without this sync on windows; stat reports 0 for + // the file size. + if (i == n - 1) { + options.sync = true; + } + ASSERT_OK(db_->Write(options, &batch)); } } @@ -92,6 +96,10 @@ class CorruptionTest { for (iter->SeekToFirst(); iter->Valid(); iter->Next()) { uint64_t key; Slice in(iter->key()); + if (in == "" || in == "~") { + // Ignore boundary keys. + continue; + } if (!ConsumeDecimalNumber(&in, &key) || !in.empty() || key < next_expected) { @@ -123,7 +131,7 @@ class CorruptionTest { FileType type; std::string fname; int picked_number = -1; - for (int i = 0; i < filenames.size(); i++) { + for (size_t i = 0; i < filenames.size(); i++) { if (ParseFileName(filenames[i], &number, &type) && type == filetype && int(number) > picked_number) { // Pick latest file @@ -233,7 +241,23 @@ TEST(CorruptionTest, TableFile) { dbi->TEST_CompactRange(1, NULL, NULL); Corrupt(kTableFile, 100, 1); - Check(99, 99); + Check(90, 99); +} + +TEST(CorruptionTest, TableFileRepair) { + options_.block_size = 2 * kValueSize; // Limit scope of corruption + options_.paranoid_checks = true; + Reopen(); + Build(100); + DBImpl* dbi = reinterpret_cast(db_); + dbi->TEST_CompactMemTable(); + dbi->TEST_CompactRange(0, NULL, NULL); + dbi->TEST_CompactRange(1, NULL, NULL); + + Corrupt(kTableFile, 100, 1); + RepairDB(); + Reopen(); + Check(95, 99); } TEST(CorruptionTest, TableFileIndexData) { @@ -299,7 +323,7 @@ TEST(CorruptionTest, CompactionInputError) { ASSERT_EQ(1, Property("leveldb.num-files-at-level" + NumberToString(last))); Corrupt(kTableFile, 100, 1); - Check(9, 9); + Check(5, 9); // Force compactions by writing lots of values Build(10000); @@ -307,32 +331,23 @@ TEST(CorruptionTest, CompactionInputError) { } TEST(CorruptionTest, CompactionInputErrorParanoid) { - Options options; - options.paranoid_checks = true; - options.write_buffer_size = 1048576; - Reopen(&options); + options_.paranoid_checks = true; + options_.write_buffer_size = 512 << 10; + Reopen(); DBImpl* dbi = reinterpret_cast(db_); - // Fill levels >= 1 so memtable compaction outputs to level 1 - for (int level = 1; level < config::kNumLevels; level++) { - dbi->Put(WriteOptions(), "", "begin"); - dbi->Put(WriteOptions(), "~", "end"); + // Make multiple inputs so we need to compact. + for (int i = 0; i < 2; i++) { + Build(10); dbi->TEST_CompactMemTable(); + Corrupt(kTableFile, 100, 1); + env_.SleepForMicroseconds(100000); } + dbi->CompactRange(NULL, NULL); - Build(10); - dbi->TEST_CompactMemTable(); - ASSERT_EQ(1, Property("leveldb.num-files-at-level0")); - - Corrupt(kTableFile, 100, 1); - Check(9, 9); - - // Write must eventually fail because of corrupted table - Status s; + // Write must fail because of corrupted table std::string tmp1, tmp2; - for (int i = 0; i < 10000 && s.ok(); i++) { - s = db_->Put(WriteOptions(), Key(i, &tmp1), Value(i, &tmp2)); - } + Status s = db_->Put(WriteOptions(), Key(5, &tmp1), Value(5, &tmp2)); ASSERT_TRUE(!s.ok()) << "write did not fail in corrupted paranoid db"; } diff --git a/cppForSwig/leveldb/db/db_bench.cc b/cppForSwig/leveldb/db/db_bench.cc index 7abdf8758..fc46d8969 100644 --- a/cppForSwig/leveldb/db/db_bench.cc +++ b/cppForSwig/leveldb/db/db_bench.cc @@ -128,7 +128,7 @@ class RandomGenerator { pos_ = 0; } - Slice Generate(int len) { + Slice Generate(size_t len) { if (pos_ + len > data_.size()) { pos_ = 0; assert(len < data_.size()); @@ -139,11 +139,11 @@ class RandomGenerator { }; static Slice TrimSpace(Slice s) { - int start = 0; + size_t start = 0; while (start < s.size() && isspace(s[start])) { start++; } - int limit = s.size(); + size_t limit = s.size(); while (limit > start && isspace(s[limit-1])) { limit--; } @@ -399,7 +399,7 @@ class Benchmark { heap_counter_(0) { std::vector files; Env::Default()->GetChildren(FLAGS_db, &files); - for (int i = 0; i < files.size(); i++) { + for (size_t i = 0; i < files.size(); i++) { if (Slice(files[i]).starts_with("heap-")) { Env::Default()->DeleteFile(std::string(FLAGS_db) + "/" + files[i]); } diff --git a/cppForSwig/leveldb/db/db_impl.cc b/cppForSwig/leveldb/db/db_impl.cc index 395d3172a..faf5e7d7b 100644 --- a/cppForSwig/leveldb/db/db_impl.cc +++ b/cppForSwig/leveldb/db/db_impl.cc @@ -113,14 +113,14 @@ Options SanitizeOptions(const std::string& dbname, return result; } -DBImpl::DBImpl(const Options& options, const std::string& dbname) - : env_(options.env), - internal_comparator_(options.comparator), - internal_filter_policy_(options.filter_policy), - options_(SanitizeOptions( - dbname, &internal_comparator_, &internal_filter_policy_, options)), - owns_info_log_(options_.info_log != options.info_log), - owns_cache_(options_.block_cache != options.block_cache), +DBImpl::DBImpl(const Options& raw_options, const std::string& dbname) + : env_(raw_options.env), + internal_comparator_(raw_options.comparator), + internal_filter_policy_(raw_options.filter_policy), + options_(SanitizeOptions(dbname, &internal_comparator_, + &internal_filter_policy_, raw_options)), + owns_info_log_(options_.info_log != raw_options.info_log), + owns_cache_(options_.block_cache != raw_options.block_cache), dbname_(dbname), db_lock_(NULL), shutting_down_(NULL), @@ -130,15 +130,15 @@ DBImpl::DBImpl(const Options& options, const std::string& dbname) logfile_(NULL), logfile_number_(0), log_(NULL), + seed_(0), tmp_batch_(new WriteBatch), bg_compaction_scheduled_(false), - manual_compaction_(NULL), - consecutive_compaction_errors_(0) { + manual_compaction_(NULL) { mem_->Ref(); has_imm_.Release_Store(NULL); // Reserve ten files or so for other uses and give the rest to TableCache. - const int table_cache_size = options.max_open_files - kNumNonTableCacheFiles; + const int table_cache_size = options_.max_open_files - kNumNonTableCacheFiles; table_cache_ = new TableCache(dbname_, &options_, table_cache_size); versions_ = new VersionSet(dbname_, &options_, table_cache_, @@ -216,6 +216,12 @@ void DBImpl::MaybeIgnoreError(Status* s) const { } void DBImpl::DeleteObsoleteFiles() { + if (!bg_error_.ok()) { + // After a background error, we don't know whether a new version may + // or may not have been committed, so we cannot safely garbage collect. + return; + } + // Make a set of all of the live files std::set live = pending_outputs_; versions_->AddLiveFiles(&live); @@ -494,7 +500,7 @@ Status DBImpl::WriteLevel0Table(MemTable* mem, VersionEdit* edit, return s; } -Status DBImpl::CompactMemTable() { +void DBImpl::CompactMemTable() { mutex_.AssertHeld(); assert(imm_ != NULL); @@ -522,9 +528,9 @@ Status DBImpl::CompactMemTable() { imm_ = NULL; has_imm_.Release_Store(NULL); DeleteObsoleteFiles(); + } else { + RecordBackgroundError(s); } - - return s; } void DBImpl::CompactRange(const Slice* begin, const Slice* end) { @@ -567,16 +573,18 @@ void DBImpl::TEST_CompactRange(int level, const Slice* begin,const Slice* end) { } MutexLock l(&mutex_); - while (!manual.done) { - while (manual_compaction_ != NULL) { - bg_cv_.Wait(); - } - manual_compaction_ = &manual; - MaybeScheduleCompaction(); - while (manual_compaction_ == &manual) { + while (!manual.done && !shutting_down_.Acquire_Load() && bg_error_.ok()) { + if (manual_compaction_ == NULL) { // Idle + manual_compaction_ = &manual; + MaybeScheduleCompaction(); + } else { // Running either my compaction or another compaction. bg_cv_.Wait(); } } + if (manual_compaction_ == &manual) { + // Cancel my manual compaction since we aborted early for some reason. + manual_compaction_ = NULL; + } } Status DBImpl::TEST_CompactMemTable() { @@ -595,12 +603,22 @@ Status DBImpl::TEST_CompactMemTable() { return s; } +void DBImpl::RecordBackgroundError(const Status& s) { + mutex_.AssertHeld(); + if (bg_error_.ok()) { + bg_error_ = s; + bg_cv_.SignalAll(); + } +} + void DBImpl::MaybeScheduleCompaction() { mutex_.AssertHeld(); if (bg_compaction_scheduled_) { // Already scheduled } else if (shutting_down_.Acquire_Load()) { // DB is being deleted; no more background compactions + } else if (!bg_error_.ok()) { + // Already got an error; no more changes } else if (imm_ == NULL && manual_compaction_ == NULL && !versions_->NeedsCompaction()) { @@ -618,30 +636,12 @@ void DBImpl::BGWork(void* db) { void DBImpl::BackgroundCall() { MutexLock l(&mutex_); assert(bg_compaction_scheduled_); - if (!shutting_down_.Acquire_Load()) { - Status s = BackgroundCompaction(); - if (s.ok()) { - // Success - consecutive_compaction_errors_ = 0; - } else if (shutting_down_.Acquire_Load()) { - // Error most likely due to shutdown; do not wait - } else { - // Wait a little bit before retrying background compaction in - // case this is an environmental problem and we do not want to - // chew up resources for failed compactions for the duration of - // the problem. - bg_cv_.SignalAll(); // In case a waiter can proceed despite the error - Log(options_.info_log, "Waiting after background compaction error: %s", - s.ToString().c_str()); - mutex_.Unlock(); - ++consecutive_compaction_errors_; - int seconds_to_sleep = 1; - for (int i = 0; i < 3 && i < consecutive_compaction_errors_ - 1; ++i) { - seconds_to_sleep *= 2; - } - env_->SleepForMicroseconds(seconds_to_sleep * 1000000); - mutex_.Lock(); - } + if (shutting_down_.Acquire_Load()) { + // No more background work when shutting down. + } else if (!bg_error_.ok()) { + // No more background work after a background error. + } else { + BackgroundCompaction(); } bg_compaction_scheduled_ = false; @@ -652,11 +652,12 @@ void DBImpl::BackgroundCall() { bg_cv_.SignalAll(); } -Status DBImpl::BackgroundCompaction() { +void DBImpl::BackgroundCompaction() { mutex_.AssertHeld(); if (imm_ != NULL) { - return CompactMemTable(); + CompactMemTable(); + return; } Compaction* c; @@ -690,6 +691,9 @@ Status DBImpl::BackgroundCompaction() { c->edit()->AddFile(c->level() + 1, f->number, f->file_size, f->smallest, f->largest); status = versions_->LogAndApply(c->edit(), &mutex_); + if (!status.ok()) { + RecordBackgroundError(status); + } VersionSet::LevelSummaryStorage tmp; Log(options_.info_log, "Moved #%lld to level-%d %lld bytes %s: %s\n", static_cast(f->number), @@ -700,6 +704,9 @@ Status DBImpl::BackgroundCompaction() { } else { CompactionState* compact = new CompactionState(c); status = DoCompactionWork(compact); + if (!status.ok()) { + RecordBackgroundError(status); + } CleanupCompaction(compact); c->ReleaseInputs(); DeleteObsoleteFiles(); @@ -713,9 +720,6 @@ Status DBImpl::BackgroundCompaction() { } else { Log(options_.info_log, "Compaction error: %s", status.ToString().c_str()); - if (options_.paranoid_checks && bg_error_.ok()) { - bg_error_ = status; - } } if (is_manual) { @@ -731,7 +735,6 @@ Status DBImpl::BackgroundCompaction() { } manual_compaction_ = NULL; } - return status; } void DBImpl::CleanupCompaction(CompactionState* compact) { @@ -1001,6 +1004,9 @@ Status DBImpl::DoCompactionWork(CompactionState* compact) { if (status.ok()) { status = InstallCompactionResults(compact); } + if (!status.ok()) { + RecordBackgroundError(status); + } VersionSet::LevelSummaryStorage tmp; Log(options_.info_log, "compacted to: %s", versions_->LevelSummary(&tmp)); @@ -1027,7 +1033,8 @@ static void CleanupIteratorState(void* arg1, void* arg2) { } // namespace Iterator* DBImpl::NewInternalIterator(const ReadOptions& options, - SequenceNumber* latest_snapshot) { + SequenceNumber* latest_snapshot, + uint32_t* seed) { IterState* cleanup = new IterState; mutex_.Lock(); *latest_snapshot = versions_->LastSequence(); @@ -1051,13 +1058,15 @@ Iterator* DBImpl::NewInternalIterator(const ReadOptions& options, cleanup->version = versions_->current(); internal_iter->RegisterCleanup(CleanupIteratorState, cleanup, NULL); + *seed = ++seed_; mutex_.Unlock(); return internal_iter; } Iterator* DBImpl::TEST_NewInternalIterator() { SequenceNumber ignored; - return NewInternalIterator(ReadOptions(), &ignored); + uint32_t ignored_seed; + return NewInternalIterator(ReadOptions(), &ignored, &ignored_seed); } int64_t DBImpl::TEST_MaxNextLevelOverlappingBytes() { @@ -1114,12 +1123,21 @@ Status DBImpl::Get(const ReadOptions& options, Iterator* DBImpl::NewIterator(const ReadOptions& options) { SequenceNumber latest_snapshot; - Iterator* internal_iter = NewInternalIterator(options, &latest_snapshot); + uint32_t seed; + Iterator* iter = NewInternalIterator(options, &latest_snapshot, &seed); return NewDBIterator( - &dbname_, env_, user_comparator(), internal_iter, + this, user_comparator(), iter, (options.snapshot != NULL ? reinterpret_cast(options.snapshot)->number_ - : latest_snapshot)); + : latest_snapshot), + seed); +} + +void DBImpl::RecordReadSample(Slice key) { + MutexLock l(&mutex_); + if (versions_->current()->RecordReadSample(key)) { + MaybeScheduleCompaction(); + } } const Snapshot* DBImpl::GetSnapshot() { @@ -1172,13 +1190,23 @@ Status DBImpl::Write(const WriteOptions& options, WriteBatch* my_batch) { { mutex_.Unlock(); status = log_->AddRecord(WriteBatchInternal::Contents(updates)); + bool sync_error = false; if (status.ok() && options.sync) { status = logfile_->Sync(); + if (!status.ok()) { + sync_error = true; + } } if (status.ok()) { status = WriteBatchInternal::InsertInto(updates, mem_); } mutex_.Lock(); + if (sync_error) { + // The state of the log file is indeterminate: the log record we + // just added may or may not show up when the DB is re-opened. + // So we force the DB into a mode where all future writes fail. + RecordBackgroundError(status); + } } if (updates == tmp_batch_) tmp_batch_->Clear(); diff --git a/cppForSwig/leveldb/db/db_impl.h b/cppForSwig/leveldb/db/db_impl.h index 3c8d711ae..cfc998164 100644 --- a/cppForSwig/leveldb/db/db_impl.h +++ b/cppForSwig/leveldb/db/db_impl.h @@ -59,13 +59,19 @@ class DBImpl : public DB { // file at a level >= 1. int64_t TEST_MaxNextLevelOverlappingBytes(); + // Record a sample of bytes read at the specified internal key. + // Samples are taken approximately once every config::kReadBytesPeriod + // bytes. + void RecordReadSample(Slice key); + private: friend class DB; struct CompactionState; struct Writer; Iterator* NewInternalIterator(const ReadOptions&, - SequenceNumber* latest_snapshot); + SequenceNumber* latest_snapshot, + uint32_t* seed); Status NewDB(); @@ -81,8 +87,8 @@ class DBImpl : public DB { // Compact the in-memory write buffer to disk. Switches to a new // log-file/memtable and writes a new descriptor iff successful. - Status CompactMemTable() - EXCLUSIVE_LOCKS_REQUIRED(mutex_); + // Errors are recorded in bg_error_. + void CompactMemTable() EXCLUSIVE_LOCKS_REQUIRED(mutex_); Status RecoverLogFile(uint64_t log_number, VersionEdit* edit, @@ -96,10 +102,12 @@ class DBImpl : public DB { EXCLUSIVE_LOCKS_REQUIRED(mutex_); WriteBatch* BuildBatchGroup(Writer** last_writer); + void RecordBackgroundError(const Status& s); + void MaybeScheduleCompaction() EXCLUSIVE_LOCKS_REQUIRED(mutex_); static void BGWork(void* db); void BackgroundCall(); - Status BackgroundCompaction() EXCLUSIVE_LOCKS_REQUIRED(mutex_); + void BackgroundCompaction() EXCLUSIVE_LOCKS_REQUIRED(mutex_); void CleanupCompaction(CompactionState* compact) EXCLUSIVE_LOCKS_REQUIRED(mutex_); Status DoCompactionWork(CompactionState* compact) @@ -135,6 +143,7 @@ class DBImpl : public DB { WritableFile* logfile_; uint64_t logfile_number_; log::Writer* log_; + uint32_t seed_; // For sampling. // Queue of writers. std::deque writers_; @@ -163,7 +172,6 @@ class DBImpl : public DB { // Have we encountered a background error in paranoid mode? Status bg_error_; - int consecutive_compaction_errors_; // Per level compaction stats. stats_[level] stores the stats for // compactions that produced data for the specified "level". diff --git a/cppForSwig/leveldb/db/db_iter.cc b/cppForSwig/leveldb/db/db_iter.cc index 87dca2ded..3b2035e9e 100644 --- a/cppForSwig/leveldb/db/db_iter.cc +++ b/cppForSwig/leveldb/db/db_iter.cc @@ -5,12 +5,14 @@ #include "db/db_iter.h" #include "db/filename.h" +#include "db/db_impl.h" #include "db/dbformat.h" #include "leveldb/env.h" #include "leveldb/iterator.h" #include "port/port.h" #include "util/logging.h" #include "util/mutexlock.h" +#include "util/random.h" namespace leveldb { @@ -46,15 +48,16 @@ class DBIter: public Iterator { kReverse }; - DBIter(const std::string* dbname, Env* env, - const Comparator* cmp, Iterator* iter, SequenceNumber s) - : dbname_(dbname), - env_(env), + DBIter(DBImpl* db, const Comparator* cmp, Iterator* iter, SequenceNumber s, + uint32_t seed) + : db_(db), user_comparator_(cmp), iter_(iter), sequence_(s), direction_(kForward), - valid_(false) { + valid_(false), + rnd_(seed), + bytes_counter_(RandomPeriod()) { } virtual ~DBIter() { delete iter_; @@ -100,8 +103,12 @@ class DBIter: public Iterator { } } - const std::string* const dbname_; - Env* const env_; + // Pick next gap with average value of config::kReadBytesPeriod. + ssize_t RandomPeriod() { + return rnd_.Uniform(2*config::kReadBytesPeriod); + } + + DBImpl* db_; const Comparator* const user_comparator_; Iterator* const iter_; SequenceNumber const sequence_; @@ -112,13 +119,23 @@ class DBIter: public Iterator { Direction direction_; bool valid_; + Random rnd_; + ssize_t bytes_counter_; + // No copying allowed DBIter(const DBIter&); void operator=(const DBIter&); }; inline bool DBIter::ParseKey(ParsedInternalKey* ikey) { - if (!ParseInternalKey(iter_->key(), ikey)) { + Slice k = iter_->key(); + ssize_t n = k.size() + iter_->value().size(); + bytes_counter_ -= n; + while (bytes_counter_ < 0) { + bytes_counter_ += RandomPeriod(); + db_->RecordReadSample(k); + } + if (!ParseInternalKey(k, ikey)) { status_ = Status::Corruption("corrupted internal key in DBIter"); return false; } else { @@ -144,12 +161,13 @@ void DBIter::Next() { saved_key_.clear(); return; } + // saved_key_ already contains the key to skip past. + } else { + // Store in saved_key_ the current key so we skip it below. + SaveKey(ExtractUserKey(iter_->key()), &saved_key_); } - // Temporarily use saved_key_ as storage for key to skip. - std::string* skip = &saved_key_; - SaveKey(ExtractUserKey(iter_->key()), skip); - FindNextUserEntry(true, skip); + FindNextUserEntry(true, &saved_key_); } void DBIter::FindNextUserEntry(bool skipping, std::string* skip) { @@ -288,12 +306,12 @@ void DBIter::SeekToLast() { } // anonymous namespace Iterator* NewDBIterator( - const std::string* dbname, - Env* env, + DBImpl* db, const Comparator* user_key_comparator, Iterator* internal_iter, - const SequenceNumber& sequence) { - return new DBIter(dbname, env, user_key_comparator, internal_iter, sequence); + SequenceNumber sequence, + uint32_t seed) { + return new DBIter(db, user_key_comparator, internal_iter, sequence, seed); } } // namespace leveldb diff --git a/cppForSwig/leveldb/db/db_iter.h b/cppForSwig/leveldb/db/db_iter.h index d9e1b174a..04927e937 100644 --- a/cppForSwig/leveldb/db/db_iter.h +++ b/cppForSwig/leveldb/db/db_iter.h @@ -11,15 +11,17 @@ namespace leveldb { +class DBImpl; + // Return a new iterator that converts internal keys (yielded by // "*internal_iter") that were live at the specified "sequence" number // into appropriate user keys. extern Iterator* NewDBIterator( - const std::string* dbname, - Env* env, + DBImpl* db, const Comparator* user_key_comparator, Iterator* internal_iter, - const SequenceNumber& sequence); + SequenceNumber sequence, + uint32_t seed); } // namespace leveldb diff --git a/cppForSwig/leveldb/db/db_test.cc b/cppForSwig/leveldb/db/db_test.cc index 49aae04db..280b01c14 100644 --- a/cppForSwig/leveldb/db/db_test.cc +++ b/cppForSwig/leveldb/db/db_test.cc @@ -57,8 +57,11 @@ void DelayMilliseconds(int millis) { // Special Env used to delay background operations class SpecialEnv : public EnvWrapper { public: - // sstable Sync() calls are blocked while this pointer is non-NULL. - port::AtomicPointer delay_sstable_sync_; + // sstable/log Sync() calls are blocked while this pointer is non-NULL. + port::AtomicPointer delay_data_sync_; + + // sstable/log Sync() calls return an error. + port::AtomicPointer data_sync_error_; // Simulate no-space errors while this pointer is non-NULL. port::AtomicPointer no_space_; @@ -75,11 +78,9 @@ class SpecialEnv : public EnvWrapper { bool count_random_reads_; AtomicCounter random_read_counter_; - AtomicCounter sleep_counter_; - AtomicCounter sleep_time_counter_; - explicit SpecialEnv(Env* base) : EnvWrapper(base) { - delay_sstable_sync_.Release_Store(NULL); + delay_data_sync_.Release_Store(NULL); + data_sync_error_.Release_Store(NULL); no_space_.Release_Store(NULL); non_writable_.Release_Store(NULL); count_random_reads_ = false; @@ -88,17 +89,17 @@ class SpecialEnv : public EnvWrapper { } Status NewWritableFile(const std::string& f, WritableFile** r) { - class SSTableFile : public WritableFile { + class DataFile : public WritableFile { private: SpecialEnv* env_; WritableFile* base_; public: - SSTableFile(SpecialEnv* env, WritableFile* base) + DataFile(SpecialEnv* env, WritableFile* base) : env_(env), base_(base) { } - ~SSTableFile() { delete base_; } + ~DataFile() { delete base_; } Status Append(const Slice& data) { if (env_->no_space_.Acquire_Load() != NULL) { // Drop writes on the floor @@ -110,7 +111,10 @@ class SpecialEnv : public EnvWrapper { Status Close() { return base_->Close(); } Status Flush() { return base_->Flush(); } Status Sync() { - while (env_->delay_sstable_sync_.Acquire_Load() != NULL) { + if (env_->data_sync_error_.Acquire_Load() != NULL) { + return Status::IOError("simulated data sync error"); + } + while (env_->delay_data_sync_.Acquire_Load() != NULL) { DelayMilliseconds(100); } return base_->Sync(); @@ -147,8 +151,9 @@ class SpecialEnv : public EnvWrapper { Status s = target()->NewWritableFile(f, r); if (s.ok()) { - if (strstr(f.c_str(), ".sst") != NULL) { - *r = new SSTableFile(this, *r); + if (strstr(f.c_str(), ".ldb") != NULL || + strstr(f.c_str(), ".log") != NULL) { + *r = new DataFile(this, *r); } else if (strstr(f.c_str(), "MANIFEST") != NULL) { *r = new ManifestFile(this, *r); } @@ -179,12 +184,6 @@ class SpecialEnv : public EnvWrapper { } return s; } - - virtual void SleepForMicroseconds(int micros) { - sleep_counter_.Increment(); - sleep_time_counter_.IncrementBy(micros); - } - }; class DBTest { @@ -322,7 +321,7 @@ class DBTest { } // Check reverse iteration results are the reverse of forward results - int matched = 0; + size_t matched = 0; for (iter->SeekToLast(); iter->Valid(); iter->Prev()) { ASSERT_LT(matched, forward.size()); ASSERT_EQ(IterStatus(iter), forward[forward.size() - matched - 1]); @@ -484,6 +483,24 @@ class DBTest { } return false; } + + // Returns number of files renamed. + int RenameLDBToSST() { + std::vector filenames; + ASSERT_OK(env_->GetChildren(dbname_, &filenames)); + uint64_t number; + FileType type; + int files_renamed = 0; + for (size_t i = 0; i < filenames.size(); i++) { + if (ParseFileName(filenames[i], &number, &type) && type == kTableFile) { + const std::string from = TableFileName(dbname_, number); + const std::string to = SSTTableFileName(dbname_, number); + ASSERT_OK(env_->RenameFile(from, to)); + files_renamed++; + } + } + return files_renamed; + } }; TEST(DBTest, Empty) { @@ -525,11 +542,11 @@ TEST(DBTest, GetFromImmutableLayer) { ASSERT_OK(Put("foo", "v1")); ASSERT_EQ("v1", Get("foo")); - env_->delay_sstable_sync_.Release_Store(env_); // Block sync calls + env_->delay_data_sync_.Release_Store(env_); // Block sync calls Put("k1", std::string(100000, 'x')); // Fill memtable Put("k2", std::string(100000, 'y')); // Trigger compaction ASSERT_EQ("v1", Get("foo")); - env_->delay_sstable_sync_.Release_Store(NULL); // Release sync calls + env_->delay_data_sync_.Release_Store(NULL); // Release sync calls } while (ChangeOptions()); } @@ -1516,41 +1533,13 @@ TEST(DBTest, NoSpace) { Compact("a", "z"); const int num_files = CountFiles(); env_->no_space_.Release_Store(env_); // Force out-of-space errors - env_->sleep_counter_.Reset(); - for (int i = 0; i < 5; i++) { + for (int i = 0; i < 10; i++) { for (int level = 0; level < config::kNumLevels-1; level++) { dbfull()->TEST_CompactRange(level, NULL, NULL); } } env_->no_space_.Release_Store(NULL); ASSERT_LT(CountFiles(), num_files + 3); - - // Check that compaction attempts slept after errors - ASSERT_GE(env_->sleep_counter_.Read(), 5); -} - -TEST(DBTest, ExponentialBackoff) { - Options options = CurrentOptions(); - options.env = env_; - Reopen(&options); - - ASSERT_OK(Put("foo", "v1")); - ASSERT_EQ("v1", Get("foo")); - Compact("a", "z"); - env_->non_writable_.Release_Store(env_); // Force errors for new files - env_->sleep_counter_.Reset(); - env_->sleep_time_counter_.Reset(); - for (int i = 0; i < 5; i++) { - dbfull()->TEST_CompactRange(2, NULL, NULL); - } - env_->non_writable_.Release_Store(NULL); - - // Wait for compaction to finish - DelayMilliseconds(1000); - - ASSERT_GE(env_->sleep_counter_.Read(), 5); - ASSERT_LT(env_->sleep_counter_.Read(), 10); - ASSERT_GE(env_->sleep_time_counter_.Read(), 10e6); } TEST(DBTest, NonWritableFileSystem) { @@ -1573,6 +1562,37 @@ TEST(DBTest, NonWritableFileSystem) { env_->non_writable_.Release_Store(NULL); } +TEST(DBTest, WriteSyncError) { + // Check that log sync errors cause the DB to disallow future writes. + + // (a) Cause log sync calls to fail + Options options = CurrentOptions(); + options.env = env_; + Reopen(&options); + env_->data_sync_error_.Release_Store(env_); + + // (b) Normal write should succeed + WriteOptions w; + ASSERT_OK(db_->Put(w, "k1", "v1")); + ASSERT_EQ("v1", Get("k1")); + + // (c) Do a sync write; should fail + w.sync = true; + ASSERT_TRUE(!db_->Put(w, "k2", "v2").ok()); + ASSERT_EQ("v1", Get("k1")); + ASSERT_EQ("NOT_FOUND", Get("k2")); + + // (d) make sync behave normally + env_->data_sync_error_.Release_Store(NULL); + + // (e) Do a non-sync write; should fail + w.sync = false; + ASSERT_TRUE(!db_->Put(w, "k3", "v3").ok()); + ASSERT_EQ("v1", Get("k1")); + ASSERT_EQ("NOT_FOUND", Get("k2")); + ASSERT_EQ("NOT_FOUND", Get("k3")); +} + TEST(DBTest, ManifestWriteError) { // Test for the following problem: // (a) Compaction produces file F @@ -1632,6 +1652,22 @@ TEST(DBTest, MissingSSTFile) { << s.ToString(); } +TEST(DBTest, StillReadSST) { + ASSERT_OK(Put("foo", "bar")); + ASSERT_EQ("bar", Get("foo")); + + // Dump the memtable to disk. + dbfull()->TEST_CompactMemTable(); + ASSERT_EQ("bar", Get("foo")); + Close(); + ASSERT_GT(RenameLDBToSST(), 0); + Options options = CurrentOptions(); + options.paranoid_checks = true; + Status s = TryReopen(&options); + ASSERT_TRUE(s.ok()); + ASSERT_EQ("bar", Get("foo")); +} + TEST(DBTest, FilesDeletedAfterCompaction) { ASSERT_OK(Put("foo", "v2")); Compact("a", "z"); @@ -1663,7 +1699,7 @@ TEST(DBTest, BloomFilter) { dbfull()->TEST_CompactMemTable(); // Prevent auto compactions triggered by seeks - env_->delay_sstable_sync_.Release_Store(env_); + env_->delay_data_sync_.Release_Store(env_); // Lookup present keys. Should rarely read from small sstable. env_->random_read_counter_.Reset(); @@ -1684,7 +1720,7 @@ TEST(DBTest, BloomFilter) { fprintf(stderr, "%d missing => %d reads\n", N, reads); ASSERT_LE(reads, 3*N/100); - env_->delay_sstable_sync_.Release_Store(NULL); + env_->delay_data_sync_.Release_Store(NULL); Close(); delete options.block_cache; delete options.filter_policy; @@ -1744,7 +1780,7 @@ static void MTThreadBody(void* arg) { ASSERT_EQ(k, key); ASSERT_GE(w, 0); ASSERT_LT(w, kNumThreads); - ASSERT_LE(c, reinterpret_cast( + ASSERT_LE(static_cast(c), reinterpret_cast( t->state->counter[w].Acquire_Load())); } } diff --git a/cppForSwig/leveldb/db/dbformat.h b/cppForSwig/leveldb/db/dbformat.h index f7f64dafb..5d8a032bd 100644 --- a/cppForSwig/leveldb/db/dbformat.h +++ b/cppForSwig/leveldb/db/dbformat.h @@ -38,6 +38,9 @@ static const int kL0_StopWritesTrigger = 12; // space if the same key space is being repeatedly overwritten. static const int kMaxMemCompactLevel = 2; +// Approximate gap in bytes between samples of data read during iteration. +static const int kReadBytesPeriod = 1048576; + } // namespace config class InternalKey; diff --git a/cppForSwig/leveldb/db/filename.cc b/cppForSwig/leveldb/db/filename.cc index 3c4d49f64..da32946d9 100644 --- a/cppForSwig/leveldb/db/filename.cc +++ b/cppForSwig/leveldb/db/filename.cc @@ -30,6 +30,11 @@ std::string LogFileName(const std::string& name, uint64_t number) { } std::string TableFileName(const std::string& name, uint64_t number) { + assert(number > 0); + return MakeFileName(name, number, "ldb"); +} + +std::string SSTTableFileName(const std::string& name, uint64_t number) { assert(number > 0); return MakeFileName(name, number, "sst"); } @@ -71,7 +76,7 @@ std::string OldInfoLogFileName(const std::string& dbname) { // dbname/LOG // dbname/LOG.old // dbname/MANIFEST-[0-9]+ -// dbname/[0-9]+.(log|sst) +// dbname/[0-9]+.(log|sst|ldb) bool ParseFileName(const std::string& fname, uint64_t* number, FileType* type) { @@ -106,7 +111,7 @@ bool ParseFileName(const std::string& fname, Slice suffix = rest; if (suffix == Slice(".log")) { *type = kLogFile; - } else if (suffix == Slice(".sst")) { + } else if (suffix == Slice(".sst") || suffix == Slice(".ldb")) { *type = kTableFile; } else if (suffix == Slice(".dbtmp")) { *type = kTempFile; diff --git a/cppForSwig/leveldb/db/filename.h b/cppForSwig/leveldb/db/filename.h index d5d09b114..87a752605 100644 --- a/cppForSwig/leveldb/db/filename.h +++ b/cppForSwig/leveldb/db/filename.h @@ -37,6 +37,11 @@ extern std::string LogFileName(const std::string& dbname, uint64_t number); // "dbname". extern std::string TableFileName(const std::string& dbname, uint64_t number); +// Return the legacy file name for an sstable with the specified number +// in the db named by "dbname". The result will be prefixed with +// "dbname". +extern std::string SSTTableFileName(const std::string& dbname, uint64_t number); + // Return the name of the descriptor file for the db named by // "dbname" and the specified incarnation number. The result will be // prefixed with "dbname". diff --git a/cppForSwig/leveldb/db/filename_test.cc b/cppForSwig/leveldb/db/filename_test.cc index 5a26da472..a32556dea 100644 --- a/cppForSwig/leveldb/db/filename_test.cc +++ b/cppForSwig/leveldb/db/filename_test.cc @@ -27,6 +27,7 @@ TEST(FileNameTest, Parse) { { "100.log", 100, kLogFile }, { "0.log", 0, kLogFile }, { "0.sst", 0, kTableFile }, + { "0.ldb", 0, kTableFile }, { "CURRENT", 0, kCurrentFile }, { "LOCK", 0, kDBLockFile }, { "MANIFEST-2", 2, kDescriptorFile }, diff --git a/cppForSwig/leveldb/db/repair.cc b/cppForSwig/leveldb/db/repair.cc index 022d52f3d..96c9b37af 100644 --- a/cppForSwig/leveldb/db/repair.cc +++ b/cppForSwig/leveldb/db/repair.cc @@ -244,60 +244,133 @@ class Repairer { void ExtractMetaData() { std::vector kept; for (size_t i = 0; i < table_numbers_.size(); i++) { - TableInfo t; - t.meta.number = table_numbers_[i]; - Status status = ScanTable(&t); - if (!status.ok()) { - std::string fname = TableFileName(dbname_, table_numbers_[i]); - Log(options_.info_log, "Table #%llu: ignoring %s", - (unsigned long long) table_numbers_[i], - status.ToString().c_str()); - ArchiveFile(fname); - } else { - tables_.push_back(t); - } + ScanTable(table_numbers_[i]); } } - Status ScanTable(TableInfo* t) { - std::string fname = TableFileName(dbname_, t->meta.number); + Iterator* NewTableIterator(const FileMetaData& meta) { + // Same as compaction iterators: if paranoid_checks are on, turn + // on checksum verification. + ReadOptions r; + r.verify_checksums = options_.paranoid_checks; + return table_cache_->NewIterator(r, meta.number, meta.file_size); + } + + void ScanTable(uint64_t number) { + TableInfo t; + t.meta.number = number; + std::string fname = TableFileName(dbname_, number); + Status status = env_->GetFileSize(fname, &t.meta.file_size); + if (!status.ok()) { + // Try alternate file name. + fname = SSTTableFileName(dbname_, number); + Status s2 = env_->GetFileSize(fname, &t.meta.file_size); + if (s2.ok()) { + status = Status::OK(); + } + } + if (!status.ok()) { + ArchiveFile(TableFileName(dbname_, number)); + ArchiveFile(SSTTableFileName(dbname_, number)); + Log(options_.info_log, "Table #%llu: dropped: %s", + (unsigned long long) t.meta.number, + status.ToString().c_str()); + return; + } + + // Extract metadata by scanning through table. int counter = 0; - Status status = env_->GetFileSize(fname, &t->meta.file_size); - if (status.ok()) { - Iterator* iter = table_cache_->NewIterator( - ReadOptions(), t->meta.number, t->meta.file_size); - bool empty = true; - ParsedInternalKey parsed; - t->max_sequence = 0; - for (iter->SeekToFirst(); iter->Valid(); iter->Next()) { - Slice key = iter->key(); - if (!ParseInternalKey(key, &parsed)) { - Log(options_.info_log, "Table #%llu: unparsable key %s", - (unsigned long long) t->meta.number, - EscapeString(key).c_str()); - continue; - } + Iterator* iter = NewTableIterator(t.meta); + bool empty = true; + ParsedInternalKey parsed; + t.max_sequence = 0; + for (iter->SeekToFirst(); iter->Valid(); iter->Next()) { + Slice key = iter->key(); + if (!ParseInternalKey(key, &parsed)) { + Log(options_.info_log, "Table #%llu: unparsable key %s", + (unsigned long long) t.meta.number, + EscapeString(key).c_str()); + continue; + } - counter++; - if (empty) { - empty = false; - t->meta.smallest.DecodeFrom(key); - } - t->meta.largest.DecodeFrom(key); - if (parsed.sequence > t->max_sequence) { - t->max_sequence = parsed.sequence; - } + counter++; + if (empty) { + empty = false; + t.meta.smallest.DecodeFrom(key); } - if (!iter->status().ok()) { - status = iter->status(); + t.meta.largest.DecodeFrom(key); + if (parsed.sequence > t.max_sequence) { + t.max_sequence = parsed.sequence; } - delete iter; } + if (!iter->status().ok()) { + status = iter->status(); + } + delete iter; Log(options_.info_log, "Table #%llu: %d entries %s", - (unsigned long long) t->meta.number, + (unsigned long long) t.meta.number, counter, status.ToString().c_str()); - return status; + + if (status.ok()) { + tables_.push_back(t); + } else { + RepairTable(fname, t); // RepairTable archives input file. + } + } + + void RepairTable(const std::string& src, TableInfo t) { + // We will copy src contents to a new table and then rename the + // new table over the source. + + // Create builder. + std::string copy = TableFileName(dbname_, next_file_number_++); + WritableFile* file; + Status s = env_->NewWritableFile(copy, &file); + if (!s.ok()) { + return; + } + TableBuilder* builder = new TableBuilder(options_, file); + + // Copy data. + Iterator* iter = NewTableIterator(t.meta); + int counter = 0; + for (iter->SeekToFirst(); iter->Valid(); iter->Next()) { + builder->Add(iter->key(), iter->value()); + counter++; + } + delete iter; + + ArchiveFile(src); + if (counter == 0) { + builder->Abandon(); // Nothing to save + } else { + s = builder->Finish(); + if (s.ok()) { + t.meta.file_size = builder->FileSize(); + } + } + delete builder; + builder = NULL; + + if (s.ok()) { + s = file->Close(); + } + delete file; + file = NULL; + + if (counter > 0 && s.ok()) { + std::string orig = TableFileName(dbname_, t.meta.number); + s = env_->RenameFile(copy, orig); + if (s.ok()) { + Log(options_.info_log, "Table #%llu: %d entries repaired", + (unsigned long long) t.meta.number, counter); + tables_.push_back(t); + } + } + if (!s.ok()) { + env_->DeleteFile(copy); + } } Status WriteDescriptor() { diff --git a/cppForSwig/leveldb/db/table_cache.cc b/cppForSwig/leveldb/db/table_cache.cc index 497db2707..e3d82cd3e 100644 --- a/cppForSwig/leveldb/db/table_cache.cc +++ b/cppForSwig/leveldb/db/table_cache.cc @@ -54,6 +54,12 @@ Status TableCache::FindTable(uint64_t file_number, uint64_t file_size, RandomAccessFile* file = NULL; Table* table = NULL; s = env_->NewRandomAccessFile(fname, &file); + if (!s.ok()) { + std::string old_fname = SSTTableFileName(dbname_, file_number); + if (env_->NewRandomAccessFile(old_fname, &file).ok()) { + s = Status::OK(); + } + } if (s.ok()) { s = Table::Open(*options_, file, file_size, &table); } diff --git a/cppForSwig/leveldb/db/version_set.cc b/cppForSwig/leveldb/db/version_set.cc index 4fd1ddef2..517edd3b1 100644 --- a/cppForSwig/leveldb/db/version_set.cc +++ b/cppForSwig/leveldb/db/version_set.cc @@ -289,6 +289,51 @@ static bool NewestFirst(FileMetaData* a, FileMetaData* b) { return a->number > b->number; } +void Version::ForEachOverlapping(Slice user_key, Slice internal_key, + void* arg, + bool (*func)(void*, int, FileMetaData*)) { + // TODO(sanjay): Change Version::Get() to use this function. + const Comparator* ucmp = vset_->icmp_.user_comparator(); + + // Search level-0 in order from newest to oldest. + std::vector tmp; + tmp.reserve(files_[0].size()); + for (uint32_t i = 0; i < files_[0].size(); i++) { + FileMetaData* f = files_[0][i]; + if (ucmp->Compare(user_key, f->smallest.user_key()) >= 0 && + ucmp->Compare(user_key, f->largest.user_key()) <= 0) { + tmp.push_back(f); + } + } + if (!tmp.empty()) { + std::sort(tmp.begin(), tmp.end(), NewestFirst); + for (uint32_t i = 0; i < tmp.size(); i++) { + if (!(*func)(arg, 0, tmp[i])) { + return; + } + } + } + + // Search other levels. + for (int level = 1; level < config::kNumLevels; level++) { + size_t num_files = files_[level].size(); + if (num_files == 0) continue; + + // Binary search to find earliest index whose largest key >= internal_key. + uint32_t index = FindFile(vset_->icmp_, files_[level], internal_key); + if (index < num_files) { + FileMetaData* f = files_[level][index]; + if (ucmp->Compare(user_key, f->smallest.user_key()) < 0) { + // All of "f" is past any data for user_key + } else { + if (!(*func)(arg, level, f)) { + return; + } + } + } + } +} + Status Version::Get(const ReadOptions& options, const LookupKey& k, std::string* value, @@ -401,6 +446,44 @@ bool Version::UpdateStats(const GetStats& stats) { return false; } +bool Version::RecordReadSample(Slice internal_key) { + ParsedInternalKey ikey; + if (!ParseInternalKey(internal_key, &ikey)) { + return false; + } + + struct State { + GetStats stats; // Holds first matching file + int matches; + + static bool Match(void* arg, int level, FileMetaData* f) { + State* state = reinterpret_cast(arg); + state->matches++; + if (state->matches == 1) { + // Remember first match. + state->stats.seek_file = f; + state->stats.seek_file_level = level; + } + // We can stop iterating once we have a second match. + return state->matches < 2; + } + }; + + State state; + state.matches = 0; + ForEachOverlapping(ikey.user_key, internal_key, &state, &State::Match); + + // Must have at least two matches since we want to merge across + // files. But what if we have a single file that contains many + // overwrites and deletions? Should we have another mechanism for + // finding such files? + if (state.matches >= 2) { + // 1MB cost is about 1 seek (see comment in Builder::Apply). + return UpdateStats(state.stats); + } + return false; +} + void Version::Ref() { ++refs_; } @@ -435,10 +518,13 @@ int Version::PickLevelForMemTableOutput( if (OverlapInLevel(level + 1, &smallest_user_key, &largest_user_key)) { break; } - GetOverlappingInputs(level + 2, &start, &limit, &overlaps); - const int64_t sum = TotalFileSize(overlaps); - if (sum > kMaxGrandParentOverlapBytes) { - break; + if (level + 2 < config::kNumLevels) { + // Check that file does not overlap too many grandparent bytes. + GetOverlappingInputs(level + 2, &start, &limit, &overlaps); + const int64_t sum = TotalFileSize(overlaps); + if (sum > kMaxGrandParentOverlapBytes) { + break; + } } level++; } @@ -452,6 +538,8 @@ void Version::GetOverlappingInputs( const InternalKey* begin, const InternalKey* end, std::vector* inputs) { + assert(level >= 0); + assert(level < config::kNumLevels); inputs->clear(); Slice user_begin, user_end; if (begin != NULL) { @@ -788,12 +876,6 @@ Status VersionSet::LogAndApply(VersionEdit* edit, port::Mutex* mu) { } if (!s.ok()) { Log(options_->info_log, "MANIFEST write: %s\n", s.ToString().c_str()); - if (ManifestContains(record)) { - Log(options_->info_log, - "MANIFEST contains log record despite error; advancing to new " - "version to prevent mismatch between in-memory and logged state"); - s = Status::OK(); - } } } @@ -801,8 +883,6 @@ Status VersionSet::LogAndApply(VersionEdit* edit, port::Mutex* mu) { // new CURRENT file that points to it. if (s.ok() && !new_manifest_file.empty()) { s = SetCurrentFile(env_, dbname_, manifest_file_number_); - // No need to double-check MANIFEST in case of error since it - // will be discarded below. } mu->Lock(); @@ -1036,31 +1116,6 @@ const char* VersionSet::LevelSummary(LevelSummaryStorage* scratch) const { return scratch->buffer; } -// Return true iff the manifest contains the specified record. -bool VersionSet::ManifestContains(const std::string& record) const { - std::string fname = DescriptorFileName(dbname_, manifest_file_number_); - Log(options_->info_log, "ManifestContains: checking %s\n", fname.c_str()); - SequentialFile* file = NULL; - Status s = env_->NewSequentialFile(fname, &file); - if (!s.ok()) { - Log(options_->info_log, "ManifestContains: %s\n", s.ToString().c_str()); - return false; - } - log::Reader reader(file, NULL, true/*checksum*/, 0); - Slice r; - std::string scratch; - bool result = false; - while (reader.ReadRecord(&r, &scratch)) { - if (r == Slice(record)) { - result = true; - break; - } - } - delete file; - Log(options_->info_log, "ManifestContains: result = %d\n", result ? 1 : 0); - return result; -} - uint64_t VersionSet::ApproximateOffsetOf(Version* v, const InternalKey& ikey) { uint64_t result = 0; for (int level = 0; level < config::kNumLevels; level++) { diff --git a/cppForSwig/leveldb/db/version_set.h b/cppForSwig/leveldb/db/version_set.h index 9d084fdb7..8dc14b8e0 100644 --- a/cppForSwig/leveldb/db/version_set.h +++ b/cppForSwig/leveldb/db/version_set.h @@ -78,6 +78,12 @@ class Version { // REQUIRES: lock is held bool UpdateStats(const GetStats& stats); + // Record a sample of bytes read at the specified internal key. + // Samples are taken approximately once every config::kReadBytesPeriod + // bytes. Returns true if a new compaction may need to be triggered. + // REQUIRES: lock is held + bool RecordReadSample(Slice key); + // Reference count management (so Versions do not disappear out from // under live iterators) void Ref(); @@ -114,6 +120,15 @@ class Version { class LevelFileNumIterator; Iterator* NewConcatenatingIterator(const ReadOptions&, int level) const; + // Call func(arg, level, f) for every file that overlaps user_key in + // order from newest to oldest. If an invocation of func returns + // false, makes no more calls. + // + // REQUIRES: user portion of internal_key == user_key. + void ForEachOverlapping(Slice user_key, Slice internal_key, + void* arg, + bool (*func)(void*, int, FileMetaData*)); + VersionSet* vset_; // VersionSet to which this Version belongs Version* next_; // Next version in linked list Version* prev_; // Previous version in linked list @@ -277,8 +292,6 @@ class VersionSet { void AppendVersion(Version* v); - bool ManifestContains(const std::string& record) const; - Env* const env_; const std::string dbname_; const Options* const options_; diff --git a/cppForSwig/leveldb/doc/impl.html b/cppForSwig/leveldb/doc/impl.html index e870795d2..28817fe0d 100644 --- a/cppForSwig/leveldb/doc/impl.html +++ b/cppForSwig/leveldb/doc/impl.html @@ -11,7 +11,7 @@

    Files

    The implementation of leveldb is similar in spirit to the representation of a single - + Bigtable tablet (section 5.3). However the organization of the files that make up the representation is somewhat different and is explained below. diff --git a/cppForSwig/leveldb/include/leveldb/db.h b/cppForSwig/leveldb/include/leveldb/db.h index da8b11a8c..5ffb29d52 100644 --- a/cppForSwig/leveldb/include/leveldb/db.h +++ b/cppForSwig/leveldb/include/leveldb/db.h @@ -14,7 +14,7 @@ namespace leveldb { // Update Makefile if you change these static const int kMajorVersion = 1; -static const int kMinorVersion = 12; +static const int kMinorVersion = 15; struct Options; struct ReadOptions; diff --git a/cppForSwig/leveldb/include/leveldb/env.h b/cppForSwig/leveldb/include/leveldb/env.h index fa32289f5..b2072d02c 100644 --- a/cppForSwig/leveldb/include/leveldb/env.h +++ b/cppForSwig/leveldb/include/leveldb/env.h @@ -13,9 +13,9 @@ #ifndef STORAGE_LEVELDB_INCLUDE_ENV_H_ #define STORAGE_LEVELDB_INCLUDE_ENV_H_ -#include #include #include +#include #include #include "leveldb/status.h" diff --git a/cppForSwig/leveldb/port/atomic_pointer.h b/cppForSwig/leveldb/port/atomic_pointer.h index d847c81f3..a9866b230 100644 --- a/cppForSwig/leveldb/port/atomic_pointer.h +++ b/cppForSwig/leveldb/port/atomic_pointer.h @@ -19,26 +19,15 @@ #ifndef PORT_ATOMIC_POINTER_H_ #define PORT_ATOMIC_POINTER_H_ - #include - #ifdef LEVELDB_CSTDATOMIC_PRESENT - //#include - // ***** ADDED BY AREINER FROM GOATPIG'S WINDOWS-PORT INSTRUCTIONS - #ifdef WIN32 - #include - #else - #include - #endif - // ***** ADDED BY AREINER FROM GOATPIG'S WINDOWS-PORT INSTRUCTIONS +#include #endif - #ifdef OS_WIN - #include +#include #endif - #ifdef OS_MACOSX - #include +#include #endif #if defined(_M_X64) || defined(__x86_64__) @@ -61,6 +50,13 @@ namespace port { // http://msdn.microsoft.com/en-us/library/ms684208(v=vs.85).aspx #define LEVELDB_HAVE_MEMORY_BARRIER +// Mac OS +#elif defined(OS_MACOSX) +inline void MemoryBarrier() { + OSMemoryBarrier(); +} +#define LEVELDB_HAVE_MEMORY_BARRIER + // Gcc on x86 #elif defined(ARCH_CPU_X86_FAMILY) && defined(__GNUC__) inline void MemoryBarrier() { @@ -79,13 +75,6 @@ inline void MemoryBarrier() { } #define LEVELDB_HAVE_MEMORY_BARRIER -// Mac OS -#elif defined(OS_MACOSX) -inline void MemoryBarrier() { - OSMemoryBarrier(); -} -#define LEVELDB_HAVE_MEMORY_BARRIER - // ARM Linux #elif defined(ARCH_CPU_ARM_FAMILY) && defined(__linux__) typedef void (*LinuxKernelMemoryBarrierFunc)(void); diff --git a/cppForSwig/leveldb/port/port.h b/cppForSwig/leveldb/port/port.h index e667db40d..72378de6d 100644 --- a/cppForSwig/leveldb/port/port.h +++ b/cppForSwig/leveldb/port/port.h @@ -12,6 +12,10 @@ // of what the new port_.h file must provide. #if defined(LEVELDB_PLATFORM_POSIX) # include "port/port_posix.h" +#ifdef _MSC_VER + #include + #define fileno _fileno +#endif #elif defined(LEVELDB_PLATFORM_CHROMIUM) # include "port/port_chromium.h" #endif diff --git a/cppForSwig/leveldb/port/port_posix.h b/cppForSwig/leveldb/port/port_posix.h index 8a7c09f65..f2b89bffb 100644 --- a/cppForSwig/leveldb/port/port_posix.h +++ b/cppForSwig/leveldb/port/port_posix.h @@ -41,12 +41,6 @@ #include #endif -// ***** ADDED BY AREINER FROM GOATPIG'S WINDOWS-PORT INSTRUCTIONS -#ifdef _MSC_VER - #include -#endif -// ***** ADDED BY AREINER FROM GOATPIG'S WINDOWS-PORT INSTRUCTIONS - #include #ifdef SNAPPY #include diff --git a/cppForSwig/leveldb/table/filter_block_test.cc b/cppForSwig/leveldb/table/filter_block_test.cc index 3a2a07cf5..8c4a4741f 100644 --- a/cppForSwig/leveldb/table/filter_block_test.cc +++ b/cppForSwig/leveldb/table/filter_block_test.cc @@ -29,7 +29,7 @@ class TestHashFilter : public FilterPolicy { virtual bool KeyMayMatch(const Slice& key, const Slice& filter) const { uint32_t h = Hash(key.data(), key.size(), 1); - for (int i = 0; i + 4 <= filter.size(); i += 4) { + for (size_t i = 0; i + 4 <= filter.size(); i += 4) { if (h == DecodeFixed32(filter.data() + i)) { return true; } diff --git a/cppForSwig/leveldb/util/arena.cc b/cppForSwig/leveldb/util/arena.cc index 9551d6a3a..9367f7149 100644 --- a/cppForSwig/leveldb/util/arena.cc +++ b/cppForSwig/leveldb/util/arena.cc @@ -40,7 +40,7 @@ char* Arena::AllocateFallback(size_t bytes) { } char* Arena::AllocateAligned(size_t bytes) { - const int align = sizeof(void*); // We'll align to pointer size + const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8; assert((align & (align-1)) == 0); // Pointer size should be a power of 2 size_t current_mod = reinterpret_cast(alloc_ptr_) & (align-1); size_t slop = (current_mod == 0 ? 0 : align - current_mod); diff --git a/cppForSwig/leveldb/util/arena.h b/cppForSwig/leveldb/util/arena.h index 8f7dde226..73bbf1cb9 100644 --- a/cppForSwig/leveldb/util/arena.h +++ b/cppForSwig/leveldb/util/arena.h @@ -5,9 +5,9 @@ #ifndef STORAGE_LEVELDB_UTIL_ARENA_H_ #define STORAGE_LEVELDB_UTIL_ARENA_H_ -#include #include #include +#include #include namespace leveldb { diff --git a/cppForSwig/leveldb/util/arena_test.cc b/cppForSwig/leveldb/util/arena_test.cc index 63d177803..58e870ec4 100644 --- a/cppForSwig/leveldb/util/arena_test.cc +++ b/cppForSwig/leveldb/util/arena_test.cc @@ -40,7 +40,7 @@ TEST(ArenaTest, Simple) { r = arena.Allocate(s); } - for (int b = 0; b < s; b++) { + for (size_t b = 0; b < s; b++) { // Fill the "i"th allocation with a known bit pattern r[b] = i % 256; } @@ -51,10 +51,10 @@ TEST(ArenaTest, Simple) { ASSERT_LE(arena.MemoryUsage(), bytes * 1.10); } } - for (int i = 0; i < allocated.size(); i++) { + for (size_t i = 0; i < allocated.size(); i++) { size_t num_bytes = allocated[i].first; const char* p = allocated[i].second; - for (int b = 0; b < num_bytes; b++) { + for (size_t b = 0; b < num_bytes; b++) { // Check the "i"th allocation for the known bit pattern ASSERT_EQ(int(p[b]) & 0xff, i % 256); } diff --git a/cppForSwig/leveldb/util/bloom_test.cc b/cppForSwig/leveldb/util/bloom_test.cc index 0bf8e8d6e..77fb1b315 100644 --- a/cppForSwig/leveldb/util/bloom_test.cc +++ b/cppForSwig/leveldb/util/bloom_test.cc @@ -126,7 +126,8 @@ TEST(BloomTest, VaryingLengths) { } Build(); - ASSERT_LE(FilterSize(), (length * 10 / 8) + 40) << length; + ASSERT_LE(FilterSize(), static_cast((length * 10 / 8) + 40)) + << length; // All added keys must match for (int i = 0; i < length; i++) { diff --git a/cppForSwig/leveldb/util/coding_test.cc b/cppForSwig/leveldb/util/coding_test.cc index fb5726e33..521541ea6 100644 --- a/cppForSwig/leveldb/util/coding_test.cc +++ b/cppForSwig/leveldb/util/coding_test.cc @@ -112,13 +112,13 @@ TEST(Coding, Varint64) { } std::string s; - for (int i = 0; i < values.size(); i++) { + for (size_t i = 0; i < values.size(); i++) { PutVarint64(&s, values[i]); } const char* p = s.data(); const char* limit = p + s.size(); - for (int i = 0; i < values.size(); i++) { + for (size_t i = 0; i < values.size(); i++) { ASSERT_TRUE(p < limit); uint64_t actual; const char* start = p; @@ -143,7 +143,7 @@ TEST(Coding, Varint32Truncation) { std::string s; PutVarint32(&s, large_value); uint32_t result; - for (int len = 0; len < s.size() - 1; len++) { + for (size_t len = 0; len < s.size() - 1; len++) { ASSERT_TRUE(GetVarint32Ptr(s.data(), s.data() + len, &result) == NULL); } ASSERT_TRUE(GetVarint32Ptr(s.data(), s.data() + s.size(), &result) != NULL); @@ -162,7 +162,7 @@ TEST(Coding, Varint64Truncation) { std::string s; PutVarint64(&s, large_value); uint64_t result; - for (int len = 0; len < s.size() - 1; len++) { + for (size_t len = 0; len < s.size() - 1; len++) { ASSERT_TRUE(GetVarint64Ptr(s.data(), s.data() + len, &result) == NULL); } ASSERT_TRUE(GetVarint64Ptr(s.data(), s.data() + s.size(), &result) != NULL); diff --git a/cppForSwig/leveldb/util/env_posix.cc b/cppForSwig/leveldb/util/env_posix.cc index a3f197da8..e1cbebd1b 100644 --- a/cppForSwig/leveldb/util/env_posix.cc +++ b/cppForSwig/leveldb/util/env_posix.cc @@ -175,172 +175,83 @@ class PosixMmapReadableFile: public RandomAccessFile { } }; -// We preallocate up to an extra megabyte and use memcpy to append new -// data to the file. This is safe since we either properly close the -// file before reading from it, or for log files, the reading code -// knows enough to skip zero suffixes. -class PosixMmapFile : public WritableFile { +class PosixWritableFile : public WritableFile { private: std::string filename_; - int fd_; - size_t page_size_; - size_t map_size_; // How much extra memory to map at a time - char* base_; // The mapped region - char* limit_; // Limit of the mapped region - char* dst_; // Where to write next (in range [base_,limit_]) - char* last_sync_; // Where have we synced up to - uint64_t file_offset_; // Offset of base_ in file - - // Have we done an munmap of unsynced data? - bool pending_sync_; - - // Roundup x to a multiple of y - static size_t Roundup(size_t x, size_t y) { - return ((x + y - 1) / y) * y; - } + FILE* file_; - size_t TruncateToPageBoundary(size_t s) { - s -= (s & (page_size_ - 1)); - assert((s % page_size_) == 0); - return s; - } + public: + PosixWritableFile(const std::string& fname, FILE* f) + : filename_(fname), file_(f) { } - bool UnmapCurrentRegion() { - bool result = true; - if (base_ != NULL) { - if (last_sync_ < limit_) { - // Defer syncing this data until next Sync() call, if any - pending_sync_ = true; - } - if (munmap(base_, limit_ - base_) != 0) { - result = false; - } - file_offset_ += limit_ - base_; - base_ = NULL; - limit_ = NULL; - last_sync_ = NULL; - dst_ = NULL; - - // Increase the amount we map the next time, but capped at 1MB - if (map_size_ < (1<<20)) { - map_size_ *= 2; - } + ~PosixWritableFile() { + if (file_ != NULL) { + // Ignoring any potential errors + fclose(file_); } - return result; } - bool MapNewRegion() { - assert(base_ == NULL); - if (ftruncate(fd_, file_offset_ + map_size_) < 0) { - return false; - } - void* ptr = mmap(NULL, map_size_, PROT_READ | PROT_WRITE, MAP_SHARED, - fd_, file_offset_); - if (ptr == MAP_FAILED) { - return false; + virtual Status Append(const Slice& data) { + size_t r = fwrite_unlocked(data.data(), 1, data.size(), file_); + if (r != data.size()) { + return IOError(filename_, errno); } - base_ = reinterpret_cast(ptr); - limit_ = base_ + map_size_; - dst_ = base_; - last_sync_ = base_; - return true; + return Status::OK(); } - public: - PosixMmapFile(const std::string& fname, int fd, size_t page_size) - : filename_(fname), - fd_(fd), - page_size_(page_size), - map_size_(Roundup(65536, page_size)), - base_(NULL), - limit_(NULL), - dst_(NULL), - last_sync_(NULL), - file_offset_(0), - pending_sync_(false) { - assert((page_size & (page_size - 1)) == 0); - } - - - ~PosixMmapFile() { - if (fd_ >= 0) { - PosixMmapFile::Close(); + virtual Status Close() { + Status result; + if (fclose(file_) != 0) { + result = IOError(filename_, errno); } + file_ = NULL; + return result; } - virtual Status Append(const Slice& data) { - const char* src = data.data(); - size_t left = data.size(); - while (left > 0) { - assert(base_ <= dst_); - assert(dst_ <= limit_); - size_t avail = limit_ - dst_; - if (avail == 0) { - if (!UnmapCurrentRegion() || - !MapNewRegion()) { - return IOError(filename_, errno); - } - } - - size_t n = (left <= avail) ? left : avail; - memcpy(dst_, src, n); - dst_ += n; - src += n; - left -= n; + virtual Status Flush() { + if (fflush_unlocked(file_) != 0) { + return IOError(filename_, errno); } return Status::OK(); } - virtual Status Close() { - Status s; - size_t unused = limit_ - dst_; - if (!UnmapCurrentRegion()) { - s = IOError(filename_, errno); - } else if (unused > 0) { - // Trim the extra space at the end of the file - if (ftruncate(fd_, file_offset_ - unused) < 0) { - s = IOError(filename_, errno); - } + Status SyncDirIfManifest() { + const char* f = filename_.c_str(); + const char* sep = strrchr(f, '/'); + Slice basename; + std::string dir; + if (sep == NULL) { + dir = "."; + basename = f; + } else { + dir = std::string(f, sep - f); + basename = sep + 1; } - - if (close(fd_) < 0) { - if (s.ok()) { - s = IOError(filename_, errno); + Status s; + if (basename.starts_with("MANIFEST")) { + int fd = open(dir.c_str(), O_RDONLY); + if (fd < 0) { + s = IOError(dir, errno); + } else { + if (fsync(fd) < 0) { + s = IOError(dir, errno); + } + close(fd); } } - - fd_ = -1; - base_ = NULL; - limit_ = NULL; return s; } - virtual Status Flush() { - return Status::OK(); - } - virtual Status Sync() { - Status s; - - if (pending_sync_) { - // Some unmapped data was not synced - pending_sync_ = false; - if (fdatasync(fd_) < 0) { - s = IOError(filename_, errno); - } + // Ensure new files referred to by the manifest are in the filesystem. + Status s = SyncDirIfManifest(); + if (!s.ok()) { + return s; } - - if (dst_ > last_sync_) { - // Find the beginnings of the pages that contain the first and last - // bytes to be synced. - size_t p1 = TruncateToPageBoundary(last_sync_ - base_); - size_t p2 = TruncateToPageBoundary(dst_ - base_ - 1); - last_sync_ = dst_; - if (msync(base_ + p1, p2 - p1 + page_size_, MS_SYNC) < 0) { - s = IOError(filename_, errno); - } + if (fflush_unlocked(file_) != 0 || + fdatasync(fileno(file_)) != 0) { + s = Status::IOError(filename_, strerror(errno)); } - return s; } }; @@ -431,12 +342,12 @@ class PosixEnv : public Env { virtual Status NewWritableFile(const std::string& fname, WritableFile** result) { Status s; - const int fd = open(fname.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0644); - if (fd < 0) { + FILE* f = fopen(fname.c_str(), "w"); + if (f == NULL) { *result = NULL; s = IOError(fname, errno); } else { - *result = new PosixMmapFile(fname, fd, page_size_); + *result = new PosixWritableFile(fname, f); } return s; } @@ -599,7 +510,6 @@ class PosixEnv : public Env { return NULL; } - size_t page_size_; pthread_mutex_t mu_; pthread_cond_t bgsignal_; pthread_t bgthread_; @@ -614,8 +524,7 @@ class PosixEnv : public Env { MmapLimiter mmap_limit_; }; -PosixEnv::PosixEnv() : page_size_(getpagesize()), - started_bgthread_(false) { +PosixEnv::PosixEnv() : started_bgthread_(false) { PthreadCall("mutex_init", pthread_mutex_init(&mu_, NULL)); PthreadCall("cvar_init", pthread_cond_init(&bgsignal_, NULL)); } diff --git a/cppForSwig/leveldb/util/random.h b/cppForSwig/leveldb/util/random.h index 07538242e..ddd51b1c7 100644 --- a/cppForSwig/leveldb/util/random.h +++ b/cppForSwig/leveldb/util/random.h @@ -16,7 +16,12 @@ class Random { private: uint32_t seed_; public: - explicit Random(uint32_t s) : seed_(s & 0x7fffffffu) { } + explicit Random(uint32_t s) : seed_(s & 0x7fffffffu) { + // Avoid bad seeds. + if (seed_ == 0 || seed_ == 2147483647L) { + seed_ = 1; + } + } uint32_t Next() { static const uint32_t M = 2147483647L; // 2^31-1 static const uint64_t A = 16807; // bits 14, 8, 7, 5, 2, 1, 0 diff --git a/cppForSwig/leveldb/util/testharness.cc b/cppForSwig/leveldb/util/testharness.cc index eb1bdd554..402fab34d 100644 --- a/cppForSwig/leveldb/util/testharness.cc +++ b/cppForSwig/leveldb/util/testharness.cc @@ -38,7 +38,7 @@ int RunAllTests() { int num = 0; if (tests != NULL) { - for (int i = 0; i < tests->size(); i++) { + for (size_t i = 0; i < tests->size(); i++) { const Test& t = (*tests)[i]; if (matcher != NULL) { std::string name = t.base; diff --git a/cppForSwig/leveldb/util/testutil.cc b/cppForSwig/leveldb/util/testutil.cc index 538d09516..bee56bf75 100644 --- a/cppForSwig/leveldb/util/testutil.cc +++ b/cppForSwig/leveldb/util/testutil.cc @@ -32,7 +32,7 @@ std::string RandomKey(Random* rnd, int len) { extern Slice CompressibleString(Random* rnd, double compressed_fraction, - int len, std::string* dst) { + size_t len, std::string* dst) { int raw = static_cast(len * compressed_fraction); if (raw < 1) raw = 1; std::string raw_data; diff --git a/cppForSwig/leveldb/util/testutil.h b/cppForSwig/leveldb/util/testutil.h index 824e655bd..adad3fc1e 100644 --- a/cppForSwig/leveldb/util/testutil.h +++ b/cppForSwig/leveldb/util/testutil.h @@ -24,7 +24,7 @@ extern std::string RandomKey(Random* rnd, int len); // "N*compressed_fraction" bytes and return a Slice that references // the generated data. extern Slice CompressibleString(Random* rnd, double compressed_fraction, - int len, std::string* dst); + size_t len, std::string* dst); // A wrapper that allows injection of errors. class ErrorEnv : public EnvWrapper { diff --git a/cppForSwig/leveldb_windows_port/leveldb_msvc11_port.vcxproj b/cppForSwig/leveldb_windows_port/leveldb_msvc11_port.vcxproj index 27e9c4a28..c969c9dff 100644 --- a/cppForSwig/leveldb_windows_port/leveldb_msvc11_port.vcxproj +++ b/cppForSwig/leveldb_windows_port/leveldb_msvc11_port.vcxproj @@ -17,14 +17,6 @@ Release x64 - - WinXP Release - Win32 - - - WinXP Release - x64 - WinXP_32 Win32 @@ -33,22 +25,6 @@ WinXP_32 x64 - - WinXP_release_x86 - Win32 - - - WinXP_release_x86 - x64 - - - Win_XP_32 - Win32 - - - Win_XP_32 - x64 - {01CD1176-66F7-44AB-9E7B-8AEECC9915E8} @@ -200,8 +176,8 @@ Level3 Disabled - _CRT_SECURE_NO_WARNINGS;LEVELDB_PLATFORM_POSIX;LEVELDB_CSTDATOMIC_PRESENT;NOMINMAX;WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) - ..\pthreads-w32-2-9-1-release\Pre-built.2\include;.\win32_posix;..\leveldb;..\leveldb\include;%(AdditionalIncludeDirectories) + _CRT_SECURE_NO_WARNINGS;LEVELDB_PLATFORM_POSIX;LEVELDB_HAVE_MEMORY_BARRIER;NOMINMAX;WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + .\win32_posix;..\leveldb;..\leveldb\include;%(AdditionalIncludeDirectories) MultiThreadedDebug
    @@ -209,8 +185,9 @@ true - pthreadVC2.lib - ..\pthreads-w32-2-9-1-release\Pre-built.2\lib\x86;%(AdditionalLibraryDirectories) + + + %(AdditionalLibraryDirectories)
    @@ -241,7 +218,7 @@ Level3 Disabled - _CRT_SECURE_NO_WARNINGS;LEVELDB_PLATFORM_POSIX;LEVELDB_CSTDATOMIC_PRESENT;NOMINMAX;WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + _CRT_SECURE_NO_WARNINGS;LEVELDB_PLATFORM_POSIX;LEVELDB_HAVE_MEMORY_BARRIER;NOMINMAX;WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) ..\pthreads-w32-2-9-1-release\Pre-built.2\include;.\win32_posix;..\leveldb;..\leveldb\include;%(AdditionalIncludeDirectories) MultiThreadedDebug
    @@ -281,7 +258,7 @@ Full true false - _CRT_SECURE_NO_WARNINGS;LEVELDB_PLATFORM_POSIX;LEVELDB_CSTDATOMIC_PRESENT;NOMINMAX;WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + _CRT_SECURE_NO_WARNINGS;LEVELDB_PLATFORM_POSIX;LEVELDB_HAVE_MEMORY_BARRIER;NOMINMAX;USE_CONDVAR;NDEBUG;%(PreprocessorDefinitions) MultiThreaded .\win32_posix;..\leveldb;..\leveldb\include;%(AdditionalIncludeDirectories) None @@ -311,9 +288,9 @@ MaxSpeed true true - _CRT_SECURE_NO_WARNINGS;LEVELDB_PLATFORM_POSIX;LEVELDB_HAVE_MEMORY_BARRIER;NOMINMAX;WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + _CRT_SECURE_NO_WARNINGS;LEVELDB_PLATFORM_POSIX;LEVELDB_HAVE_MEMORY_BARRIER;NDEBUG;LEVELDB_HAVE_MEMORY_BARRIER;NOMINMAX;%(PreprocessorDefinitions) MultiThreaded - ..\pthreads-w32-2-9-1-release\Pre-built.2\include;.\win32_posix;..\leveldb;..\leveldb\include;%(AdditionalIncludeDirectories) + .\win32_posix;..\leveldb;..\leveldb\include;%(AdditionalIncludeDirectories)
    Console @@ -324,10 +301,11 @@ ..\pthreads-w32-2-9-1-release\Pre-built.2\lib\x86;..\pthreads-w32-2-9-1-release\Pre-built.2\dll\x86;$(SolutionDir)$(Platform)\libs;C:\WinDDK\7600.16385.1\lib\ATL\i386 - pthreadVC2.lib + + - ..\pthreads-w32-2-9-1-release\Pre-built.2\lib\x86;%(AdditionalLibraryDirectories) + %(AdditionalLibraryDirectories) @@ -338,7 +316,7 @@ MaxSpeed true true - _CRT_SECURE_NO_WARNINGS;LEVELDB_PLATFORM_POSIX;LEVELDB_CSTDATOMIC_PRESENT;NOMINMAX;WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + _CRT_SECURE_NO_WARNINGS;LEVELDB_PLATFORM_POSIX;LEVELDB_HAVE_MEMORY_BARRIER;NOMINMAX;USE_CONDVAR;NDEBUG;%(PreprocessorDefinitions) MultiThreaded .\win32_posix;..\leveldb;..\leveldb\include;%(AdditionalIncludeDirectories)
    @@ -366,7 +344,7 @@ MaxSpeed true true - _CRT_SECURE_NO_WARNINGS;LEVELDB_PLATFORM_POSIX;LEVELDB_CSTDATOMIC_PRESENT;NOMINMAX;WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + _CRT_SECURE_NO_WARNINGS;LEVELDB_PLATFORM_POSIX;LEVELDB_HAVE_MEMORY_BARRIER;NDEBUG;LEVELDB_CSTDATOMIC_PRESENT;NOMINMAX;WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) MultiThreaded ..\pthreads-w32-2-9-1-release\Pre-built.2\include;.\win32_posix;..\leveldb;..\leveldb\include;%(AdditionalIncludeDirectories)
    @@ -405,7 +383,6 @@ - @@ -443,7 +420,7 @@ - + diff --git a/cppForSwig/leveldb_windows_port/win32_posix/Win_TranslatePath.cpp b/cppForSwig/leveldb_windows_port/win32_posix/Win_TranslatePath.cpp new file mode 100644 index 000000000..6c5e2d0fb --- /dev/null +++ b/cppForSwig/leveldb_windows_port/win32_posix/Win_TranslatePath.cpp @@ -0,0 +1,13 @@ +#include + +std::wstring Win_TranslatePath(std::string path) +{ + wchar_t rtw[MAX_PATH]; + MultiByteToWideChar(CP_UTF8, 0, path.c_str(), -1, rtw, MAX_PATH); + + std::wstring rtstring; + + rtstring.assign(rtw); + + return rtstring; +} diff --git a/cppForSwig/leveldb_windows_port/win32_posix/Win_TranslatePath.h b/cppForSwig/leveldb_windows_port/win32_posix/Win_TranslatePath.h new file mode 100644 index 000000000..51761c29a --- /dev/null +++ b/cppForSwig/leveldb_windows_port/win32_posix/Win_TranslatePath.h @@ -0,0 +1,4 @@ +#include +#include + +std::wstring Win_TranslatePath(std::string path); \ No newline at end of file diff --git a/cppForSwig/leveldb_windows_port/win32_posix/dirent_win32.c b/cppForSwig/leveldb_windows_port/win32_posix/dirent_win32.cpp similarity index 56% rename from cppForSwig/leveldb_windows_port/win32_posix/dirent_win32.c rename to cppForSwig/leveldb_windows_port/win32_posix/dirent_win32.cpp index f7a65df33..c24203f8a 100644 --- a/cppForSwig/leveldb_windows_port/win32_posix/dirent_win32.c +++ b/cppForSwig/leveldb_windows_port/win32_posix/dirent_win32.cpp @@ -14,30 +14,28 @@ #include /* _findfirst and _findnext set errno iff they return -1 */ #include #include - -#ifdef __cplusplus -extern "C" -{ -#endif +#include typedef ptrdiff_t handle_type; /* C99's intptr_t not sufficiently portable */ struct DIR { handle_type handle; /* -1 for failed rewind */ - struct _finddata_t info; + //struct _finddata_t info; + struct _wfinddata_t winfo; struct dirent result; /* d_name null iff first time */ - char *name; /* null-terminated char string */ + //char *name; /* null-terminated char string */ + wchar_t *wname; /* null-terminated wchar string */ }; -DIR *opendir(const char *name) +/*DIR *opendir(const char *name) { DIR *dir = 0; if(name && name[0]) { size_t base_length = strlen(name); - const char *all = /* search pattern must end with suitable wildcard */ + const char *all = strchr("/\\", name[base_length - 1]) ? "*" : "/*"; if((dir = (DIR *) malloc(sizeof *dir)) != 0 && @@ -58,13 +56,77 @@ DIR *opendir(const char *name) { dir->result.d_name = 0; } - else /* rollback */ + else { free(dir->name); free(dir); dir = 0; } } + else + { + free(dir); + dir = 0; + errno = ENOMEM; + } + } + else + { + errno = EINVAL; + } + + return dir; +}*/ + +DIR *opendir(const char *name) +{ + wchar_t *namew = (wchar_t*)malloc(sizeof(wchar_t)*(strlen(name)+1)); + MultiByteToWideChar(CP_UTF8, 0, name, -1, namew, strlen(name)+1); + + DIR *rtdir = opendir(namew); + free(namew); + + return rtdir; +} + +DIR *opendir(const wchar_t *name) +{ + DIR *dir = 0; + + if(name && name[0]) + { + size_t base_length = wcslen(name); + const wchar_t *all = /* search pattern must end with suitable wildcard */ + wcschr(L"/\\", name[base_length - 1]) ? L"*" : L"/*"; + + if((dir = (DIR *) malloc(sizeof *dir)) != 0 && + (dir->wname = (wchar_t *) malloc(sizeof(wchar_t)*(base_length + wcslen(all) + 2))) != 0) + { + dir->result.d_name = 0; + + //make sure '.' is prepended to paths starting with '/' + if(name[0]==L'/') + { + dir->wname[0] = L'.'; + dir->wname[1] = 0; + } + else dir->wname[0] = 0; + + wcscat(wcscat(dir->wname, name), all); + + if((dir->handle = + (handle_type) _wfindfirst(dir->wname, &dir->winfo)) != -1) + { + dir->result.wd_name = 0; + dir->result.d_name = (char*)malloc(MAX_PATH); + } + else /* rollback */ + { + free(dir->wname); + free(dir); + dir = 0; + } + } else /* rollback */ { free(dir); @@ -91,8 +153,9 @@ int closedir(DIR *dir) result = _findclose(dir->handle); } - free(dir->name); - free(dir); + if(dir->result.d_name) free(dir->result.d_name); + free(dir->wname); + free(dir); } if(result == -1) /* map all errors to EBADF */ @@ -109,10 +172,11 @@ struct dirent *readdir(DIR *dir) if(dir && dir->handle != -1) { - if(!dir->result.d_name || _findnext(dir->handle, &dir->info) != -1) + if(!dir->result.wd_name || _wfindnext(dir->handle, &dir->winfo) != -1) { result = &dir->result; - result->d_name = dir->info.name; + result->wd_name = dir->winfo.name; + WideCharToMultiByte(CP_UTF8, 0, dir->winfo.name, -1, result->d_name, MAX_PATH, 0, 0); } } else @@ -128,8 +192,9 @@ void rewinddir(DIR *dir) if(dir && dir->handle != -1) { _findclose(dir->handle); - dir->handle = (handle_type) _findfirst(dir->name, &dir->info); - dir->result.d_name = 0; + dir->handle = (handle_type) _wfindfirst(dir->wname, &dir->winfo); + dir->result.wd_name = 0; + dir->result.d_name[0] = 0; } else { @@ -137,10 +202,6 @@ void rewinddir(DIR *dir) } } -#ifdef __cplusplus -} -#endif - /* Copyright Kevlin Henney, 1997, 2003, 2012. All rights reserved. diff --git a/cppForSwig/leveldb_windows_port/win32_posix/dirent_win32.h b/cppForSwig/leveldb_windows_port/win32_posix/dirent_win32.h index aba9df994..a412921a4 100644 --- a/cppForSwig/leveldb_windows_port/win32_posix/dirent_win32.h +++ b/cppForSwig/leveldb_windows_port/win32_posix/dirent_win32.h @@ -11,19 +11,16 @@ */ -#ifdef __cplusplus -extern "C" -{ -#endif - typedef struct DIR DIR; struct dirent { char *d_name; + wchar_t *wd_name; }; DIR *opendir(const char *); +DIR *opendir(const wchar_t *); int closedir(DIR *); struct dirent *readdir(DIR *); void rewinddir(DIR *); @@ -42,9 +39,4 @@ void rewinddir(DIR *); But that said, if there are any problems please get in touch. */ - -#ifdef __cplusplus -} -#endif - #endif \ No newline at end of file diff --git a/cppForSwig/leveldb_windows_port/win32_posix/pthread.h b/cppForSwig/leveldb_windows_port/win32_posix/pthread.h index c4040ef52..c8a05d174 100644 --- a/cppForSwig/leveldb_windows_port/win32_posix/pthread.h +++ b/cppForSwig/leveldb_windows_port/win32_posix/pthread.h @@ -3,6 +3,9 @@ Minimal pthread port for leveldb purpose Cancelation isn't supported ***/ +#ifndef PTHREADS +#define PTHREADS + #include #include @@ -16,11 +19,24 @@ Cancelation isn't supported typedef CRITICAL_SECTION pthread_mutex_t; typedef DWORD pthread_t; typedef int pthread_attr_t; -typedef CONDITION_VARIABLE pthread_cond_t; +#ifdef USE_CONDVAR + typedef CONDITION_VARIABLE pthread_cond_t; +#else + struct pthread_cond_t_ + { + volatile long Broadcast; + volatile long resetEvent; + HANDLE mu; + HANDLE EV; + }; + + typedef struct pthread_cond_t_ * pthread_cond_t; + +#endif typedef int * pthread_condattr_t; #define PTHREAD_ONCE_INIT 0 -typedef long pthread_once_t; +typedef volatile long pthread_once_t; int pthread_mutex_init(pthread_mutex_t *mu, const int mutex_attr); int pthread_mutex_lock(pthread_mutex_t *mu); @@ -39,4 +55,4 @@ DWORD pthread_self(); int pthread_once(pthread_once_t *once, void (*func)(void)); - +#endif diff --git a/cppForSwig/leveldb_windows_port/win32_posix/pthread_win32port.cpp b/cppForSwig/leveldb_windows_port/win32_posix/pthread_win32port.cpp index 40894f2fb..0a29093fd 100644 --- a/cppForSwig/leveldb_windows_port/win32_posix/pthread_win32port.cpp +++ b/cppForSwig/leveldb_windows_port/win32_posix/pthread_win32port.cpp @@ -40,6 +40,7 @@ int pthread_create(pthread_t *tid, pthread_attr_t *attr, void*(*start)(void*), v return -1; } +#ifdef USE_CONDVAR int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr) { InitializeConditionVariable(cond); @@ -71,31 +72,66 @@ int pthread_cond_destroy(pthread_cond_t *cond) { return 0; } +#else //in case it has to run on WinXP, condition variables aren't supported, have to implement them with oldschool WinAPI calls. +int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr) +{ + (*cond) = (pthread_cond_t_*)malloc(sizeof(pthread_cond_t_)); -int pthread_once(pthread_once_t *once, void (*func)(void)) + (*cond)->Broadcast = 0; + (*cond)->resetEvent = 0; + (*cond)->mu = 0; + (*cond)->EV = CreateEvent(NULL, 0, 0, NULL); + + return 0; +} + +int pthread_cond_signal(pthread_cond_t *cond) +{ + SetEvent((*cond)->EV); + return 0; +} + +int pthread_cond_broadcast(pthread_cond_t *cond) { - long state = *once; + _InterlockedExchange(&(*cond)->Broadcast, 1); + SetEvent((*cond)->EV); + + return 0; +} - _ReadWriteBarrier(); +int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mu) +{ + LeaveCriticalSection(mu); - while (state != 1) + WaitForSingleObject((*cond)->EV, INFINITE); + + EnterCriticalSection(mu); + + if((*cond)->Broadcast) + SetEvent((*cond)->EV); + + return 0; +} + +int pthread_cond_destroy(pthread_cond_t *cond) +{ + CloseHandle((*cond)->EV); + free(*cond); + *cond = 0; + return 0; +} +#endif + +int pthread_once(pthread_once_t *once, void (*func)(void)) +{ + while(*once!=1) { - if (!state) + if(!InterlockedCompareExchange(once, 2, 0)) { - if (!InterlockedCompareExchange(once, 2, 0)) - { - func(); - *once = 1; - - return 0; - } + if(*once==2) func(); //consume once to prevent reordering + *once=1; + return 0; } - - YieldProcessor(); - - _ReadWriteBarrier(); - - state = *once; } return 0; diff --git a/cppForSwig/leveldb_windows_port/win32_posix/win32_posix.cc b/cppForSwig/leveldb_windows_port/win32_posix/win32_posix.cc index 344bf00e7..c5be7b808 100644 --- a/cppForSwig/leveldb_windows_port/win32_posix/win32_posix.cc +++ b/cppForSwig/leveldb_windows_port/win32_posix/win32_posix.cc @@ -1,3 +1,8 @@ +/**************************************************************************************/ +/*** goatpig's (moothecowlord@gmail.com) msvc port, free to use, reuse and modify ***/ +/**************************************************************************************/ + + #include /***Path name resolution: have to preppend a '.' to paths starting with '/' or they'll be resolved as "system disk"/pathname @@ -6,6 +11,19 @@ next null bytes. Not only will this yield messed up reads, CRCs will fail and the DB won't setup, returning with an error. ***/ +int fread_unlockd(void *_DstBuf, size_t _EleSize, size_t _Count, FILE *_File) +{ + int fd = _fileno(_File); + + unsigned int rt = _read(fd, _DstBuf, _EleSize*_Count); + if(_eof(fd)) + { + int abc; + _fread_nolock(&abc, 1, 1, _File); + } + + return rt; +} int pread_win32(int fd, void *buff, unsigned int size, off_t offset) { @@ -16,15 +34,13 @@ int pread_win32(int fd, void *buff, unsigned int size, off_t offset) The main issue of this port is to respect atomicity of pread: it's fair to assume pread doesn't modify the file pointer at all, so a port that would read the file, setting the pointer from the read, then rewind it, wouldn't be atomic regarding the file pointer. - - Unsure if this is what ReadFile does with these OVERLAPPED settings, have to test it. ***/ unsigned int rt=-1; HANDLE hF = (HANDLE)_get_osfhandle(fd); if(hF) { OVERLAPPED ol; - memset(&ol, 0, sizeof(OVERLAPPED)); //verify that this works for synchronous files as expected + memset(&ol, 0, sizeof(OVERLAPPED)); ol.Offset = offset; ReadFile(hF, buff, size, (DWORD*)&rt, &ol); @@ -64,13 +80,6 @@ int ftruncate_win32(int fd, off_t length) //file size has been changed, set pointer to back to cpos SetFilePointer((HANDLE)hF, cpos, 0, FILE_BEGIN); - //set all new bytes to 0 - /*byte *null_bytes = (byte*)malloc(length); - memset(null_bytes, 0, length); - _write(fd, null_bytes, length); - free(null_bytes); - - SetFilePointer((HANDLE)hF, cpos, 0, FILE_BEGIN);*/ return 0; //returns 0 on success } } @@ -83,9 +92,9 @@ int ftruncate_win32(int fd, off_t length) int access_win32(const char *path, int mode) { - char *win32_path = posix_path_to_win32(path); + wchar_t *win32_path = posix_path_to_win32(path); - int i = _access(win32_path, mode); + int i = _waccess(win32_path, mode); free(win32_path); return i; @@ -93,9 +102,9 @@ int access_win32(const char *path, int mode) int unlink_win32(const char *path) { - char *win32_path = posix_path_to_win32(path); + wchar_t *win32_path = posix_path_to_win32(path); - int i = _unlink(win32_path); + int i = _wunlink(win32_path); free(win32_path); return i; @@ -103,9 +112,9 @@ int unlink_win32(const char *path) int open_win32(const char *path, int flag, int pmode) { - char *win32_path = posix_path_to_win32(path); + wchar_t *win32_path = posix_path_to_win32(path); - int i = _open(win32_path, flag | _O_BINARY, pmode); + int i = _wopen(win32_path, flag | _O_BINARY, pmode); free(win32_path); return i; @@ -113,8 +122,10 @@ int open_win32(const char *path, int flag, int pmode) int open_win32(const char *path, int flag) { - /***In leveldb, Open(2) may be called to get a file descriptor for a directory. Win32's _open won't accept directories as entry so you're kebab. This port is particularly - annoying as you need to simulate calls to a single posix function that split into 2 different types of routine based on the input. + /***In leveldb, Open(2) may be called to get a file descriptor for a directory. + Win32's _open won't accept directories as entry so you're kebab. + This port is particularly annoying as you need to simulate calls to a single + posix function that split into 2 different types of routine based on the input. This how this shit will go: 1) Open the path with WinAPI call @@ -131,14 +142,15 @@ int open_win32(const char *path, int flag) //if(flag && _O_TRUNC) opening |= TRUNCATE_EXISTING; } - char *win32_path = posix_path_to_win32(path); - if(GetFileAttributes(win32_path)==FILE_ATTRIBUTE_DIRECTORY) + wchar_t *win32_path = posix_path_to_win32(path); + DWORD f = GetFileAttributesW(win32_path); + if(f & FILE_ATTRIBUTE_DIRECTORY) { attributes |= FILE_FLAG_BACKUP_SEMANTICS; desired_access |= GENERIC_WRITE; //grant additional write access to folders, can't flush the folder without it } - fHandle = CreateFile(win32_path, desired_access, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, disposition, attributes, NULL); + fHandle = CreateFileW(win32_path, desired_access, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, disposition, attributes, NULL); free(win32_path); if(fHandle!=INVALID_HANDLE_VALUE) @@ -155,24 +167,24 @@ int open_win32(const char *path, int flag) int mkdir_win32(const char *path, int mode) { - /*** POSIX mkdir also sets directory access rights through 'mod'. While this makes sense in UNIX OSs, it's more or less pointless on a Windows platform. Access rights can be - mimiced on Windows, but the software would require certain rights that I suspect would require running it at admin. Enforcing rights is however irrelevant to proper function + /*** POSIX mkdir also sets directory access rights through 'mode'. While this makes sense in UNIX OSs, it's more or less pointless on a Windows platform. Access rights can be + mimiced on Windows, but the software would require certain token rights that I suspect would require running it as admin. Enforcing rights is however irrelevant for proper function of mkdir on Windows. Will come back to this later for full implementation if it reveals itself needed. Some behavior to pay attention to: - mkdir can only create a single directory per call. If there are several of them to create in the indicate path, you have to cut down the path name to every single unmade path. + mkdir can only create a single directory per call. If there are several of them to create in the indicated path, you have to cut down the path name to every single unmade path. ***/ - char *rm_path = posix_path_to_win32(path); - int l = strlen(rm_path), s=0, i=0; + wchar_t *rm_path = posix_path_to_win32(path); + int l = wcslen(rm_path), s=0, i=0; while(sd_name, ".") && strcmp(killfile->d_name, "..")) //skip all . and .. returned by dirent + if(wcscmp(killfile->wd_name, L".") && wcscmp(killfile->wd_name, L"..")) //skip all . and .. returned by dirent { - strcpy(filepath +fpl, killfile->d_name); - if(GetFileAttributes(filepath)==FILE_ATTRIBUTE_DIRECTORY) //check path is a folder + wcscpy(filepath +fpl, killfile->wd_name); + if(GetFileAttributesW(filepath) & FILE_ATTRIBUTE_DIRECTORY) //check path is a folder rmdir_resovled(filepath); //if it's a folder, call rmdir on it else - _unlink(filepath); //else delete the file + _wunlink(filepath); //else delete the file } } free(filepath); } closedir(dempty); - i = RemoveDirectory(win32_path); + i = RemoveDirectoryW(win32_path); if(!i) { _set_errno(EBADF); @@ -279,21 +291,21 @@ int rmdir_win32(const char *path) ***/ int i=0; - char *win32_path = posix_path_to_win32(path); - if(win32_path[strlen(win32_path)-1]=='*') //wildcard handling + wchar_t *win32_path = posix_path_to_win32(path); + if(win32_path[wcslen(win32_path)-1]=='*') //wildcard handling { //get wild card - char *wildcard = (char*)malloc(MAX_PATH); - memset(wildcard, 0, MAX_PATH); - int l = strlen(win32_path), s=l-2; //-2 for the * + wchar_t *wildcard = (wchar_t*)malloc(sizeof(wchar_t)*MAX_PATH); + memset(wildcard, 0, sizeof(wchar_t)*MAX_PATH); + int l = wcslen(win32_path), s=l-1; //-1 for the * int wildcard_length=0; while(s) { - if(win32_path[s]=='/') + if(win32_path[s]==L'/') { wildcard_length = l-1 -s-1; - memcpy(wildcard, win32_path +s+1, wildcard_length); + memcpy(wildcard, win32_path +s+1, sizeof(wchar_t)*wildcard_length); wildcard[wildcard_length] = 0; win32_path[s+1]=0; //take wildcard away from origin path @@ -304,10 +316,10 @@ int rmdir_win32(const char *path) s--; } - char *delpath = (char*)malloc(MAX_PATH); - char *checkwc = (char*)malloc(MAX_PATH); - strcpy(delpath, win32_path); - int dpl = strlen(win32_path); + wchar_t *delpath = (wchar_t*)malloc((sizeof(wchar_t)*MAX_PATH)); + wchar_t *checkwc = (wchar_t*)malloc((sizeof(wchar_t)*MAX_PATH)); + wcscpy(delpath, win32_path); + int dpl = wcslen(win32_path); //find all directories in path DIR* dirrm = opendir(win32_path); @@ -316,16 +328,16 @@ int rmdir_win32(const char *path) dirent *dirp = 0; while((dirp=readdir(dirrm))) { - if(strcmp(dirp->d_name, ".") && strcmp(dirp->d_name, "..")) //skip all . and .. returned by dirent + if(wcscmp(dirp->wd_name, L".") && wcscmp(dirp->wd_name, L"..")) //skip all . and .. returned by dirent { - strcpy(delpath +dpl, dirp->d_name); - if(GetFileAttributes(delpath)==FILE_ATTRIBUTE_DIRECTORY) //check path is a folder + wcscpy(delpath +dpl, dirp->wd_name); + if(GetFileAttributesW(delpath) & FILE_ATTRIBUTE_DIRECTORY) //check path is a folder { //check path against wildcard - strcpy(checkwc, dirp->d_name); + wcscpy(checkwc, dirp->wd_name); checkwc[wildcard_length] = 0; - if(!strcmp(checkwc, wildcard)) //wild card matches + if(!wcscmp(checkwc, wildcard)) //wild card matches { i |= rmdir_resovled(delpath); } @@ -353,13 +365,13 @@ int rmdir_win32(std::string st_in) int rename_win32(const char *oldname, const char *newname) { - char *oldname_win32, *newname_win32; + wchar_t *oldname_win32, *newname_win32; oldname_win32 = posix_path_to_win32(oldname); newname_win32 = posix_path_to_win32(newname); int o=0; - if(!MoveFileEx(oldname_win32, newname_win32, MOVEFILE_REPLACE_EXISTING)) //posix rename replaces existing files + if(!MoveFileExW(oldname_win32, newname_win32, MOVEFILE_REPLACE_EXISTING)) //posix rename replaces existing files { DWORD last_error = GetLastError(); if(last_error==ERROR_FILE_NOT_FOUND) _set_errno(ENOENT); @@ -375,16 +387,9 @@ int rename_win32(const char *oldname, const char *newname) int stat_win32(const char *path, struct stat *Sin) { - /*** - Regarding stat: for stat to point at the function and not the struct in win32, sys/types.h has to be declared BEFORE sys/stat.h, which isn't the case with env_posix.cc - This one is a bit painful. In order for the function to be accessible, it has to be defined after the struct. Since the attempt of this port is to limit modification of source file - to a maximum, the idea is to #define stat to stat_win32 and perform all the required handling over here. However life isn't so simple: this #define makes the struct unaccessible - since #define doesn't discriminate between the function and the struct. However, now that stat (the function) has been defined to another name (stat_win32) we can redefine the - structure. However we can't simply use this #define before the sys/stat.h include since stat would then be redefined as a function, cancelling the whole process - ***/ - char *path_win32 = posix_path_to_win32(path); + wchar_t *path_win32 = posix_path_to_win32(path); - int i = _stat(path_win32, (struct _stat64i32*)Sin); + int i = _wstat(path_win32, (struct _stat64i32*)Sin); free(path_win32); return i; @@ -392,14 +397,14 @@ int stat_win32(const char *path, struct stat *Sin) FILE* fopen_win32(const char *path, const char *mode) { - char *mode_win32 = (char*)malloc(5); - strcpy(mode_win32, mode); - strcat(mode_win32, "b"); + wchar_t *mode_win32 = (wchar_t*)malloc(10); + MultiByteToWideChar(CP_UTF8, 0, mode, -1, mode_win32, 10); + wcscat(mode_win32, L"b"); - char *path_win32 = posix_path_to_win32(path); + wchar_t *path_win32 = posix_path_to_win32(path); FILE *f; - f = _fsopen(path_win32, mode_win32, _SH_DENYNO); + f = _wfsopen(path_win32, mode_win32, _SH_DENYNO); free(mode_win32); free(path_win32); @@ -504,7 +509,7 @@ int fcntl_win32(int fd, unsigned int command, flock *f) return -1; } -char *posix_path_to_win32(const char *posix_path) +wchar_t *posix_path_to_win32(const char *posix_path) { /*** appends . to the begining of the filename if it starts by a \\ or / make sure only one type of slash is used on the file name***/ @@ -514,7 +519,7 @@ char *posix_path_to_win32(const char *posix_path) if(posix_path[0]=='\\' || posix_path[0]=='/') { - win32_path[0] = '.'; + win32_path[0] = L'.'; strcpy(win32_path +1, posix_path); l++; } @@ -525,11 +530,15 @@ char *posix_path_to_win32(const char *posix_path) if(win32_path[i]=='\\') win32_path[i]='/'; } + + wchar_t *pathw = (wchar_t*)malloc(sizeof(wchar_t)*(strlen(win32_path)+1)); + MultiByteToWideChar(CP_UTF8, 0, win32_path, -1, pathw, strlen(win32_path)+1); + free(win32_path); - return win32_path; + return pathw; } -char *posix_path_to_win32_full(const char *posix_path) +wchar_t *posix_path_to_win32_full(const char *posix_path) { int i=0; int l=strlen(posix_path) +1; @@ -552,7 +561,11 @@ char *posix_path_to_win32_full(const char *posix_path) i++; } - return win32_path; + wchar_t *pathw = (wchar_t*)malloc(sizeof(wchar_t)*(strlen(win32_path)+1)); + MultiByteToWideChar(CP_UTF8, 0, win32_path, -1, pathw, strlen(win32_path)+1); + free(win32_path); + + return pathw; } diff --git a/cppForSwig/leveldb_windows_port/win32_posix/win32_posix.h b/cppForSwig/leveldb_windows_port/win32_posix/win32_posix.h index 685383ab2..fa2d7283a 100644 --- a/cppForSwig/leveldb_windows_port/win32_posix/win32_posix.h +++ b/cppForSwig/leveldb_windows_port/win32_posix/win32_posix.h @@ -11,6 +11,7 @@ #include #include #include +#include "stdint.h" #if defined LEVELDB_DLL #define LEVELDB_EXPORT __declspec(dllimport) @@ -37,7 +38,9 @@ typedef __ssize_t ssize_t; #endif //straight forward ports -#define fread_unlocked _fread_nolock +#define fread_unlocked _fread_nolock +#define fwrite_unlocked _fwrite_nolock +#define fflush_unlocked _fflush_nolock #define fsync fsync_win32 #define fdatasync fsync_win32 #define close _close @@ -47,10 +50,17 @@ typedef __ssize_t ssize_t; #define rmdir rmdir_win32 #define mkdir mkdir_win32 #define rename rename_win32 -#define stat stat_win32 #define fopen fopen_win32 +#define stat stat_win32 + -/*redo pathing for opendir in custom dirent*/ +/*** +Regarding stat: for stat to point at the function and not the struct in win32, sys/types.h has to be declared BEFORE sys/stat.h, which isn't the case with env_posix.cc +This one is a bit painful. In order for the function to be accessible, it has to be defined after the struct. Since the attempt of this port is to limit modification of source file +to a maximum, the idea is to #define stat to stat_win32 and perform all the required handling over here. However life isn't so simple: this #define makes the struct unaccessible +since #define doesn't discriminate between the function and the struct. However, now that stat (the function) has been defined to another name (stat_win32) we can redefine the +structure. However we can't simply use this #define before the sys/stat.h include since stat would then be redefined as a function, cancelling the whole process +***/ struct stat { _dev_t st_dev; @@ -79,10 +89,11 @@ int rename_win32(const char *oldname, const char *newname); int stat_win32(const char *path, struct stat *Sin); FILE* fopen_win32(const char *path, const char *mode); int fsync_win32(int fd); +int fread_unlockd(void *_DstBuf, size_t _EleSize, size_t _Count, FILE *_file); -char *posix_path_to_win32(const char *posix_path); -char *posix_path_to_win32_full(const char *posix_path); +wchar_t *posix_path_to_win32(const char *posix_path); +wchar_t *posix_path_to_win32_full(const char *posix_path); #define va_copy(d,s) ((d) = (s)) diff --git a/cppForSwig/leveldb_wrapper.cpp b/cppForSwig/leveldb_wrapper.cpp index d0b038715..0468ca9cd 100644 --- a/cppForSwig/leveldb_wrapper.cpp +++ b/cppForSwig/leveldb_wrapper.cpp @@ -1,6 +1,6 @@ //////////////////////////////////////////////////////////////////////////////// // // -// Copyright(C) 2011-2013, Armory Technologies, Inc. // +// Copyright (C) 2011-2014, Armory Technologies, Inc. // // Distributed under the GNU Affero General Public License (AGPL v3) // // See LICENSE or http://www.gnu.org/licenses/agpl.html // // // @@ -20,6 +20,278 @@ vector LevelDBWrapper::ifaceVect_(0); + + +//////////////////////////////////////////////////////////////////////////////// +LDBIter::LDBIter(leveldb::DB* dbptr, bool fill_cache) +{ + db_ = dbptr; + leveldb::ReadOptions readopts; + readopts.fill_cache = fill_cache; + iter_ = db_->NewIterator(readopts); + isDirty_ = true; +} + + + +//////////////////////////////////////////////////////////////////////////////// +bool LDBIter::isValid(DB_PREFIX dbpref) +{ + if(!isValid() || iter_->key().size() == 0) + return false; + return iter_->key()[0] == (char)dbpref; +} + + +//////////////////////////////////////////////////////////////////////////////// +bool LDBIter::advance(void) +{ + iter_->Next(); + isDirty_ = true; + return isValid(); +} + +//////////////////////////////////////////////////////////////////////////////// +bool LDBIter::advance(DB_PREFIX prefix) +{ + iter_->Next(); + isDirty_ = true; + return isValid(prefix); +} + +//////////////////////////////////////////////////////////////////////////////// +bool LDBIter::readIterData(void) +{ + if(!isValid()) + { + isDirty_ = true; + return false; + } + + currKey_.setNewData((uint8_t*)iter_->key().data(), iter_->key().size()); + currValue_.setNewData((uint8_t*)iter_->value().data(), iter_->value().size()); + isDirty_ = false; + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +bool LDBIter::advanceAndRead(void) +{ + if(!advance()) + return false; + return readIterData(); +} + +//////////////////////////////////////////////////////////////////////////////// +bool LDBIter::advanceAndRead(DB_PREFIX prefix) +{ + if(!advance(prefix)) + return false; + return readIterData(); +} + + +//////////////////////////////////////////////////////////////////////////////// +BinaryData LDBIter::getKey(void) +{ + if(isDirty_) + { + LOGERR << "Returning dirty key ref"; + return BinaryData(0); + } + return currKey_.getRawRef().copy(); +} + +//////////////////////////////////////////////////////////////////////////////// +BinaryData LDBIter::getValue(void) +{ + if(isDirty_) + { + LOGERR << "Returning dirty value ref"; + return BinaryData(0); + } + return currValue_.getRawRef().copy(); +} + +//////////////////////////////////////////////////////////////////////////////// +BinaryDataRef LDBIter::getKeyRef(void) +{ + if(isDirty_) + { + LOGERR << "Returning dirty key ref"; + return BinaryDataRef(); + } + return currKey_.getRawRef(); +} + +//////////////////////////////////////////////////////////////////////////////// +BinaryDataRef LDBIter::getValueRef(void) +{ + if(isDirty_) + { + LOGERR << "Returning dirty value ref"; + return BinaryDataRef(); + } + return currValue_.getRawRef(); +} + + +//////////////////////////////////////////////////////////////////////////////// +BinaryRefReader& LDBIter::getKeyReader(void) +{ + if(isDirty_) + LOGERR << "Returning dirty key reader"; + return currKey_; +} + +//////////////////////////////////////////////////////////////////////////////// +BinaryRefReader& LDBIter::getValueReader(void) +{ + if(isDirty_) + LOGERR << "Returning dirty value reader"; + return currValue_; +} + + +//////////////////////////////////////////////////////////////////////////////// +bool LDBIter::seekTo(BinaryDataRef key) +{ + if(isNull()) + return false; + iter_->Seek(binaryDataRefToSlice(key)); + return readIterData(); +} + +//////////////////////////////////////////////////////////////////////////////// +bool LDBIter::seekTo(DB_PREFIX pref, BinaryDataRef key) +{ + BinaryWriter bw(key.getSize() + 1); + bw.put_uint8_t((uint8_t)pref); + bw.put_BinaryData(key); + bool didSucceed = seekTo(bw.getDataRef()); + if(didSucceed) + readIterData(); + return didSucceed; +} + + +//////////////////////////////////////////////////////////////////////////////// +bool LDBIter::seekToExact(BinaryDataRef key) +{ + if(!seekTo(key)) + return false; + + return checkKeyExact(key); +} + +//////////////////////////////////////////////////////////////////////////////// +bool LDBIter::seekToExact(DB_PREFIX pref, BinaryDataRef key) +{ + if(!seekTo(pref, key)) + return false; + + return checkKeyExact(pref, key); +} + +//////////////////////////////////////////////////////////////////////////////// +bool LDBIter::seekToStartsWith(BinaryDataRef key) +{ + if(!seekTo(key)) + return false; + + return checkKeyStartsWith(key); + +} + +//////////////////////////////////////////////////////////////////////////////// +bool LDBIter::seekToStartsWith(DB_PREFIX prefix) +{ + BinaryWriter bw(1); + bw.put_uint8_t((uint8_t)prefix); + if(!seekTo(bw.getDataRef())) + return false; + + return checkKeyStartsWith(bw.getDataRef()); + +} + +//////////////////////////////////////////////////////////////////////////////// +bool LDBIter::seekToStartsWith(DB_PREFIX pref, BinaryDataRef key) +{ + if(!seekTo(pref, key)) + return false; + + return checkKeyStartsWith(pref, key); +} + + +//////////////////////////////////////////////////////////////////////////////// +bool LDBIter::seekToFirst(void) +{ + if(isNull()) + return false; + iter_->SeekToFirst(); + readIterData(); + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +bool LDBIter::checkKeyExact(BinaryDataRef key) +{ + if(isDirty_ && !readIterData()) + return false; + + return (key==currKey_.getRawRef()); +} + +//////////////////////////////////////////////////////////////////////////////// +bool LDBIter::checkKeyExact(DB_PREFIX prefix, BinaryDataRef key) +{ + BinaryWriter bw(key.getSize() + 1); + bw.put_uint8_t((uint8_t)prefix); + bw.put_BinaryData(key); + if(isDirty_ && !readIterData()) + return false; + + return (bw.getDataRef()==currKey_.getRawRef()); +} + +//////////////////////////////////////////////////////////////////////////////// +bool LDBIter::checkKeyStartsWith(BinaryDataRef key) +{ + if(isDirty_ && !readIterData()) + return false; + + return (currKey_.getRawRef().startsWith(key)); +} + +//////////////////////////////////////////////////////////////////////////////// +bool LDBIter::verifyPrefix(DB_PREFIX prefix, bool advanceReader) +{ + if(isDirty_ && !readIterData()) + return false; + + if(currKey_.getSizeRemaining() < 1) + return false; + + if(advanceReader) + return (currKey_.get_uint8_t() == (uint8_t)prefix); + else + return (currKey_.getRawRef()[0] == (uint8_t)prefix); +} + +//////////////////////////////////////////////////////////////////////////////// +bool LDBIter::checkKeyStartsWith(DB_PREFIX prefix, BinaryDataRef key) +{ + BinaryWriter bw(key.getSize() + 1); + bw.put_uint8_t((uint8_t)prefix); + bw.put_BinaryData(key); + return checkKeyStartsWith(bw.getDataRef()); +} + + + + //////////////////////////////////////////////////////////////////////////////// bool InterfaceToLDB::checkStatus(leveldb::Status stat, bool warn) { @@ -79,7 +351,6 @@ void InterfaceToLDB::init() dbIsOpen_ = false; for(uint8_t i=0; iNewIterator(leveldb::ReadOptions()); batches_[db] = NULL; batchStarts_[db] = 0; @@ -250,18 +524,15 @@ void InterfaceToLDB::nukeHeadersDB(void) { SCOPED_TIMER("nukeHeadersDB"); LOGINFO << "Destroying headers DB, to be rebuilt."; - seekTo(HEADERS, DB_PREFIX_HEADHASH, BinaryData(0)); - leveldb::Iterator* iter = iters_[HEADERS]; + LDBIter ldbIter = getIterator(HEADERS); + ldbIter.seekToFirst(); startBatch(HEADERS); - do + while(ldbIter.isValid()) { - if(!iter->Valid()) - break; - - batches_[HEADERS]->Delete(iter->key()); - - } while(advanceIterAndRead(iter)); + batches_[HEADERS]->Delete(binaryDataRefToSlice(ldbIter.getKeyRef())); + ldbIter.advanceAndRead(); + } commitBatch(HEADERS); @@ -285,12 +556,6 @@ void InterfaceToLDB::closeDatabases(void) SCOPED_TIMER("closeDatabases"); for(uint32_t db=0; dbGet(STD_READ_OPTS, ldbKey, &lastGetValue_); @@ -632,11 +896,9 @@ void InterfaceToLDB::deleteValue(DB_SELECT db, //} ///////////////////////////////////////////////////////////////////////////// -bool InterfaceToLDB::startBlkDataIteration(DB_PREFIX prefix) +bool InterfaceToLDB::startBlkDataIteration(LDBIter & ldbIter, DB_PREFIX prefix) { - SCOPED_TIMER("startBlkDataIteration"); - seekTo(BLKDATA, prefix, BinaryData(0)); - return true; + return ldbIter.seekToStartsWith(prefix); } @@ -646,91 +908,35 @@ bool InterfaceToLDB::startBlkDataIteration(DB_PREFIX prefix) // the iterator already on the next desired block. So our "advance" op may // have finished before it started. Alternatively, we may be on this block // because we checked it and decide we don't care, so we want to skip it. -bool InterfaceToLDB::advanceToNextBlock(bool skip) +bool InterfaceToLDB::advanceToNextBlock(LDBIter & ldbIter, bool skip) { char prefix = DB_PREFIX_TXDATA; - - leveldb::Iterator* it = iters_[BLKDATA]; BinaryData key; while(1) { if(skip) - it->Next(); + ldbIter.advanceAndRead(); - if( !it->Valid() || it->key()[0] != (char)DB_PREFIX_TXDATA) + //if( !it->Valid() || it->key()[0] != (char)DB_PREFIX_TXDATA) + if(!ldbIter.isValid(DB_PREFIX_TXDATA)) return false; - else if( it->key().size() == 5) - { - iteratorToRefReaders(it, currReadKey_, currReadValue_); + else if(ldbIter.getKeyRef().getSize() == 5) return true; - } if(!skip) - it->Next(); + ldbIter.advanceAndRead(); + } LOGERR << "we should never get here..."; return false; } -///////////////////////////////////////////////////////////////////////////// -bool InterfaceToLDB::dbIterIsValid(DB_SELECT db, DB_PREFIX prefix) -{ - bool anyPrefixIsOkay = (prefix==DB_PREFIX_COUNT); - - if(!iters_[db]->Valid()) - return false; - - if(currReadKey_.getSize() == 0) - { - LOGERR << "Iter is valid but somehow key is zero length...?"; - return false; - } - - if(anyPrefixIsOkay) - return true; - - if(currReadKey_.getRawRef()[0] != (uint8_t)prefix) - return false; - - return true; -} - - -///////////////////////////////////////////////////////////////////////////// -// If we are seeking into the HEADERS DB, then ignore prefix -bool InterfaceToLDB::seekTo(DB_SELECT db, BinaryDataRef key) -{ - if(iterIsDirty_[db]) - resetIterator(db); - - iters_[db]->Seek(binaryDataRefToSlice(key)); - if(!iters_[db]->Valid()) - return false; - - iteratorToRefReaders(iters_[db], currReadKey_, currReadValue_); - bool isMatch = (currReadKey_.getRawRef()==key); - return isMatch; -} - -///////////////////////////////////////////////////////////////////////////// -// If we are seeking into the HEADERS DB, then ignore prefix -bool InterfaceToLDB::seekTo(DB_SELECT db, - DB_PREFIX prefix, - BinaryDataRef key) -{ - BinaryWriter bw(key.getSize() + 1); - bw.put_uint8_t((uint8_t)prefix); - bw.put_BinaryData(key); - return seekTo(db, bw.getData()); -} - - //////////////////////////////////////////////////////////////////////////////// // We frequently have a Tx hash and need to determine the Hgt/Dup/Index of it. // And frequently when we do, we plan to read the tx right afterwards, so we // should leave the itereator there. -bool InterfaceToLDB::seekToTxByHash(BinaryDataRef txHash) +bool InterfaceToLDB::seekToTxByHash(LDBIter & ldbIter, BinaryDataRef txHash) { SCOPED_TIMER("seekToTxByHash"); StoredTxHints sths = getHintsForTxHash(txHash); @@ -738,84 +944,45 @@ bool InterfaceToLDB::seekToTxByHash(BinaryDataRef txHash) for(uint32_t i=0; iNewIterator(leveldb::ReadOptions()); - iterIsDirty_[db] = false; - - if(seekToPrevKey) - seekTo(db, key); -} - -///////////////////////////////////////////////////////////////////////////// -// Returns refs to key and value that become invalid when iterator is moved -void InterfaceToLDB::iteratorToRefReaders( leveldb::Iterator* it, - BinaryRefReader & brrKey, - BinaryRefReader & brrValue) -{ - brrKey.setNewData((uint8_t*)(it->key().data()), it->key().size()); - brrValue.setNewData((uint8_t*)(it->value().data()), it->value().size()); -} - - ///////////////////////////////////////////////////////////////////////////// -bool InterfaceToLDB::readStoredScriptHistoryAtIter(StoredScriptHistory & ssh) +bool InterfaceToLDB::readStoredScriptHistoryAtIter(LDBIter & ldbIter, + StoredScriptHistory & ssh) { SCOPED_TIMER("readStoredScriptHistoryAtIter"); - resetIterReaders(); - checkPrefixByte(DB_PREFIX_SCRIPT); + ldbIter.resetReaders(); + ldbIter.verifyPrefix(DB_PREFIX_SCRIPT, false); - BinaryDataRef sshKey = currReadKey_.getRawRef(); + BinaryDataRef sshKey = ldbIter.getKeyRef(); ssh.unserializeDBKey(sshKey, true); - ssh.unserializeDBValue(currReadValue_); + ssh.unserializeDBValue(ldbIter.getValueReader()); if(!ssh.useMultipleEntries_) return true; @@ -823,10 +990,8 @@ bool InterfaceToLDB::readStoredScriptHistoryAtIter(StoredScriptHistory & ssh) if(ssh.totalTxioCount_ == 0) LOGWARN << "How did we end up with zero Txios in an SSH?"; - // If for some reason we hit the end of the DB without any tx, bail - bool iterValid = advanceIterAndRead(BLKDATA, DB_PREFIX_SCRIPT); - if(!iterValid) + if( !ldbIter.advanceAndRead(DB_PREFIX_SCRIPT)) { LOGERR << "No sub-SSH entries after the SSH"; return false; @@ -835,21 +1000,20 @@ bool InterfaceToLDB::readStoredScriptHistoryAtIter(StoredScriptHistory & ssh) // Now start iterating over the sub histories map::iterator iter; uint32_t numTxioRead = 0; - while(iters_[BLKDATA]->Valid()) + do { - uint32_t sz = currReadKey_.getSize(); - BinaryDataRef keyNoPrefix= currReadKey_.getRawRef().getSliceRef(1,sz-1); + uint32_t sz = ldbIter.getKeyRef().getSize(); + BinaryDataRef keyNoPrefix= ldbIter.getKeyRef().getSliceRef(1,sz-1); if(!keyNoPrefix.startsWith(ssh.uniqueKey_)) break; pair keyValPair; keyValPair.first = keyNoPrefix.getSliceCopy(sz-5, 4); - keyValPair.second.unserializeDBKey(currReadKey_.getRawRef()); - keyValPair.second.unserializeDBValue(currReadValue_); + keyValPair.second.unserializeDBKey(ldbIter.getKeyRef()); + keyValPair.second.unserializeDBValue(ldbIter.getValueReader()); iter = ssh.subHistMap_.insert(keyValPair).first; numTxioRead += iter->second.txioSet_.size(); - advanceIterAndRead(BLKDATA, DB_PREFIX_SCRIPT); - } + } while( ldbIter.advanceAndRead(DB_PREFIX_SCRIPT) ); if(numTxioRead != ssh.totalTxioCount_) { @@ -891,14 +1055,17 @@ void InterfaceToLDB::putStoredScriptHistory( StoredScriptHistory & ssh) void InterfaceToLDB::getStoredScriptHistorySummary( StoredScriptHistory & ssh, BinaryDataRef scrAddrStr) { - bool foundMatch = seekTo(BLKDATA, DB_PREFIX_SCRIPT, scrAddrStr); - if(!foundMatch) + LDBIter ldbIter = getIterator(BLKDATA); + bool seekTrue = ldbIter.seekTo(DB_PREFIX_SCRIPT, scrAddrStr); + + if(!ldbIter.seekToExact(DB_PREFIX_SCRIPT, scrAddrStr)) { ssh.uniqueKey_.resize(0); return; } - ssh.unserializeDBKey(currReadKey_.getRawRef()); - ssh.unserializeDBValue(currReadValue_.getRawRef()); + + ssh.unserializeDBKey(ldbIter.getKeyRef()); + ssh.unserializeDBValue(ldbIter.getValueRef()); } //////////////////////////////////////////////////////////////////////////////// @@ -906,15 +1073,14 @@ void InterfaceToLDB::getStoredScriptHistory( StoredScriptHistory & ssh, BinaryDataRef scrAddrStr) { SCOPED_TIMER("getStoredScriptHistory"); - seekTo(BLKDATA, DB_PREFIX_SCRIPT, scrAddrStr); - uint32_t sz = currReadKey_.getSize(); - if(currReadKey_.getRawRef().getSliceRef(1,sz-1) != scrAddrStr) + LDBIter ldbIter = getIterator(BLKDATA); + if(!ldbIter.seekToExact(DB_PREFIX_SCRIPT, scrAddrStr)) { ssh.uniqueKey_.resize(0); return; } - readStoredScriptHistoryAtIter(ssh); + readStoredScriptHistoryAtIter(ldbIter, ssh); } @@ -1027,35 +1193,6 @@ bool InterfaceToLDB::getFullUTXOMapForSSH( -///////////////////////////////////////////////////////////////////////////// -bool InterfaceToLDB::advanceIterAndRead(leveldb::Iterator* iter) -{ - if(!iter->Valid()) - return false; - - iter->Next(); - if(!iter->Valid()) - return false; - - iteratorToRefReaders(iter, currReadKey_, currReadValue_); - return true; -} - -///////////////////////////////////////////////////////////////////////////// -bool InterfaceToLDB::advanceIterAndRead(DB_SELECT db, DB_PREFIX prefix) -{ - if(iterIsDirty_[db]) - LOGERR << "DB has been changed since this iterator was created"; - - if(!iters_[db]->Valid()) - return false; - - if(advanceIterAndRead(iters_[db])) - return checkPrefixByte(prefix, true); - else - return false; -} - //////////////////////////////////////////////////////////////////////////////// // We must guarantee that we don't overwrite data if @@ -1102,9 +1239,8 @@ void InterfaceToLDB::addRegisteredScript(BinaryDataRef rawScript, void InterfaceToLDB::readAllHeaders(map & headerMap, map & storedMap) { - seekTo(HEADERS, DB_PREFIX_HEADHASH, BinaryData(0)); - if(!iters_[HEADERS]->Valid() || - currReadKey_.get_uint8_t() != (uint8_t)DB_PREFIX_HEADHASH) + LDBIter ldbIter = getIterator(HEADERS); + if(!ldbIter.seekToStartsWith(DB_PREFIX_HEADHASH)) { LOGWARN << "No headers in DB yet!"; return; @@ -1115,24 +1251,24 @@ void InterfaceToLDB::readAllHeaders(map & headerMap, BlockHeader regHead; do { - resetIterReaders(); - checkPrefixByte(DB_PREFIX_HEADHASH); + ldbIter.resetReaders(); + ldbIter.verifyPrefix(DB_PREFIX_HEADHASH); - if(currReadKey_.getSizeRemaining() != 32) + if(ldbIter.getKeyReader().getSizeRemaining() != 32) { LOGERR << "How did we get header hash not 32 bytes?"; continue; } - currReadKey_.get_BinaryData(sbh.thisHash_, 32); + ldbIter.getKeyReader().get_BinaryData(sbh.thisHash_, 32); - sbh.unserializeDBValue(HEADERS, currReadValue_); + sbh.unserializeDBValue(HEADERS, ldbIter.getValueRef()); regHead.unserialize(sbh.dataCopy_); headerMap[sbh.thisHash_] = regHead; storedMap[sbh.thisHash_] = sbh; - } while(advanceIterAndRead(HEADERS, DB_PREFIX_HEADHASH)); + } while(ldbIter.advanceAndRead(DB_PREFIX_HEADHASH)); } @@ -1451,8 +1587,8 @@ bool InterfaceToLDB::getStoredHeader( StoredHeader & sbh, { ////// // Do the iterator thing because we're going to traverse the whole block - bool isInDB = seekTo(BLKDATA, DBUtils.getBlkDataKey(blockHgt, blockDup)); - if(!isInDB) + LDBIter ldbIter = getIterator(BLKDATA); + if(!ldbIter.seekToExact(DBUtils.getBlkDataKey(blockHgt, blockDup))) { LOGERR << "Header heigh&dup is not in BLKDATA DB"; LOGERR << "("<Valid()) + ldbIter.advanceAndRead(); + while(ldbIter.checkKeyStartsWith(blkDataKey)) { - if(!currReadKey_.getRawRef().startsWith(blkDataKey)) - break; // We can't just read the the tx, because we have to guarantee // there's a place for it in the sbh.stxMap_ - BLKDATA_TYPE bdtype = DBUtils.readBlkDataKey(currReadKey_, + BLKDATA_TYPE bdtype = DBUtils.readBlkDataKey(ldbIter.getKeyReader(), tempHgt, tempDup, currIdx); @@ -1659,7 +1793,8 @@ bool InterfaceToLDB::readStoredBlockAtIter(StoredHeader & sbh) if(KEY_NOT_IN_MAP(currIdx, sbh.stxMap_)) sbh.stxMap_[currIdx] = StoredTx(); - readStoredTxAtIter(sbh.blockHeight_, + readStoredTxAtIter(ldbIter, + sbh.blockHeight_, sbh.duplicateID_, sbh.stxMap_[currIdx]); } @@ -1671,7 +1806,8 @@ bool InterfaceToLDB::readStoredBlockAtIter(StoredHeader & sbh) // We assume we have a valid iterator left at the beginning of (potentially) a // transaction in this block. It's okay if it starts at at TxOut entry (in // some instances we may not have a Tx entry, but only the TxOuts). -bool InterfaceToLDB::readStoredTxAtIter( uint32_t height, +bool InterfaceToLDB::readStoredTxAtIter( LDBIter & ldbIter, + uint32_t height, uint8_t dupID, StoredTx & stx) { @@ -1679,20 +1815,22 @@ bool InterfaceToLDB::readStoredTxAtIter( uint32_t height, BinaryData blkPrefix = DBUtils.getBlkDataKey(height, dupID); // Make sure that we are still within the desired block (but beyond header) - resetIterReaders(); - BinaryDataRef key = currReadKey_.getRawRef(); + ldbIter.resetReaders(); + BinaryDataRef key = ldbIter.getKeyRef(); if(!key.startsWith(blkPrefix) || key.getSize() < 7) return false; + // Check that we are at a tx with the correct height & dup uint32_t storedHgt; uint8_t storedDup; uint16_t storedIdx; - DBUtils.readBlkDataKey(currReadKey_, storedHgt, storedDup, storedIdx); + DBUtils.readBlkDataKey(ldbIter.getKeyReader(), storedHgt, storedDup, storedIdx); if(storedHgt != height || storedDup != dupID) return false; + // Make sure the stx has correct height/dup/idx stx.blockHeight_ = storedHgt; stx.duplicateID_ = storedDup; @@ -1709,16 +1847,19 @@ bool InterfaceToLDB::readStoredTxAtIter( uint32_t height, // Reset the key again, and then cycle through entries until no longer // on an entry with the correct prefix. Use do-while because we've // already verified the iterator is at a valid tx entry - resetIterReaders(); + ldbIter.resetReaders(); do { + + // Stop if key doesn't start with [PREFIX | HGT | DUP | TXIDX] - if(!currReadKey_.getRawRef().startsWith(txPrefix)) + if(!ldbIter.checkKeyStartsWith(txPrefix)) break; + // Read the prefix, height and dup uint16_t txOutIdx; - BLKDATA_TYPE bdtype = DBUtils.readBlkDataKey(currReadKey_, + BLKDATA_TYPE bdtype = DBUtils.readBlkDataKey(ldbIter.getKeyReader(), stx.blockHeight_, stx.duplicateID_, stx.txIndex_, @@ -1728,14 +1869,14 @@ bool InterfaceToLDB::readStoredTxAtIter( uint32_t height, if(bdtype == BLKDATA_TX) { // Get everything else from the iter value - stx.unserializeDBValue(currReadValue_); + stx.unserializeDBValue(ldbIter.getValueRef()); nbytes += stx.dataCopy_.getSize(); } else if(bdtype == BLKDATA_TXOUT) { stx.stxoMap_[txOutIdx] = StoredTxOut(); StoredTxOut & stxo = stx.stxoMap_[txOutIdx]; - readStoredTxOutAtIter(height, dupID, stx.txIndex_, stxo); + readStoredTxOutAtIter(ldbIter, height, dupID, stx.txIndex_, stxo); stxo.parentHash_ = stx.thisHash_; stxo.txVersion_ = stx.version_; nbytes += stxo.dataCopy_.getSize(); @@ -1745,7 +1886,9 @@ bool InterfaceToLDB::readStoredTxAtIter( uint32_t height, LOGERR << "Unexpected BLKDATA entry while iterating"; return false; } - } while(advanceIterAndRead(BLKDATA, DB_PREFIX_TXDATA)); + + } while(ldbIter.advanceAndRead(DB_PREFIX_TXDATA)); + // If have the correct size, save it, otherwise ignore the computation stx.numBytes_ = stx.haveAllTxOut() ? nbytes : UINT32_MAX; @@ -1756,22 +1899,24 @@ bool InterfaceToLDB::readStoredTxAtIter( uint32_t height, //////////////////////////////////////////////////////////////////////////////// bool InterfaceToLDB::readStoredTxOutAtIter( + LDBIter & ldbIter, uint32_t height, uint8_t dupID, uint16_t txIndex, StoredTxOut & stxo) { - if(currReadKey_.getSize() < 9) + if(ldbIter.getKeyRef().getSize() < 9) return false; - currReadKey_.resetPosition(); + ldbIter.resetReaders(); // Check that we are at a tx with the correct height & dup & txIndex uint32_t keyHgt; uint8_t keyDup; uint16_t keyTxIdx; uint16_t keyTxOutIdx; - DBUtils.readBlkDataKey(currReadKey_, keyHgt, keyDup, keyTxIdx, keyTxOutIdx); + DBUtils.readBlkDataKey(ldbIter.getKeyReader(), + keyHgt, keyDup, keyTxIdx, keyTxOutIdx); if(keyHgt != height || keyDup != dupID || keyTxIdx != txIndex) return false; @@ -1781,7 +1926,7 @@ bool InterfaceToLDB::readStoredTxOutAtIter( stxo.txIndex_ = txIndex; stxo.txOutIndex_ = keyTxOutIdx; - stxo.unserializeDBValue(currReadValue_); + stxo.unserializeDBValue(ldbIter.getValueRef()); return true; } @@ -1797,7 +1942,8 @@ Tx InterfaceToLDB::getFullTxCopy( BinaryData ldbKey6B ) return Tx(); } - if(!seekTo(BLKDATA, DB_PREFIX_TXDATA, ldbKey6B)) + LDBIter ldbIter = getIterator(BLKDATA); + if(!ldbIter.seekToStartsWith(DB_PREFIX_TXDATA, ldbKey6B)) { LOGERR << "TxRef key does not exist in BLKDATA DB"; return Tx(); @@ -1805,7 +1951,10 @@ Tx InterfaceToLDB::getFullTxCopy( BinaryData ldbKey6B ) BinaryData hgtx = ldbKey6B.getSliceCopy(0,4); StoredTx stx; - readStoredTxAtIter( DBUtils.hgtxToHeight(hgtx), DBUtils.hgtxToDupID(hgtx), stx); + readStoredTxAtIter( ldbIter, + DBUtils.hgtxToHeight(hgtx), + DBUtils.hgtxToDupID(hgtx), + stx); if(!stx.haveAllTxOut()) { @@ -1857,7 +2006,7 @@ TxOut InterfaceToLDB::getTxOutCopy( BinaryData ldbKey6B, uint16_t txOutIdx) TxRef parent(ldbKey6B, this); brr.advance(2); - txoOut.unserialize(brr.getCurrPtr(), 0, parent, (uint32_t)txOutIdx); + txoOut.unserialize_checked(brr.getCurrPtr(), brr.getSizeRemaining(), 0, parent, (uint32_t)txOutIdx); return txoOut; } @@ -1900,7 +2049,9 @@ TxIn InterfaceToLDB::getTxInCopy( BinaryData ldbKey6B, uint16_t txInIdx) TxRef parent(ldbKey6B, this); uint8_t const * txInStart = brr.exposeDataPtr() + 34 + offsetsIn[txInIdx]; uint32_t txInLength = offsetsIn[txInIdx+1] - offsetsIn[txInIdx]; - return TxIn(txInStart, txInLength, parent, txInIdx); + TxIn txin; + txin.unserialize_checked(txInStart, brr.getSize() - 34 - offsetsIn[txInIdx], txInLength, parent, txInIdx); + return txin; } } @@ -2023,6 +2174,7 @@ bool InterfaceToLDB::getStoredTx_byHash( StoredTx & stx, return false; } + LDBIter ldbIter = getIterator(BLKDATA); BinaryRefReader brrHints(hintsDBVal); uint32_t numHints = (uint32_t)brrHints.get_var_int(); uint32_t height; @@ -2031,9 +2183,16 @@ bool InterfaceToLDB::getStoredTx_byHash( StoredTx & stx, for(uint32_t i=0; iValid(); - BinaryData prevIterLoc; - if(restoreIterAtEnd) - prevIterLoc = sliceToBinaryData(iters_[db]->key()); - KVLIST outList; outList.reserve(100); - if(iters_[db]) - { - delete iters_[db]; - iters_[db] = NULL; - } - - - iters_[db] = dbs_[db]->NewIterator(leveldb::ReadOptions()); - iters_[db]->SeekToFirst(); - for (; iters_[db]->Valid(); iters_[db]->Next()) + LDBIter ldbIter = getIterator(db); + ldbIter.seekToFirst(); + for(ldbIter.seekToFirst(); ldbIter.isValid(); ldbIter.advanceAndRead()) { size_t last = outList.size(); outList.push_back( pair() ); - sliceToBinaryData(iters_[db]->key(), outList[last].first); - sliceToBinaryData(iters_[db]->value(), outList[last].second); + outList[last].first = ldbIter.getKey(); + outList[last].second = ldbIter.getValue(); } - if(restoreIterAtEnd) - seekTo(db, prevIterLoc); - return outList; } @@ -2537,11 +2644,6 @@ void InterfaceToLDB::printAllDatabaseEntries(DB_SELECT db) { SCOPED_TIMER("printAllDatabaseEntries"); - bool restoreIterAtEnd = iters_[db]->Valid(); - BinaryData prevIterLoc; - if(restoreIterAtEnd) - prevIterLoc = sliceToBinaryData(iters_[db]->key()); - cout << "Printing DB entries... (DB=" << db << ")" << endl; KVLIST dbList = getAllDatabaseEntries(db); if(dbList.size() == 0) @@ -2555,9 +2657,6 @@ void InterfaceToLDB::printAllDatabaseEntries(DB_SELECT db) cout << " \"" << dbList[i].first.toHexStr() << "\" "; cout << " \"" << dbList[i].second.toHexStr() << "\" " << endl; } - - if(restoreIterAtEnd) - seekTo(db, prevIterLoc); } #define PPRINTENTRY(TYPE, IND) \ @@ -2579,11 +2678,6 @@ void InterfaceToLDB::pprintBlkDataDB(uint32_t indent) SCOPED_TIMER("pprintBlkDataDB"); DB_SELECT db = BLKDATA; - bool restoreIterAtEnd = iters_[db]->Valid(); - BinaryData prevIterLoc; - if(restoreIterAtEnd) - prevIterLoc = sliceToBinaryData(iters_[db]->key()); - cout << "Pretty-printing BLKDATA DB" << endl; KVLIST dbList = getAllDatabaseEntries(db); if(dbList.size() == 0) @@ -2652,8 +2746,6 @@ void InterfaceToLDB::pprintBlkDataDB(uint32_t indent) } - if(restoreIterAtEnd) - seekTo(db, prevIterLoc); } diff --git a/cppForSwig/leveldb_wrapper.h b/cppForSwig/leveldb_wrapper.h index b21d8eb5a..2f5782462 100644 --- a/cppForSwig/leveldb_wrapper.h +++ b/cppForSwig/leveldb_wrapper.h @@ -1,6 +1,6 @@ //////////////////////////////////////////////////////////////////////////////// // // -// Copyright(C) 2011-2013, Armory Technologies, Inc. // +// Copyright (C) 2011-2014, Armory Technologies, Inc. // // Distributed under the GNU Affero General Public License (AGPL v3) // // See LICENSE or http://www.gnu.org/licenses/agpl.html // // // @@ -34,6 +34,12 @@ #define KVLIST vector > +#define DEFAULT_LDB_BLOCK_SIZE 32*1024 + +// Use this to create iterators that are intended for bulk scanning +// It's actually that the ReadOptions::fill_cache arg needs to be false +#define BULK_SCAN false + class BlockHeader; class Tx; class TxIn; @@ -143,12 +149,6 @@ class StoredScriptHistory; // understand how to use it safely. Only use getValue() unless there // is reason to believe that the optimization is needed. // -// Similarly, when using the seek/start/advance iterator methods, -// keep in mind that various submethods you call may move the -// iterator out from under you. To be safe from this issue, it is -// best to copy the data behind currReadKey_ and currReadValue_ -// after you move the iterator. -// // // // NOTE 2: Batch writing operations are smoothed so that multiple, nested @@ -160,28 +160,88 @@ class StoredScriptHistory; // -class InterfaceToLDB +//////////////////////////////////////////////////////////////////////////////// +class LDBIter { -private: +public: + + // fill_cache argument should be false for large bulk scans + LDBIter(void) { db_=NULL; iter_=NULL; isDirty_=true;} + LDBIter(leveldb::DB* dbptr, bool fill_cache=true); + ~LDBIter(void) { destroy(); } + void destroy(void) {if(iter_!=NULL) delete iter_; iter_ = NULL; db_ = NULL;} + + bool isNull(void) { return iter_==NULL; } + bool isValid(void) { return (!isNull() && iter_->Valid()); } + bool isValid(DB_PREFIX dbpref); + + bool readIterData(void); + + bool advance(void); + bool advance(DB_PREFIX prefix); + bool advanceAndRead(void); + bool advanceAndRead(DB_PREFIX prefix); + + BinaryData getKey(void) ; + BinaryData getValue(void) ; + BinaryDataRef getKeyRef(void) ; + BinaryDataRef getValueRef(void) ; + BinaryRefReader& getKeyReader(void) ; + BinaryRefReader& getValueReader(void) ; + + // All the seekTo* methods do the exact same thing, the variant simply + // determines the meaning of the return true/false value. + bool seekTo(BinaryDataRef key); + bool seekTo(DB_PREFIX pref, BinaryDataRef key); + bool seekToExact(BinaryDataRef key); + bool seekToExact(DB_PREFIX pref, BinaryDataRef key); + bool seekToStartsWith(BinaryDataRef key); + bool seekToStartsWith(DB_PREFIX prefix); + bool seekToStartsWith(DB_PREFIX pref, BinaryDataRef key); + bool seekToFirst(void); + + // Return true if the iterator is currently on valid data, with key match + bool checkKeyExact(BinaryDataRef key); + bool checkKeyExact(DB_PREFIX prefix, BinaryDataRef key); + bool checkKeyStartsWith(BinaryDataRef key); + bool checkKeyStartsWith(DB_PREFIX prefix, BinaryDataRef key); + + bool verifyPrefix(DB_PREFIX prefix, bool advanceReader=true); + + void resetReaders(void){currKey_.resetPosition();currValue_.resetPosition();} - ///////////////////////////////////////////////////////////////////////////// leveldb::Slice binaryDataToSlice(BinaryData const & bd) {return leveldb::Slice((char*)bd.getPtr(), bd.getSize());} leveldb::Slice binaryDataRefToSlice(BinaryDataRef const & bdr) {return leveldb::Slice((char*)bdr.getPtr(), bdr.getSize());} +private: - ///////////////////////////////////////////////////////////////////////////// - // NOTE: These ref readers become invalid as soon as the iterator is moved! - void iteratorToRefReaders( leveldb::Iterator* it, - BinaryRefReader & brrKey, - BinaryRefReader & brrValue); + leveldb::DB* db_; + leveldb::Iterator* iter_; + BinaryRefReader currKey_; + BinaryRefReader currValue_; + bool isDirty_; + +}; + + + +//////////////////////////////////////////////////////////////////////////////// +class InterfaceToLDB +{ +private: ///////////////////////////////////////////////////////////////////////////// - void deleteIterator(DB_SELECT db); + leveldb::Slice binaryDataToSlice(BinaryData const & bd) + {return leveldb::Slice((char*)bd.getPtr(), bd.getSize());} + leveldb::Slice binaryDataRefToSlice(BinaryDataRef const & bdr) + {return leveldb::Slice((char*)bdr.getPtr(), bdr.getSize());} + + ///////////////////////////////////////////////////////////////////////////// // These four sliceTo* methods make copies, and thus safe to use even after @@ -199,9 +259,6 @@ class InterfaceToLDB BinaryRefReader sliceToBinaryRefReader(leveldb::Slice slice); - ///////////////////////////////////////////////////////////////////////////// - void resetIterReaders(void) - { currReadKey_.resetPosition(); currReadValue_.resetPosition(); } public: ///////////////////////////////////////////////////////////////////////////// @@ -240,7 +297,9 @@ class InterfaceToLDB uint32_t getTopBlockHeight(DB_SELECT db); ///////////////////////////////////////////////////////////////////////////// - void resetIterator(DB_SELECT db, bool seekToPrevKey=false); + LDBIter getIterator(DB_SELECT db, bool fill_cache=true) + { return LDBIter(dbs_[db], fill_cache); } + ///////////////////////////////////////////////////////////////////////////// // Get value using pre-created slice @@ -269,9 +328,6 @@ class InterfaceToLDB BinaryRefReader getValueReader(DB_SELECT db, BinaryDataRef keyWithPrefix); BinaryRefReader getValueReader(DB_SELECT db, DB_PREFIX prefix, BinaryDataRef key); - BinaryData getIterKeyCopy(void) { return currReadKey_.getRawRef().copy();} - BinaryData getIterValueCopy(void) { return currReadValue_.getRawRef().copy();} - BinaryData getHashForDBKey(BinaryData dbkey); BinaryData getHashForDBKey(uint32_t hgt, @@ -297,7 +353,7 @@ class InterfaceToLDB BinaryDataRef key); // Move the iterator to the first entry >= txHash - bool seekToTxByHash(BinaryDataRef txHash); + bool seekToTxByHash(LDBIter & ldbIter, BinaryDataRef txHash); ///////////////////////////////////////////////////////////////////////////// @@ -305,8 +361,7 @@ class InterfaceToLDB // the iterator already on the next desired block. So our "advance" op may // have finished before it started. Alternatively, we may be on this block // because we checked it and decide we don't care, so we want to skip it. - bool advanceToNextBlock(bool skip=false); - bool advanceIterAndRead(leveldb::Iterator* iter); + bool advanceToNextBlock(LDBIter & iter, bool skip=false); bool advanceIterAndRead(DB_SELECT, DB_PREFIX); bool dbIterIsValid(DB_SELECT db, DB_PREFIX prefix=DB_PREFIX_COUNT); @@ -328,7 +383,7 @@ class InterfaceToLDB ///////////////////////////////////////////////////////////////////////////// - bool startBlkDataIteration(DB_PREFIX prefix); + bool startBlkDataIteration(LDBIter & iter, DB_PREFIX prefix); ///////////////////////////////////////////////////////////////////////////// @@ -498,14 +553,22 @@ class InterfaceToLDB // Some methods to grab data at the current iterator location. Return // false if reading fails (maybe because we were expecting to find the // specified DB entry type, but the prefix byte indicated something else - bool readStoredBlockAtIter(StoredHeader & sbh); + bool readStoredBlockAtIter(LDBIter & iter, + StoredHeader & sbh); - bool readStoredTxAtIter(uint32_t height, uint8_t dupID, StoredTx & stx); + bool readStoredTxAtIter(LDBIter & iter, + uint32_t height, + uint8_t dupID, + StoredTx & stx); - bool readStoredTxOutAtIter(uint32_t height, uint8_t dupID, uint16_t txIndex, - StoredTxOut & stxo); + bool readStoredTxOutAtIter(LDBIter & iter, + uint32_t height, + uint8_t dupID, + uint16_t txIndex, + StoredTxOut & stxo); - bool readStoredScriptHistoryAtIter( StoredScriptHistory & ssh); + bool readStoredScriptHistoryAtIter(LDBIter & iter, + StoredScriptHistory & ssh); // TxRefs are much simpler with LDB than the previous FileDataPtr construct @@ -551,15 +614,14 @@ class InterfaceToLDB StoredUndoData & sud); - ///////////////////////////////////////////////////////////////////////////// - inline bool checkPrefixByte(DB_PREFIX prefix, bool rewindWhenDone=false) - { return DBUtils.checkPrefixByte(currReadKey_, prefix, rewindWhenDone); } ///////////////////////////////////////////////////////////////////////////// bool checkStatus(leveldb::Status stat, bool warn=true); void setMaxOpenFiles(uint32_t n) { maxOpenFiles_ = n; } uint32_t getMaxOpenFiles(void) { return maxOpenFiles_; } + void setLdbBlockSize(uint32_t sz){ ldbBlockSize_ = sz; } + uint32_t getLdbBlockSize(void) { return ldbBlockSize_; } KVLIST getAllDatabaseEntries(DB_SELECT db); @@ -582,7 +644,7 @@ class InterfaceToLDB ARMORY_DB_TYPE armoryDbType_; DB_PRUNE_TYPE dbPruneType_; - leveldb::Iterator* iters_[2]; + //leveldb::Iterator* iters_[2]; leveldb::WriteBatch* batches_[2]; leveldb::DB* dbs_[2]; string dbPaths_[2]; @@ -597,11 +659,12 @@ class InterfaceToLDB vector validDupByHeight_; - BinaryRefReader currReadKey_; - BinaryRefReader currReadValue_;; - + //BinaryRefReader currReadKey_; + //BinaryRefReader currReadValue_;; string lastGetValue_; + bool dbIsOpen_; + uint32_t ldbBlockSize_; uint32_t lowestScannedUpTo_; diff --git a/cppForSwig/log.h b/cppForSwig/log.h index f68fe01c5..e303a2a4c 100644 --- a/cppForSwig/log.h +++ b/cppForSwig/log.h @@ -1,6 +1,6 @@ //////////////////////////////////////////////////////////////////////////////// // // -// Copyright(C) 2011-2013, Armory Technologies, Inc. // +// Copyright (C) 2011-2014, Armory Technologies, Inc. // // Distributed under the GNU Affero General Public License (AGPL v3) // // See LICENSE or http://www.gnu.org/licenses/agpl.html // // // @@ -61,6 +61,7 @@ #include #include #include +#include "OS_TranslatePath.h" #define FILEANDLINE "(" << __FILE__ << ":" << __LINE__ << ") " #define LOGERR (LoggerObj(LogLvlError ).getLogStream() << FILEANDLINE ) @@ -129,14 +130,14 @@ class DualStream : public LogStream { fname_ = logfile; truncateFile(fname_, maxSz); - fout_.open(fname_.c_str(), ios::app); + fout_.open(OS_TranslatePath(fname_.c_str()), ios::app); fout_ << "\n\nLog file opened at " << NowTimeInt() << ": " << fname_.c_str() << endl; } void truncateFile(string logfile, unsigned long long int maxSizeInBytes) { - ifstream is(logfile.c_str(), ios::in|ios::binary); + ifstream is(OS_TranslatePath(logfile.c_str()), ios::in|ios::binary); // If file does not exist, nothing to do if(!is.is_open()) @@ -155,7 +156,7 @@ class DualStream : public LogStream else { // Otherwise, seek to before end of log file - ifstream is(logfile.c_str(), ios::in|ios::binary); + ifstream is(OS_TranslatePath(logfile.c_str()), ios::in|ios::binary); is.seekg(fsize - maxSizeInBytes); // Allocate buffer to hold the rest of the file (about maxSizeInBytes) @@ -166,14 +167,19 @@ class DualStream : public LogStream // Create temporary file and dump the bytes there string tempfile = logfile + string("temp"); - ofstream os(tempfile.c_str(), ios::out|ios::binary); + ofstream os(OS_TranslatePath(tempfile.c_str()), ios::out|ios::binary); os.write(lastBytes, bytesToCopy); os.close(); delete[] lastBytes; // Remove the original and rename the temp file to original - remove(logfile.c_str()); - rename(tempfile.c_str(), logfile.c_str()); + #ifndef _MSC_VER + remove(logfile.c_str()); + rename(tempfile.c_str(), logfile.c_str()); + #else + _wunlink(OS_TranslatePath(logfile).c_str()); + _wrename(OS_TranslatePath(tempfile).c_str(), OS_TranslatePath(logfile).c_str()); + #endif } } @@ -370,6 +376,7 @@ inline unsigned long long int NowTimeInt(void) time(&t); return (unsigned long long int)t; } + #else #include diff --git a/cppForSwig/old_not_very_good_tests.cpp b/cppForSwig/old_not_very_good_tests.cpp new file mode 100644 index 000000000..a7d8ba940 --- /dev/null +++ b/cppForSwig/old_not_very_good_tests.cpp @@ -0,0 +1,2095 @@ +//////////////////////////////////////////////////////////////////////////////// +// // +// Copyright (C) 2011-2014, Armory Technologies, Inc. // +// Distributed under the GNU Affero General Public License (AGPL v3) // +// See LICENSE or http://www.gnu.org/licenses/agpl.html // +// // +//////////////////////////////////////////////////////////////////////////////// +#include +#include +#include +#include +#include +#include "UniversalTimer.h" +#include "BinaryData.h" +#include "BtcUtils.h" +#include "BlockUtils.h" +#include "EncryptionUtils.h" +//#include "FileDataPtr.h" +#include "PartialMerkle.h" +#include "leveldb_wrapper.h" + +#include "leveldb/db.h" + + +using namespace std; + + + +//////////////////////////////////////////////////////////////////////////////// +void BaseTests(void); +void TestReadAndOrganizeChain(string blkdir); +void TestFindNonStdTx(string blkdir); +void TestReadAndOrganizeChainWithWallet(string blkdir); +void TestBalanceConstruction(string blkdir); +void TestReadAndUpdateBlkFile(string blkdir); +void TestScanForWalletTx(string blkdir); +void TestReorgBlockchain(string blkdir); +void TestZeroConf(void); +void TestCrypto(void); +void TestMerkle(void); +void TestECDSA(void); +void TestPointCompression(void); +void TestFileCache(void); +void TestReadBlkFileUpdate(string testblockdir, string blkdir); + +void TestOutOfOrder(string blkdir); +void TestLevelDB(string testLDBDir, string blkfilepath=""); +void TestLDBScanBlockchain(string testdbpath); +void TestLdbBlockchainUtils(string blkdir); + +//////////////////////////////////////////////////////////////////////////////// + +void printTestHeader(string TestName) +{ + cout << endl; + for(int i=0; i<80; i++) cout << "*"; + cout << endl << "Execute test: " << TestName << endl; + for(int i=0; i<80; i++) cout << "*"; + cout << endl; +} + +bool copyFile(string src, string dst) +{ + uint32_t srcsz = BtcUtils::GetFileSize(src); + if(srcsz == FILE_DOES_NOT_EXIST) + return false; + + BinaryData temp(srcsz); + ifstream is(src.c_str(), ios::in | ios::binary); + is.read((char*)temp.getPtr(), srcsz); + is.close(); + + ofstream os(dst.c_str(), ios::out | ios::binary); + os.write((char*)temp.getPtr(), srcsz); + os.close(); + return true; +} + + +string pathJoin(string dir, string file) +{ + int const TOTALSZ = dir.size() + file.size() + 10; + char * path = new char[TOTALSZ]; + sprintf(path, "%s/%s", dir.c_str(), file.c_str()); + string ret(path); + return ret; +} + + +BinaryData h2b(string s) +{ + return BinaryData::CreateFromHex(s); +} + + +int main(void) +{ + BaseTests(); + + BlockDataManager_LevelDB::GetInstance().SelectNetwork("Test"); + + + //string blkdir("/home/alan/.bitcoin"); + //string blkdir("/home/alan/.bitcoin/testnet/"); + //string blkdir("C:/Users/VBox/AppData/Roaming/Bitcoin"); + string blkdir("C:/Users/Andy/AppData/Roaming/Bitcoin/testnet"); + //string multitest("./multiblktest"); + + + //printTestHeader("Read-and-Organize-Blockchain"); + TestReadAndOrganizeChain(blkdir); + + //printTestHeader("Wallet-Relevant-Tx-Scan"); + TestScanForWalletTx(blkdir); + + //printTestHeader("Find-Non-Standard-Tx"); + //TestFindNonStdTx(blkdir); + + //printTestHeader("Read-and-Organize-Blockchain-With-Wallet"); + //TestReadAndOrganizeChainWithWallet(blkdir); + + //printTestHeader("Test-Balance-Construction"); + //TestBalanceConstruction(blkdir); + + //printTestHeader("Read-and-Update-Blockchain"); + //TestReadAndUpdateBlkFile(multitest); + + //printTestHeader("Blockchain-Reorg-Unit-Test"); + //TestReorgBlockchain(""); + + //printTestHeader("Test-out-of-order calls"); + //TestOutOfOrder(blkdir); + + + //printTestHeader("Testing Zero-conf handling"); + //TestZeroConf(); + + printTestHeader("Testing merkle-root calculation"); + TestMerkle(); + + //printTestHeader("Crypto-KDF-and-AES-methods"); + //TestCrypto(); + + //printTestHeader("Crypto-ECDSA-sign-verify"); + //TestECDSA(); + + //printTestHeader("ECDSA Point Compression"); + //TestPointCompression(); + + //printTestHeader("Testing file cache"); + //TestFileCache(); + + //printTestHeader("Testing LevelDB"); + //TestLevelDB("blk0001db", blkdir + string("/blk0001.dat")); + //TestLDBScanBlockchain("blk0001db"); + + + //printTestHeader("Testing LDB Blockchain utilities"); + //TestLdbBlockchainUtils(blkdir); + + + ///////////////////////////////////////////////////////////////////////////// + // ***** Print out all timings to stdout and a csv file ***** + // Any method, anywhere, that called UniversalTimer + // will end up having it's named timers printed out + // This file can be loaded into a spreadsheet, + // but it's not the prettiest thing... + UniversalTimer::instance().print(); + UniversalTimer::instance().printCSV("timings.csv"); + cout << endl << endl; + char pause[256]; + cout << "enter anything to exit" << endl; + cin >> pause; +} + + + + +void assertError(bool isTrue, string msg) +{ + if(isTrue) + return; + + cerr << "***ERROR: Failed BaseTest. Msg:" << endl; + cerr << msg.c_str() << endl; + exit(1); +} + + +void BaseTests(void) +{ + + BinaryData pushScript1 = h2b("493046022100c6abd6466c0cca354bebe9e9fb200ebd2924726a390eec8d76643cbb7959a070022100a5e02686e49819644d1f10d39fd59a41eb5c6f7bc28923f65cad72651fc8131401410459fd82189b81772258a3fc723fdda900eb8193057d4a573ee5ad39e26b58b5c12c4a51b0edd01769f96ed1998221daf0df89634a7137a8fa312d5ccc95ed89254930460221008e238f15d45c1d3739a6c65b6ad9689837f01685d0ded7adbff139e479d4a802022100b19ea60db5fdfc228a07b6d6b5115e148b861cd1085c4db1546da913a35a3f64014104ce6242d72ee67e867e6f8ec434b95fcb1889c5b485ec3414df407e11194a7ce012eda021b68f1dd124598a9b677d6e7d7c95b1b7347f5c5a08efa628ef0204e1483045022100cd58f567dba08d9f33c59efd7271e105ab19a2e9e9f7ec423d93b2c5759636ad02206f6512f67a40051c8dd5a3910b371246a409d7589afcf019cc3550bbfcc41416014104ce66c9f5068b715b62cc1622572cd98a08812d8ca01563045263c3e7af6b997e603e8e62041c4eb82dfd386a3412c34c334c34eb3c76fb0e37483fc72323f807"); + BinaryData pushScript2 = h2b("00493046022100c6abd6466c0cca354bebe9e9fb200ebd2924726a390eec8d76643cbb7959a070022100a5e02686e49819644d1f10d39fd59a41eb5c6f7bc28923f65cad72651fc8131401410459fd82189b81772258a3fc723fdda900eb8193057d4a573ee5ad39e26b58b5c12c4a51b0edd01769f96ed1998221daf0df89634a7137a8fa312d5ccc95ed89254930460221008e238f15d45c1d3739a6c65b6ad9689837f01685d0ded7adbff139e479d4a802022100b19ea60db5fdfc228a07b6d6b5115e148b861cd1085c4db1546da913a35a3f64014104ce6242d72ee67e867e6f8ec434b95fcb1889c5b485ec3414df407e11194a7ce012eda021b68f1dd124598a9b677d6e7d7c95b1b7347f5c5a08efa628ef0204e1483045022100cd58f567dba08d9f33c59efd7271e105ab19a2e9e9f7ec423d93b2c5759636ad02206f6512f67a40051c8dd5a3910b371246a409d7589afcf019cc3550bbfcc41416014104ce66c9f5068b715b62cc1622572cd98a08812d8ca01563045263c3e7af6b997e603e8e62041c4eb82dfd386a3412c34c334c34eb3c76fb0e37483fc72323f807"); + vector splitPush = splitPushOnlyScript(pushScript1); + cout << "Splitting push-only script" << endl; + for(uint32_t i=0; i txOutScripts; + vector txOutTypes; + vector hash160s; + vector txOutLDBKeys; + + txOutTypes.push_back( TXOUT_SCRIPT_STDHASH160 ) + txOutScripts.push_back( h2b("76a914a134408afa258a50ed7a1d9817f26b63cc9002cc88ac")); + txOutHash160s.push_back( h2b("a134408afa258a50ed7a1d9817f26b63cc9002cc")); + txOutLDBKeys.push_back(h2b("00a134408afa258a50ed7a1d9817f26b63cc9002cc")); + + txOutTypes.push_back( TXOUT_SCRIPT_STDPUBKEY65 ); + txOutScripts.push_back( h2b("4104b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e6537a576782eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7bac")); + txOutHash160s.push_back( h2b("6da6f1bd6c6380633bc667ba232611f5bf864be2")); + txOutLDBKeys.push_back(h2b("006da6f1bd6c6380633bc667ba232611f5bf864be2")); + + + txOutTypes.push_back( TXOUT_SCRIPT_STDPUBKEY33 ); + txOutScripts.push_back( h2b("21024005c945d86ac6b01fb04258345abea7a845bd25689edb723d5ad4068ddd3036ac")); + txOutHash160s.push_back( h2b("0c1b83d01d0ffb2bccae606963376cca3863a7ce")); + txOutLDBKeys.push_back(h2b("000c1b83d01d0ffb2bccae606963376cca3863a7ce")); + + // This was from block 150951 which was erroneously produced by MagicalTux + // This is not only non-standard, it's non-spendable + txOutTypes.push_back( TXOUT_SCRIPT_NONSTANDARD ); + txOutScripts.push_back( h2b("76a90088ac")); + txOutHash160s.push_back( BtcUtils::BadAddress_); + txOutLDBKeys.push_back(h2b("ff76a90088ac")); + + // P2SH script from tx: 4ac04b4830d115eb9a08f320ef30159cc107dfb72b29bbc2f370093f962397b4 (TxOut: 1) + // Spent in tx: fd16d6bbf1a3498ca9777b9d31ceae883eb8cb6ede1fafbdd218bae107de66fe (TxIn: 1) + // P2SH address: 3Lip6sxQymNr9LD2cAVp6wLrw8xdKBdYFG + // Hash160: d0c15a7d41500976056b3345f542d8c944077c8a + txOutTypes.push_back( TXOUT_SCRIPT_P2SH ) + txOutScripts.push_back( h2b("a914d0c15a7d41500976056b3345f542d8c944077c8a87")); // send to P2SH + txOutHash160s.push_back( h2b("d0c15a7d41500976056b3345f542d8c944077c8a")); + txOutLDBKeys.push_back( h2b("05d0c15a7d41500976056b3345f542d8c944077c8a")); + + + //realCoinbaseOut = "04b6acd549c083cb0d31ee975eb8c2ad7a41be61f0a47bc5d75e919d42704e5642d72f1804bcbab60dbd33f041d3c9edde57704a061a22c5e3cf93debf5f35daaeac" + //spendcb = "47304402201ffc44394e5a3dd9c8b55bdc12147e18574ac945d15dac026793bf3b8ff732af022035fd832549b5176126f735d87089c8c1c1319447a458a09818e173eaf0c2eef101" + //has160 = 957efec6af757ccbbcf9a436f0083c5ddaa3bf1d + //addrStr 1EdTpNBiPNPbEE4kASow2F4pUpES9jeTJE + + vector txInScripts; + vector txInPrevHashes; + vector txInTypes; + vector txInHash160s; + + prevHashCB = h2b("0000000000000000000000000000000000000000000000000000000000000000"); + prevHashReg = h2b("894862e362905c6075074d9ec4b4e2dc34720089b1e9ef4738ee1b13f3bdcdb7"); + + txInTypes.push_back(TXIN_SCRIPT_STDUNCOMPR) + txInScripts.push_back( h2b("493046022100b9daf2733055be73ae00ee0c5d78ca639d554fe779f163396c1a39b7913e7eac02210091f0deeb2e510c74354afb30cc7d8fbac81b1ca8b3940613379adc41a6ffd226014104b1537fa5bc2242d25ebf54f31e76ebabe0b3de4a4dccd9004f058d6c2caa5d31164252e1e04e5df627fae7adec27fa9d40c271fc4d30ff375ef6b26eba192bac")) + txInPrevHashes.push_back(prevHashReg); + txInHash160s.push_back(h2b("c42a8290196b2c5bcb35471b45aa0dc096baed5e")); + + txInTypes.push_back(TXIN_SCRIPT_STDCOMPR); + txInScripts.push_back( h2b("47304402205299224886e5e3402b0e9fa3527bcfe1d73c4e2040f18de8dd17f116e3365a1102202590dcc16c4b711daae6c37977ba579ca65bcaa8fba2bd7168a984be727ccf7a01210315122ff4d41d9fe3538a0a8c6c7f813cf12a901069a43d6478917246dc92a782")); + txInPrevHashes.push_back(prevHashReg); + txInHash160s.push_back("03214fc1433a287e964d6c4242093c34e4ed0001"); + + + txInTypes.push_back(TXIN_SCRIPT_COINBASE) + txInScripts.push_back( h2b("0310920304000071c3124d696e656420627920425443204775696c640800b75f950e000000")); + txInPrevHashes.push_back(prevHashCB); + txInHash160s.push_back( BtcUtils::BadAddress_); + + // Spending P2SH output as above: fd16d6bbf1a3498ca9777b9d31ceae883eb8cb6ede1fafbdd218bae107de66fe (TxIn: 1, 219 B) + // Leading 0x00 byte is required due to a bug in OP_CHECKMULTISIG + txInTypes.push_back(TXIN_SCRIPT_SPENDP2SH) + txInScripts.push_back( h2b("004830450221009254113fa46918f299b1d18ec918613e56cffbeba0960db05f66b51496e5bf3802201e229de334bd753a2b08b36cc3f38f5263a23e9714a737520db45494ec095ce80148304502206ee62f539d5cd94f990b7abfda77750f58ff91043c3f002501e5448ef6dba2520221009d29229cdfedda1dd02a1a90bb71b30b77e9c3fc28d1353f054c86371f6c2a8101475221034758cefcb75e16e4dfafb32383b709fa632086ea5ca982712de6add93060b17a2103fe96237629128a0ae8c3825af8a4be8fe3109b16f62af19cec0b1eb93b8717e252ae")); + txInPrevHashes.push_back(prevHashReg); + txInHash160s.push_back( BtcUtils::BadAddress_); + + + txInTypes.push_back(TXIN_SCRIPT_SPENDPUBKEY) + txInScripts.push_back( h2b("47304402201ffc44394e5a3dd9c8b55bdc12147e18574ac945d15dac026793bf3b8ff732af022035fd832549b5176126f735d87089c8c1c1319447a458a09818e173eaf0c2eef101")); + txInPrevHashes.push_back(prevHashReg); + txInHash160s.push_back( BtcUtils::BadAddress_); + //txInHash160s.push_back( h2b("957efec6af757ccbbcf9a436f0083c5ddaa3bf1d")); // this one can't be determined + + + uint32_t nOutTest = txOutScripts.size(); + TXOUT_SCRIPT_TYPE outType; + BinaryData a160Out_1, a160Out_2; + BinaryData keyOut_1, keyOut_2; + for(uint32_t test=0; test txInScripts; + vector txInPrevHashes; + vector txInTypes; + vector txInHash160s; + + + // Test difficulty-to-double + vector diffBits; + vector diffDbls; + + diffBits.push_back(h2b("ffff001d")); + diffDbls.push_back(1.0); + + diffBits.push_back(h2b("be2f021a")); + diffDbls.push_back(7672999.920164138); + + diffBits.push_back(h2b("3daa011a")); + diffDbls.push_back(10076292.883418716); + + for(uint32_t test=0; test const & ledger = wlt.getAddrByIndex(i).getTxLedger(); + for(uint32_t j=0; j const & ledgerAll = wlt.getTxLedger(); + for(uint32_t j=0; j sortedUTOs = bdm.getUnspentTxOutsForWallet(myWallet, 1); + vector sortedUTOs = myWallet.getSpendableTxOutList(); + + int i=1; + cout << " Sorting Method: " << i << endl; + cout << " Value\t#Conf\tTxHash\tTxIdx" << endl; + for(int j=0; j levect = wlt.getAddrLedgerEntriesForTx(txSelf); + //for(int i=0; i wltList; + wltList.push_back(&wlt1); + wltList.push_back(&wlt2); + + bdm.registerWallet(&wlt1); + bdm.registerWallet(&wlt2); + + + ///////////////////////////////////////////////////////////////////////////// + cout << "Reading data from blockchain... (with wallet scan)" << endl; + TIMER_START("BDM_Load_Scan_Blockchain_With_Wallet"); + bdm.SetBlkFileLocation(blkdir, 4, 1); + bdm.parseEntireBlockchain(); + TIMER_STOP("BDM_Load_Scan_Blockchain_With_Wallet"); + cout << endl << endl; + + ///////////////////////////////////////////////////////////////////////////// + cout << endl << "Organizing blockchain: " ; + TIMER_START("BDM_Organize_Chain"); + bool isGenOnMainChain = bdm.organizeChain(); + TIMER_STOP("BDM_Organize_Chain"); + cout << (isGenOnMainChain ? "No Reorg!" : "Reorg Detected!") << endl; + cout << endl << endl; + + cout << endl << "Updating wallet (1) based on initial blockchain scan" << endl; + TIMER_WRAP(bdm.scanBlockchainForTx(wlt1)); + cout << "Printing Wallet(1) Ledger" << endl; + wlt1.pprintLedger(); + + cout << endl << "Updating wallet (2) based on initial blockchain scan" << endl; + TIMER_WRAP(bdm.scanBlockchainForTx(wlt2)); + cout << "Printing Wallet(2) Ledger" << endl; + wlt2.pprintLedger(); + + cout << endl << "Rescanning wlt2 multiple times" << endl; + TIMER_WRAP(bdm.scanBlockchainForTx(wlt2)); + TIMER_WRAP(bdm.scanBlockchainForTx(wlt2)); + cout << "Printing Wallet(2) Ledger AGAIN" << endl; + wlt2.pprintLedger(); + + + cout << endl << "ADD a new address to Wlt(1) requiring a blockchain rescan..." << endl; + // This address contains a tx with a non-std TxOut, but the other TxOuts are valid + myAddress.createFromHex("6c27c8e67b7376f3ab63553fe37a4481c4f951cf"); + wlt1.addAddress(myAddress); + bdm.scanBlockchainForTx(wlt1); + wlt1.pprintLedger(); + + cout << endl << "ADD new address to wlt(2) but as a just-created addr not requiring rescan" << endl; + myAddress.createFromHex("6c27c8e67b7376f3ab63553fe37a4481c4f951cf"); + wlt2.addNewAddress(myAddress); + bdm.scanBlockchainForTx(wlt2); + wlt2.pprintLedger(); + + + cout << endl << "Create new, unregistered wallet, scan it once (should rescan)..." << endl; + BtcWallet wlt3; + myAddress.createFromHex("72e20a94d6b2ed34a3b4d3757c1fed5152071993"); wlt3.addAddress(myAddress); + wlt3.addAddress(myAddress); + bdm.scanBlockchainForTx(wlt3); + wlt3.pprintLedger(); + + + cout << endl << "Rescan unregistered wallet: addr should be registered, so no rescan..." << endl; + bdm.scanBlockchainForTx(wlt3); + wlt3.pprintLedger(); + + + cout << endl << endl; + cout << "Getting Sent-To Address List for Wlt1:" << endl; + vector targAddrVect = wlt1.createAddressBook(); + for(uint32_t i=0; i txList = targAddrVect[i].getTxList(); + + cout << targAddrVect[i].getAddr160().toHexStr() << " " + << txList.size() << endl; + + for(uint32_t j=0; j utxoF = wlt.getFullTxOutList(topBlk); + vector utxoS = wlt.getSpendableTxOutList(topBlk); + cout << "FULL:" << endl; + for(uint32_t i=0; i const & ledgerAll2 = wlt.getTxLedger(); + for(uint32_t j=0; j const & ledger = wlt.getAddrByIndex(i).getTxLedger(); + for(uint32_t j=0; j result; + + ///// + cout << "Pushing Block 3A into the BDM:" << endl; + copyFile(blk3A, "reorgTest/blk00000.dat"); + bdm.readBlkFileUpdate(); + cout << "Is last block reorg? " << (bdm.isLastBlockReorg() ? 1 : 0) << endl; + + ///// + cout << "Pushing Block 4A into the BDM:" << endl; + copyFile(blk4A, "reorgTest/blk00000.dat"); + bdm.readBlkFileUpdate(); + cout << "Is last block reorg? " << (bdm.isLastBlockReorg() ? 1 : 0) << endl; + + ///// + cout << "Pushing Block 5A into the BDM:" << endl; + copyFile(blk5A, "reorgTest/blk00000.dat"); + bdm.readBlkFileUpdate(); + cout << "Is last block reorg? " << (bdm.isLastBlockReorg() ? 1 : 0) << endl; + if(bdm.isLastBlockReorg()) + { + cout << "Reorg happened after pushing block 5A" << endl; + bdm.scanBlockchainForTx(wlt); + bdm.updateWalletAfterReorg(wlt); + } + + cout << "Checking balance of entire wallet: " << wlt.getFullBalance()/1e8 << endl; + vector const & ledgerAll3 = wlt.getTxLedger(); + for(uint32_t j=0; j const & ledger = wlt.getAddrByIndex(i).getTxLedger(); + for(uint32_t j=0; j 8) + { + cout << endl << endl; + cout << "Inserting another 0-conf tx..." << endl; + uint64_t txtime = brr.get_uint64_t(); + TxRef zcTx(brr); + bool wasAdded = bdm.addNewZeroConfTx(zcTx.serialize(), txtime, true); + + if(wasAdded) + bdm.rescanWalletZeroConf(wlt); + + cout << "UltBal: " << wlt.getFullBalance() << endl; + cout << "SpdBal: " << wlt.getSpendableBalance() << endl; + cout << "UncBal: " << wlt.getUnconfirmedBalance(currBlk) << endl; + wlt.pprintLedger(); + + cout << "Unspent TxOuts:" << endl; + vector utxoList = wlt.getSpendableTxOutList(currBlk); + uint64_t bal = 0; + for(uint32_t i=0; i txList(7); + // The "abcd" quartets are to trigger endianness errors -- without them, + // these hashes are palindromes that work regardless of your endian-handling + txList[0].createFromHex("00000000000000000000000000000000000000000000000000000000abcd0000"); + txList[1].createFromHex("11111111111111111111111111111111111111111111111111111111abcd1111"); + txList[2].createFromHex("22222222222222222222222222222222222222222222222222222222abcd2222"); + txList[3].createFromHex("33333333333333333333333333333333333333333333333333333333abcd3333"); + txList[4].createFromHex("44444444444444444444444444444444444444444444444444444444abcd4444"); + txList[5].createFromHex("55555555555555555555555555555555555555555555555555555555abcd5555"); + txList[6].createFromHex("66666666666666666666666666666666666666666666666666666666abcd6666"); + + cout << "Merkle Tree looks like the following (7 tx): " << endl; + cout << "The ** indicates the nodes we care about for partial tree test" << endl; + cout << " \n"; + cout << " _____0a10_____ \n"; + cout << " / \\ \n"; + cout << " _/ \\_ \n"; + cout << " 65df b4d6 \n"; + cout << " / \\ / \\ \n"; + cout << " 6971 22dc 5675 d0b6 \n"; + cout << " / \\ / \\ / \\ / \n"; + cout << " 0000 1111 2222 3333 4444 5555 6666 \n"; + cout << " ** ** \n"; + cout << " " << endl; + cout << endl; + + vector merkleTree = BtcUtils::calculateMerkleTree(txList); + + cout << "Full Merkle Tree (this one has been unit tested before):" << endl; + for(uint32_t i=0; i isOurs(7); + isOurs[0] = true; + isOurs[1] = true; + isOurs[2] = true; + isOurs[3] = true; + isOurs[4] = true; + isOurs[5] = true; + isOurs[6] = true; + + cout << "Start serializing a full tree" << endl; + PartialMerkleTree pmtFull(7, &isOurs, &txList); + BinaryData pmtSerFull = pmtFull.serialize(); + + cout << "Finished serializing (full)" << endl; + cout << "Merkle Root: " << pmtFull.getMerkleRoot().toHexStr() << endl; + + cout << "Starting unserialize (full):" << endl; + cout << "Serialized: " << pmtSerFull.toHexStr() << endl; + PartialMerkleTree pmtFull2(7); + pmtFull2.unserialize(pmtSerFull); + BinaryData pmtSerFull2 = pmtFull2.serialize(); + cout << "Reserializ: " << pmtSerFull2.toHexStr() << endl; + cout << "Equal? " << (pmtSerFull==pmtSerFull2 ? "True" : "False") << endl; + + cout << "Print Tree:" << endl; + pmtFull2.pprintTree(); + + + ///////////////////////////////////////////////////////////////////////////// + cout << "Starting Partial Merkle tree" << endl; + isOurs[0] = true; + isOurs[1] = false; + isOurs[2] = false; + isOurs[3] = false; + isOurs[4] = false; + isOurs[5] = true; + isOurs[6] = false; + + PartialMerkleTree pmt(7, &isOurs, &txList); + cout << "Serializing (partial)" << endl; + BinaryData pmtSer = pmt.serialize(); + PartialMerkleTree pmt2(7); + cout << "Unserializing (partial)" << endl; + pmt2.unserialize(pmtSer); + cout << "Reserializing (partial)" << endl; + BinaryData pmtSer2 = pmt2.serialize(); + cout << "Serialized (Partial): " << pmtSer.toHexStr() << endl; + cout << "Reserializ (Partial): " << pmtSer.toHexStr() << endl; + cout << "Equal? " << (pmtSer==pmtSer2 ? "True" : "False") << endl; + + cout << "Print Tree:" << endl; + pmt2.pprintTree(); + + ///////////////////////////////////////////////////////////////////////////// + cout << "Empty tree" << endl; + isOurs[0] = false; + isOurs[1] = false; + isOurs[2] = false; + isOurs[3] = false; + isOurs[4] = false; + isOurs[5] = false; + isOurs[6] = false; + + PartialMerkleTree pmt3(7, &isOurs, &txList); + cout << "Serializing (partial)" << endl; + BinaryData pmtSer3 = pmt3.serialize(); + PartialMerkleTree pmt4(7); + cout << "Unserializing (partial)" << endl; + pmt4.unserialize(pmtSer3); + cout << "Reserializing (partial)" << endl; + BinaryData pmtSer4 = pmt4.serialize(); + cout << "Equal? " << (pmtSer3==pmtSer4 ? "True" : "False") << endl; + cout << "Empty Serialized: " << pmtSer3.toHexStr() << endl; + cout << "Print Tree:" << endl; + pmt4.pprintTree(); + + + ///////////////////////////////////////////////////////////////////////////// + cout << "Single Node on edge" << endl; + isOurs[0] = false; + isOurs[1] = false; + isOurs[2] = false; + isOurs[3] = false; + isOurs[4] = false; + isOurs[5] = false; + isOurs[6] = true; + PartialMerkleTree pmt5(7, &isOurs, &txList); + cout << "Serializing (partial)" << endl; + BinaryData pmtSer5 = pmt5.serialize(); + PartialMerkleTree pmt6(7); + cout << "Unserializing (partial)" << endl; + pmt6.unserialize(pmtSer5); + cout << "Reserializing (partial)" << endl; + BinaryData pmtSer6 = pmt6.serialize(); + cout << "Equal? " << (pmtSer5==pmtSer6 ? "True" : "False") << endl; + cout << "Single Serialized: " << pmtSer5.toHexStr() << endl; + cout << "Print Tree:" << endl; + pmt6.pprintTree(); + + cout << "Four Tests: " << endl; + cout << " " << (pmtSerFull==pmtSerFull2 ? "PASS" : "FAIL") << endl; + cout << " " << (pmtSer==pmtSer2 ? "PASS" : "FAIL") << endl; + cout << " " << (pmtSer3==pmtSer4 ? "PASS" : "FAIL") << endl; + cout << " " << (pmtSer5==pmtSer6 ? "PASS" : "FAIL") << endl; + + + cout << "Super large merkle tree!" << endl; + uint32_t testSize = 100000; + vector longHash(testSize); + vector longBits(testSize); + for(uint32_t i=0; i> deadstr; + infile >> deadstr >> mem; + infile >> deadstr >> nIters; + infile >> deadstr >> hexstr; + salt.copyFrom( SecureBinaryData::CreateFromHex(string(hexstr, 64))); + infile >> deadstr; + infile >> deadstr >> hexstr; + iv.copyFrom( SecureBinaryData::CreateFromHex(string(hexstr, 64))); + infile >> deadstr >> hexstr; + cipher.copyFrom( SecureBinaryData::CreateFromHex(string(hexstr, 64))); + infile.close(); + cout << endl << endl; + + // Will try this twice, once with correct passphrase, once without + SecureBinaryData cipherTry1 = cipher; + SecureBinaryData cipherTry2 = cipher; + SecureBinaryData newKey; + + KdfRomix newKdf(mem, nIters, salt); + newKdf.printKdfParams(); + + // First test with the wrong passphrase + cout << "Attempting to decrypt with wrong passphrase" << endl; + SecureBinaryData passphrase = SecureBinaryData("This is the wrong passphrase"); + newKey = newKdf.DeriveKey( passphrase ); + CryptoAES().DecryptCFB(cipherTry1, newKey, iv); + + + // Now try correct passphrase + cout << "Attempting to decrypt with CORRECT passphrase" << endl; + passphrase = SecureBinaryData("This passphrase is tough to guess"); + newKey = newKdf.DeriveKey( passphrase ); + CryptoAES().DecryptCFB(cipherTry2, newKey, iv); +} + + + + + + +void TestECDSA(void) +{ + + SecureBinaryData msgToSign("This message came from me!"); + SecureBinaryData privData = SecureBinaryData().GenerateRandom(32); + BTC_PRIVKEY privKey = CryptoECDSA().ParsePrivateKey(privData); + BTC_PUBKEY pubKey = CryptoECDSA().ComputePublicKey(privKey); + + // Test key-match check + cout << "Do the pub-priv keypair we just created match? "; + cout << (CryptoECDSA().CheckPubPrivKeyMatch(privKey, pubKey) ? 1 : 0) << endl; + cout << endl; + + SecureBinaryData signature = CryptoECDSA().SignData(msgToSign, privKey); + cout << "Signature = " << signature.toHexStr() << endl; + cout << endl; + + bool isValid = CryptoECDSA().VerifyData(msgToSign, signature, pubKey); + cout << "SigValid? = " << (isValid ? 1 : 0) << endl; + cout << endl; + + // Test signature from blockchain: + SecureBinaryData msg = SecureBinaryData::CreateFromHex("0100000001bb664ff716b9dfc831bcc666c1767f362ad467fcfbaf4961de92e45547daab870100000062537a7652a269537a829178a91480677c5392220db736455533477d0bc2fba65502879b69537a829178a91402d7aa2e76d9066fb2b3c41ff8839a5c81bdca19879b69537a829178a91410039ce4fdb5d4ee56148fe3935b9bfbbe4ecc89879b6953aeffffffff0280969800000000001976a9140817482d2e97e4be877efe59f4bae108564549f188ac7015a7000000000062537a7652a269537a829178a91480677c5392220db736455533477d0bc2fba65502879b69537a829178a91402d7aa2e76d9066fb2b3c41ff8839a5c81bdca19879b69537a829178a91410039ce4fdb5d4ee56148fe3935b9bfbbe4ecc89879b6953ae0000000001000000"); + SecureBinaryData px = SecureBinaryData::CreateFromHex("8c006ff0d2cfde86455086af5a25b88c2b81858aab67f6a3132c885a2cb9ec38"); + SecureBinaryData py = SecureBinaryData::CreateFromHex("e700576fd46c7d72d7d22555eee3a14e2876c643cd70b1b0a77fbf46e62331ac"); + SecureBinaryData pub65 = SecureBinaryData::CreateFromHex("048c006ff0d2cfde86455086af5a25b88c2b81858aab67f6a3132c885a2cb9ec38e700576fd46c7d72d7d22555eee3a14e2876c643cd70b1b0a77fbf46e62331ac"); + //SecureBinaryData sig = SecureBinaryData::CreateFromHex("3046022100d73f633f114e0e0b324d87d38d34f22966a03b072803afa99c9408201f6d6dc6022100900e85be52ad2278d24e7edbb7269367f5f2d6f1bd338d017ca4600087766144"); + SecureBinaryData sig = SecureBinaryData::CreateFromHex("d73f633f114e0e0b324d87d38d34f22966a03b072803afa99c9408201f6d6dc6900e85be52ad2278d24e7edbb7269367f5f2d6f1bd338d017ca4600087766144"); + pubKey = CryptoECDSA().ParsePublicKey(px,py); + isValid = CryptoECDSA().VerifyData(msg, sig, pubKey); + cout << "SigValid? = " << (isValid ? 1 : 0) << endl; + + + // Test speed on signature: + uint32_t nTest = 50; + cout << "Test signature and verification speeds" << endl; + cout << "\nTiming Signing"; + TIMER_START("SigningTime"); + for(uint32_t i=0; i testPubKey(3); + testPubKey[0].createFromHex("044f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa385b6b1b8ead809ca67454d9683fcf2ba03456d6fe2c4abe2b07f0fbdbb2f1c1"); + testPubKey[1].createFromHex("04ed83704c95d829046f1ac27806211132102c34e9ac7ffa1b71110658e5b9d1bdedc416f5cefc1db0625cd0c75de8192d2b592d7e3b00bcfb4a0e860d880fd1fc"); + testPubKey[2].createFromHex("042596957532fc37e40486b910802ff45eeaa924548c0e1c080ef804e523ec3ed3ed0a9004acf927666eee18b7f5e8ad72ff100a3bb710a577256fd7ec81eb1cb3"); + + CryptoPP::ECP & ecp = CryptoECDSA::Get_secp256k1_ECP(); + for(uint32_t i=0; i<3; i++) + { + CryptoPP::Integer pubX, pubY; + pubX.Decode(testPubKey[i].getPtr()+1, 32, UNSIGNED); + pubY.Decode(testPubKey[i].getPtr()+33, 32, UNSIGNED); + BTC_ECPOINT ptPub(pubX, pubY); + + BinaryData ptFlat(65); + BinaryData ptComp(33); + + ecp.EncodePoint((byte*)ptFlat.getPtr(), ptPub, false); + ecp.EncodePoint((byte*)ptComp.getPtr(), ptPub, true); + + cout << "Point (" << i << "): " << ptFlat.toHexStr() << endl; + cout << "Point (" << i << "): " << ptComp.toHexStr() << endl; + cout << "Point (" << i << "): " << CryptoECDSA().UncompressPoint(SecureBinaryData(ptComp)).toHexStr() << endl; + cout << "Point (" << i << "): " << CryptoECDSA().CompressPoint(SecureBinaryData(testPubKey[i])).toHexStr() << endl; + } + + + +} + + + + + + + +void TestReadBlkFileUpdate(string testblockdir, string blkdir) +{ + BlockDataManager_LevelDB & bdm = BlockDataManager_LevelDB::GetInstance(); + + // Setup the files that will be copied -- sources and destinations + vector srcs(7); + srcs[0] = testblockdir + string("/blk00000.dat"); + srcs[1] = testblockdir + string("/blk00000_test1.dat"); + srcs[2] = testblockdir + string("/blk00000_test2.dat"); + srcs[3] = testblockdir + string("/blk00001_test3.dat"); + srcs[4] = testblockdir + string("/blk00002_test4.dat"); + srcs[5] = testblockdir + string("/blk00002_test5.dat"); + srcs[6] = testblockdir + string("/blk00003_test5.dat"); + + vector dsts(7); + dsts[0] = blkdir + string("/blk00000.dat"); + dsts[1] = blkdir + string("/blk00000.dat"); + dsts[2] = blkdir + string("/blk00000.dat"); + dsts[3] = blkdir + string("/blk00001.dat"); + dsts[4] = blkdir + string("/blk00002.dat"); + dsts[5] = blkdir + string("/blk00002.dat"); + dsts[6] = blkdir + string("/blk00003.dat"); + + for(uint32_t i=0; i<7; i++) + if( BtcUtils::GetFileSize(dsts[i]) != FILE_DOES_NOT_EXIST ) + remove(dsts[i].c_str()); + + + copyFile(srcs[0], dsts[0]); + bdm.SetBlkFileLocation(blkdir, 5, 0); + bdm.parseEntireBlockchain(); + cout << "Top Block: " << bdm.getTopBlockHeight() << endl; + + uint32_t t = 1; + + // TEST 1 -- Add one block + copyFile(srcs[1], dsts[1]); + bdm.readBlkFileUpdate(); + cout << "Read block update " << t++ << ": " << bdm.getTopBlockHeight() << endl; + + // TEST 2 -- Add 3 blocks + copyFile(srcs[2], dsts[2]); + bdm.readBlkFileUpdate(); + cout << "Read block update " << t++ << ": " << bdm.getTopBlockHeight() << endl; + + // TEST 3 -- Blkfile split with 1 block + copyFile(srcs[3], dsts[3]); + bdm.readBlkFileUpdate(); + cout << "Read block update " << t++ << ": " << bdm.getTopBlockHeight() << endl; + + // TEST 4 -- Blkfile split with 3 blocks + copyFile(srcs[4], dsts[4]); + bdm.readBlkFileUpdate(); + cout << "Read block update " << t++ << ": " << bdm.getTopBlockHeight() << endl; + + // TEST 5 -- Add blocks and split + copyFile(srcs[5], dsts[5]); + copyFile(srcs[6], dsts[6]); + bdm.readBlkFileUpdate(); + cout << "Read block update " << t++ << ": " << bdm.getTopBlockHeight() << endl; + +} + +void TestOutOfOrder(string blkdir) +{ + ///////////////////////////////////////////////////////////////////////////// + BlockDataManager_LevelDB & bdm = BlockDataManager_LevelDB::GetInstance(); + BinaryData myAddress; + BtcWallet wlt; + + // Main-network addresses + // I do not remember anymore what any of these addresses were for ... + // All I know is they probably have some tx history.. + myAddress.createFromHex("604875c897a079f4db88e5d71145be2093cae194"); wlt.addAddress(myAddress); + myAddress.createFromHex("8996182392d6f05e732410de4fc3fa273bac7ee6"); wlt.addAddress(myAddress); + myAddress.createFromHex("b5e2331304bc6c541ffe81a66ab664159979125b"); wlt.addAddress(myAddress); + myAddress.createFromHex("ebbfaaeedd97bc30df0d6887fd62021d768f5cb8"); wlt.addAddress(myAddress); + + // P2Pool Address + //myAddress.createFromHex("4975703dc910107e2cc1321e632d136803e218e8"); wlt.addAddress(myAddress); + + // Add some relevant testnet addresses + //myAddress.createFromHex("0c6b92101c7025643c346d9c3e23034a8a843e21"); wlt.addAddress(myAddress); + //myAddress.createFromHex("34c9f8dc91dfe1ae1c59e76cbe1aa39d0b7fc041"); wlt.addAddress(myAddress); + //myAddress.createFromHex("d77561813ca968270d5f63794ddb6aab3493605e"); wlt.addAddress(myAddress); + //myAddress.createFromHex("0e0aec36fe2545fb31a41164fb6954adcd96b342"); wlt.addAddress(myAddress); + + // These two addresses were used at one point for testing unconfirmed balances + myAddress.createFromHex("8c61f6a7558af399e404d82beddcc4692db7b30f"); wlt.addAddress(myAddress); + myAddress.createFromHex("14445409283ef413f5fb004338377bf042064922"); wlt.addAddress(myAddress); + + bdm.registerWallet(&wlt); + + + ///////////////////////////////////////////////////////////////////////////// + cout << "Reading data from blockchain... (with wallet scan)" << endl; + TIMER_START("BDM_Load_Scan_Blockchain_With_Wallet"); + bdm.SetBlkFileLocation(blkdir, 4, 1); + bdm.parseEntireBlockchain(); + TIMER_STOP("BDM_Load_Scan_Blockchain_With_Wallet"); + cout << endl << endl; + + myAddress.createFromHex("11b366edfc0a8b66feebae5c2e25a7b6a5d1cf31"); wlt.addAddress(myAddress); + myAddress.createFromHex("e826f4a4381453dcdcf9bfeedffe95de7c86ccbd"); wlt.addAddress(myAddress); + + bdm.scanBlockchainForTx(wlt); +} + + + + +void TestLevelDB(string testLDBDir, string blkfilepath) +{ + leveldb::DB* ldb; + leveldb::Options opts; + + // Setup the optoins for this particular database + opts.create_if_missing = true; + //opts.filter_policy = NewBloomFilter(10); + + /* + leveldb::Status stat = leveldb::DB::Open(opts, testLDBDir.c_str(), &ldb); + assert(leveldb::ldbCheckStatus(stat)); + + for(uint32_t i=0; i<10; i++) + { + uint32_t ncharKey = 2*(i%6)+1; + uint32_t ncharVal = 3*(i%8)+1; + BinaryData insertKey(ncharKey); + BinaryData insertVal(ncharVal); + insertKey.fill( (uint8_t)i%255 ); + insertVal.fill( (uint8_t)i%256 ); + + cout << "Inserting: (" << insertKey.toHexStr() << "," + << insertVal.toHexStr() << ")" << endl; + + stat = ldb->Put(leveldb::WriteOptions(), + insertKey.toBinStr(), + insertVal.toBinStr()); + assert(leveldb::ldbCheckStatus(stat)); + } + + leveldb::Iterator* it = ldb->NewIterator(leveldb::ReadOptions()); + uint32_t idx = 1; + for(it->SeekToFirst(); it->Valid(); it->Next()) + { + BinaryData key(it->key().ToString()); + BinaryData val(it->value().ToString()); + cout << idx++ << ": " << key.toHexStr() << ": " << val.toHexStr() << endl; + } + + cout << "Keys in DB: " << idx-1 << endl; + + string val2; + stat = ldb->Get(leveldb::ReadOptions(), "MyKey", &val2); + leveldb::ldbCheckStatus(stat); + cout << "Plowed through the error..." << endl; + + delete ldb; + //delete opts.filter_policy; + */ + + // Start new blockchain read/write test.... + opts.create_if_missing = true; + opts.compression = leveldb::kNoCompression; + leveldb::Status stat = leveldb::DB::Open(opts, testLDBDir.c_str(), &ldb); + assert(leveldb::ldbCheckStatus(stat)); + + ifstream is(blkfilepath.c_str(), ios::in | ios::binary); + assert(is.is_open()); + + uint64_t filesize = BtcUtils::GetFileSize(blkfilepath); + BinaryStreamBuffer bsb; + bsb.attachAsStreamBuffer(is, filesize); + + bool alreadyRead8B = false; + uint32_t nextBlkSize; + uint64_t nBytesRead = 0; + uint32_t nBlkRead = 0; + TIMER_START("NaiveScan"); + while(bsb.streamPull()) + { + while(bsb.reader().getSizeRemaining() > 8) + { + if(!alreadyRead8B) + { + bsb.reader().advance(4); + nextBlkSize = bsb.reader().get_uint32_t(); + nBytesRead += 8; + } + + if(bsb.reader().getSizeRemaining() < nextBlkSize) + { + alreadyRead8B = true; + break; + } + alreadyRead8B = false; + + BinaryRefReader brr(bsb.reader().getCurrPtr(), nextBlkSize); + + // Do something with the block just read + BinaryData headerHash(32); + BinaryData headerRaw(HEADER_SIZE); + brr.get_BinaryData(headerRaw, HEADER_SIZE); + BtcUtils::getHash256_NoSafetyCheck(headerRaw.getPtr(), HEADER_SIZE, headerHash); + + stat = ldb->Put(leveldb::WriteOptions(), + headerHash.toBinStr(), + headerRaw.toBinStr()); + assert(leveldb::ldbCheckStatus(stat)); + + + uint32_t nTx = brr.get_var_int(); + for(uint32_t itx=0; itxPut(leveldb::WriteOptions(), + txHash.toBinStr(), + txRaw.toBinStr()); + assert(leveldb::ldbCheckStatus(stat)); + } + + nBlkRead++; + nBytesRead += nextBlkSize; + bsb.reader().advance(nextBlkSize); + + if(nBlkRead % 5000 == 0) + cout << nBlkRead << " blocks read..." << endl; + } + } + TIMER_STOP("NaiveScan"); + + +} + + +void TestLDBScanBlockchain(string testdbpath) +{ + leveldb::Options opts; + opts.create_if_missing = true; + opts.compression = leveldb::kNoCompression; + + leveldb::DB* ldb; + leveldb::Status stat = leveldb::DB::Open(opts, testdbpath.c_str(), &ldb); + assert(leveldb::ldbCheckStatus(stat)); + + + map addrMap; + BinaryData addr; + // Main-network addresses + addr.createFromHex("47b8ad0b1d6803260ce428d9e09e2cd99fd3b359"); addrMap[addr] = 1; + addr.createFromHex("59b3d39fd92c9ee0d928e40c2603681d0badb847"); addrMap[addr] = 1; + addr.createFromHex("fe3959db250f247ad724f2af5439ca32e8be3db1"); addrMap[addr] = 1; + addr.createFromHex("b13dbee832ca3954aff224d77a240f25db5939fe"); addrMap[addr] = 1; + + map unspentOutPoints; + + leveldb::Iterator* it = ldb->NewIterator(leveldb::ReadOptions()); + uint32_t idx = 1; + vector offsetsIn; + vector offsetsOut; + + TIMER_START("Rescan_from_LevelDB"); + uint32_t nObj=0; + uint64_t allbtc=0; + cout << "Entry Lengths: "; + for(it->SeekToFirst(); it->Valid(); it->Next()) + { + BinaryData val(it->value().ToString()); + if(val.getSize()==80) // need to add another condition + continue; + + uint32_t txLen = BtcUtils::TxCalcLength(val.getPtr(), &offsetsIn, &offsetsOut); + + OutPoint op; + + for(uint32_t iout=0; ioutkey().ToString()); + op.setTxOutIndex(iout); + unspentOutPoints[op] = val; + } + } + else if(scriptLenFirstByte==67) + { + // Std spend-coinbase TxOut script + static HashString addr20(20); + BtcUtils::getHash160_NoSafetyCheck(ptr+2, 65, addr20); + if( addrMap.find(addr20) != addrMap.end() ) + { + uint64_t val = *(uint64_t*)(ptr-8); + cout << " Received a TxOut!" << val/1e8 << endl; + allbtc += val; + op.setTxHash(it->key().ToString()); + op.setTxOutIndex(iout); + unspentOutPoints[op] = val; + } + } + else + { + // Do nothing + } + } + + if(++nObj % 50000 == 0) + cout << "Processed " << nObj << " tx " << endl; + } + + for(it->SeekToFirst(); it->Valid(); it->Next()) + { + BinaryData val(it->value().ToString()); + if(val.getSize()==80) // need to add another condition + continue; + + uint32_t txLen = BtcUtils::TxCalcLength(val.getPtr(), &offsetsIn, &offsetsOut); + + OutPoint op; + map::iterator iter; + for(uint32_t iin=0; iinsecond; + unspentOutPoints.erase(iter); + } + } + } + + TIMER_STOP("Rescan_from_LevelDB"); + cout << "Total TxOuts: " << allbtc/1e8 << endl; + +} + + + +void TestLdbBlockchainUtils(string blkdir) +{ + BlockDataManager_LevelDB & bdm = BlockDataManager_LevelDB::GetInstance(); + bdm.SetBlkFileLocation(blkdir, 4, 1); + + string pathH("testldb/ldbtestHeaders"); + string pathT("testldb/ldbtestTx"); + string pathR("testldb/ldbtestTransient"); + + //bdm.setLevelDBPaths(pathH, pathT, pathR); + + +} + + + + + + + + + diff --git a/cppForSwig/sighandler.cpp b/cppForSwig/sighandler.cpp new file mode 100644 index 000000000..64e290cf3 --- /dev/null +++ b/cppForSwig/sighandler.cpp @@ -0,0 +1,98 @@ +#ifdef __linux__ + +#include "log.h" + +#include +#include +#include +#include +#include +#include +#include + + +static void sigsegv(int, siginfo_t *info, void*) +{ + const int stderr=2; + { + const char e1[] = + "\nArmory has crashed. Please provide the following " + "in your bug report:\n" + "Failed to dereference address "; + ::write(stderr, e1, sizeof(e1)-1); + } + + { + char addr[64]; + int num = snprintf(addr, sizeof(addr), "%p", info->si_addr); + ::write(stderr, addr, num); + ::write(stderr, "\n", 1); + } + + void* bt_buffer[64]; + int n = backtrace(bt_buffer, sizeof(bt_buffer)/sizeof(bt_buffer[0])); + + backtrace_symbols_fd(bt_buffer, n, stderr); + + // allow crash again, so that the user sees the pretty "Segmentation fault" + signal(SIGSEGV, 0); + + // now try to write that same error to the log file + // since Log::filename accesses what might be corrupt memory, + // we have to repeat some of the stuff above. So it can crash + // here and we still get a log on stderr + int log = open(Log::filename().c_str(), O_APPEND, O_WRONLY); + if (log != -1) + { + { + const char e1[] = + "\n\nSIGSEGV\n" + "Failed to dereference address "; + ::write(log, e1, sizeof(e1)-1); + } + { + char addr[64]; + int num = snprintf(addr, sizeof(addr), "%p", info->si_addr); + ::write(log, addr, num); + ::write(log, "\n", 1); + } + backtrace_symbols_fd(bt_buffer, n, log); + ::close(log); + } + + // now actually crash again so the user sees the error + + int *crash = (int*)0; + *crash = 0; +} + +static void installSignalHandler() +{ + static bool installed=false; + if (installed) + return; + installed = true; + + struct sigaction action; + action.sa_sigaction = sigsegv; + action.sa_flags = SA_SIGINFO | SA_NODEFER; + + sigaction(SIGSEGV, &action, 0); +} + +namespace +{ +class Init +{ +public: + Init() + { + installSignalHandler(); + } +}; +} + +static Init install; + +// kate: indent-width 3; replace-tabs on; +#endif diff --git a/cppForSwig/whereispy.sh b/cppForSwig/whereispy.sh deleted file mode 100755 index 37d0216fc..000000000 --- a/cppForSwig/whereispy.sh +++ /dev/null @@ -1,118 +0,0 @@ -#! /bin/bash - -whereispy=`whereis "python"` - -echo -echo "looking for python paths..." -echo - -v27="2.7" -v26="2.6" -pyv="" - -sopath="" -apath="" -hpath="" -pythonver="" -pythonpath="" - -for i in $whereispy; do - - if [[ "$apath" ]]; then - #kills the loop is a matching .a was found, keeps running if it can only find .so - break - fi - - if echo "$i" | grep -q "/lib"; then - pyv="" - if echo "$i" | grep -q "$v27"; then - pyv=$v27 - elif echo "$i" | grep -q "$v26"; then - pyv=$v26 - fi - - if [ "$pyv" ]; then - possiblea="" - possibleso="" - libpath=`whereis "libpython$pyv".` - for z in $libpath; do - if echo "$z" | grep -q "$pyv.a"; then - possiblea=$z - elif echo "$z" | grep -q "$pyv.so"; then - possibleso=$z - fi - done - - #whereis may not have found a lib but find can still land one - if [ -z $possiblea ] || [ -z $possibleso ]; then - pyroot=${i%/*} - extralib=`find $pyroot 2> /dev/null | grep "libpython"$pyv` - for t in $extralib; do - if echo "$t" | grep -q "$pyv.a"; then - possiblea=$t - elif echo "$t" | grep -q "$pyv.so"; then - possibleso=$t - fi - done - fi - - if [[ "$possiblea" ]] || [[ "$possibleso" ]]; then - for h in $whereispy; do - if echo "$h" | grep "/include" | grep "$pyv" | grep -q -v '_d'; then - if [[ "$possiblea" ]]; then - #found what we were looking for, make the file and quit - pythonver=$pyv - pythonpath=$i - hpath=$h - apath=$possiblea - sopath=$possibleso - break - elif [[ "$pyv" > "$pythonver" ]]; then - pythonver=$pyv - pythonpath=$i - hpath=$h - apath=$possiblea - sopath=$possibleso - fi - fi - done - fi - fi - fi -done - -failed="1" -if [[ "$apath" ]]; then - failed="" - echo - echo "found matching .a lib and include folder =)" - echo "PYTHON_INCLUDE=$hpath -PYTHON_LIB=$apath -PYVER=python$pythonver" > ./pypaths.txt -elif [[ "$sopath" ]]; then - echo - echo -n "Couldn't find static libpython (.a). Found the .so however. Continuing, but this is not a recommend configuration" 1>&2 - failed="" - echo "PYTHON_INCLUDE=$hpath -PYTHON_LIB=$sopath -PYVER=python$pythonver" > ./pypaths.txt -fi - -if [[ $failed == "1" ]]; then -echo "PYTHON_INCLUDE= -PYTHON_LIB= -PYVER=" > ./pypaths.txt - -echo -echo "Couldn't find matching versions of python and libpython ='(" -echo "At this point the configure file will abort. To make Armory, you need to manually enter your python paths in ./pypaths.txt:" -echo -echo " 'PYTHON_INCLUDE' is the path to the include python folder" -echo " 'PYTHON_LIB' is the path to the matching libpython .a or .so file" -echo " 'PYVER' is the the python version used (i.e. python2.6 or python2.7)" -echo -echo "Once you have filled these, run make from the cppForSwig folder" -echo "Aborting build" -echo -exit -fi diff --git a/default_bootstrap.torrent b/default_bootstrap.torrent new file mode 100644 index 000000000..8c743f077 Binary files /dev/null and b/default_bootstrap.torrent differ diff --git a/devchanges.txt b/devchanges.txt new file mode 100644 index 000000000..46c7e85d5 --- /dev/null +++ b/devchanges.txt @@ -0,0 +1,85 @@ +Changelog for Developers using the armoryengine codebase + + +-- Refactored code. To get armoryengine utilities as before, you must use: + from armoryengine.ALL import * + +-- Method addrStr_to_hash160() used to return just a 20-byte string. Now it + returns both a prefix string. The prefix is either '\x00' (reg addr), or + '\x05' (P2SH addr). Or the appropriate ones for your network. + +-- We have started replacing most things that use "addr160" values with + "scrAddress" values. "ScrAddr" == Script/Address, and refers to a general + TxOut scripts which may not be regular addresses. Could be P2SH, multisig, + or something non-standard. + +-- A scrAddr is unique for a recipient, meaning that if two scripts return + the same scrAddr, it means that the same private keys have control over + it (example: one TxOut may use pay2pubkey, another may use pay2pubkeyhash. + If they are to the same pubkey, they will have the same scrAddr. + +-- Use scrAddr_to_addrStr to convert from this new form to a base58-encoded + address-string, if it has such a form (basically, regular adddresses and + P2SH). Will throw a BadAddressError if not. This is all part of a more + general move to handle arbitrary scripts instead of just the standard + Pay2PubKeyHash scripts. Added a bunch of conversion methods: + + CREATE SCRIPT: + + hash160_to_p2pkhash_script( a160 ) # DUP HASH EQUAL CHKSIG + hash160_to_p2shStr( a160 ) # HASH EQUAL + pubkey_to_p2pk_script( a160 ) # CHECKSIG + scrAddr_to_script( scraddr ) # works for RegAddr and P2SH + pubkeylist_to_multisig_script([]) # NOT-TESTED-YET + + ADDR STRINGS: + + scrAddr_to_addrStr(scrAddr) + script_to_addrStr(script) + addrStr_to_scrAddr(b58str) + addrStr_to_hash160(b58str) # Now returns (PREFIX, 20B) + addrStr_is_p2sh(b58str) + + READ SCRIPT: + + script_to_scrAddr(script) + script_to_p2sh_script(script) + script_to_addrStr(script) + + +-- Script identification used to be calculated in both the python and C++ + in their own ways. Now, we have implemented C++ methods to do this, + and leverage those methods through SWIG on the python side. Wherever + you used to use "TXOUT_SCRIPT_*" constants when checking the output of + getTxOutScriptType(scr), you now compare against the CPP_TXIN_* and + CPP_TXOUT_* constants listed at the top of cppForSwig/BtcUtils.h, + or around line 400 in armoryengine/ArmoryUtils.py (and listed here!): + + CPP_TXIN_STDUNCOMPR = 0 # Signature + PubKey65 + CPP_TXIN_STDCOMPR = 1 # Signature + PubKey33 + CPP_TXIN_COINBASE = 2 # Outpoint is ('00'*32, 'ffffffff') + CPP_TXIN_SPENDPUBKEY = 3 # Signature-only + CPP_TXIN_SPENDMULTI = 4 # OP_0 (M) Sig1 Sig2 ... (N) + CPP_TXIN_SPENDP2SH = 5 # + CPP_TXIN_NONSTANDARD = 6 # Everything else + + CPP_TXOUT_STDHASH160 = 0 # DUP HASH <20B> EQUAL CHKSIG + CPP_TXOUT_STDPUBKEY65 = 1 # CHECKSIG + CPP_TXOUT_STDPUBKEY33 = 2 # CHECKSIG + CPP_TXOUT_MULTISIG = 3 # M ... N + CPP_TXOUT_P2SH = 4 # HASH <20B> EQUAL + CPP_TXOUT_NONSTANDARD = 5 # Everything else + + NOTE: The CPP_TXIN_SPENDP2SH identification should probably be renamed + to CPP_TXIN_PROBP2SH, since it's not *theoretically* 100% reliable. + It will be flagged like this only if it is a push-only script, + AND the last PushData is, itself, a *STANDARD* TxOut script. It + may very well be a p2sh-spending script but won't be flagged, if + the subscript is non-standard. This is okay, because the network + itself enforces this rule (a P2SH spend is standard, only if the + subscript is standard). However, I suppose it is possible to have + a TxIn script that gets labeled, but is not actually P2SHSPEND. + However, I'm doubtful that such a script could be valid and make + it's way through Bitcoin-Qt into the blk*.dat files. + + diff --git a/dpkgfiles/control32 b/dpkgfiles/control32 index 932640a5b..a4a9497b7 100644 --- a/dpkgfiles/control32 +++ b/dpkgfiles/control32 @@ -8,7 +8,7 @@ Homepage: http://www.bitcoinarmory.com Package: armory Architecture: i386 -Depends: python (>= 2.6), python-qt4, python-twisted, python-psutil, ${shlibs:Depends}, ${python:Depends} +Depends: python (>= 2.6), python-qt4, python-twisted, python-psutil, xdg-utils, ${shlibs:Depends} Description: Advanced Bitcoin Wallet Management Software Armory is advanced program for managing multiple Bitcoin wallets with the highest level of security available. Use top-of-the-line wallet encryption, diff --git a/dpkgfiles/control64 b/dpkgfiles/control64 index 41a564b83..2633c972b 100644 --- a/dpkgfiles/control64 +++ b/dpkgfiles/control64 @@ -8,7 +8,7 @@ Homepage: http://www.bitcoinarmory.com Package: armory Architecture: amd64 -Depends: python (>= 2.6), python-qt4, python-twisted, python-psutil, ${shlibs:Depends}, ${python:Depends} +Depends: python (>= 2.6), python-qt4, python-twisted, python-psutil, xdg-utils, ${shlibs:Depends} Description: Advanced Bitcoin Wallet Management Software Armory is advanced program for managing multiple Bitcoin wallets with the highest level of security available. Use top-of-the-line wallet encryption, diff --git a/dpkgfiles/copyright b/dpkgfiles/copyright index 5484e41a2..827f20b17 100644 --- a/dpkgfiles/copyright +++ b/dpkgfiles/copyright @@ -3,7 +3,7 @@ Armory -- Secure Bitcoin Wallet - Copyright (C) 2011-2013, Armory Technologies, Inc. + Copyright (C) 2011-2014, Armory Technologies, Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as diff --git a/dpkgfiles/make_deb_package.py b/dpkgfiles/make_deb_package.py index 98c7d7702..5c417d42a 100755 --- a/dpkgfiles/make_deb_package.py +++ b/dpkgfiles/make_deb_package.py @@ -31,7 +31,7 @@ def pwd(): if pwd().split('/')[-1]=='dpkgfiles': cd('..') -if not os.path.exists('./armoryengine.py') or \ +if not os.path.exists('./armoryengine/ArmoryUtils.py') or \ not os.path.exists('./ArmoryQt.py'): print '***ERROR: Must run this script from the root Armory directory!' exit(1) @@ -40,7 +40,7 @@ def pwd(): # Must get current Armory version from armoryengine.py # I desperately need a better way to store/read/increment version numbers vstr = '' -with open('armoryengine.py') as f: +with open('armoryengine/ArmoryUtils.py') as f: for line in f.readlines(): if line.startswith('BTCARMORY_VERSION'): vstr = line[line.index('(')+1:line.index(')')] @@ -58,7 +58,7 @@ def pwd(): pkgdir_ = 'armory_%s' % (vstr,) if not vstr: - print '***ERROR: Could not deduce version from armoryengine.py. ' + print '***ERROR: Could not deduce version from ArmoryUtils.py. ' print ' There is no good reason for this to happen. Ever! :(' exit(1) @@ -76,7 +76,7 @@ def pwd(): execAndWait('rm -f %s*' % pkgdir) execAndWait('rm -f %s*' % pkgdir_) shutil.copytree(origDir, pkgdir) -execAndWait('tar --exclude .git -zcf %s.tar.gz %s' % (pkgdir, pkgdir)) +execAndWait('tar -zcf %s.tar.gz %s' % (pkgdir, pkgdir)) cd(pkgdir) execAndWait('export DEBFULLNAME="Armory Technologies, Inc."; dh_make -s -e support@bitcoinarmory.com -f ../%s.tar.gz' % pkgdir) for f in dpkgfiles: diff --git a/dpkgfiles/rules b/dpkgfiles/rules index 1852141f3..63f85e697 100755 --- a/dpkgfiles/rules +++ b/dpkgfiles/rules @@ -13,5 +13,5 @@ dh $@ override_dh_auto_build: - $(shell dpkg-buildflags --export=sh) ; make + make PYVER=python2.6 diff --git a/extras/LastBlockHash.py b/extras/LastBlockHash.py new file mode 100644 index 000000000..ec92f8016 --- /dev/null +++ b/extras/LastBlockHash.py @@ -0,0 +1,63 @@ +import sys +sys.argv.append('--nologging') +from armoryengine import BTC_HOME_DIR + +from utilities.ArmoryUtils import binary_to_int, LITTLEENDIAN, sha256, \ + binary_to_hex, BIGENDIAN +import hashlib +import os + + + +BLOCK_SIZE_LENGTH = 4 +MAGIC_NUMBER_LENGTH = 4 +HEADER_LENGTH = 80 + +def getLastBlockFile(): + # the current last block file number + i = 80 + blkFilePath = os.path.join(BTC_HOME_DIR, 'blocks', 'blk%05d.dat' % i) + lastFile = None + while True: + try: + with open(blkFilePath, 'rb') as f: + lastFile = blkFilePath + except IOError: + break + i += 1 + blkFilePath = os.path.join(BTC_HOME_DIR, 'blocks', 'blk%05d.dat' % i) + return lastFile + +def getFileSize(f): + pos = f.tell() + # Go to the end to get the file length + f.seek(0,2) + result = f.tell() + f.seek(pos) + return result + + +def getNextBlockHash(f): + fileOffset = f.tell() + f.seek(MAGIC_NUMBER_LENGTH, 1) + blkSize = binary_to_int(f.read(BLOCK_SIZE_LENGTH), LITTLEENDIAN) + result = None + if blkSize > 0: + blkString = f.read(blkSize) + blkHdrBinary = blkString[:HEADER_LENGTH] + result = sha256(sha256(blkHdrBinary)) + else: + f.seek(0,2) + return result + +def getLastBlockHash(blkFile): + result = None + with open(blkFile, 'rb') as f: + fSize = getFileSize(f) + while f.tell() < fSize: + blockHash = getNextBlockHash(f) + if blockHash != None: + result = blockHash + return result + +print binary_to_hex(getLastBlockHash(getLastBlockFile()), BIGENDIAN) diff --git a/extras/LatestBlocksFinder.py b/extras/LatestBlocksFinder.py new file mode 100644 index 000000000..30f046b86 --- /dev/null +++ b/extras/LatestBlocksFinder.py @@ -0,0 +1,116 @@ +import sys +sys.argv.append('--nologging') + +from utilities.BinaryUnpacker import BinaryUnpacker +from utilities.ArmoryUtils import binary_to_int, LITTLEENDIAN, binary_to_hex,\ + sha256, BIGENDIAN +from utilities.BinaryPacker import BINARY_CHUNK, UINT32, VAR_INT +from collections import namedtuple + +import os +from armoryengine import BTC_HOME_DIR + +n = 10 +def getLastTwoFiles(): + i = 79 + blkFilePath = os.path.join(BTC_HOME_DIR, 'blocks', 'blk%05d.dat' % i) + lastFile = None + secondToLastFile = None + while True: + try: + with open(blkFilePath, 'rb') as f: + secondToLastFile = lastFile + lastFile = [i,blkFilePath] + except IOError: + break + i += 1 + blkFilePath = os.path.join(BTC_HOME_DIR, 'blocks', 'blk%05d.dat' % i) + return lastFile, secondToLastFile + +def getFileSize(f): + pos = f.tell() + # Go to the end to get the file length + f.seek(0,2) + result = f.tell() + f.seek(pos) + return result + + +BLOCK_SIZE_LENGTH = 4 +MAGIC_NUMBER_LENGTH = 4 +HEADER_LENGTH = 80 + +TX_OUT_HASH_LENGTH = 32 +TX_OUT_INDEX_LENGTH = 4 +SEQUENCE_LENGTH = 4 +VERSION_LENGTH = 4 +TxOut = namedtuple('TxOut', ['txOutIndex', 'value', 'script', 'txOutType']) +TxIn = namedtuple('TxIn', ['outpoint', 'script','sequence']) +Tx = namedtuple('Tx', ['txHash', 'version', 'txInList', 'txOutList']) +Block = namedtuple('Block', ['blkNum', 'blkSize', 'blkHdr', 'txCount', 'txBinary', 'txOffsetList', 'txList']) +BlockHeader = namedtuple('BlockHeader', ['version', 'prevHash', 'merkleHash', + 'time', 'bits', 'nonce' ]) + +def parseBlockHeader(blkHdrBinary): + binunpack = BinaryUnpacker(blkHdrBinary) + return BlockHeader(binunpack.get(UINT32), + binunpack.get(BINARY_CHUNK, 32), + binunpack.get(BINARY_CHUNK, 32), + binunpack.get(UINT32), + binunpack.get(UINT32), + binunpack.get(UINT32)) + +def getBlockHeight(txBinary): + binunpack = BinaryUnpacker(txBinary) + binunpack.advance(VERSION_LENGTH) + txInCount = binunpack.get(VAR_INT) + binunpack.advance(TX_OUT_HASH_LENGTH + TX_OUT_INDEX_LENGTH) + sigScriptLength = binunpack.get(VAR_INT) + binunpack.advance(1) + height = binary_to_int(binunpack.get(BINARY_CHUNK, 3)) + return height + +def getNextBlock(f): + fileOffset = f.tell() + f.seek(MAGIC_NUMBER_LENGTH, 1) + blkSize = binary_to_int(f.read(BLOCK_SIZE_LENGTH), LITTLEENDIAN) + result = None + if blkSize > 0: + binunpack = BinaryUnpacker(f.read(blkSize)) + blkHdrBinary = binunpack.get(BINARY_CHUNK, HEADER_LENGTH) + txCount = binunpack.get(VAR_INT) + txBinary = binunpack.get(BINARY_CHUNK, binunpack.getRemainingSize()) + blockHeight = getBlockHeight(txBinary) + result = [sha256(sha256(blkHdrBinary)), blockHeight, fileOffset] + else: + f.seek(0,2) + return result + +# argument is an array that consists of a file descriptor with the file number +def getAllBlocks(blkFileWithNumber): + blkInfoList = [] + with open(blkFileWithNumber[1], 'rb') as f: + fSize = getFileSize(f) + while f.tell() < fSize: + blkInfo = getNextBlock(f) + if blkInfo != None: + blkInfoWithFileNum = [blkFileWithNumber[0]] + blkInfoWithFileNum.extend(blkInfo) + blkInfoList.append(blkInfoWithFileNum) + return blkInfoList + +# only goes back 2 files so anything over 800 could get cut off +# at 2 block files worth of data +def getLastNBlocks(n=10): + lastFile, secondToLastFile = getLastTwoFiles() + + lastBlocks = getAllBlocks(lastFile) + if len(lastBlocks) < n: + secondToLastBlocks = getAllBlocks(secondToLastFile) + secondToLastBlocks.extend(lastBlocks) + lastBlocks = secondToLastBlocks + return lastBlocks[-n:] + +lastNBlocks = getLastNBlocks(3) +for blk in lastNBlocks: + print blk[0], binary_to_hex(blk[1], BIGENDIAN), blk[2], blk[3] diff --git a/extras/PromoKit.py b/extras/PromoKit.py new file mode 100644 index 000000000..3f4952095 --- /dev/null +++ b/extras/PromoKit.py @@ -0,0 +1,340 @@ +################################################################################ +# +# Copyright (C) 2011-2014, Armory Technologies, Inc. +# Distributed under the GNU Affero General Public License (AGPL v3) +# See LICENSE or http://www.gnu.org/licenses/agpl.html +# +################################################################################ + +from getpass import getpass +import os +import sys +# Must move and clear the Args list before importing anything +# Otherwise it will process the args for this script. +promoKitArgList = sys.argv +# Clear and add --testnet unless you are running this on main net +sys.argv = sys.argv[:1] +import time + +from PyQt4.QtGui import * +from pywin.scintilla import view +from armoryengine.PyBtcWallet import PyBtcWallet +from CppBlockUtils import SecureBinaryData +from armoryengine.ArmoryUtils import makeSixteenBytesEasy, NegativeValueError, \ + WalletAddressError, MIN_RELAY_TX_FEE, LOGINFO, hash160_to_p2pkhash_script +from armoryengine.BDM import TheBDM +from armoryengine.CoinSelection import calcMinSuggestedFees, PySelectCoins +from armoryengine.Transaction import PyTxDistProposal +from qtdefines import GETFONT, tr +from qtdialogs import SimplePrintableGraphicsScene + + + + +sys.path.append('..') +sys.path.append('.') + + +# Generates a list of new promo wallets +def createWalletList(n, nameString): + walletList = [] + for i in range(n): + name = "%s #%d" % (nameString, i) + newWallet = PyBtcWallet().createNewWallet( \ + withEncrypt=False, \ + shortLabel=name, \ + longLabel=name, \ + doRegisterWithBDM=False) + walletList.append(newWallet) + return walletList + +# Pulled from qtdialogs. Assumes single sheet 1.35c backups +def createPrintScene(wallet, amountString, expiresString): + + scene = SimplePrintableGraphicsScene(None, None) + + INCH = scene.INCH + MARGIN = scene.MARGIN_PIXELS + scene.resetCursor() + scene.drawPixmapFile(':/armory_logo_h36.png') + scene.newLine() + scene.drawText('Paper Backup for Armory Wallet', GETFONT('Var', 11)) + scene.newLine() + scene.drawText('http://www.bitcoinarmory.com') + + scene.newLine(extra_dy=20) + scene.drawHLine() + scene.newLine(extra_dy=20) + + + ssType = ' (Unencrypted)' + bType = tr('Single-Sheet ' + ssType) + colRect, rowHgt = scene.drawColumn(['Wallet Version:', 'Wallet ID:', \ + 'Wallet Name:', 'Backup Type:']) + scene.moveCursor(15, 0) + suf = 'c' + colRect, rowHgt = scene.drawColumn(['1.35'+suf, wallet.uniqueIDB58, \ + wallet.labelName, bType]) + scene.moveCursor(15, colRect.y() + colRect.height(), absolute=True) + + # Display warning about unprotected key data + wrap = 0.9*scene.pageRect().width() + + container = 'this wallet' + warnMsg = tr(""" + WARNING: Anyone who has access to this + page has access to all the bitcoins in %s! Please keep this + page in a safe place.""" % container) + + scene.newLine() + scene.drawText(warnMsg, GETFONT('Var', 9), wrapWidth=wrap) + + scene.newLine(extra_dy=20) + scene.drawHLine() + scene.newLine(extra_dy=20) + numLine = 'two' + + descrMsg = tr(""" + The following %s lines backup all addresses + ever generated by this wallet (previous and future). + This can be used to recover your wallet if you forget your passphrase or + suffer hardware failure and lose your wallet files. """ % numLine) + scene.drawText(descrMsg, GETFONT('var', 8), wrapWidth=wrap) + scene.newLine(extra_dy=10) + + ########################################################################### + # Finally, draw the backup information. + bottomOfSceneHeader = scene.cursorPos.y() + + code12 = wallet.addrMap['ROOT'].binPrivKey32_Plain.toBinStr() + Lines = [] + Prefix = [] + Prefix.append('Root Key:') + Lines.append(makeSixteenBytesEasy(code12[:16])) + Prefix.append('') + Lines.append(makeSixteenBytesEasy(code12[16:])) + # Draw the prefix + origX,origY = scene.getCursorXY() + scene.moveCursor(20,0) + colRect, rowHgt = scene.drawColumn([''+l+'' for l in Prefix]) + + nudgeDown = 2 # because the differing font size makes it look unaligned + scene.moveCursor(20, nudgeDown) + scene.drawColumn(Lines, + + font=GETFONT('Fixed', 8, bold=True), \ + rowHeight=rowHgt, + useHtml=False) + + scene.moveCursor(MARGIN, colRect.y()-2, absolute=True) + width = scene.pageRect().width() - 2*MARGIN + scene.drawRect( width, colRect.height()+7, edgeColor=QColor(0,0,0), fillColor=None) + + scene.newLine(extra_dy=30) + scene.drawText( tr(""" + The following QR code is for convenience only. It contains the + exact same data as the %s lines above. If you copy this backup + by hand, you can safely ignore this QR code. """ % numLine), wrapWidth=4*INCH) + + scene.moveCursor(20,0) + x,y = scene.getCursorXY() + edgeRgt = scene.pageRect().width() - MARGIN + edgeBot = scene.pageRect().height() - MARGIN + + qrSize = max(1.5*INCH, min(edgeRgt - x, edgeBot - y, 2.0*INCH)) + scene.drawQR('\n'.join(Lines), qrSize) + scene.newLine(extra_dy=25) + scene.drawHLine(7*INCH, 5) + scene.newLine(extra_dy=25) + scene.drawText(tr(""" + CONGRATULATIONS: You have received a Bitcoin Armory + promotional wallet containing %s You may collect this money by installing Bitcoin Armory + from the website shown above. After you install the software move the funds to a new + wallet or any address that you own. Do not deposit any bitcoins to this wallet. You + don't know where this paper has been! You have until %s to claim your bitcoins. After + this date we will remove all remaining bitcoins from this wallet.""" % + (amountString, expiresString)), GETFONT('Var', 11), wrapWidth=wrap) + + scene.newLine(extra_dy=25) + return scene + +# This function will display a print dialog. It may hide behind other windows +def printWalletList(walletList, amountString, expiresString): + printer = QPrinter(QPrinter.HighResolution) + printer.setPageSize(QPrinter.Letter) + + if QPrintDialog(printer).exec_(): + painter = QPainter(printer) + painter.setRenderHint(QPainter.TextAntialiasing) + for wallet in walletList: + scene = createPrintScene(wallet, amountString, expiresString) + scene.getScene().render(painter) + if wallet != walletList[-1]: + printer.newPage() + painter.end() + +# Assumes a secure wallet has been created and is provided. There should not be any +# imported addresses in this wallet. It is assumed that all of this wallet's imported +# addresses are about to be imported in this function +def importAddrsToMasterWallet(masterWallet, walletList, addrsPerWallet, masterWalletName): + masterWallet.unlock(securePassphrase = SecureBinaryData(getpass('Enter your secret string:'))) + for wallet in walletList: + for i in range(addrsPerWallet): + addr = wallet.getNextUnusedAddress() + masterWallet.importExternalAddressData(privKey = addr.binPrivKey32_Plain, + pubKey = addr.binPublicKey65) + return masterWallet + +# Distribute amount to each imported address in the wallet. +def distributeBtc(masterWallet, amount, sendingAddrList): + pytx = None + setupTheBDM() + try: + recipValuePairs = [] + utxoList = [] + for sendingAddr in sendingAddrList: + addr160 = sendingAddr.getAddr160() + # Make sure the sending addresses are in the masterWallet + if not masterWallet.hasAddr(addr160): + raise WalletAddressError, 'Address is not in wallet! [%s]' % sendingAddr.getAddrStr() + utxoList.extend(masterWallet.getAddrTxOutList(addr160)) + + + for importedAddr in masterWallet.getLinearAddrList(): + if importedAddr.chainIndex<0: + recipValuePairs.append((importedAddr.getAddr160(),amount)) + totalSpend = len(recipValuePairs)*amount + fee = calcMinSuggestedFees(utxoList, totalSpend, MIN_RELAY_TX_FEE, len(recipValuePairs))[1] + # Get the necessary utxo list + selectedUtxoList = PySelectCoins(utxoList, totalSpend, fee) + # get total value + totalAvailable = sum([u.getValue() for u in selectedUtxoList]) + totalChange = totalAvailable - (totalSpend + fee) + + # Make sure there are funds to cover the transaction. + if totalChange < 0: + print '***ERROR: you are trying to spend more than your balance!' + raise NegativeValueError + recipValuePairs.append((masterWallet.getNextUnusedAddress().getAddr160(), totalChange )) + + # ACR: To support P2SH in general, had to change createFromTxOutSelection + # to take full scripts, not just hash160 values. Convert the list + # before passing it in + scrPairs = [[hash160_to_p2pkhash_script(r), v] for r,v in recipValuePairs] + txdp = PyTxDistProposal().createFromTxOutSelection(selectedUtxoList, scrPairs) + + masterWallet.unlock(securePassphrase = SecureBinaryData(getpass('Enter your secret string:'))) + # Sign and prepare the final transaction for broadcast + masterWallet.signTxDistProposal(txdp) + pytx = txdp.prepareFinalTx() + + print '\nSigned transaction to be broadcast using Armory "offline transactions"...' + print txdp.serializeAscii() + finally: + TheBDM.execCleanShutdown() + return pytx + +def setupTheBDM(): + TheBDM.setBlocking(True) + if not TheBDM.isInitialized(): + TheBDM.registerWallet(masterWallet) + TheBDM.setOnlineMode(True) + # Only executed on the first call if blockchain not loaded yet. + LOGINFO('Blockchain loading') + while not TheBDM.getBDMState()=='BlockchainReady': + LOGINFO('Blockchain Not Ready Yet %s' % TheBDM.getBDMState()) + time.sleep(2) +# Sweep all of the funds from the imported addrs back to a +# new addrin the master wallet +def sweepImportedAddrs(masterWallet): + setupTheBDM() + recipValuePairs = [] + utxoList = [] + for importedAddr in masterWallet.getLinearAddrList(): + if importedAddr.chainIndex<0: + addr160 = importedAddr.getAddr160() + utxoList.extend(masterWallet.getAddrTxOutList(addr160)) + + # get total value + totalAvailable = sum([u.getValue() for u in utxoList]) + fee = calcMinSuggestedFees(utxoList, totalAvailable, MIN_RELAY_TX_FEE, 1)[1] + totalSpend = totalAvailable - fee + if totalSpend<0: + print '***ERROR: The fees are greater than the funds being swept!' + raise NegativeValueError + recipValuePairs.append((masterWallet.getNextUnusedAddress().getAddr160(), totalSpend )) + + # ACR: To support P2SH in general, had to change createFromTxOutSelection + # to take full scripts, not just hash160 values. Convert the list + # before passing it in + scrPairs = [[hash160_to_p2pkhash_script(r), v] for r,v in recipValuePairs] + txdp = PyTxDistProposal().createFromTxOutSelection(utxoList, scrPairs) + + masterWallet.unlock(securePassphrase = SecureBinaryData(getpass('Enter your secret string:'))) + # Sign and prepare the final transaction for broadcast + masterWallet.signTxDistProposal(txdp) + pytx = txdp.prepareFinalTx() + + print '\nSigned transaction to be broadcast using Armory "offline transactions"...' + print txdp.serializeAscii() + return pytx + + +# Example execution generates 3 promo wallets and imports 2 address each to +# a master wallet that is provided +''' +walletList = createWalletList(100, 'Cambridge Bitcoin Meetup') +masterWallet = importAddrsToMasterWallet( \ + PyBtcWallet().readWalletFile('C:\\Users\\Andy\\AppData\\Roaming\\Armory\\armory_28Xrf4hbu_.wallet', False, False),\ + walletList, 2, "Master Promo Wallet") +printWalletList(walletList, "who knows how many bitcoins!?", "April 1st, 2014") +''' + +# Example execution distribute .0001 Btc to each imported address in a master wallet +''' +masterWallet = PyBtcWallet().readWalletFile('C:\\Users\\Andy\\AppData\\Roaming\\Armory\\testnet3\\armory_2hzEdtr9c_.wallet', False, False) +pytx = distributeBtc(masterWallet, 10000, masterWallet.getLinearAddrList(withImported=False)) +''' + +# Show help whenever the args are not correct +def printHelp(): + print 'USAGE: %s --create ' % sys.argv[0] + print ' or: %s --distribute ' % sys.argv[0] + print ' or: %s --sweep ' % sys.argv[0] + exit(0) + +# Main execution path +# Do not ever access the same wallet file from two different processes at the same time +print '\n' +raw_input('PLEASE CLOSE ARMORY BEFORE RUNNING THIS SCRIPT! (press enter to continue)\n') + +if len(promoKitArgList)<3: + printHelp() + +operation = promoKitArgList[1] +walletFile = promoKitArgList[2] +if not os.path.exists(walletFile): + print 'Wallet file was not found: %s' % walletFile +masterWallet = PyBtcWallet().readWalletFile(walletFile, False, False) +if operation == '--create': + if len(promoKitArgList)<6: + printHelp() + else: + numWallets = int(promoKitArgList[3]) + addrsPerWallet = int(promoKitArgList[4]) + walletLabel = promoKitArgList[5] + walletList = createWalletList(numWallets, walletLabel) + masterWallet = importAddrsToMasterWallet( \ + masterWallet, walletList, addrsPerWallet, "Master Promo Wallet", ) + # Didn't want to fit these into the argument list. Need to edit based on event + printWalletList(walletList, "some amount of bitcoin. ", "June 1st, 2014") +elif operation == '--distribute': + if len(promoKitArgList)<4: + printHelp() + else: + amountPerAddr = int(promoKitArgList[3]) + distributeBtc(masterWallet, amountPerAddr, masterWallet.getLinearAddrList(withImported=False)) +elif operation == '--sweep': + sweepImportedAddrs(masterWallet) +else: + printHelp() diff --git a/extras/__init__.py b/extras/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/extras/armoryengine_basics.py b/extras/armoryengine_basics.py new file mode 100644 index 000000000..150639ffc --- /dev/null +++ b/extras/armoryengine_basics.py @@ -0,0 +1,114 @@ +#! /usr/bin/python +import sys +sys.path.append('..') +from armoryengine import * + +# Integer/Hex/Binary/Base58 Conversions +print '\nInteger/Hex/Binary/Base58 Conversions' +print 1, hex_to_int('0f33') +print 2, hex_to_int('0f33', BIGENDIAN) +print 3, int_to_hex(13071) +print 4, int_to_hex(13071, widthBytes=4) +print 5, int_to_hex( 3891, widthBytes=4, endOut=BIGENDIAN) +print 6, [int_to_binary(65535, widthBytes=4)] +print 7, binary_to_int('ffff') +print 8, binary_to_hex('\x00\xff\xe3\x4f') +print 9, [hex_to_binary('00ffe34f')] +print 10, binary_to_base58('\x00\xff\xe3\x4f') +print 11, [base58_to_binary('12Ux6i')] + +print '\nHash functions:' +print 12, binary_to_hex( sha256('Single-SHA256') ) +print 13, binary_to_hex( hash256('Double-SHA256') ) +print 14, binary_to_hex( hash160('ripemd160(sha256(X))') ) +print 15, binary_to_hex( HMAC512('secret', 'MsgAuthCode') )[:24] + +print '\nMay need to switch endian to match online tools' +addr160Hex = binary_to_hex( hash160('\x00'*65) ) +print 16, hex_switchEndian( addr160Hex ) +print 17, binary_to_hex( hash160('\x00'*65), BIGENDIAN ) + +print '\nAddress Conversions:' +donateStr = '1ArmoryXcfq7TnCSuZa9fQjRYwJ4bkRKfv' +donate160 = addrStr_to_hash160(donateStr) +donateStr2 = hash160_to_addrStr(donate160) +print 18, binary_to_hex(donate160) +print 19, binary_to_hex(donate160, BIGENDIAN) +print 20, donateStr2 + +print '\nBuiltin Constants and magic numbers:' +print 21, 'BITCOIN_PORT: ', BITCOIN_PORT +print 22, 'BITCOIN_RPC_PORT:', BITCOIN_RPC_PORT +print 23, 'ARMORY_RPC_PORT: ', ARMORY_RPC_PORT +print 24, 'MAGIC_BYTES: ', binary_to_hex(MAGIC_BYTES) +print 25, 'GENESIS_BLK_HASH:', GENESIS_BLOCK_HASH_HEX +print 26, 'GENESIS_TX_HASH: ', GENESIS_TX_HASH_HEX +print 27, 'ADDRBYTE: ', binary_to_hex(ADDRBYTE) +print 28, 'NETWORK: ', NETWORKS[ADDRBYTE] +print 29, 'P2SHBYTE: ', binary_to_hex(P2SHBYTE) +print 30, 'PRIVKEYBYTE: ', binary_to_hex(PRIVKEYBYTE) + +print '\nDetected values and CLI_OPTIONS:' +print 31, ' Operating System :', OS_NAME +print 32, ' OS Variant :', OS_VARIANT +print 33, ' User home-directory :', USER_HOME_DIR +print 34, ' Satoshi BTC directory :', BTC_HOME_DIR +print 35, ' Armory home dir :', ARMORY_HOME_DIR +print 36, ' LevelDB directory :', LEVELDB_DIR +print 37, ' Armory settings file :', SETTINGS_PATH +print 38, ' Armory log file :', ARMORY_LOG_FILE + +print '\nSystem Specs:' +print 39, ' Total Available RAM : %0.2f GB' % SystemSpecs.Memory +print 40, ' CPU ID string :', SystemSpecs.CpuStr +print 41, ' Number of CPU cores : %d cores' % SystemSpecs.NumCores +print 42, ' System is 64-bit :', str(SystemSpecs.IsX64) +print 43, ' Preferred Encoding :', locale.getpreferredencoding() + +print '\nRandom other utilities' +print 44, ' Curr unix time :', RightNow() +print 45, ' Curr formatted time :', unixTimeToFormatStr(RightNow()) +print 46, ' 123456 seconds is :', secondsToHumanTime(123456) +print 47, ' 123456 bytes is :', bytesToHumanSize(123456) + +print '\nCoin2Str functions align the decimal point' +print 48, ' coin2str(0.01 BTC) :', coin2str(0.01 * ONE_BTC) +print 49, ' coin2str(0.01 BTC) :', coin2str(1000000, maxZeros=4) +print 50, ' coin2str(0.01 BTC) :', coin2str(1000000, maxZeros=0) +print 51, ' coin2str(0.01 BTC) :', coin2str(2300500000, maxZeros=0) +print 51, ' coin2str(0.01 BTC) :', coin2str(160400000000, maxZeros=0) +print 51, ' coin2str(0.01 BTC) :', coin2str(10000000, maxZeros=0) + + +print '\nRaw crypto operations:' +privKey = SecureBinaryData('\xa3'*32) +pubKey = CryptoECDSA().ComputePublicKey(privKey) +addrStr = hash160_to_addrStr( hash160(pubKey.toBinStr()) ) +print 'Raw Private Key:', privKey.toHexStr() +print 'Raw Public Key: ', pubKey.toHexStr() +print 'Raw Address Str:', addrStr +print 'Encoded PrivKey:', encodePrivKeyBase58(privKey.toBinStr()) + +print '\nPyBtcAddress Operations' +addrObj = PyBtcAddress().createFromPlainKeyData(privKey) +privKey = addrObj.serializePlainPrivateKey() +pubKey = addrObj.serializePublicKey() +addrStr = addrObj.getAddrStr() +addr160 = addrObj.getAddr160() +binSig = addrObj.generateDERSignature('A msg to be signed!') +verified = addrObj.verifyDERSignature('A msg to be signed!', binSig) +print 'Obj Private Key:', binary_to_hex(privKey) +print 'Obj Public Key: ', binary_to_hex(pubKey) +print 'Obj Address Str:', addrStr +print 'Obj Address 160:', hex_switchEndian( binary_to_hex(addr160) ) +print 'Obj Signature: ', binary_to_hex(binSig) +print 'Obj SigVerifies:', verified + +print '\nUse .pprint() members of objects for debugging' +addrObj.pprint() + +print '\nUse pprintHex to visually break up large blocks of hex' +pprintHex( binary_to_hex( sha256('a')[:13] * 12 ) ) + + + diff --git a/extras/createTxFromAddrList.py b/extras/createTxFromAddrList.py index 4cb0fd031..f1edefb24 100644 --- a/extras/createTxFromAddrList.py +++ b/extras/createTxFromAddrList.py @@ -1,6 +1,6 @@ ################################################################################ # -# Copyright (C) 2011-2013, Alan C. Reiner +# Copyright (C) 2011-2014, Armory Technologies, Inc. # Distributed under the GNU Affero General Public License (AGPL v3) # See LICENSE or http://www.gnu.org/licenses/agpl.html # @@ -40,7 +40,7 @@ def createTxFromAddrList(walletObj, addrList, recipAmtPairList, \ # Check that all addresses are actually in the specified wallet for addr in addrList: - addr160 = addrStr_to_hash160(addr) + atype, addr160 = addrStr_to_hash160(addr, False) if not walletObj.hasAddr(addr160): raise WalletAddressError, 'Address is not in wallet! [%s]' % addr @@ -57,7 +57,10 @@ def createTxFromAddrList(walletObj, addrList, recipAmtPairList, \ # consequence of mixing C++ code with python via SWIG... utxoList = [] for addr in addrList: - addr160 = addrStr_to_hash160(addr) + atype, addr160 = addrStr_to_hash160(addr) + if atype==P2SHBYTE: + raise P2SHNotSupportedError + unspentTxOuts = walletObj.getAddrTxOutList(addr160, 'Spendable') utxoList.extend(unspentTxOuts[:]) @@ -84,7 +87,7 @@ def createTxFromAddrList(walletObj, addrList, recipAmtPairList, \ selectedUtxoList = PySelectCoins(utxoList, totalSpend, fee) print 'Checking that minimum required fee is satisfied for this tx...' - minValidFee = calcMinSuggestedFees(selectedUtxoList, totalSpend, fee)[1] + minValidFee = calcMinSuggestedFees(selectedUtxoList, totalSpend, fee, len(recipList))[1] if minValidFee>fee: print '***WARNING:' @@ -98,7 +101,11 @@ def createTxFromAddrList(walletObj, addrList, recipAmtPairList, \ selectedUtxoList = PySelectCoins(utxoList, totalSpend, fee) # Convert address strings to Hash160 values (and make a copy, too) - recip160List = [(addrStr_to_hash160(pair[0]), pair[1]) for pair in recipList] + def extractHash160(astr): + atype, addr160 = addrStr_to_hash160(astr, False) + return addr160 + + recip160List = [(extractHash160(pair[0]), pair[1]) for pair in recipList] # Add a change output if necessary totalSelect = sumTxOutList(selectedUtxoList) @@ -110,12 +117,17 @@ def createTxFromAddrList(walletObj, addrList, recipAmtPairList, \ # Need to add a change output, get from wallet if necessary if not changeAddr: changeAddr = walletObj.getNextUnusedAddress().getAddrStr() - recip160List.append( (addrStr_to_hash160(changeAddr), totalChange) ) + recip160List.append( (extractHash160(changeAddr), totalChange) ) print 'Creating Distribution Proposal (just an unsigned transaction)...' print [(hash160_to_addrStr(r),coin2str(v)) for r,v in recip160List] - txdp = PyTxDistProposal().createFromTxOutSelection(selectedUtxoList, recip160List) + + # ACR: To support P2SH in general, had to change createFromTxOutSelection + # to take full scripts, not just hash160 values. Convert the list + # before passing it in + scrPairs = [[hash160_to_p2pkhash_script(r), v] for r,v in recip160List] + txdp = PyTxDistProposal().createFromTxOutSelection(selectedUtxoList, scrPairs) return txdp diff --git a/extras/dlscript.py b/extras/dlscript.py new file mode 100644 index 000000000..d1fad7875 --- /dev/null +++ b/extras/dlscript.py @@ -0,0 +1,41 @@ +#! /usr/bin/python +import subprocess +import os +from sys import argv + +if len(argv)<3: + print 'Usage: %s dirToDumpDpkgs ...' % argv[0] + exit(1) + +outDir = argv[1] +pkgs = argv[2:] +if not os.path.exists(outDir): + os.makedirs(outDir) + +cmd = ['apt-get', 'install', '--yes', '--print-uris'] +cmd.extend(pkgs) +output = subprocess.check_output(cmd).split('\n') +output = filter(lambda x: x.startswith("'"), output) + +dllist = [out.split()[0][1:-1] for out in output] +filename= [os.path.join(outDir, out.split()[1]) for out in output] +md5list = [out.split()[-1].split(':')[-1] for out in output] + +try: + for dl,fil,md5 in zip(dllist, filename, md5list): + try: + subprocess.check_output(['wget', '-O', fil, dl]) + except Exception as e: + print '***Error downloading file:', dl + print '***Error: ', str(e) + continue + + out = subprocess.check_output(['md5sum', fil]).split()[0] + if not out==md5: + print '***ERROR: MD5sum does not match!' + raise +except: + for f in filename: + if os.path.exists(f): + os.remove(f) + diff --git a/extras/findpass.py b/extras/findpass.py index 508073887..0e7a5a939 100644 --- a/extras/findpass.py +++ b/extras/findpass.py @@ -7,7 +7,9 @@ sys.argv.append('--nologging') from sys import path, argv import os -from armoryengine import PyBtcWallet, RightNow + +from armoryengine.PyBtcWallet import PyBtcWallet +from armoryengine.ArmoryUtils import RightNow from CppBlockUtils import SecureBinaryData from operator import add, mul # Give an upper limit for any method to return diff --git a/extras/sample_armory_code.py b/extras/sample_armory_code.py index 318b7b933..94126cab4 100644 --- a/extras/sample_armory_code.py +++ b/extras/sample_armory_code.py @@ -27,7 +27,7 @@ print '\n\nCreating a new C++ wallet, add a few addresses...' cppWallet = Cpp.BtcWallet() cppWallet.addAddress_1_( hex_to_binary('11b366edfc0a8b66feebae5c2e25a7b6a5d1cf31') ) # hash160 (hex) - cppWallet.addAddress_1_( addrStr_to_hash160('1EbAUHsitefy3rSECh8eK2fdAWTUbpVUDN') ) # addrStr + cppWallet.addAddress_1_( addrStr_to_hash160('1EbAUHsitefy3rSECh8eK2fdAWTUbpVUDN')[1] ) # addrStr cppWallet.addAddress_1_('\x1b~\xa7*\x85\t\x12\xb7=\xd4G\xf3\xbd\xc1\x00\xf1\x00\x8b\xde\xb0') # hash160 (bin) print 'Addresses in this wallet:' @@ -222,7 +222,7 @@ def extractLineData(line): for line in httppage: if 'lessthan' in line and '1dice' in line: targ,addr,winr,mult,hous,rtrn = extractLineData(line) - diceAddr = addrStr_to_hash160(addr) + diceAddr = addrStr_to_hash160(addr)[1] diceTargetMap[diceAddr] = int(targ) dicePctWinMap[diceAddr] = float(winr[:-1])/100.0 diceWinMultMap[diceAddr] = float(mult[:-1]) diff --git a/extras/sss.py b/extras/sss.py deleted file mode 100644 index 1e419391c..000000000 --- a/extras/sss.py +++ /dev/null @@ -1,461 +0,0 @@ -################################################################################ -# -# Copyright (C) 2011-2013, Alan C. Reiner -# Distributed under the GNU Affero General Public License (AGPL v3) -# See LICENSE or http://www.gnu.org/licenses/agpl.html -# -################################################################################ - -""" -Shamir's Secret Sharing - -This is a completely self-contained python script for "fragmenting" secrets -into multiple pieces, of which any subset is sufficient for recovering the -original secret. This uses finite fields which are implemented entirely with -python's native big-integer handling (and some creative list comprehensions). - -There's nothing fast about this code -- the matrix operations and finite field -operations are intended to be compact, not fast. As such, it only goes up to -8-of-N secret splitting (no limit on N) before run times start getting totally -prohibitive. But that should cover a vast majority of use cases for this code -sample. - -""" - -import hashlib - -LITTLEENDIAN = '<'; -BIGENDIAN = '>'; - -def sha256(bits): - return hashlib.new('sha256', bits).digest() -def sha512(bits): - return hashlib.new('sha512', bits).digest() -def hash256(s): - """ Double-SHA256 """ - return sha256(sha256(s)) -def HMAC(key, msg, hashfunc=sha512): - """ This is intended to be simple, not fast. For speed, use HDWalletCrypto() """ - key = (sha512(key) if len(key)>64 else key) - key = key + ('\x00'*(64-len(key)) if len(key)<64 else '') - okey = ''.join([chr(ord('\x5c')^ord(c)) for c in key]) - ikey = ''.join([chr(ord('\x36')^ord(c)) for c in key]) - return hashfunc( okey + hashfunc(ikey + msg) ) -HMAC512 = lambda key,msg: HMAC(key,msg,sha512) - - - -##### Switch endian-ness ##### -def hex_switchEndian(s): - """ Switches the endianness of a hex string (in pairs of hex chars) """ - pairList = [s[i]+s[i+1] for i in xrange(0,len(s),2)] - return ''.join(pairList[::-1]) - -##### INT/HEXSTR ##### -def int_to_hex(i, widthBytes=0, endOut=LITTLEENDIAN): - """ - Convert an integer (int() or long()) to hexadecimal. Default behavior is - to use the smallest even number of hex characters necessary, and using - little-endian. Use the widthBytes argument to add 0-padding where needed - if you are expecting constant-length output. - """ - h = hex(i)[2:] - if isinstance(i,long): - h = h[:-1] - if len(h)%2 == 1: - h = '0'+h - if not widthBytes==0: - nZero = 2*widthBytes - len(h) - if nZero > 0: - h = '0'*nZero + h - if endOut==LITTLEENDIAN: - h = hex_switchEndian(h) - return h - -def hex_to_int(h, endIn=LITTLEENDIAN): - """ - Convert hex-string to integer (or long). Default behavior is to interpret - hex string as little-endian - """ - hstr = h[:] # copies data, no references - if endIn==LITTLEENDIAN: - hstr = hex_switchEndian(hstr) - return( int(hstr, 16) ) - - -##### HEXSTR/BINARYSTR ##### -def hex_to_binary(h, endIn=LITTLEENDIAN, endOut=LITTLEENDIAN): - """ - Converts hexadecimal to binary (in a python string). Endianness is - only switched if (endIn != endOut) - """ - bout = h[:] # copies data, no references - if not endIn==endOut: - bout = hex_switchEndian(bout) - return bout.decode('hex_codec') - - -def binary_to_hex(b, endOut=LITTLEENDIAN, endIn=LITTLEENDIAN): - """ - Converts binary to hexadecimal. Endianness is only switched - if (endIn != endOut) - """ - hout = b.encode('hex_codec') - if not endOut==endIn: - hout = hex_switchEndian(hout) - return hout - - -##### INT/BINARYSTR ##### -def int_to_binary(i, widthBytes=0, endOut=LITTLEENDIAN): - """ - Convert integer to binary. Default behavior is use as few bytes - as necessary, and to use little-endian. This can be changed with - the two optional input arguemnts. - """ - h = int_to_hex(i,widthBytes) - return hex_to_binary(h, endOut=endOut) - -def binary_to_int(b, endIn=LITTLEENDIAN): - """ - Converts binary to integer (or long). Interpret as LE by default - """ - h = binary_to_hex(b, endIn, LITTLEENDIAN) - return hex_to_int(h) - - -################################################################################ -# Convert regular hex into an easily-typed Base16 -NORMALCHARS = '0123 4567 89ab cdef'.replace(' ','') -EASY16CHARS = 'asdf ghjk wert uion'.replace(' ','') -hex_to_base16_map = {} -base16_to_hex_map = {} -for n,b in zip(NORMALCHARS,EASY16CHARS): - hex_to_base16_map[n] = b - base16_to_hex_map[b] = n - -def binary_to_easyType16(binstr): - return ''.join([hex_to_base16_map[c] for c in binary_to_hex(binstr)]) - -def easyType16_to_binary(b16str): - return hex_to_binary(''.join([base16_to_hex_map[c] for c in b16str])) - - -def makeSixteenBytesEasy(b16): - if not len(b16)==16: - raise ValueError, 'Must supply 16-byte input' - chk2 = computeChecksum(b16, nBytes=2) - et18 = binary_to_easyType16(b16 + chk2) - return ' '.join([et18[i*4:(i+1)*4] for i in range(9)]) - -def readSixteenEasyBytes(et18): - b18 = easyType16_to_binary(et18.strip().replace(' ','')) - b16 = b18[:16] - chk = b18[ 16:] - b16new = verifyChecksum(b16, chk) - if len(b16new)==0: - return ('','Error_2+') - elif not b16new==b16: - return (b16new,'Fixed_1') - else: - return (b16new,None) - -def computeChecksum(binaryStr, nBytes=4, hashFunc=hash256): - return hashFunc(binaryStr)[:nBytes] - -def verifyChecksum(binaryStr, chksum, hashFunc=hash256, fixIfNecessary=True, \ - beQuiet=False): - bin1 = str(binaryStr) - bin2 = binary_switchEndian(binaryStr) - - - if hashFunc(bin1).startswith(chksum): - return bin1 - elif hashFunc(bin2).startswith(chksum): - if not beQuiet: LOGWARN( '***Checksum valid for input with reversed endianness') - if fixIfNecessary: - return bin2 - elif fixIfNecessary: - if not beQuiet: LOGWARN('***Checksum error! Attempting to fix...'), - fixStr = fixChecksumError(bin1, chksum, hashFunc) - if len(fixStr)>0: - if not beQuiet: LOGWARN('fixed!') - return fixStr - else: - # ONE LAST CHECK SPECIFIC TO MY SERIALIZATION SCHEME: - # If the string was originally all zeros, chksum is hash256('') - # ...which is a known value, and frequently used in my files - if chksum==hex_to_binary('5df6e0e2'): - if not beQuiet: LOGWARN('fixed!') - return '' - - # ID a checksum byte error... - origHash = hashFunc(bin1) - for i in range(len(chksum)): - chkArray = [chksum[j] for j in range(len(chksum))] - for ch in range(256): - chkArray[i] = chr(ch) - if origHash.startswith(''.join(chkArray)): - LOGWARN('***Checksum error! Incorrect byte in checksum!') - return bin1 - - LOGWARN('Checksum fix failed') - return '' - - - -# START FINITE FIELD OPERATIONS -class FiniteFieldError(Exception): pass - -################################################################################### -class FiniteField(object): - PRIMES = { 1: 2**8-5, # mainly for testing - 2: 2**16-39, - 4: 2**32-5, - 8: 2**64-59, - 16: 2**128-797, - 20: 2**160-543, - 24: 2**192-333, - 32: 2**256-357, - 48: 2**384-317, - 64: 2**512-569, - 96: 2**768-825, - 128: 2**1024-105, - 192: 2**1536-3453, - 256: 2**2048-1157 } - - def __init__(self, nbytes): - if not self.PRIMES.has_key(nbytes): - LOGERROR('No primes available for size=%d bytes', nbytes) - self.prime = None - raise FiniteFieldError - self.prime = self.PRIMES[nbytes] - - def add(self,a,b): return (a+b) % self.prime - def subtract(self,a,b): return (a-b) % self.prime - def mult(self,a,b): return (a*b) % self.prime - - def power(self,a,b): - result = 1 - while(b>0): - b,x = divmod(b,2) - result = (result * (a if x else 1)) % self.prime - a = a*a % self.prime - return result - - def powinv(self,a): - """ USE ONLY PRIME MODULUS """ - return self.power(a,self.prime-2) - - def divide(self,a,b): - """ USE ONLY PRIME MODULUS """ - baddinv = self.powinv(b) - return self.mult(a,baddinv) - - def mtrxrmrowcol(self,mtrx,r,c): - if not len(mtrx) == len(mtrx[0]): - LOGERROR('Must be a square matrix!') - return [] - - sz = len(mtrx) - return [[mtrx[i][j] for j in range(sz) if not j==c] for i in range(sz) if not i==r] - - ################################################################################ - def mtrxdet(self,mtrx): - if len(mtrx)==1: - return mtrx[0][0] - - if not len(mtrx) == len(mtrx[0]): - LOGERROR('Must be a square matrix!') - return -1 - - result = 0; - for i in range(len(mtrx)): - mult = mtrx[0][i] * (-1 if i%2==1 else 1) - subdet = self.mtrxdet(self.mtrxrmrowcol(mtrx,0,i)) - result = self.add(result, self.mult(mult,subdet)) - return result - - ################################################################################ - def mtrxmultvect(self,mtrx, vect): - M,N = len(mtrx), len(mtrx[0]) - if not len(mtrx[0])==len(vect): - LOGERROR('Mtrx and vect are incompatible: %dx%d, %dx1', M, N, len(vect)) - return [ sum([self.mult(mtrx[i][j],vect[j]) for j in range(N)])%self.prime for i in range(M) ] - - ################################################################################ - def mtrxmult(self,m1, m2): - M1,N1 = len(m1), len(m1[0]) - M2,N2 = len(m2), len(m2[0]) - if not N1==M2: - LOGERROR('Mtrx and vect are incompatible: %dx%d, %dx%d', M1,N1, M2,N2) - inner = lambda i,j: sum([self.mult(m1[i][k],m2[k][j]) for k in range(N1)]) - return [ [inner(i,j)%self.prime for j in range(N1)] for i in range(M1) ] - - ################################################################################ - def mtrxadjoint(self,mtrx): - sz = len(mtrx) - inner = lambda i,j: self.mtrxdet(self.mtrxrmrowcol(mtrx,i,j)) - return [[((-1 if (i+j)%2==1 else 1)*inner(j,i))%self.prime for j in range(sz)] for i in range(sz)] - - ################################################################################ - def mtrxinv(self,mtrx): - det = self.mtrxdet(mtrx) - adj = self.mtrxadjoint(mtrx) - sz = len(mtrx) - return [[self.divide(adj[i][j],det) for j in range(sz)] for i in range(sz)] - -################################################################################### -def SplitSecret(secret, needed, pieces, nbytes=None): - if nbytes==None: - nbytes = len(secret) - - ff = FiniteField(nbytes) - fragments = [] - - # Convert secret to an integer - a = binary_to_int(secret,BIGENDIAN) - if not a=needed: - LOGERROR('You must create more pieces than needed to reconstruct!') - raise FiniteFieldError - - - if needed==1 or needed>8: - LOGERROR('Can split secrets into parts *requiring* at most 8 fragments') - LOGERROR('You can break it into as many optional fragments as you want') - return fragments - - - lasthmac = secret[:] - othernum = [] - for i in range(pieces+needed-1): - lasthmac = HMAC512(lasthmac, 'splitsecrets')[:nbytes] - othernum.append(lasthmac) - - othernum = [binary_to_int(n) for n in othernum] - if needed==2: - b = othernum[0] - poly = lambda x: ff.add(ff.mult(a,x), b) - for i in range(pieces): - x = othernum[i+1] - fragments.append( [x, poly(x)] ) - - elif needed==3: - def poly(x): - b = othernum[0] - c = othernum[1] - x2 = ff.power(x,2) - ax2 = ff.mult(a,x2) - bx = ff.mult(b,x) - return ff.add(ff.add(ax2,bx),c) - - for i in range(pieces): - x = othernum[i+2] - fragments.append( [x, poly(x)] ) - - else: - def poly(x): - polyout = ff.mult(a, ff.power(x,needed-1)) - for i,e in enumerate(range(needed-2,-1,-1)): - term = ff.mult(othernum[i], ff.power(x,e)) - polyout = ff.add(polyout, term) - return polyout - - for i in range(pieces): - x = othernum[i+2] - fragments.append( [x, poly(x)] ) - - - a = None - fragments = [ [int_to_binary(p, nbytes, BIGENDIAN) for p in frag] for frag in fragments] - return fragments - - -################################################################################ -def ReconstructSecret(fragments, needed, nbytes): - - ff = FiniteField(nbytes) - if needed==2: - x1,y1 = [binary_to_int(f, BIGENDIAN) for f in fragments[0]] - x2,y2 = [binary_to_int(f, BIGENDIAN) for f in fragments[1]] - - m = [[x1,1],[x2,1]] - v = [y1,y2] - - minv = ff.mtrxinv(m) - a,b = ff.mtrxmultvect(minv,v) - return int_to_binary(a, nbytes, BIGENDIAN) - - elif needed==3: - x1,y1 = [binary_to_int(f, BIGENDIAN) for f in fragments[0]] - x2,y2 = [binary_to_int(f, BIGENDIAN) for f in fragments[1]] - x3,y3 = [binary_to_int(f, BIGENDIAN) for f in fragments[2]] - - sq = lambda x: ff.power(x,2) - m = [ [sq(x1), x1 ,1], \ - [sq(x2), x2, 1], \ - [sq(x3), x3, 1] ] - v = [y1,y2,y3] - - minv = ff.mtrxinv(m) - a,b,c = ff.mtrxmultvect(minv,v) - return int_to_binary(a, nbytes, BIGENDIAN) - else: - pairs = fragments[:needed] - m = [] - v = [] - for x,y in pairs: - x = binary_to_int(x, BIGENDIAN) - y = binary_to_int(y, BIGENDIAN) - m.append([]) - for i,e in enumerate(range(needed-1,-1,-1)): - m[-1].append( ff.power(x,e) ) - v.append(y) - - minv = ff.mtrxinv(m) - outvect = ff.mtrxmultvect(minv,v) - return int_to_binary(outvect[0], nbytes, BIGENDIAN) - - - - -if __name__=="__main__": - import random - # Create and fragment a 16-byte secret - nBytes = 16 - secret = '9f'*nBytes - secbin = hex_to_binary(secret) - - need = 4; - pcs = SplitSecret(secbin, need, 10); # Create 10 pieces, any 4 needed (4-of-10) - - print 'Original Secret: %s' % secret - print 'Fragmenting into 4-of-10 pieces' - print 'Printing fragments:' - for i,xy in enumerate(pcs): - x,y = [binary_to_hex(z) for z in xy] - print ' Fragment %d: (x,y) = (%s, %s)' % (i,x,y) - - - decorated = [(i,z[0],z[1]) for i,z in enumerate(pcs)] - print 'Testing reconstruction from various subsets:' - - for test in range(10): - indices,xys = [0]*need, [[0,0] for i in range(need)] - random.shuffle(decorated) - - for j in range(need): - indices[j], xys[j][0], xys[j][1]= decorated[j] - - print (' Using fragments (%d,%d,%d,%d)' % tuple(sorted(indices))), - sec = ReconstructSecret(xys, need, nBytes) - print ' Reconstructed secret: %s' % binary_to_hex(sec) - - - - diff --git a/extras/test/FOUND_PASSWORD.txt b/extras/test/FOUND_PASSWORD.txt new file mode 100644 index 000000000..baf39fdeb --- /dev/null +++ b/extras/test/FOUND_PASSWORD.txt @@ -0,0 +1 @@ +FakeWallet123 \ No newline at end of file diff --git a/extras/test/FakeWallet123.wallet b/extras/test/FakeWallet123.wallet new file mode 100644 index 000000000..024b9c54c Binary files /dev/null and b/extras/test/FakeWallet123.wallet differ diff --git a/extras/test/FakeWallet123_backup.wallet b/extras/test/FakeWallet123_backup.wallet new file mode 100644 index 000000000..024b9c54c Binary files /dev/null and b/extras/test/FakeWallet123_backup.wallet differ diff --git a/extras/test/FindPassTest.py b/extras/test/FindPassTest.py new file mode 100644 index 000000000..918c7720a --- /dev/null +++ b/extras/test/FindPassTest.py @@ -0,0 +1,85 @@ +''' +Created on Aug 26, 2013 + +@author: Andy +''' +import sys +sys.argv.append('--nologging') +from extras.findpass import UnknownCaseSeg, MaxResultsExceeded, KnownSeg, \ + UnknownSeg, PwdSeg, PasswordFinder, WalletNotFound +import unittest + + +seg1Input = 'abc' +seg2Input = '123abcdef123ghijklmno123' + +segList1 = [['a','b','c'],['1','2'],['!']] +segOrdList1 = [[0,1,2],[2,0,1,],[0,1]] +expectedResult1 = ['a1!', 'a2!', 'b1!', 'b2!', 'c1!', 'c2!', \ + '!a1', '!a2', '!b1', '!b2', '!c1', '!c2', \ + 'a1', 'a2', 'b1', 'b2', 'c1', 'c2'] + + + +def expectedResultGenerator(expectedResult): + for x in expectedResult: + yield x + +class Test(unittest.TestCase): + + def testUnknownCaseSeg(self): + seg1 = UnknownCaseSeg(seg1Input) + segStringList1 = seg1.getSegList() + self.assertEqual(8, len(segStringList1)) + self.assertTrue(seg1Input.upper() in segStringList1) + + seg2 = UnknownCaseSeg(seg2Input) + segStringList2 = seg2.getSegList() + expectedSeg2Len = 2**len(seg2Input.translate(None, '123')); + self.assertEqual(expectedSeg2Len, len(segStringList2)) + self.assertTrue(seg2Input.upper() in segStringList2) + self.assertRaises(MaxResultsExceeded, seg2.getSegList, expectedSeg2Len-1) + + def testKnownSeg(self): + seg1 = KnownSeg(seg1Input) + segStringList1 = seg1.getSegList() + self.assertEqual([seg1Input], segStringList1) + + seg2 = KnownSeg(seg2Input) + self.assertEqual(1, seg2.getSegListLen()) + self.assertEqual([seg2Input], seg2.getSegList()) + + def testUnknownSeg(self): + seg1 = UnknownSeg(seg1Input,2,3) + self.assertEqual(36, seg1.getSegListLen()) + segStringList1 = seg1.getSegList() + self.assertEqual(36, len(segStringList1)) + self.assertTrue(seg1Input in segStringList1) + + seg2Len = 5 + seg2 = UnknownSeg(seg2Input, seg2Len, seg2Len) + expectedSeg2KnownLen = len(set(seg2Input)) + self.assertEqual(expectedSeg2KnownLen, len(seg2.known)) + segStringList2 = seg2.getSegList() + expectedSeg2Len = expectedSeg2KnownLen ** seg2Len + self.assertEqual(expectedSeg2Len, len(segStringList2)) + self.assertTrue(seg2Input[0] * seg2Len in segStringList2) + self.assertRaises(MaxResultsExceeded, seg2.getSegList, expectedSeg2Len-1) + + + def testPasswordFinder(self): + # Name of wallet is the password followed by '.wallet' + passwordFinder = PasswordFinder(walletPath='FakeWallet123.wallet') + self.assertTrue(passwordFinder.wallet.isLocked) + theExpectedResultGenerator = expectedResultGenerator(expectedResult1) + for i, result in enumerate(passwordFinder.passwordGenerator(segList1, segOrdList1)): + expectedResult = expectedResult1[i] + self.assertEqual(expectedResult, result) + self.assertEqual(len(expectedResult1), \ + passwordFinder.countPasswords(segList1, segOrdList1)) + passwordFinderSegList = [KnownSeg('Wallet').getSegList(), + UnknownSeg('123', 3, 3).getSegList(), + UnknownCaseSeg('Fake').getSegList()] + expectedFoundPassword = 'FakeWallet123' + foundPassword = passwordFinder.searchForPassword(passwordFinderSegList, segOrdList1) + self.assertEqual(foundPassword, expectedFoundPassword) diff --git a/extras/test/OnlineVersionOfEncryptedWallet.bin b/extras/test/OnlineVersionOfEncryptedWallet.bin new file mode 100644 index 000000000..c04591c65 Binary files /dev/null and b/extras/test/OnlineVersionOfEncryptedWallet.bin differ diff --git a/extras/test/OnlineVersionOfEncryptedWallet_backup.bin b/extras/test/OnlineVersionOfEncryptedWallet_backup.bin new file mode 100644 index 000000000..d048fff3a Binary files /dev/null and b/extras/test/OnlineVersionOfEncryptedWallet_backup.bin differ diff --git a/extras/test/__init__.py b/extras/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/goats_changes/lintian related changes.txt b/goats_changes/lintian related changes.txt deleted file mode 100644 index 541971f74..000000000 --- a/goats_changes/lintian related changes.txt +++ /dev/null @@ -1,10 +0,0 @@ -changes to the testing branch: - -./Makefile: changed destination folders to /usr/lib for .py and .so files. -./cppForSwig/Makefile: added compiler arguments to enable build fortify from g++. - -./dpkgfiles/control (32 & 64): added missing dependencies to get rid of lintian errors -./dpkgfiles/armory*.desktop: chanhed exec path to the new /usr/lib folder -./dpkgfiles/make_deb_package.py: added a couple lines to wipe all the files created by this script, so that it doesn't fail while trying to run in an non empty folder. -./dpkgfiles/postinst: updated to have the .desktop files it creates point to /usr/lib in the Exec section - diff --git a/goats_changes/msvs x64 platform and debug changes.txt b/goats_changes/msvs x64 platform and debug changes.txt deleted file mode 100644 index b011332f3..000000000 --- a/goats_changes/msvs x64 platform and debug changes.txt +++ /dev/null @@ -1,38 +0,0 @@ -Adding the x64 platforms: - -Most of the work was adding the custom build steps for the asm variants of a few cryptopp symbols only meant for x64. The rest was pointing the linker to -the right folders. Make sure you have a x64 install of python will all the needed packages. I put mine in C:\Python27_64\ so all project paths redirect -to this folder in x64. In debug mode, guardian is still built as release. - -I didn't change the DebugDll and ReleaseDll configurations. Don't know what these are for. So pick between Release and Debug only. - - -breakpoints in debug mode: - -1) Pick a debug configuration (32 or 64) and build it. Make sure you link your python libs folder. MSVS will eventually complain about the missing -2) In your python libs folder, copy a new instance of python.lib (xx is the version, either 26 or 27) and rename it python_d.lib -3) open pyconfig.h in the python include folder. Around line 370, you'll find these 3 lines: - -#ifdef _DEBUG -# define Py_DEBUG -#endif - -comment then out. - -4) At this point your dll should build in debug mode. To catch breakpoints set in it you to have invoke the python script loading your dll from MSVS. -For that purpose, go to the BitcoinArmory_SwigDLL project properties. In the page at Configuration Properties/Debugging, set the following lines: - - Command: set your python.exe full path. I tried the plain registered env path but it wouldn't resolve it for some reason - I set C:\Python27\python.exe for the 32 bit version and C:\Python27_64\python.exe for the x64 one. Change to whereever your python.exe - for the right platform is. - - Command Arguments: path to the ArmoryQt.py, currently set at ..\..\ArmoryQt.py - -5) NOTE: The _CppBlockUtils.pyd dll depends on pthreadVC2.dll to load properly. This means pthreadVC2.dll HAS to be in the same folder as -_CppBlockUtils.pyd and it has to be of the right architecture or it'll all fail. - -With all this done, select BitcoinArmory_SwigDLL as your start up project (right click the project and pick "Set as StartUp Project"). If the project -is already bold, it means it is already set. At this point, press F5 to start debugging. Further F5 will resume the code once you've hit a breakpoint. -Shift+F5 stops the debugging. - - \ No newline at end of file diff --git a/guardian.py b/guardian.py index 1818b5651..8bdf55f9d 100644 --- a/guardian.py +++ b/guardian.py @@ -1,6 +1,6 @@ ################################################################################ # # -# Copyright (C) 2011-2013, Armory Technologies, Inc. # +# Copyright (C) 2011-2014, Armory Technologies, Inc. # # Distributed under the GNU Affero General Public License (AGPL v3) # # See LICENSE or http://www.gnu.org/licenses/agpl.html # # # @@ -61,15 +61,15 @@ def killProcessTree(pid): # In this case, Windows is easier because we know it has the get_children # call, because have bundled a recent version of psutil. Linux, however, # does not have that function call in earlier versions. - if not OS_LINUX: + if OS_WINDOWS: for child in psutil.Process(pid).get_children(): - killProcess(child.pid) + kill(child.pid) else: - proc = Popen("ps -o pid --ppid %d --noheaders" % pid, shell=True, stdout=PIPE) + proc = subprocess.Popen("ps -o pid --ppid %d --noheaders" % pid, shell=True, stdout=subprocess.PIPE) out,err = proc.communicate() for pid_str in out.split("\n")[:-1]: - killProcess(int(pid_str)) - + kill(int(pid_str)) + # Verify the two PIDs are valid @@ -98,11 +98,11 @@ def killProcessTree(pid): if not check_pid(pid_bitcoind, proc_name_bitcoind): #print 'bitcoind disappeared -- guardian exiting' exit(0) - + if check_pid(pid_bitcoind, proc_name_bitcoind): - # Depending on how popen was called, bitcoind may be a child of + # Depending on how popen was called, bitcoind may be a child of # pid_bitcoind. But psutil makes it easy to find those child procs # and kill them. killProcessTree(pid_bitcoind) diff --git a/img/MsgBox_critical24.png b/img/MsgBox_critical24.png new file mode 100644 index 000000000..55f5b988e Binary files /dev/null and b/img/MsgBox_critical24.png differ diff --git a/img/MsgBox_critical64.png b/img/MsgBox_critical64.png index fa703bdad..29f1c5ce3 100644 Binary files a/img/MsgBox_critical64.png and b/img/MsgBox_critical64.png differ diff --git a/imgList.xml b/imgList.xml index 4d2a4fabc..1675af573 100644 --- a/imgList.xml +++ b/imgList.xml @@ -2,6 +2,7 @@ + img/MsgBox_critical24.png img/MsgBox_critical64.png img/MsgBox_error32.png img/MsgBox_error64.png diff --git a/jasvet.py b/jasvet.py index ccce41a23..a0d2703ac 100644 --- a/jasvet.py +++ b/jasvet.py @@ -9,13 +9,16 @@ # # Licence: Public domain or CC0 -import time +import base64 import hashlib import random -import base64 +import time + import CppBlockUtils -import armoryengine -from armoryengine import * +from armoryengine.ArmoryUtils import getVersionString, BTCARMORY_VERSION, \ + ChecksumError + + FTVerbose=False version='0.1.0' @@ -529,13 +532,13 @@ def FormatText(t, sigctx=False, verbose=False): #sigctx: False=what is display r='' te=t.split('\n') for l in te: - while len(l) and l[len(l)-1] in [' ', '\t', chr(9)]: + while len(l) and l[len(l)-1] in [' ', '\r', '\t', chr(9)]: l=l[:-1] if not len(l) or l[len(l)-1]!='\r': l+='\r' if not sigctx: if len(l) and l[0]=='-': - l='- '+l[1:] + l='- '+l r+=l+'\n' r=r[:-2] @@ -602,10 +605,13 @@ def readSigBlock(r): signature = base64.b64encode(decoded[:65]) msg = decoded[65:] elif name == CLEARSIGN_MSG_TYPE_MARKER: - # Always starts with a blank line (\r\n\r\n) chop that off with the comment oand process the rest + # First get rid of the Clearsign marker and everything before it in case the user + # added extra lines that would confuse the parsing that follows + # The message is preceded by a blank line (\r\n\r\n) chop that off with the comment and process the rest # For Clearsign the message is unencoded since the message could include the \r\n\r\n we only ignore # the first and combine the rest. - msg = RNRN.join(r.split(RNRN)[1:]) + msg = r.split(BEGIN_MARKER+CLEARSIGN_MSG_TYPE_MARKER+DASHX5)[1] + msg = RNRN.join(msg.split(RNRN)[1:]) msg = msg.split(RN+DASHX5)[0] # Only the signature is encoded, use the original r to pull out the encoded signature encoded = r.split(BEGIN_MARKER)[2].split(DASHX5)[1].split(BITCOIN_SIG_TYPE_MARKER)[0] diff --git a/osx_picobit/Armory-script.sh b/osx_picobit/Armory-script.sh deleted file mode 100755 index 567a54cda..000000000 --- a/osx_picobit/Armory-script.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -# This is the initial driver script being executed by the Armory application on Mac OS. -# Its role is to set up the environment before passing control onto Python. - -DIRNAME="`dirname $0`" -ARMORYDIR="$DIRNAME/py/usr/lib/armory" -LIBDIR="$DIRNAME/../Dependencies" -FRDIR="$DIRNAME/../Frameworks" - -# Set environment variables so the Python executable finds its stuff. -export PYTHONPATH="$ARMORYDIR" -export DYLD_LIBRARY_PATH="${LIBDIR}:${FRDIR}" -export DYLD_FRAMEWORK_PATH="${LIBDIR}:${FRDIR}" - -$DIRNAME/Python $ARMORYDIR/ArmoryQt.py \ No newline at end of file diff --git a/osx_picobit/build-app.py b/osx_picobit/build-app.py deleted file mode 100755 index 23644f91e..000000000 --- a/osx_picobit/build-app.py +++ /dev/null @@ -1,377 +0,0 @@ -#!/usr/env python -"""Build Armory as a Mac OS X Application.""" - -import os -import sys -import hashlib -import shutil -import glob - -# Name of app -appname = "Armory.app" - -# Where will this script do its work? -workdir = os.path.join(os.getcwd(), "workspace") - -# Option to make to build in parallel -parallel = "-j7" - -# Enable debugging ??? -# os.environ['CFLAGS'] = '-g -O1' -# os.environ['CXXFLAGS'] = '-g -O1' - -# List of all files needed to download. Each item is -# (Name, filename, url, sha-1 or None) -distfiles = [ - ['Python', "Python-2.7.5.tar.bz2" , "http://python.org/ftp/python/2.7.5/Python-2.7.5.tar.bz2", "6cfada1a739544a6fa7f2601b500fba02229656b"], - ['setuptools', "setuptools-1.1.4.tar.gz", "https://pypi.python.org/packages/source/s/setuptools/setuptools-1.1.4.tar.gz", "b8bf9c2b8a114045598f0e16681d6a63a4d6cdf9"], - ['Pip', "pip-1.4.1.tar.gz", "https://pypi.python.org/packages/source/p/pip/pip-1.4.1.tar.gz", "9766254c7909af6d04739b4a7732cc29e9a48cb0"], - ["psutil", "psutil-1.0.1.tar.gz", "http://psutil.googlecode.com/files/psutil-1.0.1.tar.gz", None], - ['Twisted', "Twisted-13.1.0.tar.bz2", "https://pypi.python.org/packages/source/T/Twisted/Twisted-13.1.0.tar.bz2", "7f6e07b8098b248157ac26378fafa9e018f279a7"], - ['libpng', "libpng-1.5.14.mountain_lion.bottle.tar.gz", "https://downloads.sf.net/project/machomebrew/Bottles/libpng-1.5.14.mountain_lion.bottle.tar.gz", "5e7feb640d654df0c2ac072d86e46ce9df9eaeee"], - ["Qt", "qt-4.8.5.mountain_lion.bottle.2.tar.gz", "https://downloads.sf.net/project/machomebrew/Bottles/qt-4.8.5.mountain_lion.bottle.2.tar.gz", "b361f521d413409c0e4397f2fc597c965ca44e56"], - ["sip", "sip-4.14.6.tar.gz", "http://download.sf.net/project/pyqt/sip/sip-4.14.6/sip-4.14.6.tar.gz", 'e9dfe98ab1418914c78fd3ac457a4e724aac9821'], - ["pyqt", "PyQt-mac-gpl-4.10.1.tar.gz", "http://downloads.sf.net/project/pyqt/PyQt4/PyQt-4.10.1/PyQt-mac-gpl-4.10.1.tar.gz", 'cf20699c4db8d3031c19dd51df8857bba1a4956b'], -] - -pypath_txt_template="""PYTHON_INCLUDE=%s/include/python2.7/ -PYTHON_LIB=%s/lib/python2.7/config/libpython2.7.a -PYVER=python2.7 -""" - -#Where Homebrew stores stuff. Not needed, but can save downloading time. -homebrew_cache = "/Library/Caches/Homebrew" - -# Python's prefix inside the App (for installing stuff) -pyprefix = os.path.join(workdir, appname, "Contents/Frameworks/Python.framework/Versions/2.7/") -pysitepackages = os.path.join(pyprefix, "lib/python2.7/site-packages") - -# Subdirectories under workdir -dl = 'downloads' - -# Now repack the information in distfiles -tarfile = {} -for d in distfiles: - tarfile[d[0]] = d[1] - -qtconf = """ -[Paths] -Prefix = %s -""" - -def main(): - makedir(workdir) - os.chdir(workdir) - download_all() - makedir('build') - makedir('tmp') - make_empty_app() - compile_python() - compile_pip() - install_libpng() - install_qt() - compile_sip() - compile_pyqt() - # Strangely pip and easy_install fail to install psutil the right place, and place it in - # /usr/local (!). Try avoing pip. - #pip_install("twisted", "twisted") - #pip_install("psutil", "psutil") - compile_twisted() - compile_psutil() - compile_armory() - make_ressources() - cleanup_app() - -def makedir(dir): - "Creates a subdirectory if it is not there." - if not os.path.isdir(dir): - print "Creating directory", dir - os.mkdir(dir) - -def download_all(): - "Download all distribution files - or grab them from Homebrew." - makedir(dl) - for n, f, u, s in distfiles: - myfile = os.path.join(dl, f) - hbfile = os.path.join(homebrew_cache, f) - if os.path.exists(myfile): - # Already there, check sha1 - check_sha(myfile, s) - elif os.path.exists(hbfile): - print "Found %s in Homebrew." % (f,) - check_sha(hbfile, s) - os.symlink(hbfile, myfile) - else: - system("cd '%s' && curl -OL '%s'" % (dl, u)) - check_sha(myfile, s) - print "\n\nALL DOWNLOADS COMPLETED.\n\n" - -def check_sha(f, sha): - print "Checking", f, - check = hashlib.sha1() - check.update(open(f).read()) - digest = check.hexdigest() - if sha is None: - print "SHA-1 =", digest - elif sha == digest: - print "OK" - else: - raise RuntimeError("SHA Checksum failed for "+f) - -def make_empty_app(): - "Make the empty .app bundle structure" - makedir(appname) - makedir(appname+"/Contents") - makedir(appname+"/Contents/MacOS") - makedir(appname+"/Contents/MacOS/py") - makedir(appname+"/Contents/Frameworks") - makedir(appname+"/Contents/Resources") - makedir(appname+"/Contents/Dependencies") - -def compile_python(): - "Install Python inside the app." - #unpack("Python-2.7.5.tar.bz2", "Python-2.7.5") - unpack(tarfile['Python']) - os.chdir("build/Python-2.7.5") - if os.path.exists('libpython2.7.a'): - print "Python already compiled." - else: - # Configure and compile Python - system("./configure --enable-ipv6 --prefix=%s --enable-framework='%s' > ../../python_configure.log" - % (os.path.join(workdir, "tmp"), - os.path.join(workdir, appname, "Contents", "Frameworks"))) - system("make %s > ../../python_make.log 2>&1" % (parallel,)) - #Install Python into App. - pyexe = "%s/%s/Contents/MacOS/Python" % (workdir, appname) - if os.path.exists(pyexe): - print "Python is already installed in the App" - else: - system("make install PYTHONAPPSDIR=%s > ../../python_install.log 2>&1" - % (os.path.join(workdir, 'tmp'),)) - system("cp -p '%s/tmp/Build Applet.app/Contents/MacOS/Python' %s/%s/Contents/MacOS" - % (workdir, workdir, appname)) - os.chdir("../..") - # Make the new Python the default one - os.environ['PATH'] = "%s:%s" % (os.path.join(workdir, appname, - "Contents/Frameworks/Python.framework/Versions/2.7/bin"), - os.environ['PATH']) - print "PATH is now", os.environ['PATH'] - -def unpack(tarfile, target=None): - if target is None: - if tarfile.endswith('.tar.gz'): - target = tarfile[:-7] - elif tarfile.endswith('.tar.bz2'): - target = tarfile[:-8] - else: - raise RuntimeError("Cannot get basename of "+tarfile) - if not os.path.exists(os.path.join("build", target)): - system("cd build && tar xfz "+os.path.join('..', 'downloads', tarfile)) - -def system(cmd): - print "SYSTEM:", cmd - x = os.system(cmd) - if x: - raise RuntimeError("Command failed: "+cmd) - -def compile_pip(): - "Installs pip in the newly built Python." - pipexe = os.path.join(workdir, appname, - "Contents/Frameworks/Python.framework/Versions/2.7/bin/pip") - if os.path.exists(pipexe): - print "Pip already installed" - else: - cmd = "python -s setup.py --no-user-cfg install --force --verbose" - print "Installing setuptools." - unpack(tarfile['setuptools']) - system("cd build/setuptools-1.1.4 && ( %s > ../../setuptools-install.log 2>&1 ) " % (cmd,)) - print "Installing pip." - unpack(tarfile['Pip']) - system("cd build/pip-1.4.1 && ( %s > ../../pip-install.log 2>&1 ) " % (cmd,)) - -def install_libpng(): - target = os.path.join(workdir, appname, "Contents", "Dependencies", - "libpng15.15.dylib") - if os.path.exists(target): - print "libpng already installed." - else: - print "Unpacking libpng." - system("cd %s/tmp && tar xfz %s" - % (workdir, - os.path.join('..', 'downloads', tarfile['libpng']))) - print "Copying dylib into application" - system("cp -p %s %s" - % (workdir + "/tmp/libpng/1.5.14/lib/libpng15.15.dylib", target)) - -def install_qt(): - if os.path.exists(os.path.join(workdir, appname, "Contents", "Dependencies", "qt")): - print "Qt already installed." - else: - print "Unpacking Qt into application." - system("cd %s/Contents/Dependencies && tar xfz %s" - % (appname, os.path.join('..', '..', '..', 'downloads', tarfile['Qt']))) - print "Softlinking inside application" - files = os.listdir(appname+"/Contents/Dependencies/qt/4.8.5/lib") - olddir = os.getcwd() - os.chdir(os.path.join(appname, "Contents", "Frameworks")) - for f in ['QtCore', 'QtGui', 'QtXml']: - system("ln -sf ../Dependencies/qt/4.8.5/lib/%s.framework" % (f,)) - os.chdir(olddir) - fname = os.path.join(workdir, appname, "Contents/Dependencies/qt/4.8.5/bin/qt.conf") - f = open(fname, "w") - f.write(qtconf % (os.path.join(workdir, appname, "Contents/Dependencies/qt/4.8.5/"),)) - f.close() - # Put Qt stuff on the path - os.environ['PATH'] = "%s:%s" % (os.path.join(workdir, appname, - "Contents/Dependencies/qt/4.8.5/bin"), - os.environ['PATH']) - #print "PATH is now", os.environ['PATH'] - try: - old = ":"+os.environ['DYLD_FRAMEWORK_PATH'] - except KeyError: - old = "" - os.environ['DYLD_FRAMEWORK_PATH'] = "%s:%s" % (os.path.join(workdir, appname, - "Contents/Frameworks"), old) - #print "DYLD_FRAMEWORK_PATH is now", os.environ['DYLD_FRAMEWORK_PATH'] - os.environ['QTDIR'] = os.path.join(workdir, appname, "Contents/Dependencies/qt/4.8.5/") - os.environ['QMAKESPEC'] = os.path.join(os.environ['QTDIR'], "mkspecs/macx-g++") - -def compile_sip(): - "Install sip (needed by pyqt)." - unpack(tarfile['sip']) - if os.path.exists(os.path.join(pysitepackages, "sip.so")): - print "Sip is already installed." - else: - print "Installing sip." - os.chdir("build/sip-4.14.6") - system("python configure.py --destdir='%s' --bindir='%s/bin' --incdir='%s/include' --sipdir='%s/share/sip' > ../../sip-configure.log" - % (pysitepackages, pyprefix, pyprefix, pyprefix)) - system("make > ../../sip-make.log") - system("make install > ../../sip.make-install.log") - os.chdir("../..") - -def compile_pyqt(): - "Install pyqt (needed by pyqt)." - unpack(tarfile['pyqt']) - if os.path.exists(os.path.join(pysitepackages, "PyQt4")): - print "Pyqt is already installed." - else: - print "Installing pyqt." - os.chdir("build/PyQt-mac-gpl-4.10.1") - system("python ./configure-ng.py --confirm-license --sip-incdir='%s' > ../../pyqt-configure.log 2>&1" - % (os.path.join(pyprefix, 'include'))) - system("make %s > ../../pyqt-make.log 2>&1" % (parallel,)) - system("make install > ../../pyqt.make-install.log 2>&1") - os.chdir("../..") - -def pip_install(package, lookfor): - """Install package with pip. - - For some reason this appears to be broken. Pip installs psutil in /usr/local instead of - inside the app. Strange. Do not use! - """ - if os.path.exists(os.path.join(pysitepackages, lookfor)): - print package, "already installed" - else: - print "Installing %s using pip." % (package,) - system("pip install %s > pip-%s.log 2>&1" % (package, package)) - -def compile_twisted(): - "Installs twisted in Python." - if glob.glob(pysitepackages+"/Twisted*"): - print "Twisted already installed" - else: - cmd = "python -s setup.py --no-user-cfg install --force --verbose" - print "Installing Twisted." - unpack(tarfile['Twisted']) - system("cd build/Twisted-13.1.0 && ( %s > ../../twisted-install.log 2>&1 ) " % (cmd,)) - -def compile_psutil(): - "Installs psutil in Python." - if glob.glob(pysitepackages+"/psutil*"): - print "Psutil already installed" - else: - cmd = "python -s setup.py --no-user-cfg install --force --verbose" - print "Installing Psutil." - unpack(tarfile['psutil']) - system("cd build/psutil-1.0.1 && ( %s > ../../psutil-install.log 2>&1 ) " % (cmd,)) - -def compile_armory(): - "Compiles and installs the main part of Armory." - # Always compile - even if already in app - os.chdir('..') # Leave workspace directory. - pypath_txt = "../cppForSwig/pypaths.txt" - print "\n\nNOW COMPILING AND INSTALLING ARMORY.\n" - print "Writing", pypath_txt - f = open(pypath_txt, "w") - f.write(pypath_txt_template % (pyprefix, pyprefix)) - f.close() - system("cd .. && make all") - system("cd .. && make DESTDIR='%s' install" - % (os.path.join(workdir, appname, "Contents/MacOS/py"),)) - appscript = os.path.join(workdir, appname, "Contents/MacOS/Armory") - system("cp Armory-script.sh '%s'" % (appscript,)) - system("chmod +x '%s'" % (appscript,)) - -def make_ressources(): - "Populate the Resources folder." - cont = os.path.join(workdir, appname, 'Contents') - res = os.path.join(cont, 'Resources') - system("cp Info.plist '%s'" % (cont,)) - system("cd '%s' && cp ../MacOS/py/usr/share/armory/img/armory_icon_fullres.icns Icon.icns" % (res,)) - -def cleanup_app(): - "Try to remove as much unnecessary junk as possible." - show_app_size() - print "Removing Python test-suite." - testdir = os.path.join(pyprefix, "lib/python2.7/test") - if os.path.exists(testdir): - shutil.rmtree(testdir) - print "Removing .pyo and unneeded .py files." - remove_python_files(pyprefix) - remove_python_files(os.path.join(workdir, appname, 'Contents/MacOS/py')) - show_app_size() - print "Removing unneeded Qt frameworks." - keep = ['QtCore.framework', 'QtGui.framework', 'QtXml.framework'] - qtdir = os.path.join(workdir, appname, "Contents/Dependencies/qt/4.8.5") - dir = os.path.join(qtdir, 'lib') - for f in os.listdir(dir): - if f not in keep: - pathname = os.path.join(dir, f) - if os.path.isdir(pathname): - shutil.rmtree(pathname) - else: - os.remove(pathname) - for f in os.listdir(qtdir): - if f.endswith('.app') or f == 'tests': - shutil.rmtree(os.path.join(qtdir,f)) - show_app_size() - -def show_app_size(): - "Show the size of the app." - print "Size of application: ", - sys.stdout.flush() - os.system("cd workspace && du -hs '%s'" % (appname,)) - -def remove_python_files(top): - "Remove .pyo files and any .py files where the .pyc file exists." - n_pyo = 0 - n_py_rem = 0 - n_py_kept = 0 - for (dirname, dirs, files) in os.walk(top): - for f in files: - prename, ext = os.path.splitext(f) - if ext == '.pyo': - os.remove(os.path.join(dirname, f)) - n_pyo += 1 - elif ext == '.py': - if (f + 'c') in files: - os.remove(os.path.join(dirname, f)) - n_py_rem += 1 - else: - n_py_kept += 1 - print "Removed %i .pyo files." % (n_pyo,) - print "Removes %i .py files (kept %i)." % (n_py_rem, n_py_kept) - -main() - diff --git a/osxbuild/Armory-script.sh b/osxbuild/Armory-script.sh new file mode 100755 index 000000000..bf1269606 --- /dev/null +++ b/osxbuild/Armory-script.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# This is the initial driver script executed by the Armory application on OS X. +# Its role is to set up the environment before passing control to Python. + +# Set environment variables so the Python executable finds its stuff. +# Note that `dirname $0` gives a relative path. We'd like the absolute path. +DIRNAME="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +ARMORYDIR="$DIRNAME/py/usr/lib/armory" +LIBDIR="$DIRNAME/../Dependencies" +FRDIR="$DIRNAME/../Frameworks" + +export PYTHONPATH="$ARMORYDIR" +export DYLD_LIBRARY_PATH="${LIBDIR}:${FRDIR}" +export DYLD_FRAMEWORK_PATH="${LIBDIR}:${FRDIR}" + +# OS X chokes if you try to pass command line args when none exist. Pass only +# if there are args to pass. +#OSXVER=`sw_vers -productVersion | awk '{ print substr( $0, 0, 4 ) }'` +if [ $# == "0" ]; then + $DIRNAME/Python $ARMORYDIR/ArmoryQt.py +else + $DIRNAME/Python $ARMORYDIR/ArmoryQt.py "$@" +fi diff --git a/osxbuild/Info.plist b/osxbuild/Info.plist old mode 100644 new mode 100755 index 3b86c9769..21b7db4ed --- a/osxbuild/Info.plist +++ b/osxbuild/Info.plist @@ -5,8 +5,8 @@ CFBundleDisplayName Armory Bitcoin Client CFBundleExecutable Armory CFBundleIdentifier com.armory.armory - CFBundleIconFile Icon - CFBundleInfoDictionaryVersion 0.87.2 + CFBundleIconFile Icon.icns + CFBundleInfoDictionaryVersion 0.89.99.3 CFBundleName Armory Bitcoin Client NSPrincipalClass NSApplication LSMinimumSystemVersion 10.6.0 diff --git a/osxbuild/build-app.py b/osxbuild/build-app.py new file mode 100755 index 000000000..9466e68f2 --- /dev/null +++ b/osxbuild/build-app.py @@ -0,0 +1,787 @@ +#!/usr/env python +"""Build Armory as a Mac OS X Application.""" + +import os +from os import path +import sys +import hashlib +import shutil +import glob +import time +import datetime +import optparse +import tarfile + +from subprocess import Popen, PIPE + +# Set some constants up front +#swigBinVer = '2.0.12' +pythonVer = '2.7.5' +setToolVer = '2.1.2' +pipVer = '1.5.2' +psutilVer = '1.2.1' +twistedVer = '13.2.0' +libpngVer = '1.6.8' +qtVer = '4.8.5' +sipVer = '4.15.5' # NB: I'm occasionally forced to upgrade alongside PyQt. +pyQtVer = '4.10.4' # NB: When I'm upgraded, SIP usually has to be upgraded too. +LOGFILE = 'build-app.log.txt' +LOGPATH = path.abspath( path.join(os.getcwd(), LOGFILE)) +ARMORYDIR = '..' +WORKDIR = path.join(os.getcwd(), 'workspace') +APPDIR = path.join(WORKDIR, 'Armory.app') # actually make it local +DLDIR = path.join(WORKDIR, 'downloads') +UNPACKDIR = path.join(WORKDIR, 'unpackandbuild') +#SWIGDIR = path.join(UNPACKDIR, 'swig', swigBinVer) +INSTALLDIR = path.join(WORKDIR, 'install') +PYPREFIX = path.join(APPDIR, 'Contents/Frameworks/Python.framework/Versions/2.7') +PYSITEPKGS = path.join(PYPREFIX, 'lib/python2.7/site-packages') +MAKEFLAGS = '-j4' + +QTBUILTFLAG = path.join(UNPACKDIR, 'qt/qt_install_success.txt') + +#pypath_txt_template=""" PYTHON_INCLUDE=%s/include/python2.7/ PYTHON_LIB=%s/lib/python2.7/config/libpython2.7.a PYVER=python2.7 """ +pypathData = 'PYTHON_INCLUDE=%s/include/python2.7/' % PYPREFIX +pypathData += '\nPYTHON_LIB=%s/lib/python2.7/config/libpython2.7.a' % PYPREFIX +pypathData += '\nPYTHON_LIB_DIR=%s/lib/python2.7/config/' % PYPREFIX +pypathData += '\nPYVER=python2.7' + +# If no arguments specified, then do the minimal amount of work necessary +# Assume that only one flag is specified. These should be +parser = optparse.OptionParser(usage="%prog [options]\n") +parser.add_option('--fromscratch', dest='fromscratch', default=False, action='store_true', help='Remove all prev-downloaded: redownload and rebuild all') +parser.add_option('--rebuildall', dest='rebuildall', default=False, action='store_true', help='Remove all prev-built; no redownload, only rebuild') +parser.add_option('--qtcheckout', dest='qtcheckout', default='4.8', type='str', help='Specify commit to checkout, after a pull') +parser.add_option('--qtupdate', dest='qtupdate', default=False, action='store_true', help='Rebuild only qt libraries, if already built') +parser.add_option('--qtrebuild', dest='qtrebuild', default=False, action='store_true', help='Rebuild only qt libraries, if already built') +parser.add_option('--precompiledQt',dest='precompiledQt',default=False, action='store_true', help='Download and use a precompiled version of Qt') +(CLIOPTS, CLIARGS) = parser.parse_args() + +################################################################################ +# Now actually start the download&build process + +# Make sure all the dirs exist +def main(): + + if path.exists(LOGFILE): + os.remove(LOGFILE) + + delete_prev_data(CLIOPTS) + + makedir(WORKDIR) + makedir(DLDIR) + makedir(UNPACKDIR) + makedir(INSTALLDIR) + + # For git repos, the "ID" is branch name. Otherwise, its' the md5sum + for pkgname, fname, url, ID in distfiles: + # Skip download Qt-git if downloading Qt, and vice versa + logprint('\n\n') + if((pkgname.lower()=='qt-git' and CLIOPTS.precompiledQt) or \ + (pkgname.lower()=='qt' and not CLIOPTS.precompiledQt) ): + continue + downloadPkg(pkgname, fname, url, ID) + + logprint("\n\nALL DOWNLOADS COMPLETED.\n\n") + + make_empty_app() + compile_python() + compile_pip() + install_libpng() + install_qt() + compile_sip() + compile_pyqt() + compile_twisted() + compile_psutil() + #unzip_swig() + compile_armory() + make_resources() + cleanup_app() + # Force Finder to update the Icon + execAndWait("touch " + APPDIR) + make_targz() + +################################################################################ +# Write the string to both console and log file +def logprint(s): + print s + with open(LOGFILE,'a') as f: + f.write(s if s.endswith('\n') else s+'\n') + +################################################################################ +""" +# While this worked well to capture the output, buffering made it impossible +# to write the data to stdout in real time (it woud all buffer up to the end +def execAndWait(syscmd, cwd=None): + try: + logprint('*'*80) + logprint('Executing: "%s"' % syscmd) + proc = Popen(syscmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, shell=True, cwd=cwd) + while proc.poll() == None: + time.sleep(0.25) + + except Exception as e: + logprint('\n' + '-'*80) + logprint('ERROR: %s' % str(e)) + logprint('-'*80 + '\n') + finally: + out,err = proc.communicate() + logprint('STDOUT:') + logprint(out if len(out.strip()) > 0 else "") + logprint('STDERR:') + logprint(err if len(err.strip()) > 0 else "") + logprint('*'*80) + return [out,err] +""" + +################################################################################ +def getRightNowStr(): + dateFmt = '%Y-%b-%d %I:%M%p' + dtobj = datetime.datetime.fromtimestamp(time.time()) + dtstr = u'' + dtobj.strftime(dateFmt).decode('utf-8') + return dtstr[:-2] + dtstr[-2:].lower() + +################################################################################ +def execAndWait(syscmd, cwd=None): + try: + syscmd += ' 2>&1 | tee -a %s' % LOGPATH + logprint('*'*80) + logprint(getRightNowStr()) + logprint('Executing: "%s"' % syscmd) + logprint('Executing from: "%s"' % (os.getcwd() if cwd is None else cwd)) + proc = Popen(syscmd, shell=True, cwd=cwd) + while proc.poll() == None: + time.sleep(0.25) + logprint('Finished executing: "%s"' % syscmd) + logprint('Finished executing from: "%s"' % (os.getcwd() if cwd is None else cwd)) + logprint('*'*80) + except Exception as e: + logprint('\n' + '-'*80) + logprint('ERROR: %s' % str(e)) + logprint('-'*80 + '\n') + +################################################################################ +def makedir(dirname): + if not path.isdir(dirname): + logprint( "Creating directory: %s" % dirname) + os.mkdir(dirname) + +################################################################################ +def movepath(pathname, newpath): + if path.exists(pathname): + logprint('Moving directory tree: \nFrom "%s"\nTo "%s"' % (pathname, newpath)) + shutil.move(pathname, newpath) + +################################################################################ +def removetree(pathname): + if path.exists(pathname): + logprint('Removing directory tree: %s' % pathname) + shutil.rmtree(pathname) + +################################################################################ +def removefile(pathname): + if path.exists(pathname): + logprint('Removing file: %s' % pathname) + os.remove(pathname) + +################################################################################ +def copytree(pathname, newpath): + if path.exists(pathname): + logprint('Copying directory tree: \nFrom "%s"\nTo "%s"' % (pathname, newpath)) + shutil.copytree(pathname, newpath) + +################################################################################ +def copyfile(src, dst): + logprint('Copying file: "%s" --> "%s"' % (src,dst)) + shutil.copy(src,dst) + +################################################################################ +def check_sha(filename, sha): + logprint("Checking %s" % filename) + check = hashlib.sha1() + with open(filename,'rb') as f: + check.update(f.read()) + digest = check.hexdigest() + + if sha is None: + logprint("SHA-1 =", digest) + return digest + elif sha==digest: + logprint("SHA-1 matches: OK") + return True + else: + logprint("SHA-1 does not match!") + return False + +################################################################################ +def getTarUnpackPath(tarName, inDir=None): + """ + NOTE: THIS FUNCTION IS NOT RELIABLE. It only works if the tar file would + extract a single directory with all contents in that dir. If it + unpacks more than one file or directory, the output will be incomplete. + """ + tarPath = tarName + if inDir is not None: + tarPath = path.join(inDir, tarName) + + tar = tarfile.open(tarPath,'r') + theDir = tar.next().name.split('/')[0] + tar.close() + return theDir + +################################################################################ +def unpack(tarName, fromDir=DLDIR, toDir=UNPACKDIR, overwrite=False): + """ + This is not a versatile function. It expects tar files with a single + unpack directory. I will expand this function as necessary if we + need tar files that aren't a single bundled dir. + """ + if fromDir is not None: + tardl = path.join(fromDir, tarName) + + if not path.exists(toDir): + os.mkdir(toDir) + + # Use tarfile module to pick out the base dir u + extractPath = getTarUnpackPath(tarName, fromDir) + extractPath = path.join(toDir, extractPath) + if path.exists(extractPath): + if overwrite: + removetree(path.join(toDir, extractPath)) + else: + return extractPath + + flistBefore = set(os.listdir(toDir)) + if tarName.endswith('tar.gz') or tarName.endswith('tgz'): + execAndWait('tar -zxf %s -C %s' % (tardl, toDir)) + elif tarName.endswith('tar.bz2') or tarName.endswith('tbz'): + execAndWait('tar -jxf %s -C %s' % (tardl, toDir)) + else: + raise RuntimeError('Not a recognized tar name') + newStuff = [] + for objPath in os.listdir(toDir): + if not objPath in flistBefore: + newStuff.append(path.join(toDir, objPath)) + + logprint('Unpacked: %s' % tardl) + for objPath in newStuff: + logprint(' ' + objPath) + + if len(newStuff) > 1: + logprint('*** ERROR: tarfile unpacked more than one object! ***' ) + + return newStuff[0] if len(newStuff)==1 else newStuff + +################################################################################ +def downloadPkg(pkgname, fname, url, ID, toDir=DLDIR): + myfile = path.join(toDir, fname) + isGitRepo = url.lower().startswith('git:') + doDL = True + if isGitRepo: + clonename = '.'.join(url.split('/')[-1].split('.')[:-1]) + clonedir = path.join(toDir, clonename) + branch = ID[:] # the ID value is actually the branch name + if path.exists(clonedir) or path.exists(path.join(DLDIR,fname)): + doDL=False + elif path.exists(myfile): + if check_sha(myfile, ID): + logprint("File already exists: %s" % myfile) + doDL = False + else: + removefile(myfile) + logprint("File exists but wrong hash. Redownload %s" % myfile) + + # Start the download if needed + if doDL: + if isGitRepo: + logprint('Cloning "%s" to "%s"' % (url, clonedir)) + execAndWait('git clone %s' % url, cwd=toDir) + execAndWait('git checkout %s' % ID) + else: + logprint('Downloading from %s' % url) + execAndWait('curl -OL "%s"' % url, cwd=toDir) + +# List of all files needed to download. Each item is +# (Name, filename, url, sha-1 or None) +distfiles = [] +distfiles.append( [ 'Python', \ + "Python-%s.tar.bz2" % pythonVer, \ + "http://python.org/ftp/python/%s/Python-%s.tar.bz2" % (pythonVer, pythonVer), \ + "6cfada1a739544a6fa7f2601b500fba02229656b" ] ) + +distfiles.append( [ 'setuptools', \ + "setuptools-%s.tar.gz" % setToolVer, \ + "https://pypi.python.org/packages/source/s/setuptools/setuptools-%s.tar.gz" % setToolVer, \ + "6a35e881a3aa2e06a1d5d4e966a9def296ec23e8" ] ) + +distfiles.append( [ 'Pip', \ + "pip-%s.tar.gz" % pipVer, \ + "https://pypi.python.org/packages/source/p/pip/pip-%s.tar.gz" % pipVer, \ + "4f43a6b04f83b8d83bee702750ff35be2a2b6af1" ] ) + +distfiles.append( [ "psutil", \ + "psutil-%s.tar.gz" % psutilVer, \ + "https://pypi.python.org/packages/source/p/psutil/psutil-%s.tar.gz" % psutilVer, \ + "c8c1842bf1c63b9068ac25a37f7aae11fcecd57f" ] ) + +distfiles.append( [ 'Twisted', \ + "Twisted-%s.tar.bz2" % twistedVer, \ + "https://pypi.python.org/packages/source/T/Twisted/Twisted-%s.tar.bz2" % twistedVer, \ + "e1d43645fd3d84dc2867f36b60d2e469a71eb01d" ] ) + +# Other lines rely on the given version. Patch this up later. +distfiles.append( [ 'libpng', \ + "libpng-%s.mavericks.bottle.tar.gz" % libpngVer, \ + "https://downloads.sf.net/project/machomebrew/Bottles/libpng-%s.mavericks.bottle.tar.gz" % libpngVer, \ + "666d5ba290d72b0cfa13366232eb0ffcc701d21f" ] ) + +#distfiles.append( [ "Qt", \ + #"qt-4.8.5.mountain_lion.bottle.2.tar.gz", \ + #"https://downloads.sf.net/project/machomebrew/Bottles/qt-4.8.5.mountain_lion.bottle.2.tar.gz", \ + #"b361f521d413409c0e4397f2fc597c965ca44e56" #] ) + +#distfiles.append( [ "Qt", \ +# "qt-everywhere-opensource-src-5.2.1.tar.gz", \ +# "http://download.qt-project.org/official_releases/qt/5.2/5.2.1/single/qt-everywhere-opensource-src-5.2.1.tar.gz", \ +# "31a5cf175bb94dbde3b52780d3be802cbeb19d65" ] ) + +# Pre-packaged source not used for now. Use Git instead. +distfiles.append( [ "Qt", \ + "qt-everywhere-opensource-src-%s.tar.gz" % qtVer, \ + "http://download.qt-project.org/official_releases/qt/4.8/%s/qt-everywhere-opensource-src-%s.tar.gz" % (qtVer, qtVer), \ + "745f9ebf091696c0d5403ce691dc28c039d77b9e" ] ) + +#distfiles.append( [ "Qt-git", \ +# "qt5_git_repo.tar.gz", \ +# 'git://gitorious.org/qt/qt5.git', +# 'stable' ] ) + +distfiles.append( [ "Qt-git", \ + "qt4_git_repo.tar.gz", \ + 'git://gitorious.org/qt/qt.git', + '4.8' ] ) + +distfiles.append( [ "Webkit-for-Qt", \ + "libWebKitSystemInterfaceMavericks.a", \ + "http://trac.webkit.org/export/162166/trunk/WebKitLibraries/libWebKitSystemInterfaceMavericks.a", \ + "bb071fb69cad0cec1f2ecb082ee34f44bd76ac93" ] ) + +#distfiles.append( [ "Qt-p1", \ + #"Ie9a72e3b.patch", \ + #'https://gist.github.com/cliffrowley/f526019bb3182c237836/raw/459bfcfe340baa306eed81720b0734f4bede94d7/Ie9a72e3b.patch', \ + #'829a8e9644c143be11c03ee7b2b9c8b042708b41' #] ) + +#distfiles.append( [ 'Qt-p2', \ + #'qt-4.8-libcpp.diff', \ + #'https://gist.github.com/cliffrowley/7380124/raw/ff6c681b282bbc1fe4a58e2f0c37905062ebd58b/qt-4.8-libcpp.diff', \ + #'4215caf4b8c84236f85093a1f0d24739a1c5ccfd' #] ) + +#distfiles.append( [ 'Qt-p3', \ + #'qt-4.8-libcpp-configure.diff', \ + #'https://gist.github.com/cliffrowley/93ce53b9dd8d8bb65530/raw/d3f6a6f039c5826df04fcfa56569394ea5069b9e/qt-4.8-libcpp-configure.diff', \ + #'a0b5189097937410113f22b78579c4eac940def9' #] ) + +distfiles.append( [ "sip", \ + "sip-%s.tar.gz" % sipVer, \ + "http://sourceforge.net/projects/pyqt/files/sip/sip-%s/sip-%s.tar.gz" % (sipVer, sipVer), \ + 'a5f6342dbb3cdc1fb61440ee8acb805f5fec3c41' ] ) + +# Other lines rely on the given version. Patch this up later. +distfiles.append( [ "pyqt", \ + "PyQt-mac-gpl-%s.tar.gz" % pyQtVer, \ + "http://downloads.sf.net/project/pyqt/PyQt4/PyQt-%s/PyQt-mac-gpl-%s.tar.gz" % (pyQtVer, pyQtVer), \ + 'ba5465f92fb43c9f0a5b948fa25df5045f160bf0' ] ) + +#distfiles.append( [ "pyqt", \ +# "PyQt-gpl-5.2.tar.gz", \ +# "http://downloads.sf.net/project/pyqt/PyQt5/PyQt-5.2/PyQt-gpl-5.2.tar.gz", \ +# 'a1c232d34ab268587c127ad3097c725ee1a70cf0' ] ) + +# May roll our own SWIG/PCRE someday. For now, assume the user already has SWIG. +#distfiles.append( [ 'swig', \ +# "swig-2.0.12.mavericks.bottle.tar.gz", \ +# "https://downloads.sf.net/project/machomebrew/Bottles/swig-2.0.12.mavericks.bottle.tar.gz", \ +# "5e429bc6228e8ec1f154ee5eb9497b54b8b765d5" ] ) + +# Now repack the information in distfiles +tarfilesToDL = {} +for d in distfiles: + tarfilesToDL[d[0]] = d[1] + +################################################################################ +def make_empty_app(): + 'Make the empty .app bundle structure' + makedir(APPDIR) + makedir(path.join(APPDIR,'Contents')) + makedir(path.join(APPDIR,'Contents/MacOS')) + makedir(path.join(APPDIR,'Contents/MacOS/py')) + makedir(path.join(APPDIR,'Contents/Frameworks')) + makedir(path.join(APPDIR,'Contents/Resources')) + makedir(path.join(APPDIR,'Contents/Dependencies')) + +################################################################################ +def compile_python(): + logprint('Installing python.') + bldPath = unpack(tarfilesToDL['Python']) + + # ./configure + frameDir = path.join(APPDIR, 'Contents/Frameworks') + execAndWait('./configure --enable-ipv6 --prefix=%s --enable-framework="%s"' % \ + (INSTALLDIR, frameDir), cwd=bldPath) + + # make + execAndWait('make %s' % MAKEFLAGS, cwd=bldPath) + pyexe = path.join(APPDIR, 'Contents/MacOS/Python') + + # make install + srcDir = path.join(INSTALLDIR, 'Build Applet.app/Contents/MacOS/Python') + dstDir = path.join(APPDIR, 'Contents/MacOS') + execAndWait('make install PYTHONAPPSDIR=%s' % INSTALLDIR, cwd=bldPath) + execAndWait('cp -p "%s" %s' % (srcDir, dstDir), cwd=bldPath) + + # Update $PATH var + newPath = path.join(PYPREFIX, 'bin') + os.environ['PATH'] = '%s:%s' % (newPath, os.environ['PATH']) + logprint('PATH is now %s' % os.environ['PATH']) + +################################################################################ +def compile_pip(): + logprint('Installing setuptools.') + pipexe = path.join(PYPREFIX, 'bin/pip') + if path.exists(pipexe): + logprint('Pip already installed') + else: + logprint('Installing pip.') + command = 'python -s setup.py --no-user-cfg install --force --verbose' + + # Unpack and build setuptools + setupDir = unpack(tarfilesToDL['setuptools']) + execAndWait(command, cwd=setupDir) + + # Unpack and build pip + pipDir = unpack(tarfilesToDL['Pip']) + execAndWait(command, cwd=pipDir) + +################################################################################ +def install_libpng(): + logprint('Installing libpng') + dylib = 'libpng16.16.dylib' + target = path.join(APPDIR, 'Contents/Dependencies', dylib) + if path.exists(target): + logprint('libpng already installed.') + else: + pngDir = unpack(tarfilesToDL['libpng']) + src = path.join(pngDir, '%s/lib' % libpngVer, dylib) + copyfile(src, target) + +################################################################################ +def compile_qt(): + logprint('Compiling Qt') + + # Already cloned to the qtDLDir, then tar it and move the dir to + # qtBuildDir. Then we will build inside the qtBuildDir, using qtInstDir + # as the prefix. + qtDLDir = path.join(DLDIR, 'qt') + qtBuildDir = path.join(UNPACKDIR, 'qt') + qtInstDir = path.join(INSTALLDIR, 'qt') + qtTarFile = path.join(DLDIR, 'qt4_git_repo.tar.gz') +# qtTarFile = path.join(DLDIR, 'qt5_git_repo.tar.gz') + + # If we did a fresh download, it's already uncompressed in DLDir. Move it + # to where it should be in the UNPACKDIR + if path.exists(qtDLDir): + if path.exists(qtBuildDir): + removetree(qtBuildDir) + movepath(qtDLDir, qtBuildDir) + + # If it's not in the bld dir, unpack it from the tar file + # If it's not in the tar file, either, we have a problem. + if not path.exists(qtBuildDir): + if not path.exists(qtTarFile): + raise RuntimeError('*** ERROR: No cloned repo and no tar file...? ***') + logprint('Unpacking qt repo from tarfile') + logprint('Remove qt4_git_repo.tar.gz to re-clone HEAD') +# logprint('Remove qt5_git_repo.tar.gz to re-clone HEAD') + gitdir = unpack(tarfilesToDL['Qt-git']) + elif not path.exists(qtTarFile): + logprint('Tarring downloaded repo for future use') + execAndWait('tar -zcf %s qt' % qtTarFile, cwd=UNPACKDIR) + + # Webkit-for-Qt is not a tar archive, it's actually just a single .a file + webkita = tarfilesToDL['Webkit-for-Qt'] + src = path.join(DLDIR, webkita) + dst = path.join(qtBuildDir, 'src/3rdparty/webkit/WebKitLibraries', webkita) + copyfile(src, dst) + #for patch in ['Qt-p1', 'Qt-p2', 'Qt-p3']: + # execAndWait('patch -p1 < ../../downloads/' + tarfilesToDL[patch], cwd=qtBuildDir) + #execAndWait('patch -p1 < ../../../qt-maverick-stability.patch', cwd=qtBuildDir) + + ##### Configure + command = './configure -prefix "%s" -system-zlib -confirm-license -opensource ' + command += '-nomake demos -nomake examples -nomake docs -cocoa -fast -release ' + command += '-no-qt3support -arch x86_64 -no-3dnow ' + command += '-platform unsupported/macx-clang' + execAndWait(command % qtInstDir, cwd=qtBuildDir) + + ##### Make + execAndWait('make %s' % MAKEFLAGS, cwd=qtBuildDir) + + ##### Make Install + # This will actually happen outside this function, since the INSTALLDIR + # Gets wiped every build + +################################################################################ +def install_qt(): + if CLIOPTS.precompiledQt: + logprint('Unpacking precompiled Qt.') + qtdir = unpack(tarfilesToDL['Qt']) + raise RuntimeError('Using precompiled Qt is not supported, yet') + else: + if not path.exists(QTBUILTFLAG): + compile_qt() + execAndWait('touch %s' % QTBUILTFLAG) + else: + logprint('QT already compiled. Skipping compile step.') + + # Even if it's already built, we'll always "make install" and then + # set a bunch of environment variables (INSTALLDIR is wiped on every + # Run of this script, so all "make install" steps need to be re-run + qtInstDir = path.join(INSTALLDIR, 'qt') + qtBinDir = path.join(qtInstDir, 'bin') + qtBuildDir = path.join(UNPACKDIR, 'qt') + + qtconf = path.join(qtBinDir, 'qt.conf') + execAndWait('make install', cwd=qtBuildDir) + + newcwd = path.join(APPDIR, 'Contents/Frameworks') + for mod in ['QtCore', 'QtGui', 'QtXml', 'QtNetwork']: + src = path.join(qtInstDir, 'lib', mod+'.framework') + dst = path.join(APPDIR, 'Contents/Frameworks', mod+'.framework') + if path.exists(dst): + removetree(dst) + copytree(src, dst) + + with open(qtconf, 'w') as f: + f.write('[Paths]\nPrefix = %s' % qtInstDir) + + try: + old = ':'+os.environ['DYLD_FRAMEWORK_PATH'] + except KeyError: + old = '' + + frmpath = path.join(APPDIR, 'Contents/Frameworks') + os.environ['PATH'] = '%s:%s' % (qtBinDir, os.environ['PATH']) + os.environ['DYLD_FRAMEWORK_PATH'] = '%s:%s' % (frmpath, old) + os.environ['QTDIR'] = qtInstDir + os.environ['QMAKESPEC'] = path.join(os.environ['QTDIR'], 'mkspecs/macx-g++') + logprint('All the following ENV vars are now set:') + for var in ['PATH','DYLD_FRAMEWORK_PATH', 'QTDIR', 'QMAKESPEC']: + logprint(' %s: \n %s' % (var, os.environ[var])) + +################################################################################ +def compile_sip(): + logprint('Installing sip') + if path.exists(path.join(PYSITEPKGS, 'sip.so')): + logprint('Sip is already installed.') + else: + #os.chdir('build/sip-4.14.6') + sipPath = unpack(tarfilesToDL['sip']) + command = 'python configure.py' + command += ' --destdir="%s"' % PYSITEPKGS + command += ' --bindir="%s/bin"' % PYPREFIX + command += ' --incdir="%s/include"' % PYPREFIX + command += ' --sipdir="%s/share/sip"' % PYPREFIX + execAndWait(command, cwd=sipPath) + execAndWait('make', cwd=sipPath) + + # Must run "make install" again even if it was previously built (since + # the APPDIR and INSTALLDIR are wiped every time the script is run) + execAndWait('make install', cwd=sipPath) + +################################################################################ +def compile_pyqt(): + logprint('Install PyQt4') +# logprint('Install PyQt5') +# if path.exists(path.join(PYSITEPKGS, 'PyQt5')): + if path.exists(path.join(PYSITEPKGS, 'PyQt4')): + logprint('Pyqt is already installed.') + else: + pyqtPath = unpack(tarfilesToDL['pyqt']) + incDir = path.join(PYPREFIX, 'include') + execAndWait('python ./configure-ng.py --confirm-license --sip-incdir="%s"' % incDir, cwd=pyqtPath) + execAndWait('make %s' % MAKEFLAGS, cwd=pyqtPath) + + # Need to add pyrcc4 to the PATH + execAndWait('make install', cwd=pyqtPath) + pyrccPath = path.join(UNPACKDIR, 'PyQt-mac-gpl-%s/pyrcc' % pyQtVer) + os.environ['PATH'] = '%s:%s' % (pyrccPath, os.environ['PATH']) + +################################################################################ +''' +def pip_install(package, lookfor): + """Install package with pip. + + For some reason this appears to be broken. Pip installs psutil in /usr/local instead of + inside the app. Strange. Do not use! + """ + if path.exists(path.join(PYSITEPKGS, lookfor)): + print package, "already installed" + else: + print "Installing %s using pip." % (package,) + execAndWait("pip install %s > pip-%s.log 2>&1" % (package, package)) +''' + +################################################################################ +def compile_twisted(): + logprint('Installing python-twisted') + + if glob.glob(PYSITEPKGS + '/Twisted*'): + logprint('Twisted already installed') + else: + command = "python -s setup.py --no-user-cfg install --force --verbose" + twpath = unpack(tarfilesToDL['Twisted']) + execAndWait(command, cwd=twpath) + +################################################################################ +def compile_psutil(): + logprint('Installing psutil') + + if glob.glob(PYSITEPKGS + '/psutil*'): + logprint('Psutil already installed') + else: + command = 'python -s setup.py --no-user-cfg install --force --verbose' + psPath = unpack(tarfilesToDL['psutil']) + execAndWait(command, cwd=psPath) + +################################################################################ +def compile_armory(): + logprint('Compiling and installing Armory') + # Always compile - even if already in app + #os.chdir('..') # Leave workspace directory. + pypathpath = path.join(ARMORYDIR, 'cppForSwig/pypaths.txt') + logprint('Writing ' + pypathpath) + with open(pypathpath, 'w') as f: + f.write(pypathData) + + appscript = path.join(APPDIR, 'Contents/MacOS/Armory') + pydir = path.join(APPDIR, 'Contents/MacOS/py') + execAndWait('make all', cwd='..') + execAndWait('make DESTDIR="%s" install' % pydir, cwd='..') + copyfile('Armory-script.sh', appscript) + execAndWait('chmod +x "%s"' % appscript) + +################################################################################ +def make_resources(): + "Populate the Resources folder." + cont = path.join(APPDIR, 'Contents') + copyfile('Info.plist', cont) + + icnsArm = '../img/armory_icon_fullres.icns' + icnsRes = path.join(cont, 'Resources/Icon.icns') + copyfile(icnsArm, icnsRes) + +################################################################################ +#def unzip_swig(): +# '''Unzip the SWIG binary.''' +# logprint('Unzipping the SWIG binary.') +# swigDir = unpack(tarfilesToDL['swig']) + +################################################################################ +def cleanup_app(): + "Try to remove as much unnecessary junk as possible." + show_app_size() + print "Removing Python test-suite." + testdir = path.join(PYPREFIX, "lib/python2.7/test") + if path.exists(testdir): + removetree(testdir) + print "Removing .pyo and unneeded .py files." + remove_python_files(PYPREFIX) + remove_python_files(path.join(APPDIR, 'Contents/MacOS/py')) + show_app_size() + +################################################################################ +def make_targz(): + ver = getVersionStr() + execAndWait('tar -zcf ../armory_%s_osx.tar.gz Armory.app' % ver, cwd=WORKDIR) + +################################################################################ +def getVersionStr(): + with open('../armoryengine/ArmoryUtils.py') as f: + for line in f.readlines(): + if line.startswith('BTCARMORY_VERSION'): + vstr = line[line.index('(')+1:line.index(')')] + vquad = tuple([int(v) for v in vstr.replace(' ','').split(',')]) + print vquad, len(vquad) + vstr = '%d.%02d' % vquad[:2] + if (vquad[2] > 0 or vquad[3] > 0): + vstr += '.%d' % vquad[2] + if vquad[3] > 0: + vstr += '.%d' % vquad[3] + return vstr + + +################################################################################ +def show_app_size(): + "Show the size of the app." + logprint("Size of application: ") + sys.stdout.flush() + execAndWait('du -hs "%s"' % APPDIR) + +################################################################################ +def remove_python_files(top): + "Remove .pyo files and any .py files where the .pyc file exists." + n_pyo = 0 + n_py_rem = 0 + n_py_kept = 0 + for (dirname, dirs, files) in os.walk(top): + for f in files: + prename, ext = path.splitext(f) + if ext == '.pyo': + removefile(path.join(dirname, f)) + n_pyo += 1 + elif ext == '.py': + if (f + 'c') in files: + removefile(path.join(dirname, f)) + n_py_rem += 1 + else: + n_py_kept += 1 + logprint("Removes %i .py files (kept %i)." % (n_py_rem, n_py_kept)) + +################################################################################ +def delete_prev_data(opts): + # If we ran this before, we should have a qt dir here + prevQtDir = path.join(UNPACKDIR, 'qt') + + def resetQtRepo(): + if path.exists(prevQtDir): + makefile = path.join(prevQtDir, 'Makefile') + if path.exists(makefile): + execAndWait('make clean', cwd=prevQtDir) + removefile(makefile) + removefile(QTBUILTFLAG) + + # Always remove previously-built application files + removetree(APPDIR) + removetree(INSTALLDIR) + + # When building from scratch + if opts.fromscratch: + removetree(UNPACKDIR) # Clear all unpacked tar files + removetree(DLDIR) # Clear even the downloaded files + elif opts.rebuildall: + removetree(UNPACKDIR) + elif not opts.qtcheckout == '4.8': + resetQtRepo() + execAndWait('git pull') + execAndWait('git checkout %s' % opts.qtcheckout) + elif opts.qtupdate: + resetQtRepo() + execAndWait('git pull') + elif opts.qtrebuild: + resetQtRepo() + else: + logprint('Using all packages previously downloaded and built') + +################################################################################ +if __name__ == "__main__": + main() diff --git a/osx_picobit/osx_build_notes.txt b/osxbuild/osx_build_notes.txt similarity index 59% rename from osx_picobit/osx_build_notes.txt rename to osxbuild/osx_build_notes.txt index 53073cfed..ee5c9bd5b 100644 --- a/osx_picobit/osx_build_notes.txt +++ b/osxbuild/osx_build_notes.txt @@ -1,6 +1,7 @@ -This directory was prepared by forum user, "picobit", and reviewed -by the ATI team. These notes describe what had to be done on a fresh -install of OSX 10.9 in order to get build-app.py working. +This directory was prepared by forum user, "picobit", and reviewed by the ATI +team. These notes describe what had to be done on fresh installs of OS X +10.7 (Lion), 10.8 (Mountain Lion) and 10.9 (Mavericks) in order to get +build-app.py working. (1) Install xcode from the Apple store (free) @@ -14,7 +15,7 @@ install of OSX 10.9 in order to get build-app.py working. (https://gist.github.com/WyseNynja/4200620), execute the following to install and update brew: - $ ruby -e "$(curl -fsSkL raw.github.com/mxcl/homebrew/go)" + $ ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)" $ touch ~/.bashrc $ echo "export CFLAGS=\"-arch x86_64\"" >> ~/.bashrc $ echo "export ARCHFLAGS=\"-arch x86_64\"" >> ~/.bashrc @@ -24,8 +25,7 @@ install of OSX 10.9 in order to get build-app.py working. (5) Execute "brew install swig" -(6) Open cppForSwig/Makefile and comment out the DO_EXEC_WHEREISPY - line (around line 20). I tried to branch around it, but it - didn't work for some reason. - -(7) "cd" into the osx_picobit directory, and "python build-app.py" +(6) "cd" into the "osxbuild" directory, and execute "python build-app.py". + Armory will be found under the "workspace" subdirectory. Armory.app can be + moved elsewhere on the system, including under /Applications so that it's + accessible by the OS X Launchpad. diff --git a/osxbuild/Armory b/osxbuild_old_higuys/Armory similarity index 100% rename from osxbuild/Armory rename to osxbuild_old_higuys/Armory diff --git a/osx_picobit/Info.plist b/osxbuild_old_higuys/Info.plist old mode 100755 new mode 100644 similarity index 81% rename from osx_picobit/Info.plist rename to osxbuild_old_higuys/Info.plist index 21b7db4ed..3b86c9769 --- a/osx_picobit/Info.plist +++ b/osxbuild_old_higuys/Info.plist @@ -5,8 +5,8 @@ CFBundleDisplayName Armory Bitcoin Client CFBundleExecutable Armory CFBundleIdentifier com.armory.armory - CFBundleIconFile Icon.icns - CFBundleInfoDictionaryVersion 0.89.99.3 + CFBundleIconFile Icon + CFBundleInfoDictionaryVersion 0.87.2 CFBundleName Armory Bitcoin Client NSPrincipalClass NSApplication LSMinimumSystemVersion 10.6.0 diff --git a/osxbuild/deploy.sh b/osxbuild_old_higuys/deploy.sh similarity index 100% rename from osxbuild/deploy.sh rename to osxbuild_old_higuys/deploy.sh diff --git a/qrcodenative.py b/qrcodenative.py index 05f0b0ea8..08892f0d2 100644 --- a/qrcodenative.py +++ b/qrcodenative.py @@ -1,981 +1,981 @@ -import math - -#QRCode for Python -# -#Ported from the Javascript library by Sam Curren -# -#QRCode for Javascript -#http://d-project.googlecode.com/svn/trunk/misc/qrcode/js/qrcode.js -# -#Copyright (c) 2009 Kazuhiko Arase -# -#URL: http://www.d-project.com/ -# -#Licensed under the MIT license: -# http://www.opensource.org/licenses/mit-license.php -# -# The word "QR Code" is registered trademark of -# DENSO WAVE INCORPORATED -# http://www.denso-wave.com/qrcode/faqpatent-e.html - - -class QR8bitByte: - - def __init__(self, data): - self.mode = QRMode.MODE_8BIT_BYTE - self.data = data - - def getLength(self): - return len(self.data) - - def write(self, buffer): - for i in range(len(self.data)): - #// not JIS ... - buffer.put(ord(self.data[i]), 8) - def __repr__(self): - return self.data - -class QRCode: - def __init__(self, typeNumber, errorCorrectLevel): - self.typeNumber = typeNumber - self.errorCorrectLevel = errorCorrectLevel - self.modules = None - self.moduleCount = 0 - self.dataCache = None - self.dataList = [] - def addData(self, data): - newData = QR8bitByte(data) - self.dataList.append(newData) - self.dataCache = None - def isDark(self, row, col): - if (row < 0 or self.moduleCount <= row or col < 0 or self.moduleCount <= col): - raise Exception("%s,%s - %s" % (row, col, self.moduleCount)) - return self.modules[row][col] - def getModuleCount(self): - return self.moduleCount - def make(self): - self.makeImpl(False, self.getBestMaskPattern() ) - def makeImpl(self, test, maskPattern): - - self.moduleCount = self.typeNumber * 4 + 17 - self.modules = [None for x in range(self.moduleCount)] - - for row in range(self.moduleCount): - - self.modules[row] = [None for x in range(self.moduleCount)] - - for col in range(self.moduleCount): - self.modules[row][col] = None #//(col + row) % 3; - - self.setupPositionProbePattern(0, 0) - self.setupPositionProbePattern(self.moduleCount - 7, 0) - self.setupPositionProbePattern(0, self.moduleCount - 7) - self.setupPositionAdjustPattern() - self.setupTimingPattern() - self.setupTypeInfo(test, maskPattern) - - if (self.typeNumber >= 7): - self.setupTypeNumber(test) - - if (self.dataCache == None): - self.dataCache = QRCode.createData(self.typeNumber, self.errorCorrectLevel, self.dataList) - self.mapData(self.dataCache, maskPattern) - - def setupPositionProbePattern(self, row, col): - - for r in range(-1, 8): - - if (row + r <= -1 or self.moduleCount <= row + r): continue - - for c in range(-1, 8): - - if (col + c <= -1 or self.moduleCount <= col + c): continue - - if ( (0 <= r and r <= 6 and (c == 0 or c == 6) ) - or (0 <= c and c <= 6 and (r == 0 or r == 6) ) - or (2 <= r and r <= 4 and 2 <= c and c <= 4) ): - self.modules[row + r][col + c] = True; - else: - self.modules[row + r][col + c] = False; - - def getBestMaskPattern(self): - - minLostPoint = 0 - pattern = 0 - - for i in range(8): - - self.makeImpl(True, i); - - lostPoint = QRUtil.getLostPoint(self); - - if (i == 0 or minLostPoint > lostPoint): - minLostPoint = lostPoint - pattern = i - - return pattern - - def createMovieClip(self): - raise Exception("Method not relevant to Python port") - - def makeImage(self): - pass - #boxsize = 10 #pixels per box - #offset = 4 #boxes as border - #pixelsize = (self.getModuleCount() + offset + offset) * boxsize - - #im = Image.new("RGB", (pixelsize, pixelsize), "white") - #d = ImageDraw.Draw(im) - - #for r in range(self.getModuleCount()): - #for c in range(self.getModuleCount()): - #if (self.isDark(r, c) ): - #x = (c + offset) * boxsize - #y = (r + offset) * boxsize - #b = [(x,y),(x+boxsize,y+boxsize)] - #d.rectangle(b,fill="black") - #del d - #return im - - def setupTimingPattern(self): - - for r in range(8, self.moduleCount - 8): - if (self.modules[r][6] != None): - continue - self.modules[r][6] = (r % 2 == 0) - - for c in range(8, self.moduleCount - 8): - if (self.modules[6][c] != None): - continue - self.modules[6][c] = (c % 2 == 0) - - def setupPositionAdjustPattern(self): - - pos = QRUtil.getPatternPosition(self.typeNumber) - - for i in range(len(pos)): - - for j in range(len(pos)): - - row = pos[i] - col = pos[j] - - if (self.modules[row][col] != None): - continue - - for r in range(-2, 3): - - for c in range(-2, 3): - - if (r == -2 or r == 2 or c == -2 or c == 2 or (r == 0 and c == 0) ): - self.modules[row + r][col + c] = True - else: - self.modules[row + r][col + c] = False - - def setupTypeNumber(self, test): - - bits = QRUtil.getBCHTypeNumber(self.typeNumber) - - for i in range(18): - mod = (not test and ( (bits >> i) & 1) == 1) - self.modules[i // 3][i % 3 + self.moduleCount - 8 - 3] = mod; - - for i in range(18): - mod = (not test and ( (bits >> i) & 1) == 1) - self.modules[i % 3 + self.moduleCount - 8 - 3][i // 3] = mod; - - def setupTypeInfo(self, test, maskPattern): - - data = (self.errorCorrectLevel << 3) | maskPattern - bits = QRUtil.getBCHTypeInfo(data) - - #// vertical - for i in range(15): - - mod = (not test and ( (bits >> i) & 1) == 1) - - if (i < 6): - self.modules[i][8] = mod - elif (i < 8): - self.modules[i + 1][8] = mod - else: - self.modules[self.moduleCount - 15 + i][8] = mod - - #// horizontal - for i in range(15): - - mod = (not test and ( (bits >> i) & 1) == 1); - - if (i < 8): - self.modules[8][self.moduleCount - i - 1] = mod - elif (i < 9): - self.modules[8][15 - i - 1 + 1] = mod - else: - self.modules[8][15 - i - 1] = mod - - #// fixed module - self.modules[self.moduleCount - 8][8] = (not test) - - def mapData(self, data, maskPattern): - - inc = -1 - row = self.moduleCount - 1 - bitIndex = 7 - byteIndex = 0 - - for col in range(self.moduleCount - 1, 0, -2): - - if (col == 6): col-=1 - - while (True): - - for c in range(2): - - if (self.modules[row][col - c] == None): - - dark = False - - if (byteIndex < len(data)): - dark = ( ( (data[byteIndex] >> bitIndex) & 1) == 1) - - mask = QRUtil.getMask(maskPattern, row, col - c) - - if (mask): - dark = not dark - - self.modules[row][col - c] = dark - bitIndex-=1 - - if (bitIndex == -1): - byteIndex+=1 - bitIndex = 7 - - row += inc - - if (row < 0 or self.moduleCount <= row): - row -= inc - inc = -inc - break - PAD0 = 0xEC - PAD1 = 0x11 - - @staticmethod - def createData(typeNumber, errorCorrectLevel, dataList): - - rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel) - - buffer = QRBitBuffer(); - - for i in range(len(dataList)): - data = dataList[i] - buffer.put(data.mode, 4) - buffer.put(data.getLength(), QRUtil.getLengthInBits(data.mode, typeNumber) ) - data.write(buffer) - - #// calc num max data. - totalDataCount = 0; - for i in range(len(rsBlocks)): - totalDataCount += rsBlocks[i].dataCount - - if (buffer.getLengthInBits() > totalDataCount * 8): - raise Exception("code length overflow. (" - + buffer.getLengthInBits() - + ">" - + totalDataCount * 8 - + ")") - - #// end code - if (buffer.getLengthInBits() + 4 <= totalDataCount * 8): - buffer.put(0, 4) - - #// padding - while (buffer.getLengthInBits() % 8 != 0): - buffer.putBit(False) - - #// padding - while (True): - - if (buffer.getLengthInBits() >= totalDataCount * 8): - break - buffer.put(QRCode.PAD0, 8) - - if (buffer.getLengthInBits() >= totalDataCount * 8): - break - buffer.put(QRCode.PAD1, 8) - - return QRCode.createBytes(buffer, rsBlocks) - - @staticmethod - def createBytes(buffer, rsBlocks): - - offset = 0 - - maxDcCount = 0 - maxEcCount = 0 - - dcdata = [0 for x in range(len(rsBlocks))] - ecdata = [0 for x in range(len(rsBlocks))] - - for r in range(len(rsBlocks)): - - dcCount = rsBlocks[r].dataCount - ecCount = rsBlocks[r].totalCount - dcCount - - maxDcCount = max(maxDcCount, dcCount) - maxEcCount = max(maxEcCount, ecCount) - - dcdata[r] = [0 for x in range(dcCount)] - - for i in range(len(dcdata[r])): - dcdata[r][i] = 0xff & buffer.buffer[i + offset] - offset += dcCount - - rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount) - rawPoly = QRPolynomial(dcdata[r], rsPoly.getLength() - 1) - - modPoly = rawPoly.mod(rsPoly) - ecdata[r] = [0 for x in range(rsPoly.getLength()-1)] - for i in range(len(ecdata[r])): - modIndex = i + modPoly.getLength() - len(ecdata[r]) - if (modIndex >= 0): - ecdata[r][i] = modPoly.get(modIndex) - else: - ecdata[r][i] = 0 - - totalCodeCount = 0 - for i in range(len(rsBlocks)): - totalCodeCount += rsBlocks[i].totalCount - - data = [None for x in range(totalCodeCount)] - index = 0 - - for i in range(maxDcCount): - for r in range(len(rsBlocks)): - if (i < len(dcdata[r])): - data[index] = dcdata[r][i] - index+=1 - - for i in range(maxEcCount): - for r in range(len(rsBlocks)): - if (i < len(ecdata[r])): - data[index] = ecdata[r][i] - index+=1 - - return data - - -class QRMode: - MODE_NUMBER = 1 << 0 - MODE_ALPHA_NUM = 1 << 1 - MODE_8BIT_BYTE = 1 << 2 - MODE_KANJI = 1 << 3 - -class QRErrorCorrectLevel: - L = 1 - M = 0 - Q = 3 - H = 2 - -class QRMaskPattern: - PATTERN000 = 0 - PATTERN001 = 1 - PATTERN010 = 2 - PATTERN011 = 3 - PATTERN100 = 4 - PATTERN101 = 5 - PATTERN110 = 6 - PATTERN111 = 7 - -class QRUtil(object): - PATTERN_POSITION_TABLE = [ - [], - [6, 18], - [6, 22], - [6, 26], - [6, 30], - [6, 34], - [6, 22, 38], - [6, 24, 42], - [6, 26, 46], - [6, 28, 50], - [6, 30, 54], - [6, 32, 58], - [6, 34, 62], - [6, 26, 46, 66], - [6, 26, 48, 70], - [6, 26, 50, 74], - [6, 30, 54, 78], - [6, 30, 56, 82], - [6, 30, 58, 86], - [6, 34, 62, 90], - [6, 28, 50, 72, 94], - [6, 26, 50, 74, 98], - [6, 30, 54, 78, 102], - [6, 28, 54, 80, 106], - [6, 32, 58, 84, 110], - [6, 30, 58, 86, 114], - [6, 34, 62, 90, 118], - [6, 26, 50, 74, 98, 122], - [6, 30, 54, 78, 102, 126], - [6, 26, 52, 78, 104, 130], - [6, 30, 56, 82, 108, 134], - [6, 34, 60, 86, 112, 138], - [6, 30, 58, 86, 114, 142], - [6, 34, 62, 90, 118, 146], - [6, 30, 54, 78, 102, 126, 150], - [6, 24, 50, 76, 102, 128, 154], - [6, 28, 54, 80, 106, 132, 158], - [6, 32, 58, 84, 110, 136, 162], - [6, 26, 54, 82, 110, 138, 166], - [6, 30, 58, 86, 114, 142, 170] - ] - - G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0) - G18 = (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0) - G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1) - - @staticmethod - def getBCHTypeInfo(data): - d = data << 10; - while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) >= 0): - d ^= (QRUtil.G15 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) ) ) - - return ( (data << 10) | d) ^ QRUtil.G15_MASK - @staticmethod - def getBCHTypeNumber(data): - d = data << 12; - while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) >= 0): - d ^= (QRUtil.G18 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) ) ) - return (data << 12) | d - @staticmethod - def getBCHDigit(data): - digit = 0; - while (data != 0): - digit += 1 - data >>= 1 - return digit - @staticmethod - def getPatternPosition(typeNumber): - return QRUtil.PATTERN_POSITION_TABLE[typeNumber - 1] - @staticmethod - def getMask(maskPattern, i, j): - if maskPattern == QRMaskPattern.PATTERN000 : return (i + j) % 2 == 0 - if maskPattern == QRMaskPattern.PATTERN001 : return i % 2 == 0 - if maskPattern == QRMaskPattern.PATTERN010 : return j % 3 == 0 - if maskPattern == QRMaskPattern.PATTERN011 : return (i + j) % 3 == 0 - if maskPattern == QRMaskPattern.PATTERN100 : return (math.floor(i / 2) + math.floor(j / 3) ) % 2 == 0 - if maskPattern == QRMaskPattern.PATTERN101 : return (i * j) % 2 + (i * j) % 3 == 0 - if maskPattern == QRMaskPattern.PATTERN110 : return ( (i * j) % 2 + (i * j) % 3) % 2 == 0 - if maskPattern == QRMaskPattern.PATTERN111 : return ( (i * j) % 3 + (i + j) % 2) % 2 == 0 - raise Exception("bad maskPattern:" + maskPattern); - @staticmethod - def getErrorCorrectPolynomial(errorCorrectLength): - a = QRPolynomial([1], 0); - for i in range(errorCorrectLength): - a = a.multiply(QRPolynomial([1, QRMath.gexp(i)], 0) ) - return a - @staticmethod - def getLengthInBits(mode, type): - - if 1 <= type and type < 10: - - #// 1 - 9 - - if mode == QRMode.MODE_NUMBER : return 10 - if mode == QRMode.MODE_ALPHA_NUM : return 9 - if mode == QRMode.MODE_8BIT_BYTE : return 8 - if mode == QRMode.MODE_KANJI : return 8 - raise Exception("mode:" + mode) - - elif (type < 27): - - #// 10 - 26 - - if mode == QRMode.MODE_NUMBER : return 12 - if mode == QRMode.MODE_ALPHA_NUM : return 11 - if mode == QRMode.MODE_8BIT_BYTE : return 16 - if mode == QRMode.MODE_KANJI : return 10 - raise Exception("mode:" + mode) - - elif (type < 41): - - #// 27 - 40 - - if mode == QRMode.MODE_NUMBER : return 14 - if mode == QRMode.MODE_ALPHA_NUM : return 13 - if mode == QRMode.MODE_8BIT_BYTE : return 16 - if mode == QRMode.MODE_KANJI : return 12 - raise Exception("mode:" + mode) - - else: - raise Exception("type:" + type) - @staticmethod - def getLostPoint(qrCode): - - moduleCount = qrCode.getModuleCount(); - - lostPoint = 0; - - #// LEVEL1 - - for row in range(moduleCount): - - for col in range(moduleCount): - - sameCount = 0; - dark = qrCode.isDark(row, col); - - for r in range(-1, 2): - - if (row + r < 0 or moduleCount <= row + r): - continue - - for c in range(-1, 2): - - if (col + c < 0 or moduleCount <= col + c): - continue - if (r == 0 and c == 0): - continue - - if (dark == qrCode.isDark(row + r, col + c) ): - sameCount+=1 - if (sameCount > 5): - lostPoint += (3 + sameCount - 5) - - #// LEVEL2 - - for row in range(moduleCount - 1): - for col in range(moduleCount - 1): - count = 0; - if (qrCode.isDark(row, col ) ): count+=1 - if (qrCode.isDark(row + 1, col ) ): count+=1 - if (qrCode.isDark(row, col + 1) ): count+=1 - if (qrCode.isDark(row + 1, col + 1) ): count+=1 - if (count == 0 or count == 4): - lostPoint += 3 - - #// LEVEL3 - - for row in range(moduleCount): - for col in range(moduleCount - 6): - if (qrCode.isDark(row, col) - and not qrCode.isDark(row, col + 1) - and qrCode.isDark(row, col + 2) - and qrCode.isDark(row, col + 3) - and qrCode.isDark(row, col + 4) - and not qrCode.isDark(row, col + 5) - and qrCode.isDark(row, col + 6) ): - lostPoint += 40 - - for col in range(moduleCount): - for row in range(moduleCount - 6): - if (qrCode.isDark(row, col) - and not qrCode.isDark(row + 1, col) - and qrCode.isDark(row + 2, col) - and qrCode.isDark(row + 3, col) - and qrCode.isDark(row + 4, col) - and not qrCode.isDark(row + 5, col) - and qrCode.isDark(row + 6, col) ): - lostPoint += 40 - - #// LEVEL4 - - darkCount = 0; - - for col in range(moduleCount): - for row in range(moduleCount): - if (qrCode.isDark(row, col) ): - darkCount+=1 - - ratio = abs(100 * darkCount / moduleCount / moduleCount - 50) / 5 - lostPoint += ratio * 10 - - return lostPoint - -class QRMath: - - @staticmethod - def glog(n): - if (n < 1): - raise Exception("glog(" + n + ")") - return LOG_TABLE[n]; - @staticmethod - def gexp(n): - while n < 0: - n += 255 - while n >= 256: - n -= 255 - return EXP_TABLE[n]; - -EXP_TABLE = [x for x in range(256)] - -LOG_TABLE = [x for x in range(256)] - -for i in range(8): - EXP_TABLE[i] = 1 << i; - -for i in range(8, 256): - EXP_TABLE[i] = EXP_TABLE[i - 4] ^ EXP_TABLE[i - 5] ^ EXP_TABLE[i - 6] ^ EXP_TABLE[i - 8] - -for i in range(255): - LOG_TABLE[EXP_TABLE[i] ] = i - -class QRPolynomial: - - def __init__(self, num, shift): - - if (len(num) == 0): - raise Exception(num.length + "/" + shift) - - offset = 0 - - while offset < len(num) and num[offset] == 0: - offset += 1 - - self.num = [0 for x in range(len(num)-offset+shift)] - for i in range(len(num) - offset): - self.num[i] = num[i + offset] - - - def get(self, index): - return self.num[index] - def getLength(self): - return len(self.num) - def multiply(self, e): - num = [0 for x in range(self.getLength() + e.getLength() - 1)]; - - for i in range(self.getLength()): - for j in range(e.getLength()): - num[i + j] ^= QRMath.gexp(QRMath.glog(self.get(i) ) + QRMath.glog(e.get(j) ) ) - - return QRPolynomial(num, 0); - def mod(self, e): - - if (self.getLength() - e.getLength() < 0): - return self; - - ratio = QRMath.glog(self.get(0) ) - QRMath.glog(e.get(0) ) - - num = [0 for x in range(self.getLength())] - - for i in range(self.getLength()): - num[i] = self.get(i); - - for i in range(e.getLength()): - num[i] ^= QRMath.gexp(QRMath.glog(e.get(i) ) + ratio) - - # recursive call - return QRPolynomial(num, 0).mod(e); - -class QRRSBlock: - - RS_BLOCK_TABLE = [ - - #// L - #// M - #// Q - #// H - - #// 1 - [1, 26, 19], - [1, 26, 16], - [1, 26, 13], - [1, 26, 9], - - #// 2 - [1, 44, 34], - [1, 44, 28], - [1, 44, 22], - [1, 44, 16], - - #// 3 - [1, 70, 55], - [1, 70, 44], - [2, 35, 17], - [2, 35, 13], - - #// 4 - [1, 100, 80], - [2, 50, 32], - [2, 50, 24], - [4, 25, 9], - - #// 5 - [1, 134, 108], - [2, 67, 43], - [2, 33, 15, 2, 34, 16], - [2, 33, 11, 2, 34, 12], - - #// 6 - [2, 86, 68], - [4, 43, 27], - [4, 43, 19], - [4, 43, 15], - - #// 7 - [2, 98, 78], - [4, 49, 31], - [2, 32, 14, 4, 33, 15], - [4, 39, 13, 1, 40, 14], - - #// 8 - [2, 121, 97], - [2, 60, 38, 2, 61, 39], - [4, 40, 18, 2, 41, 19], - [4, 40, 14, 2, 41, 15], - - #// 9 - [2, 146, 116], - [3, 58, 36, 2, 59, 37], - [4, 36, 16, 4, 37, 17], - [4, 36, 12, 4, 37, 13], - - #// 10 - [2, 86, 68, 2, 87, 69], - [4, 69, 43, 1, 70, 44], - [6, 43, 19, 2, 44, 20], - [6, 43, 15, 2, 44, 16], - - # 11 - [4, 101, 81], - [1, 80, 50, 4, 81, 51], - [4, 50, 22, 4, 51, 23], - [3, 36, 12, 8, 37, 13], - - # 12 - [2, 116, 92, 2, 117, 93], - [6, 58, 36, 2, 59, 37], - [4, 46, 20, 6, 47, 21], - [7, 42, 14, 4, 43, 15], - - # 13 - [4, 133, 107], - [8, 59, 37, 1, 60, 38], - [8, 44, 20, 4, 45, 21], - [12, 33, 11, 4, 34, 12], - - # 14 - [3, 145, 115, 1, 146, 116], - [4, 64, 40, 5, 65, 41], - [11, 36, 16, 5, 37, 17], - [11, 36, 12, 5, 37, 13], - - # 15 - [5, 109, 87, 1, 110, 88], - [5, 65, 41, 5, 66, 42], - [5, 54, 24, 7, 55, 25], - [11, 36, 12], - - # 16 - [5, 122, 98, 1, 123, 99], - [7, 73, 45, 3, 74, 46], - [15, 43, 19, 2, 44, 20], - [3, 45, 15, 13, 46, 16], - - # 17 - [1, 135, 107, 5, 136, 108], - [10, 74, 46, 1, 75, 47], - [1, 50, 22, 15, 51, 23], - [2, 42, 14, 17, 43, 15], - - # 18 - [5, 150, 120, 1, 151, 121], - [9, 69, 43, 4, 70, 44], - [17, 50, 22, 1, 51, 23], - [2, 42, 14, 19, 43, 15], - - # 19 - [3, 141, 113, 4, 142, 114], - [3, 70, 44, 11, 71, 45], - [17, 47, 21, 4, 48, 22], - [9, 39, 13, 16, 40, 14], - - # 20 - [3, 135, 107, 5, 136, 108], - [3, 67, 41, 13, 68, 42], - [15, 54, 24, 5, 55, 25], - [15, 43, 15, 10, 44, 16], - - # 21 - [4, 144, 116, 4, 145, 117], - [17, 68, 42], - [17, 50, 22, 6, 51, 23], - [19, 46, 16, 6, 47, 17], - - # 22 - [2, 139, 111, 7, 140, 112], - [17, 74, 46], - [7, 54, 24, 16, 55, 25], - [34, 37, 13], - - # 23 - [4, 151, 121, 5, 152, 122], - [4, 75, 47, 14, 76, 48], - [11, 54, 24, 14, 55, 25], - [16, 45, 15, 14, 46, 16], - - # 24 - [6, 147, 117, 4, 148, 118], - [6, 73, 45, 14, 74, 46], - [11, 54, 24, 16, 55, 25], - [30, 46, 16, 2, 47, 17], - - # 25 - [8, 132, 106, 4, 133, 107], - [8, 75, 47, 13, 76, 48], - [7, 54, 24, 22, 55, 25], - [22, 45, 15, 13, 46, 16], - - # 26 - [10, 142, 114, 2, 143, 115], - [19, 74, 46, 4, 75, 47], - [28, 50, 22, 6, 51, 23], - [33, 46, 16, 4, 47, 17], - - # 27 - [8, 152, 122, 4, 153, 123], - [22, 73, 45, 3, 74, 46], - [8, 53, 23, 26, 54, 24], - [12, 45, 15, 28, 46, 16], - - # 28 - [3, 147, 117, 10, 148, 118], - [3, 73, 45, 23, 74, 46], - [4, 54, 24, 31, 55, 25], - [11, 45, 15, 31, 46, 16], - - # 29 - [7, 146, 116, 7, 147, 117], - [21, 73, 45, 7, 74, 46], - [1, 53, 23, 37, 54, 24], - [19, 45, 15, 26, 46, 16], - - # 30 - [5, 145, 115, 10, 146, 116], - [19, 75, 47, 10, 76, 48], - [15, 54, 24, 25, 55, 25], - [23, 45, 15, 25, 46, 16], - - # 31 - [13, 145, 115, 3, 146, 116], - [2, 74, 46, 29, 75, 47], - [42, 54, 24, 1, 55, 25], - [23, 45, 15, 28, 46, 16], - - # 32 - [17, 145, 115], - [10, 74, 46, 23, 75, 47], - [10, 54, 24, 35, 55, 25], - [19, 45, 15, 35, 46, 16], - - # 33 - [17, 145, 115, 1, 146, 116], - [14, 74, 46, 21, 75, 47], - [29, 54, 24, 19, 55, 25], - [11, 45, 15, 46, 46, 16], - - # 34 - [13, 145, 115, 6, 146, 116], - [14, 74, 46, 23, 75, 47], - [44, 54, 24, 7, 55, 25], - [59, 46, 16, 1, 47, 17], - - # 35 - [12, 151, 121, 7, 152, 122], - [12, 75, 47, 26, 76, 48], - [39, 54, 24, 14, 55, 25], - [22, 45, 15, 41, 46, 16], - - # 36 - [6, 151, 121, 14, 152, 122], - [6, 75, 47, 34, 76, 48], - [46, 54, 24, 10, 55, 25], - [2, 45, 15, 64, 46, 16], - - # 37 - [17, 152, 122, 4, 153, 123], - [29, 74, 46, 14, 75, 47], - [49, 54, 24, 10, 55, 25], - [24, 45, 15, 46, 46, 16], - - # 38 - [4, 152, 122, 18, 153, 123], - [13, 74, 46, 32, 75, 47], - [48, 54, 24, 14, 55, 25], - [42, 45, 15, 32, 46, 16], - - # 39 - [20, 147, 117, 4, 148, 118], - [40, 75, 47, 7, 76, 48], - [43, 54, 24, 22, 55, 25], - [10, 45, 15, 67, 46, 16], - - # 40 - [19, 148, 118, 6, 149, 119], - [18, 75, 47, 31, 76, 48], - [34, 54, 24, 34, 55, 25], - [20, 45, 15, 61, 46, 16] - - ] - - def __init__(self, totalCount, dataCount): - self.totalCount = totalCount - self.dataCount = dataCount - - @staticmethod - def getRSBlocks(typeNumber, errorCorrectLevel): - rsBlock = QRRSBlock.getRsBlockTable(typeNumber, errorCorrectLevel); - if rsBlock == None: - raise Exception("bad rs block @ typeNumber:" + typeNumber + "/errorCorrectLevel:" + errorCorrectLevel) - - length = len(rsBlock) / 3 - - list = [] - - for i in range(length): - - count = rsBlock[i * 3 + 0] - totalCount = rsBlock[i * 3 + 1] - dataCount = rsBlock[i * 3 + 2] - - for j in range(count): - list.append(QRRSBlock(totalCount, dataCount)) - - return list; - - @staticmethod - def getRsBlockTable(typeNumber, errorCorrectLevel): - if errorCorrectLevel == QRErrorCorrectLevel.L: - return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0]; - elif errorCorrectLevel == QRErrorCorrectLevel.M: - return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1]; - elif errorCorrectLevel == QRErrorCorrectLevel.Q: - return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2]; - elif errorCorrectLevel == QRErrorCorrectLevel.H: - return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3]; - else: - return None; - -class QRBitBuffer: - def __init__(self): - self.buffer = [] - self.length = 0 - def __repr__(self): - return ".".join([str(n) for n in self.buffer]) - def get(self, index): - bufIndex = math.floor(index / 8) - val = ( (self.buffer[bufIndex] >> (7 - index % 8) ) & 1) == 1 - print "get ", val - return ( (self.buffer[bufIndex] >> (7 - index % 8) ) & 1) == 1 - def put(self, num, length): - for i in range(length): - self.putBit( ( (num >> (length - i - 1) ) & 1) == 1) - def getLengthInBits(self): - return self.length - def putBit(self, bit): - bufIndex = self.length // 8 - if len(self.buffer) <= bufIndex: - self.buffer.append(0) - if bit: - self.buffer[bufIndex] |= (0x80 >> (self.length % 8) ) - self.length+=1 +import math + +#QRCode for Python +# +#Ported from the Javascript library by Sam Curren +# +#QRCode for Javascript +#http://d-project.googlecode.com/svn/trunk/misc/qrcode/js/qrcode.js +# +#Copyright (c) 2009 Kazuhiko Arase +# +#URL: http://www.d-project.com/ +# +#Licensed under the MIT license: +# http://www.opensource.org/licenses/mit-license.php +# +# The word "QR Code" is registered trademark of +# DENSO WAVE INCORPORATED +# http://www.denso-wave.com/qrcode/faqpatent-e.html + + +class QR8bitByte: + + def __init__(self, data): + self.mode = QRMode.MODE_8BIT_BYTE + self.data = data + + def getLength(self): + return len(self.data) + + def write(self, buffer): + for i in range(len(self.data)): + #// not JIS ... + buffer.put(ord(self.data[i]), 8) + def __repr__(self): + return self.data + +class QRCode: + def __init__(self, typeNumber, errorCorrectLevel): + self.typeNumber = typeNumber + self.errorCorrectLevel = errorCorrectLevel + self.modules = None + self.moduleCount = 0 + self.dataCache = None + self.dataList = [] + def addData(self, data): + newData = QR8bitByte(data) + self.dataList.append(newData) + self.dataCache = None + def isDark(self, row, col): + if (row < 0 or self.moduleCount <= row or col < 0 or self.moduleCount <= col): + raise Exception("%s,%s - %s" % (row, col, self.moduleCount)) + return self.modules[row][col] + def getModuleCount(self): + return self.moduleCount + def make(self): + self.makeImpl(False, self.getBestMaskPattern() ) + def makeImpl(self, test, maskPattern): + + self.moduleCount = self.typeNumber * 4 + 17 + self.modules = [None for x in range(self.moduleCount)] + + for row in range(self.moduleCount): + + self.modules[row] = [None for x in range(self.moduleCount)] + + for col in range(self.moduleCount): + self.modules[row][col] = None #//(col + row) % 3; + + self.setupPositionProbePattern(0, 0) + self.setupPositionProbePattern(self.moduleCount - 7, 0) + self.setupPositionProbePattern(0, self.moduleCount - 7) + self.setupPositionAdjustPattern() + self.setupTimingPattern() + self.setupTypeInfo(test, maskPattern) + + if (self.typeNumber >= 7): + self.setupTypeNumber(test) + + if (self.dataCache == None): + self.dataCache = QRCode.createData(self.typeNumber, self.errorCorrectLevel, self.dataList) + self.mapData(self.dataCache, maskPattern) + + def setupPositionProbePattern(self, row, col): + + for r in range(-1, 8): + + if (row + r <= -1 or self.moduleCount <= row + r): continue + + for c in range(-1, 8): + + if (col + c <= -1 or self.moduleCount <= col + c): continue + + if ( (0 <= r and r <= 6 and (c == 0 or c == 6) ) + or (0 <= c and c <= 6 and (r == 0 or r == 6) ) + or (2 <= r and r <= 4 and 2 <= c and c <= 4) ): + self.modules[row + r][col + c] = True; + else: + self.modules[row + r][col + c] = False; + + def getBestMaskPattern(self): + + minLostPoint = 0 + pattern = 0 + + for i in range(8): + + self.makeImpl(True, i); + + lostPoint = QRUtil.getLostPoint(self); + + if (i == 0 or minLostPoint > lostPoint): + minLostPoint = lostPoint + pattern = i + + return pattern + + def createMovieClip(self): + raise Exception("Method not relevant to Python port") + + def makeImage(self): + pass + #boxsize = 10 #pixels per box + #offset = 4 #boxes as border + #pixelsize = (self.getModuleCount() + offset + offset) * boxsize + + #im = Image.new("RGB", (pixelsize, pixelsize), "white") + #d = ImageDraw.Draw(im) + + #for r in range(self.getModuleCount()): + #for c in range(self.getModuleCount()): + #if (self.isDark(r, c) ): + #x = (c + offset) * boxsize + #y = (r + offset) * boxsize + #b = [(x,y),(x+boxsize,y+boxsize)] + #d.rectangle(b,fill="black") + #del d + #return im + + def setupTimingPattern(self): + + for r in range(8, self.moduleCount - 8): + if (self.modules[r][6] != None): + continue + self.modules[r][6] = (r % 2 == 0) + + for c in range(8, self.moduleCount - 8): + if (self.modules[6][c] != None): + continue + self.modules[6][c] = (c % 2 == 0) + + def setupPositionAdjustPattern(self): + + pos = QRUtil.getPatternPosition(self.typeNumber) + + for i in range(len(pos)): + + for j in range(len(pos)): + + row = pos[i] + col = pos[j] + + if (self.modules[row][col] != None): + continue + + for r in range(-2, 3): + + for c in range(-2, 3): + + if (r == -2 or r == 2 or c == -2 or c == 2 or (r == 0 and c == 0) ): + self.modules[row + r][col + c] = True + else: + self.modules[row + r][col + c] = False + + def setupTypeNumber(self, test): + + bits = QRUtil.getBCHTypeNumber(self.typeNumber) + + for i in range(18): + mod = (not test and ( (bits >> i) & 1) == 1) + self.modules[i // 3][i % 3 + self.moduleCount - 8 - 3] = mod; + + for i in range(18): + mod = (not test and ( (bits >> i) & 1) == 1) + self.modules[i % 3 + self.moduleCount - 8 - 3][i // 3] = mod; + + def setupTypeInfo(self, test, maskPattern): + + data = (self.errorCorrectLevel << 3) | maskPattern + bits = QRUtil.getBCHTypeInfo(data) + + #// vertical + for i in range(15): + + mod = (not test and ( (bits >> i) & 1) == 1) + + if (i < 6): + self.modules[i][8] = mod + elif (i < 8): + self.modules[i + 1][8] = mod + else: + self.modules[self.moduleCount - 15 + i][8] = mod + + #// horizontal + for i in range(15): + + mod = (not test and ( (bits >> i) & 1) == 1); + + if (i < 8): + self.modules[8][self.moduleCount - i - 1] = mod + elif (i < 9): + self.modules[8][15 - i - 1 + 1] = mod + else: + self.modules[8][15 - i - 1] = mod + + #// fixed module + self.modules[self.moduleCount - 8][8] = (not test) + + def mapData(self, data, maskPattern): + + inc = -1 + row = self.moduleCount - 1 + bitIndex = 7 + byteIndex = 0 + + for col in range(self.moduleCount - 1, 0, -2): + + if (col == 6): col-=1 + + while (True): + + for c in range(2): + + if (self.modules[row][col - c] == None): + + dark = False + + if (byteIndex < len(data)): + dark = ( ( (data[byteIndex] >> bitIndex) & 1) == 1) + + mask = QRUtil.getMask(maskPattern, row, col - c) + + if (mask): + dark = not dark + + self.modules[row][col - c] = dark + bitIndex-=1 + + if (bitIndex == -1): + byteIndex+=1 + bitIndex = 7 + + row += inc + + if (row < 0 or self.moduleCount <= row): + row -= inc + inc = -inc + break + PAD0 = 0xEC + PAD1 = 0x11 + + @staticmethod + def createData(typeNumber, errorCorrectLevel, dataList): + + rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel) + + buffer = QRBitBuffer(); + + for i in range(len(dataList)): + data = dataList[i] + buffer.put(data.mode, 4) + buffer.put(data.getLength(), QRUtil.getLengthInBits(data.mode, typeNumber) ) + data.write(buffer) + + #// calc num max data. + totalDataCount = 0; + for i in range(len(rsBlocks)): + totalDataCount += rsBlocks[i].dataCount + + if (buffer.getLengthInBits() > totalDataCount * 8): + raise Exception("code length overflow. (" + + buffer.getLengthInBits() + + ">" + + totalDataCount * 8 + + ")") + + #// end code + if (buffer.getLengthInBits() + 4 <= totalDataCount * 8): + buffer.put(0, 4) + + #// padding + while (buffer.getLengthInBits() % 8 != 0): + buffer.putBit(False) + + #// padding + while (True): + + if (buffer.getLengthInBits() >= totalDataCount * 8): + break + buffer.put(QRCode.PAD0, 8) + + if (buffer.getLengthInBits() >= totalDataCount * 8): + break + buffer.put(QRCode.PAD1, 8) + + return QRCode.createBytes(buffer, rsBlocks) + + @staticmethod + def createBytes(buffer, rsBlocks): + + offset = 0 + + maxDcCount = 0 + maxEcCount = 0 + + dcdata = [0 for x in range(len(rsBlocks))] + ecdata = [0 for x in range(len(rsBlocks))] + + for r in range(len(rsBlocks)): + + dcCount = rsBlocks[r].dataCount + ecCount = rsBlocks[r].totalCount - dcCount + + maxDcCount = max(maxDcCount, dcCount) + maxEcCount = max(maxEcCount, ecCount) + + dcdata[r] = [0 for x in range(dcCount)] + + for i in range(len(dcdata[r])): + dcdata[r][i] = 0xff & buffer.buffer[i + offset] + offset += dcCount + + rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount) + rawPoly = QRPolynomial(dcdata[r], rsPoly.getLength() - 1) + + modPoly = rawPoly.mod(rsPoly) + ecdata[r] = [0 for x in range(rsPoly.getLength()-1)] + for i in range(len(ecdata[r])): + modIndex = i + modPoly.getLength() - len(ecdata[r]) + if (modIndex >= 0): + ecdata[r][i] = modPoly.get(modIndex) + else: + ecdata[r][i] = 0 + + totalCodeCount = 0 + for i in range(len(rsBlocks)): + totalCodeCount += rsBlocks[i].totalCount + + data = [None for x in range(totalCodeCount)] + index = 0 + + for i in range(maxDcCount): + for r in range(len(rsBlocks)): + if (i < len(dcdata[r])): + data[index] = dcdata[r][i] + index+=1 + + for i in range(maxEcCount): + for r in range(len(rsBlocks)): + if (i < len(ecdata[r])): + data[index] = ecdata[r][i] + index+=1 + + return data + + +class QRMode: + MODE_NUMBER = 1 << 0 + MODE_ALPHA_NUM = 1 << 1 + MODE_8BIT_BYTE = 1 << 2 + MODE_KANJI = 1 << 3 + +class QRErrorCorrectLevel: + L = 1 + M = 0 + Q = 3 + H = 2 + +class QRMaskPattern: + PATTERN000 = 0 + PATTERN001 = 1 + PATTERN010 = 2 + PATTERN011 = 3 + PATTERN100 = 4 + PATTERN101 = 5 + PATTERN110 = 6 + PATTERN111 = 7 + +class QRUtil(object): + PATTERN_POSITION_TABLE = [ + [], + [6, 18], + [6, 22], + [6, 26], + [6, 30], + [6, 34], + [6, 22, 38], + [6, 24, 42], + [6, 26, 46], + [6, 28, 50], + [6, 30, 54], + [6, 32, 58], + [6, 34, 62], + [6, 26, 46, 66], + [6, 26, 48, 70], + [6, 26, 50, 74], + [6, 30, 54, 78], + [6, 30, 56, 82], + [6, 30, 58, 86], + [6, 34, 62, 90], + [6, 28, 50, 72, 94], + [6, 26, 50, 74, 98], + [6, 30, 54, 78, 102], + [6, 28, 54, 80, 106], + [6, 32, 58, 84, 110], + [6, 30, 58, 86, 114], + [6, 34, 62, 90, 118], + [6, 26, 50, 74, 98, 122], + [6, 30, 54, 78, 102, 126], + [6, 26, 52, 78, 104, 130], + [6, 30, 56, 82, 108, 134], + [6, 34, 60, 86, 112, 138], + [6, 30, 58, 86, 114, 142], + [6, 34, 62, 90, 118, 146], + [6, 30, 54, 78, 102, 126, 150], + [6, 24, 50, 76, 102, 128, 154], + [6, 28, 54, 80, 106, 132, 158], + [6, 32, 58, 84, 110, 136, 162], + [6, 26, 54, 82, 110, 138, 166], + [6, 30, 58, 86, 114, 142, 170] + ] + + G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0) + G18 = (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0) + G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1) + + @staticmethod + def getBCHTypeInfo(data): + d = data << 10; + while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) >= 0): + d ^= (QRUtil.G15 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) ) ) + + return ( (data << 10) | d) ^ QRUtil.G15_MASK + @staticmethod + def getBCHTypeNumber(data): + d = data << 12; + while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) >= 0): + d ^= (QRUtil.G18 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) ) ) + return (data << 12) | d + @staticmethod + def getBCHDigit(data): + digit = 0; + while (data != 0): + digit += 1 + data >>= 1 + return digit + @staticmethod + def getPatternPosition(typeNumber): + return QRUtil.PATTERN_POSITION_TABLE[typeNumber - 1] + @staticmethod + def getMask(maskPattern, i, j): + if maskPattern == QRMaskPattern.PATTERN000 : return (i + j) % 2 == 0 + if maskPattern == QRMaskPattern.PATTERN001 : return i % 2 == 0 + if maskPattern == QRMaskPattern.PATTERN010 : return j % 3 == 0 + if maskPattern == QRMaskPattern.PATTERN011 : return (i + j) % 3 == 0 + if maskPattern == QRMaskPattern.PATTERN100 : return (math.floor(i / 2) + math.floor(j / 3) ) % 2 == 0 + if maskPattern == QRMaskPattern.PATTERN101 : return (i * j) % 2 + (i * j) % 3 == 0 + if maskPattern == QRMaskPattern.PATTERN110 : return ( (i * j) % 2 + (i * j) % 3) % 2 == 0 + if maskPattern == QRMaskPattern.PATTERN111 : return ( (i * j) % 3 + (i + j) % 2) % 2 == 0 + raise Exception("bad maskPattern:" + maskPattern); + @staticmethod + def getErrorCorrectPolynomial(errorCorrectLength): + a = QRPolynomial([1], 0); + for i in range(errorCorrectLength): + a = a.multiply(QRPolynomial([1, QRMath.gexp(i)], 0) ) + return a + @staticmethod + def getLengthInBits(mode, type): + + if 1 <= type and type < 10: + + #// 1 - 9 + + if mode == QRMode.MODE_NUMBER : return 10 + if mode == QRMode.MODE_ALPHA_NUM : return 9 + if mode == QRMode.MODE_8BIT_BYTE : return 8 + if mode == QRMode.MODE_KANJI : return 8 + raise Exception("mode:" + mode) + + elif (type < 27): + + #// 10 - 26 + + if mode == QRMode.MODE_NUMBER : return 12 + if mode == QRMode.MODE_ALPHA_NUM : return 11 + if mode == QRMode.MODE_8BIT_BYTE : return 16 + if mode == QRMode.MODE_KANJI : return 10 + raise Exception("mode:" + mode) + + elif (type < 41): + + #// 27 - 40 + + if mode == QRMode.MODE_NUMBER : return 14 + if mode == QRMode.MODE_ALPHA_NUM : return 13 + if mode == QRMode.MODE_8BIT_BYTE : return 16 + if mode == QRMode.MODE_KANJI : return 12 + raise Exception("mode:" + mode) + + else: + raise Exception("type:" + type) + @staticmethod + def getLostPoint(qrCode): + + moduleCount = qrCode.getModuleCount(); + + lostPoint = 0; + + #// LEVEL1 + + for row in range(moduleCount): + + for col in range(moduleCount): + + sameCount = 0; + dark = qrCode.isDark(row, col); + + for r in range(-1, 2): + + if (row + r < 0 or moduleCount <= row + r): + continue + + for c in range(-1, 2): + + if (col + c < 0 or moduleCount <= col + c): + continue + if (r == 0 and c == 0): + continue + + if (dark == qrCode.isDark(row + r, col + c) ): + sameCount+=1 + if (sameCount > 5): + lostPoint += (3 + sameCount - 5) + + #// LEVEL2 + + for row in range(moduleCount - 1): + for col in range(moduleCount - 1): + count = 0; + if (qrCode.isDark(row, col ) ): count+=1 + if (qrCode.isDark(row + 1, col ) ): count+=1 + if (qrCode.isDark(row, col + 1) ): count+=1 + if (qrCode.isDark(row + 1, col + 1) ): count+=1 + if (count == 0 or count == 4): + lostPoint += 3 + + #// LEVEL3 + + for row in range(moduleCount): + for col in range(moduleCount - 6): + if (qrCode.isDark(row, col) + and not qrCode.isDark(row, col + 1) + and qrCode.isDark(row, col + 2) + and qrCode.isDark(row, col + 3) + and qrCode.isDark(row, col + 4) + and not qrCode.isDark(row, col + 5) + and qrCode.isDark(row, col + 6) ): + lostPoint += 40 + + for col in range(moduleCount): + for row in range(moduleCount - 6): + if (qrCode.isDark(row, col) + and not qrCode.isDark(row + 1, col) + and qrCode.isDark(row + 2, col) + and qrCode.isDark(row + 3, col) + and qrCode.isDark(row + 4, col) + and not qrCode.isDark(row + 5, col) + and qrCode.isDark(row + 6, col) ): + lostPoint += 40 + + #// LEVEL4 + + darkCount = 0; + + for col in range(moduleCount): + for row in range(moduleCount): + if (qrCode.isDark(row, col) ): + darkCount+=1 + + ratio = abs(100 * darkCount / moduleCount / moduleCount - 50) / 5 + lostPoint += ratio * 10 + + return lostPoint + +class QRMath: + + @staticmethod + def glog(n): + if (n < 1): + raise Exception("glog(" + n + ")") + return LOG_TABLE[n]; + @staticmethod + def gexp(n): + while n < 0: + n += 255 + while n >= 256: + n -= 255 + return EXP_TABLE[n]; + +EXP_TABLE = [x for x in range(256)] + +LOG_TABLE = [x for x in range(256)] + +for i in range(8): + EXP_TABLE[i] = 1 << i; + +for i in range(8, 256): + EXP_TABLE[i] = EXP_TABLE[i - 4] ^ EXP_TABLE[i - 5] ^ EXP_TABLE[i - 6] ^ EXP_TABLE[i - 8] + +for i in range(255): + LOG_TABLE[EXP_TABLE[i] ] = i + +class QRPolynomial: + + def __init__(self, num, shift): + + if (len(num) == 0): + raise Exception(num.length + "/" + shift) + + offset = 0 + + while offset < len(num) and num[offset] == 0: + offset += 1 + + self.num = [0 for x in range(len(num)-offset+shift)] + for i in range(len(num) - offset): + self.num[i] = num[i + offset] + + + def get(self, index): + return self.num[index] + def getLength(self): + return len(self.num) + def multiply(self, e): + num = [0 for x in range(self.getLength() + e.getLength() - 1)]; + + for i in range(self.getLength()): + for j in range(e.getLength()): + num[i + j] ^= QRMath.gexp(QRMath.glog(self.get(i) ) + QRMath.glog(e.get(j) ) ) + + return QRPolynomial(num, 0); + def mod(self, e): + + if (self.getLength() - e.getLength() < 0): + return self; + + ratio = QRMath.glog(self.get(0) ) - QRMath.glog(e.get(0) ) + + num = [0 for x in range(self.getLength())] + + for i in range(self.getLength()): + num[i] = self.get(i); + + for i in range(e.getLength()): + num[i] ^= QRMath.gexp(QRMath.glog(e.get(i) ) + ratio) + + # recursive call + return QRPolynomial(num, 0).mod(e); + +class QRRSBlock: + + RS_BLOCK_TABLE = [ + + #// L + #// M + #// Q + #// H + + #// 1 + [1, 26, 19], + [1, 26, 16], + [1, 26, 13], + [1, 26, 9], + + #// 2 + [1, 44, 34], + [1, 44, 28], + [1, 44, 22], + [1, 44, 16], + + #// 3 + [1, 70, 55], + [1, 70, 44], + [2, 35, 17], + [2, 35, 13], + + #// 4 + [1, 100, 80], + [2, 50, 32], + [2, 50, 24], + [4, 25, 9], + + #// 5 + [1, 134, 108], + [2, 67, 43], + [2, 33, 15, 2, 34, 16], + [2, 33, 11, 2, 34, 12], + + #// 6 + [2, 86, 68], + [4, 43, 27], + [4, 43, 19], + [4, 43, 15], + + #// 7 + [2, 98, 78], + [4, 49, 31], + [2, 32, 14, 4, 33, 15], + [4, 39, 13, 1, 40, 14], + + #// 8 + [2, 121, 97], + [2, 60, 38, 2, 61, 39], + [4, 40, 18, 2, 41, 19], + [4, 40, 14, 2, 41, 15], + + #// 9 + [2, 146, 116], + [3, 58, 36, 2, 59, 37], + [4, 36, 16, 4, 37, 17], + [4, 36, 12, 4, 37, 13], + + #// 10 + [2, 86, 68, 2, 87, 69], + [4, 69, 43, 1, 70, 44], + [6, 43, 19, 2, 44, 20], + [6, 43, 15, 2, 44, 16], + + # 11 + [4, 101, 81], + [1, 80, 50, 4, 81, 51], + [4, 50, 22, 4, 51, 23], + [3, 36, 12, 8, 37, 13], + + # 12 + [2, 116, 92, 2, 117, 93], + [6, 58, 36, 2, 59, 37], + [4, 46, 20, 6, 47, 21], + [7, 42, 14, 4, 43, 15], + + # 13 + [4, 133, 107], + [8, 59, 37, 1, 60, 38], + [8, 44, 20, 4, 45, 21], + [12, 33, 11, 4, 34, 12], + + # 14 + [3, 145, 115, 1, 146, 116], + [4, 64, 40, 5, 65, 41], + [11, 36, 16, 5, 37, 17], + [11, 36, 12, 5, 37, 13], + + # 15 + [5, 109, 87, 1, 110, 88], + [5, 65, 41, 5, 66, 42], + [5, 54, 24, 7, 55, 25], + [11, 36, 12], + + # 16 + [5, 122, 98, 1, 123, 99], + [7, 73, 45, 3, 74, 46], + [15, 43, 19, 2, 44, 20], + [3, 45, 15, 13, 46, 16], + + # 17 + [1, 135, 107, 5, 136, 108], + [10, 74, 46, 1, 75, 47], + [1, 50, 22, 15, 51, 23], + [2, 42, 14, 17, 43, 15], + + # 18 + [5, 150, 120, 1, 151, 121], + [9, 69, 43, 4, 70, 44], + [17, 50, 22, 1, 51, 23], + [2, 42, 14, 19, 43, 15], + + # 19 + [3, 141, 113, 4, 142, 114], + [3, 70, 44, 11, 71, 45], + [17, 47, 21, 4, 48, 22], + [9, 39, 13, 16, 40, 14], + + # 20 + [3, 135, 107, 5, 136, 108], + [3, 67, 41, 13, 68, 42], + [15, 54, 24, 5, 55, 25], + [15, 43, 15, 10, 44, 16], + + # 21 + [4, 144, 116, 4, 145, 117], + [17, 68, 42], + [17, 50, 22, 6, 51, 23], + [19, 46, 16, 6, 47, 17], + + # 22 + [2, 139, 111, 7, 140, 112], + [17, 74, 46], + [7, 54, 24, 16, 55, 25], + [34, 37, 13], + + # 23 + [4, 151, 121, 5, 152, 122], + [4, 75, 47, 14, 76, 48], + [11, 54, 24, 14, 55, 25], + [16, 45, 15, 14, 46, 16], + + # 24 + [6, 147, 117, 4, 148, 118], + [6, 73, 45, 14, 74, 46], + [11, 54, 24, 16, 55, 25], + [30, 46, 16, 2, 47, 17], + + # 25 + [8, 132, 106, 4, 133, 107], + [8, 75, 47, 13, 76, 48], + [7, 54, 24, 22, 55, 25], + [22, 45, 15, 13, 46, 16], + + # 26 + [10, 142, 114, 2, 143, 115], + [19, 74, 46, 4, 75, 47], + [28, 50, 22, 6, 51, 23], + [33, 46, 16, 4, 47, 17], + + # 27 + [8, 152, 122, 4, 153, 123], + [22, 73, 45, 3, 74, 46], + [8, 53, 23, 26, 54, 24], + [12, 45, 15, 28, 46, 16], + + # 28 + [3, 147, 117, 10, 148, 118], + [3, 73, 45, 23, 74, 46], + [4, 54, 24, 31, 55, 25], + [11, 45, 15, 31, 46, 16], + + # 29 + [7, 146, 116, 7, 147, 117], + [21, 73, 45, 7, 74, 46], + [1, 53, 23, 37, 54, 24], + [19, 45, 15, 26, 46, 16], + + # 30 + [5, 145, 115, 10, 146, 116], + [19, 75, 47, 10, 76, 48], + [15, 54, 24, 25, 55, 25], + [23, 45, 15, 25, 46, 16], + + # 31 + [13, 145, 115, 3, 146, 116], + [2, 74, 46, 29, 75, 47], + [42, 54, 24, 1, 55, 25], + [23, 45, 15, 28, 46, 16], + + # 32 + [17, 145, 115], + [10, 74, 46, 23, 75, 47], + [10, 54, 24, 35, 55, 25], + [19, 45, 15, 35, 46, 16], + + # 33 + [17, 145, 115, 1, 146, 116], + [14, 74, 46, 21, 75, 47], + [29, 54, 24, 19, 55, 25], + [11, 45, 15, 46, 46, 16], + + # 34 + [13, 145, 115, 6, 146, 116], + [14, 74, 46, 23, 75, 47], + [44, 54, 24, 7, 55, 25], + [59, 46, 16, 1, 47, 17], + + # 35 + [12, 151, 121, 7, 152, 122], + [12, 75, 47, 26, 76, 48], + [39, 54, 24, 14, 55, 25], + [22, 45, 15, 41, 46, 16], + + # 36 + [6, 151, 121, 14, 152, 122], + [6, 75, 47, 34, 76, 48], + [46, 54, 24, 10, 55, 25], + [2, 45, 15, 64, 46, 16], + + # 37 + [17, 152, 122, 4, 153, 123], + [29, 74, 46, 14, 75, 47], + [49, 54, 24, 10, 55, 25], + [24, 45, 15, 46, 46, 16], + + # 38 + [4, 152, 122, 18, 153, 123], + [13, 74, 46, 32, 75, 47], + [48, 54, 24, 14, 55, 25], + [42, 45, 15, 32, 46, 16], + + # 39 + [20, 147, 117, 4, 148, 118], + [40, 75, 47, 7, 76, 48], + [43, 54, 24, 22, 55, 25], + [10, 45, 15, 67, 46, 16], + + # 40 + [19, 148, 118, 6, 149, 119], + [18, 75, 47, 31, 76, 48], + [34, 54, 24, 34, 55, 25], + [20, 45, 15, 61, 46, 16] + + ] + + def __init__(self, totalCount, dataCount): + self.totalCount = totalCount + self.dataCount = dataCount + + @staticmethod + def getRSBlocks(typeNumber, errorCorrectLevel): + rsBlock = QRRSBlock.getRsBlockTable(typeNumber, errorCorrectLevel); + if rsBlock == None: + raise Exception("bad rs block @ typeNumber:" + typeNumber + "/errorCorrectLevel:" + errorCorrectLevel) + + length = len(rsBlock) / 3 + + list = [] + + for i in range(length): + + count = rsBlock[i * 3 + 0] + totalCount = rsBlock[i * 3 + 1] + dataCount = rsBlock[i * 3 + 2] + + for j in range(count): + list.append(QRRSBlock(totalCount, dataCount)) + + return list; + + @staticmethod + def getRsBlockTable(typeNumber, errorCorrectLevel): + if errorCorrectLevel == QRErrorCorrectLevel.L: + return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0]; + elif errorCorrectLevel == QRErrorCorrectLevel.M: + return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1]; + elif errorCorrectLevel == QRErrorCorrectLevel.Q: + return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2]; + elif errorCorrectLevel == QRErrorCorrectLevel.H: + return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3]; + else: + return None; + +class QRBitBuffer: + def __init__(self): + self.buffer = [] + self.length = 0 + def __repr__(self): + return ".".join([str(n) for n in self.buffer]) + def get(self, index): + bufIndex = math.floor(index / 8) + val = ( (self.buffer[bufIndex] >> (7 - index % 8) ) & 1) == 1 + print "get ", val + return ( (self.buffer[bufIndex] >> (7 - index % 8) ) & 1) == 1 + def put(self, num, length): + for i in range(length): + self.putBit( ( (num >> (length - i - 1) ) & 1) == 1) + def getLengthInBits(self): + return self.length + def putBit(self, bit): + bufIndex = self.length // 8 + if len(self.buffer) <= bufIndex: + self.buffer.append(0) + if bit: + self.buffer[bufIndex] |= (0x80 >> (self.length % 8) ) + self.length+=1 diff --git a/qtdefines.py b/qtdefines.py index 50b02f2bc..8f5e01918 100644 --- a/qtdefines.py +++ b/qtdefines.py @@ -1,15 +1,21 @@ ################################################################################ # # -# Copyright (C) 2011-2013, Armory Technologies, Inc. # +# Copyright (C) 2011-2014, Armory Technologies, Inc. # # Distributed under the GNU Affero General Public License (AGPL v3) # # See LICENSE or http://www.gnu.org/licenses/agpl.html # # # ################################################################################ -from armoryengine import * +import struct +from tempfile import mkstemp + from PyQt4.QtCore import * -from PyQt4.QtGui import * +from PyQt4.QtGui import * +import urllib + from armorycolors import Colors, htmlColor -from tempfile import mkstemp +from armoryengine.ArmoryUtils import * +from armoryengine.BinaryUnpacker import * + SETTINGS_PATH = os.path.join(ARMORY_HOME_DIR, 'ArmorySettings.txt') USERMODE = enum('Standard', 'Advanced', 'Expert') @@ -27,13 +33,33 @@ STYLE_PLAIN = QFrame.Box | QFrame.Plain STYLE_STYLED = QFrame.StyledPanel | QFrame.Raised STYLE_NONE = QFrame.NoFrame - +VERTICAL = 'vertical' +HORIZONTAL = 'horizontal' CHANGE_ADDR_DESCR_STRING = '[[ Change received ]]' -HTTP_VERSION_FILE = 'http://bitcoinarmory.com/versions.txt' - +HTTP_VERSION_FILE = 'https://bitcoinarmory.com/versions.txt' +BUG_REPORT_URL = 'https://bitcoinarmory.com/scripts/receive_debug.php' +PRIVACY_URL = 'https://bitcoinarmory.com/privacy-policy' +# For announcements handling +ANNOUNCE_FETCH_INTERVAL = 1 * HOUR +if CLI_OPTIONS.testAnnounceCode: + HTTP_ANNOUNCE_FILE = \ + 'https://s3.amazonaws.com/bitcoinarmory-testing/testannounce.txt' +else: + HTTP_ANNOUNCE_FILE = 'https://bitcoinarmory.com/atiannounce.txt' + +# Keep track of dialogs and wizard that are executing +runningDialogsList = [] + +def AddToRunningDialogsList(func): + def wrapper(*args, **kwargs): + runningDialogsList.append(args[0]) + result = func(*args, **kwargs) + runningDialogsList.remove(args[0]) + return result + return wrapper ################################################################################ -def tr(txt): +def tr(txt, replList=None, pluralList=None): """ This is a common convention for implementing translations, where all translatable strings are put int the _(...) function, and that method @@ -50,28 +76,43 @@ def tr(txt): Instead it should really look like: - myLabel = QRichLabel( _(''' + myLabel = QRichLabel( tr(''' This text is split across mulitple lines and it will acquire a space after each line as well as include newlines because it's HTML and uses
    . ''' )) + + Added easy plural handling: + + Just add an extra argument to specify a variable on which plurality + should be chosen, and then decorate your text with + + @{singular|plural}@ + + For instance: + + tr('The @{cat|cats}@ danced. @{It was|They were}@ happy.', nCat) + tr('The @{cat|%d cats}@ danced. @{It was|They were}@ happy.'%nCat, nCat) + tr('The @{cat|cats}@ attacked the @{dog|dogs}@', nCat, nDog) + + This should work well for """ txt = toUnicode(txt) lines = [l.strip() for l in txt.split('\n')] - outText = (' '.join(lines)).strip() + txt = (' '.join(lines)).strip() # Eventually we do something cool with this transalate function. + # It will be defined elsewhere, but for now stubbed with identity fn TRANSLATE = lambda x: x - return TRANSLATE(outText) + txt = TRANSLATE(txt) + return formatWithPlurals(txt, replList, pluralList) ################################################################################ - - def HLINE(style=QFrame.Plain): qf = QFrame() qf.setFrameStyle(QFrame.HLine | style) @@ -84,17 +125,21 @@ def VLINE(style=QFrame.Plain): - # Setup fixed-width and var-width fonts def GETFONT(ftype, sz=10, bold=False, italic=False): fnt = None if ftype.lower().startswith('fix'): if OS_WINDOWS: fnt = QFont("Courier", sz) + elif OS_MACOSX: + fnt = QFont("Menlo", sz) else: fnt = QFont("DejaVu Sans Mono", sz) elif ftype.lower().startswith('var'): - fnt = QFont("Verdana", sz) + if OS_MACOSX: + fnt = QFont("Lucida Grande", sz) + else: + fnt = QFont("Verdana", sz) #if OS_WINDOWS: #fnt = QFont("Tahoma", sz) #else: @@ -102,6 +147,8 @@ def GETFONT(ftype, sz=10, bold=False, italic=False): elif ftype.lower().startswith('money'): if OS_WINDOWS: fnt = QFont("Courier", sz) + elif OS_MACOSX: + fnt = QFont("Menlo", sz) else: fnt = QFont("DejaVu Sans Mono", sz) else: @@ -247,6 +294,10 @@ def __init__(self, txt, doWrap=True, \ self.setWordWrap(doWrap) self.setAlignment(hAlign | vAlign) self.setText(txt, **kwargs) + # Fixes a problem with QLabel resizing based on content + # ACR: ... and makes other problems. Removing for now. + #self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.MinimumExpanding) + #self.setMinimumHeight(int(relaxedSizeStr(self, 'QWERTYqypgj')[1])) def setText(self, text, color=None, size=None, bold=None, italic=None): text = unicode(text) @@ -382,24 +433,11 @@ def leaveEvent(self, ev): ssStr = "QLabel { background-color : %s }" % htmlColor('LBtnNormalBG') self.setStyleSheet(ssStr) - ################################################################################ -def createToolTipObject(tiptext, iconSz=2): - """ - The is to signal to Qt that it should be interpretted as HTML/Rich - text even if no HTML tags are used. This appears to be necessary for Qt - to wrap the tooltip text - """ - fgColor = htmlColor('ToolTipQ') - lbl = QLabel('(?)' % (iconSz, fgColor)) - lbl.setToolTip('' + tiptext) - lbl.setMaximumWidth(relaxedSizeStr(lbl, '(?)')[0]) - lbl.connect(lbl, SIGNAL('clicked()'), lambda: printlbl) - return lbl - - -################################################################################ -def MsgBoxCustom(wtype, title, msg, wCancel=False, yesStr=None, noStr=None): +# The optionalMsg argument is not word wrapped so the caller is responsible for limiting +# the length of the longest line in the optionalMsg +def MsgBoxCustom(wtype, title, msg, wCancel=False, yesStr=None, noStr=None, + optionalMsg=None): """ Creates a message box with custom button text and icon """ @@ -432,9 +470,9 @@ def __init__(self, dtype, dtitle, wmsg, withCancel=False, yesStr=None, noStr=Non lblMsg.setTextFormat(Qt.RichText) lblMsg.setWordWrap(True) lblMsg.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + lblMsg.setOpenExternalLinks(True) w,h = tightSizeNChar(lblMsg, 70) lblMsg.setMinimumSize( w, 3.2*h ) - buttonbox = QDialogButtonBox() if dtype==MSGBOX.Question: @@ -447,14 +485,14 @@ def __init__(self, dtype, dtitle, wmsg, withCancel=False, yesStr=None, noStr=Non buttonbox.addButton(btnYes,QDialogButtonBox.AcceptRole) buttonbox.addButton(btnNo, QDialogButtonBox.RejectRole) else: - if not yesStr: yesStr = '&OK' - if not noStr: noStr = '&Cancel' + cancelStr = '&Cancel' if (noStr is not None or withCancel) else '' + yesStr = '&OK' if (yesStr is None) else yesStr btnOk = QPushButton(yesStr) - btnCancel = QPushButton(noStr) + btnCancel = QPushButton(cancelStr) self.connect(btnOk, SIGNAL('clicked()'), self.accept) self.connect(btnCancel, SIGNAL('clicked()'), self.reject) buttonbox.addButton(btnOk, QDialogButtonBox.AcceptRole) - if withCancel or noStr: + if cancelStr: buttonbox.addButton(btnCancel, QDialogButtonBox.RejectRole) spacer = QSpacerItem(20, 10, QSizePolicy.Fixed, QSizePolicy.Expanding) @@ -463,7 +501,14 @@ def __init__(self, dtype, dtitle, wmsg, withCancel=False, yesStr=None, noStr=Non layout.addItem( spacer, 0,0, 1,2) layout.addWidget(msgIcon, 1,0, 1,1) layout.addWidget(lblMsg, 1,1, 1,1) - layout.addWidget(buttonbox, 3,0, 1,2) + if optionalMsg: + optionalTextLabel = QLabel(optionalMsg) + optionalTextLabel.setTextFormat(Qt.RichText) + optionalTextLabel.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + w,h = tightSizeNChar(optionalTextLabel, 70) + optionalTextLabel.setMinimumSize( w, 3.2*h ) + layout.addWidget(optionalTextLabel, 2,0,1,2) + layout.addWidget(buttonbox, 3,0, 1,2) layout.setSpacing(20) self.setLayout(layout) self.setWindowTitle(dtitle) @@ -528,7 +573,6 @@ def __init__(self, dtype, dtitle, wmsg, dmsg=None, withCancel=False): self.connect(btnNo, SIGNAL('clicked()'), self.reject) buttonbox.addButton(btnYes,QDialogButtonBox.AcceptRole) buttonbox.addButton(btnNo, QDialogButtonBox.RejectRole) - else: btnOk = QPushButton('Ok') self.connect(btnOk, SIGNAL('clicked()'), self.accept) @@ -564,7 +608,7 @@ def makeLayoutFrame(dirStr, widgetList, style=QFrame.NoFrame): frm.setFrameStyle(style) frmLayout = QHBoxLayout() - if dirStr.lower().startswith('vert'): + if dirStr.lower().startswith(VERTICAL): frmLayout = QVBoxLayout() for w in widgetList: @@ -575,12 +619,12 @@ def makeLayoutFrame(dirStr, widgetList, style=QFrame.NoFrame): first = w.index('(')+1 last = w.index(')') wid,hgt = int(w[first:last]), 1 - if dirStr.lower().startswith('vert'): + if dirStr.lower().startswith(VERTICAL): wid,hgt = hgt,wid frmLayout.addItem( QSpacerItem(wid,hgt) ) elif isinstance(w,str) and w.lower().startswith('line'): frmLine = QFrame() - if dirStr.lower().startswith('vert'): + if dirStr.lower().startswith(VERTICAL): frmLine.setFrameStyle(QFrame.HLine | QFrame.Plain) else: frmLine.setFrameStyle(QFrame.VLine | QFrame.Plain) @@ -601,13 +645,13 @@ def makeLayoutFrame(dirStr, widgetList, style=QFrame.NoFrame): def addFrame(widget, style=STYLE_SUNKEN): - return makeLayoutFrame('Horiz', [widget], style) + return makeLayoutFrame(HORIZONTAL, [widget], style) def makeVertFrame(widgetList, style=QFrame.NoFrame): - return makeLayoutFrame('Vert', widgetList, style) + return makeLayoutFrame(VERTICAL, widgetList, style) def makeHorizFrame(widgetList, style=QFrame.NoFrame): - return makeLayoutFrame('Horiz', widgetList, style) + return makeLayoutFrame(HORIZONTAL, widgetList, style) def QImageLabel(imgfn, size=None, stretch='NoStretch'): @@ -662,6 +706,22 @@ def saveTableView(qtbl): +################################################################################ +# This class is intended to be an abstract frame class that +# will hold all of the functionality that is common to all +# Frames used in Armory. +# The Frames that extend this class should contain all of the +# display and control components for some screen used in Armory +# Putting this content in a frame allows it to be used on it's own +# in a dialog or as a component in a larger frame. +class ArmoryFrame(QFrame): + def __init__(self, parent, main): + super(ArmoryFrame, self).__init__(parent) + self.main = main + + # Subclasses should implement a method that returns a boolean to control + # when done, accept, next, or final button should be enabled. + self.isComplete = None ################################################################################ @@ -681,10 +741,11 @@ def __init__(self, parent=None, main=None): else: self.setWindowTitle('Armory - Bitcoin Wallet Management') self.setWindowIcon(QIcon(':/armory_icon_32x32.png')) - - - - + + @AddToRunningDialogsList + def exec_(self): + return super(ArmoryDialog, self).exec_() + ################################################################################ class QRCodeWidget(QWidget): diff --git a/qtdialogs.py b/qtdialogs.py index 5b017decc..be205cde1 100644 --- a/qtdialogs.py +++ b/qtdialogs.py @@ -1,45 +1,56 @@ ################################################################################ # # -# Copyright (C) 2011-2013, Armory Technologies, Inc. # +# Copyright (C) 2011-2014, Armory Technologies, Inc. # # Distributed under the GNU Affero General Public License (AGPL v3) # # See LICENSE or http://www.gnu.org/licenses/agpl.html # # # ################################################################################ +import functools +import shutil +import socket import sys import time -import shutil -import functools +from zipfile import ZipFile, ZIP_DEFLATED + from PyQt4.QtCore import * from PyQt4.QtGui import * -from qtdefines import * -from armoryengine import * -from armorymodels import * +from armoryengine.ALL import * from armorycolors import Colors, htmlColor +from armorymodels import * import qrc_img_resources - -MIN_PASSWD_WIDTH = lambda obj: tightSizeStr(obj, '*'*16)[0] - - - - - - - +from qtdefines import * +from armoryengine.PyBtcAddress import calcWalletIDFromRoot +from announcefetch import DEFAULT_MIN_PRIORITY +from ui.UpgradeDownloader import UpgradeDownloaderDialog +from armoryengine.PyBtcWalletRecovery import RECOVERMODE + +NO_CHANGE = 'NoChange' +MIN_PASSWD_WIDTH = lambda obj: tightSizeStr(obj, '*' * 16)[0] +STRETCH = 'Stretch' +CLICKED = 'clicked()' +BACKUP_TYPE_135A = '1.35a' +BACKUP_TYPE_135C = '1.35c' +BACKUP_TYPE_0_TEXT = tr('Version 0 (from script, 9 lines)') +BACKUP_TYPE_135a_TEXT = tr('Version 1.35a (5 lines Unencrypted)') +BACKUP_TYPE_135a_SP_TEXT = tr('Version 1.35a (5 lines + SecurePrint\xe2\x84\xa2)') +BACKUP_TYPE_135c_TEXT = tr('Version 1.35c (3 lines Unencrypted)') +BACKUP_TYPE_135c_SP_TEXT = tr('Version 1.35c (3 lines + SecurePrint\xe2\x84\xa2)') ################################################################################ class DlgUnlockWallet(ArmoryDialog): def __init__(self, wlt, parent=None, main=None, unlockMsg='Unlock Wallet', \ - returnResult=False): + returnResult=False, returnPassphrase=False): super(DlgUnlockWallet, self).__init__(parent, main) self.wlt = wlt self.returnResult = returnResult + self.returnPassphrase = returnPassphrase ##### Upper layout - lblDescr = QLabel("Enter your passphrase to unlock this wallet") + lblDescr = QLabel("Enter your passphrase to unlock this wallet") lblPasswd = QLabel("Passphrase:") self.edtPasswd = QLineEdit() self.edtPasswd.setEchoMode(QLineEdit.Password) @@ -48,15 +59,15 @@ def __init__(self, wlt, parent=None, main=None, unlockMsg='Unlock Wallet', \ self.btnAccept = QPushButton("Unlock") self.btnCancel = QPushButton("Cancel") - self.connect(self.btnAccept, SIGNAL('clicked()'), self.acceptPassphrase) - self.connect(self.btnCancel, SIGNAL('clicked()'), self.reject) + self.connect(self.btnAccept, SIGNAL(CLICKED), self.acceptPassphrase) + self.connect(self.btnCancel, SIGNAL(CLICKED), self.reject) buttonBox = QDialogButtonBox() buttonBox.addButton(self.btnAccept, QDialogButtonBox.AcceptRole) buttonBox.addButton(self.btnCancel, QDialogButtonBox.RejectRole) layoutUpper = QGridLayout() - layoutUpper.addWidget(lblDescr, 1, 0, 1, 2) - layoutUpper.addWidget(lblPasswd, 2, 0, 1, 1) + layoutUpper.addWidget(lblDescr, 1, 0, 1, 2) + layoutUpper.addWidget(lblPasswd, 2, 0, 1, 1) layoutUpper.addWidget(self.edtPasswd, 2, 1, 1, 1) self.frmUpper = QFrame() self.frmUpper.setLayout(layoutUpper) @@ -64,7 +75,7 @@ def __init__(self, wlt, parent=None, main=None, unlockMsg='Unlock Wallet', \ ##### Lower layout # Add scrambled keyboard (EN-US only) - ttipScramble= self.main.createToolTipWidget( \ + ttipScramble = self.main.createToolTipWidget(\ 'Using a visual keyboard to enter your passphrase ' 'protects you against simple keyloggers. Scrambling ' 'makes it difficult to use, but prevents even loggers ' @@ -80,28 +91,28 @@ def __init__(self, wlt, parent=None, main=None, unlockMsg='Unlock Wallet', \ btngrp.addButton(self.rdoScrambleFull) btngrp.setExclusive(True) defaultScramble = self.main.getSettingOrSetDefault('ScrambleDefault', 0) - if defaultScramble==0: + if defaultScramble == 0: self.rdoScrambleNone.setChecked(True) - elif defaultScramble==1: + elif defaultScramble == 1: self.rdoScrambleLite.setChecked(True) - elif defaultScramble==2: + elif defaultScramble == 2: self.rdoScrambleFull.setChecked(True) - self.connect(self.rdoScrambleNone, SIGNAL('clicked()'), self.changeScramble) - self.connect(self.rdoScrambleLite, SIGNAL('clicked()'), self.changeScramble) - self.connect(self.rdoScrambleFull, SIGNAL('clicked()'), self.changeScramble) + self.connect(self.rdoScrambleNone, SIGNAL(CLICKED), self.changeScramble) + self.connect(self.rdoScrambleLite, SIGNAL(CLICKED), self.changeScramble) + self.connect(self.rdoScrambleFull, SIGNAL(CLICKED), self.changeScramble) btnRowFrm = makeHorizFrame([self.rdoScrambleNone, \ self.rdoScrambleLite, \ self.rdoScrambleFull, \ - 'Stretch']) + STRETCH]) self.layoutKeyboard = QGridLayout() self.frmKeyboard = QFrame() self.frmKeyboard.setLayout(self.layoutKeyboard) - showOSD = self.main.getSettingOrSetDefault('KeybdOSD',False) + showOSD = self.main.getSettingOrSetDefault('KeybdOSD', False) self.layoutLower = QGridLayout() - self.layoutLower.addWidget( btnRowFrm , 0,0) - self.layoutLower.addWidget( self.frmKeyboard , 1,0) + self.layoutLower.addWidget(btnRowFrm , 0, 0) + self.layoutLower.addWidget(self.frmKeyboard , 1, 0) self.frmLower = QFrame() self.frmLower.setLayout(self.layoutLower) self.frmLower.setVisible(showOSD) @@ -111,8 +122,8 @@ def __init__(self, wlt, parent=None, main=None, unlockMsg='Unlock Wallet', \ self.btnShowOSD = QPushButton('Show Keyboard >>>') self.btnShowOSD.setCheckable(True) self.btnShowOSD.setChecked(showOSD) - self.connect(self.btnShowOSD, SIGNAL('toggled(bool)'), self.toggleOSD) - frmAccept = makeHorizFrame([self.btnShowOSD, ttipScramble, 'Stretch', buttonBox]) + self.connect(self.btnShowOSD, SIGNAL('toggled(bool)'), self.toggleOSD) + frmAccept = makeHorizFrame([self.btnShowOSD, ttipScramble, STRETCH, buttonBox]) ##### Complete Layout @@ -128,7 +139,7 @@ def __init__(self, wlt, parent=None, main=None, unlockMsg='Unlock Wallet', \ self.changeScramble() self.redrawKeys() - + ############################################################################# def toggleOSD(self): isChk = self.btnShowOSD.isChecked() @@ -143,11 +154,11 @@ def toggleOSD(self): ############################################################################# def createKeyboardKeyButton(self, keyLow, keyUp, defRow, special=None): theBtn = LetterButton(keyLow, keyUp, defRow, special, self.edtPasswd, self) - self.connect(theBtn, SIGNAL('clicked()'), theBtn.insertLetter) + self.connect(theBtn, SIGNAL(CLICKED), theBtn.insertLetter) theBtn.setMaximumWidth(40) return theBtn - + ############################################################################# def redrawKeys(self): for btn in self.btnList: @@ -155,7 +166,7 @@ def redrawKeys(self): self.btnShift.setText('SHIFT') self.btnSpace.setText('SPACE') self.btnDelete.setText('DEL') - + ############################################################################# def deleteKeyboard(self): for btn in self.btnList: @@ -177,24 +188,24 @@ def createKeyButtons(self): # the letter arrays with something more appropriate for non en-us self.letLower = r"`1234567890-=qwertyuiop[]\asdfghjkl;'zxcvbnm,./" self.letUpper = r'~!@#$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?' - self.letRows = r'11111111111112222222222222333333333334444444444' - self.letPairs = zip(self.letLower,self.letUpper,self.letRows) + self.letRows = r'11111111111112222222222222333333333334444444444' + self.letPairs = zip(self.letLower, self.letUpper, self.letRows) self.btnList = [] - for l,u,r in zip(self.letLower, self.letUpper, self.letRows): - if l=='7': + for l, u, r in zip(self.letLower, self.letUpper, self.letRows): + if l == '7': # Because QPushButtons interpret ampersands as special characters - u = 2*u + u = 2 * u if l.isdigit(): - self.btnList.append(self.createKeyboardKeyButton('#'+l,u,int(r))) + self.btnList.append(self.createKeyboardKeyButton('#' + l, u, int(r))) else: - self.btnList.append(self.createKeyboardKeyButton(l,u,int(r))) + self.btnList.append(self.createKeyboardKeyButton(l, u, int(r))) # Add shift and space keys - self.btnShift = self.createKeyboardKeyButton('', '', 5,'shift') - self.btnSpace = self.createKeyboardKeyButton(' ',' ',5,'space') - self.btnDelete= self.createKeyboardKeyButton(' ',' ',5,'delete') + self.btnShift = self.createKeyboardKeyButton('', '', 5, 'shift') + self.btnSpace = self.createKeyboardKeyButton(' ', ' ', 5, 'space') + self.btnDelete = self.createKeyboardKeyButton(' ', ' ', 5, 'delete') self.btnShift.setCheckable(True) self.btnShift.setChecked(False) @@ -202,7 +213,7 @@ def createKeyButtons(self): def reshuffleKeys(self): if self.rdoScrambleFull.isChecked(): self.changeScramble() - + ############################################################################# def changeScramble(self): self.deleteKeyboard() @@ -213,40 +224,40 @@ def changeScramble(self): if self.rdoScrambleNone.isChecked(): opt = 0 prevRow = 1 - col=0 + col = 0 for btn in self.btnList: row = btn.defRow - if not row==prevRow: - col=0 - if row>3 and col==0: - col+=1 + if not row == prevRow: + col = 0 + if row > 3 and col == 0: + col += 1 prevRow = row self.layoutKeyboard.addWidget(btn, row, col) col += 1 - self.layoutKeyboard.addWidget(self.btnShift, self.btnShift.defRow, 0, 1,3) - self.layoutKeyboard.addWidget(self.btnSpace, self.btnSpace.defRow, 4, 1,5) - self.layoutKeyboard.addWidget(self.btnDelete, self.btnDelete.defRow, 11, 1,2) + self.layoutKeyboard.addWidget(self.btnShift, self.btnShift.defRow, 0, 1, 3) + self.layoutKeyboard.addWidget(self.btnSpace, self.btnSpace.defRow, 4, 1, 5) + self.layoutKeyboard.addWidget(self.btnDelete, self.btnDelete.defRow, 11, 1, 2) self.btnShift.setMaximumWidth(1000) self.btnSpace.setMaximumWidth(1000) self.btnDelete.setMaximumWidth(1000) elif self.rdoScrambleLite.isChecked(): opt = 1 nchar = len(self.btnList) - rnd = SecureBinaryData().GenerateRandom(2*nchar).toBinStr() - newBtnList = [[self.btnList[i], rnd[2*i:2*(i+1)]] for i in range(nchar)] + rnd = SecureBinaryData().GenerateRandom(2 * nchar).toBinStr() + newBtnList = [[self.btnList[i], rnd[2 * i:2 * (i + 1)]] for i in range(nchar)] newBtnList.sort(key=lambda x: x[1]) prevRow = 0 - col=0 - for i,btn in enumerate(newBtnList): - row = i/12 - if not row==prevRow: - col=0 + col = 0 + for i, btn in enumerate(newBtnList): + row = i / 12 + if not row == prevRow: + col = 0 prevRow = row self.layoutKeyboard.addWidget(btn[0], row, col) col += 1 - self.layoutKeyboard.addWidget(self.btnShift, self.btnShift.defRow, 0, 1,3) - self.layoutKeyboard.addWidget(self.btnSpace, self.btnSpace.defRow, 4, 1,5) - self.layoutKeyboard.addWidget(self.btnDelete, self.btnDelete.defRow, 10, 1,2) + self.layoutKeyboard.addWidget(self.btnShift, self.btnShift.defRow, 0, 1, 3) + self.layoutKeyboard.addWidget(self.btnSpace, self.btnSpace.defRow, 4, 1, 5) + self.layoutKeyboard.addWidget(self.btnDelete, self.btnDelete.defRow, 10, 1, 2) self.btnShift.setMaximumWidth(1000) self.btnSpace.setMaximumWidth(1000) self.btnDelete.setMaximumWidth(1000) @@ -255,41 +266,49 @@ def changeScramble(self): extBtnList = self.btnList[:] extBtnList.extend([self.btnShift, self.btnSpace]) nchar = len(extBtnList) - rnd = SecureBinaryData().GenerateRandom(2*nchar).toBinStr() - newBtnList = [[extBtnList[i], rnd[2*i:2*(i+1)]] for i in range(nchar)] + rnd = SecureBinaryData().GenerateRandom(2 * nchar).toBinStr() + newBtnList = [[extBtnList[i], rnd[2 * i:2 * (i + 1)]] for i in range(nchar)] newBtnList.sort(key=lambda x: x[1]) prevRow = 0 - col=0 - for i,btn in enumerate(newBtnList): - row = i/12 - if not row==prevRow: - col=0 + col = 0 + for i, btn in enumerate(newBtnList): + row = i / 12 + if not row == prevRow: + col = 0 prevRow = row self.layoutKeyboard.addWidget(btn[0], row, col) col += 1 - self.layoutKeyboard.addWidget(self.btnDelete, self.btnDelete.defRow-1, 11, 1,2) + self.layoutKeyboard.addWidget(self.btnDelete, self.btnDelete.defRow - 1, 11, 1, 2) self.btnShift.setMaximumWidth(40) self.btnSpace.setMaximumWidth(40) self.btnDelete.setMaximumWidth(40) self.frmKeyboard.setLayout(self.layoutKeyboard) - self.layoutLower.addWidget(self.frmKeyboard, 1,0) + self.layoutLower.addWidget(self.frmKeyboard, 1, 0) self.main.settings.set('ScrambleDefault', opt) self.redrawKeys() - + ############################################################################# def acceptPassphrase(self): self.securePassphrase = SecureBinaryData(str(self.edtPasswd.text())) + self.edtPasswd.setText('') + if self.returnResult: self.accept() return try: - self.wlt.unlock(securePassphrase=self.securePassphrase) - self.securePassphrase.destroy() - self.edtPasswd.setText('') + if self.returnPassphrase == False: + unlockProgress = DlgProgress(self, self.main, HBar=1, + Title="Unlocking Wallet") + unlockProgress.exec_(self.wlt.unlock, securePassphrase=self.securePassphrase) + self.securePassphrase.destroy() + else: + if self.wlt.verifyPassphrase(self.securePassphrase) == False: + raise PassphraseError + self.accept() except PassphraseError: QMessageBox.critical(self, 'Invalid Passphrase', \ @@ -303,17 +322,17 @@ def acceptPassphrase(self): class LetterButton(QPushButton): def __init__(self, Low, Up, Row, Spec, edtTarget, parent): super(LetterButton, self).__init__('') - self.lower = Low - self.upper = Up - self.defRow = Row + self.lower = Low + self.upper = Up + self.defRow = Row self.special = Spec - self.target = edtTarget - self.parent = parent - + self.target = edtTarget + self.parent = parent + if self.special: - super(LetterButton, self).setFont(GETFONT('Var',8)) + super(LetterButton, self).setFont(GETFONT('Var', 8)) else: - super(LetterButton, self).setFont(GETFONT('Fixed',10)) + super(LetterButton, self).setFont(GETFONT('Fixed', 10)) if self.special == 'space': self.setText('SPACE') self.lower = ' ' @@ -331,10 +350,10 @@ def __init__(self, Low, Up, Row, Spec, edtTarget, parent): def insertLetter(self): currPwd = str(self.parent.edtPasswd.text()) insChar = self.upper if self.parent.btnShift.isChecked() else self.lower - if len(insChar)==2 and insChar.startswith('#'): + if len(insChar) == 2 and insChar.startswith('#'): insChar = insChar[1] - self.parent.edtPasswd.setText( currPwd + insChar ) + self.parent.edtPasswd.setText(currPwd + insChar) self.parent.reshuffleKeys() def pressShift(self): @@ -342,57 +361,17 @@ def pressShift(self): def pressBackspace(self): currPwd = str(self.parent.edtPasswd.text()) - if len(currPwd)>0: - self.parent.edtPasswd.setText( currPwd[:-1]) + if len(currPwd) > 0: + self.parent.edtPasswd.setText(currPwd[:-1]) self.parent.redrawKeys() - -################################################################################ -class DlgTooltip(ArmoryDialog): - def __init__(self, parentDlg=None, parentLbl=None, tiptext=''): - super(DlgTooltip, self).__init__(parentDlg, main=None) - - if not parentDlg or not tiptext: - self.accept() - - qc = QCursor.pos() - qp = QPoint(qc.x()-20, qc.y()-20) - self.move(qp) - - tiptext += '
    [Click to close]
    ' - - lblText = QRichLabel(tiptext, doWrap=True) - lblText.mousePressEvent = lambda ev: self.accept() - lblText.mouseReleaseEvent = lambda ev: self.accept() - layout = QVBoxLayout() - layout.addWidget( makeHorizFrame([lblText], STYLE_RAISED) ) - layout.setContentsMargins(0,0,0,0) - self.setLayout(layout) - - self.setStyleSheet('QDialog { background-color : %s }' % htmlColor('Foreground')) - lblText.setStyleSheet('QLabel { background-color : %s }' % htmlColor('SlightBkgdDark')) - lblText.setContentsMargins(3,3,3,3) - - self.setMinimumWidth(150) - self.setWindowFlags(Qt.SplashScreen) - - #def mouseReleaseEvent(self, ev): - #self.accept() - - def mousePressEvent(self, ev): - self.accept() - - def keyPressEvent(self, ev): - self.accept() - - ################################################################################ class DlgGenericGetPassword(ArmoryDialog): def __init__(self, descriptionStr, parent=None, main=None): super(DlgGenericGetPassword, self).__init__(parent, main) - lblDescr = QRichLabel(descriptionStr) + lblDescr = QRichLabel(descriptionStr) lblPasswd = QRichLabel("Password:") self.edtPasswd = QLineEdit() self.edtPasswd.setEchoMode(QLineEdit.Password) @@ -401,22 +380,675 @@ def __init__(self, descriptionStr, parent=None, main=None): self.btnAccept = QPushButton("OK") self.btnCancel = QPushButton("Cancel") - self.connect(self.btnAccept, SIGNAL('clicked()'), self.accept) - self.connect(self.btnCancel, SIGNAL('clicked()'), self.reject) + 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(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) + layout.addWidget(buttonBox, 3, 1, 1, 2) self.setLayout(layout) self.setWindowTitle('Enter Password') self.setWindowIcon(QIcon(self.main.iconfile)) - + +################################################################################ +class DlgBugReport(ArmoryDialog): + + def __init__(self, parent=None, main=None): + super(DlgBugReport, self).__init__(parent, main) + + tsPage = 'https://bitcoinarmory.com/troubleshooting' + faqPage = 'https://bitcoinarmory.com/faqs' + + lblDescr = QRichLabel(tr(""" + Send a bug report to the Armory team +

    + If you are having difficulties with Armory, you should first visit + our troubleshooting page and our + FAQ page which describe solutions to + many common problems. +

    + If you do not find the answer to your problem on those pages, + please describe it in detail below, and any steps taken to + reproduce the problem. The more information you provide, the + more likely we will be able to help you. +

    + Note: Please keep in mind we + are a small open-source company, and do not have a formal customer + support department. We will do our best to help you, but cannot + respond to everyone!""") % (tsPage, faqPage, htmlColor('TextBlue'))) + + self.chkNoLog = QCheckBox('Do not send log file with report') + self.chkNoLog.setChecked(False) + + self.btnMoreInfo = QLabelButton('Privacy Info') + self.connect(self.btnMoreInfo, SIGNAL(CLICKED), \ + self.main.logFilePrivacyWarning) + + self.noLogWarn = QRichLabel(tr(""" + You are unlikely to get a response unless you + provide a log file and a reasonable description with your support + request.""") % htmlColor('TextWarn')) + self.noLogWarn.setVisible(False) + + self.connect(self.chkNoLog, SIGNAL('toggled(bool)'), \ + self.noLogWarn.setVisible) + + self.lblEmail = QRichLabel(tr('Email Address:')) + self.edtEmail = QLineEdit() + self.edtEmail.setMaxLength(100) + + self.lblSubject = QRichLabel(tr('Subject:')) + self.edtSubject = QLineEdit() + self.edtSubject.setMaxLength(64) + self.edtSubject.setText("Bug Report") + + self.txtDescr = QTextEdit() + self.txtDescr.setFont(GETFONT('Fixed', 9)) + w,h = tightSizeNChar(self, 80) + self.txtDescr.setMinimumWidth(w) + self.txtDescr.setMinimumHeight(4*h) + self.lblOS = QRichLabel(tr(""" + Note: if you are using this computer to report an Armory problem + on another computer, please include the operating system of the + other computer and the version of Armory it is running.""")) + + self.btnSubmit = QPushButton(tr('Submit Report')) + self.btnCancel = QPushButton(tr('Cancel')) + self.btnbox = QDialogButtonBox() + self.btnbox.addButton(self.btnSubmit, QDialogButtonBox.AcceptRole) + self.btnbox.addButton(self.btnCancel, QDialogButtonBox.RejectRole) + self.connect(self.btnSubmit, SIGNAL(CLICKED), self.submitReport) + self.connect(self.btnCancel, SIGNAL(CLICKED), self, SLOT('reject()')) + + armoryver = getVersionString(BTCARMORY_VERSION) + lblDetect = QRichLabel( tr(""" + Detected: %s (%s) / %0.2f GB RAM / Armory version %s
    + (this data will be submitted automatically with the + report)""") % \ + (OS_NAME, OS_VARIANT[0], SystemSpecs.Memory, armoryver)) + + + layout = QGridLayout() + i = -1 + + i += 1 + layout.addWidget(lblDescr, i,0, 1,2) + + i += 1 + layout.addWidget(HLINE(), i,0, 1,2) + + i += 1 + layout.addWidget(lblDetect, i,0, 1,2) + + i += 1 + layout.addWidget(HLINE(), i,0, 1,2) + + i += 1 + layout.addWidget(self.lblEmail, i,0, 1,1) + layout.addWidget(self.edtEmail, i,1, 1,1) + + i += 1 + layout.addWidget(self.lblSubject, i,0, 1,1) + layout.addWidget(self.edtSubject, i,1, 1,1) + + i += 1 + layout.addWidget(QLabel(tr("Description of problem:")), i,0, 1,2) + + i += 1 + layout.addWidget(self.txtDescr, i,0, 1,2) + + i += 1 + frmchkbtn = makeHorizFrame([self.chkNoLog, self.btnMoreInfo, 'Stretch']) + layout.addWidget(frmchkbtn, i,0, 1,2) + + i += 1 + layout.addWidget(self.noLogWarn, i,0, 1,2) + + i += 1 + layout.addWidget(self.btnbox, i,0, 1,2) + + self.setLayout(layout) + self.setWindowTitle(tr('Submit a Bug Report')) + self.setWindowIcon(QIcon(self.main.iconfile)) + + ############################################################################# + def submitReport(self): + if self.main.getUserAgreeToPrivacy(True): + self.userAgreedToPrivacyPolicy = True + else: + return + + emailAddr = unicode(self.edtEmail.text()).strip() + emailLen = lenBytes(emailAddr) + + subjectText = unicode(self.edtSubject.text()).strip() + subjectLen = lenBytes(subjectText) + + description = unicode(self.txtDescr.toPlainText()).strip() + descrLen = lenBytes(description) + + + if emailLen == 0 or not '@' in emailAddr: + reply = MsgBoxCustom(MSGBOX.Warning, tr('Missing Email'), tr(""" + You must supply a valid email address so we can follow up on your + request."""), \ + noStr=tr('Go Back'), yesStr=tr('Submit without Email')) + + if not reply: + return + else: + emailAddr = '' + + + if descrLen < 10: + QMessageBox.warning(self, tr('Empty Description'), tr(""" + You must describe what problem you are having, and any steps + to reproduce the problem. The Armory team cannot look for + problems in the log file if it doesn't know what those problems + are!."""), QMessageBox.Ok) + return + + maxDescr = 16384 + if descrLen > maxDescr: + reply = MsgBoxCustom(MSGBOX.Warning, tr('Long Description'), tr(""" + You have exceeded the maximum size of the description that can + be submitted to our ticket system, which is %d bytes. + If you click "Continue", the last %d bytes of your description + will be removed before sending.""") % (maxDescr, descrLen-maxDescr), \ + noStr=tr('Go Back'), yesStr=tr('Continue')) + if not reply: + return + else: + description = unicode_truncate(description, maxDescr) + + + # This is a unique-but-not-traceable ID, to simply match users to log files + uniqID = binary_to_base58(hash256(USER_HOME_DIR)[:4]) + dateStr = unixTimeToFormatStr(RightNow(), '%Y%m%d_%H%M') + osvariant = OS_VARIANT[0] if OS_MACOSX else '-'.join(OS_VARIANT) + + reportMap = {} + reportMap['uniqID'] = uniqID + reportMap['OSmajor'] = OS_NAME + reportMap['OSvariant'] = osvariant + reportMap['ArmoryVer'] = getVersionString(BTCARMORY_VERSION) + reportMap['TotalRAM'] = '%0.2f' % SystemSpecs.Memory + reportMap['isAmd64'] = str(SystemSpecs.IsX64).lower() + reportMap['userEmail'] = emailAddr + reportMap['userSubject'] = subjectText + reportMap['userDescr'] = description + reportMap['userTime'] = unixTimeToFormatStr(RightNow()) + reportMap['userTimeUTC'] = unixTimeToFormatStr(RightNowUTC()) + reportMap['agreedPrivacy'] = str(self.userAgreedToPrivacyPolicy) + + combinedLogName = 'armory_log_%s_%s.txt' % (uniqID, dateStr) + combinedLogPath = os.path.join(ARMORY_HOME_DIR, combinedLogName) + self.main.saveCombinedLogFile(combinedLogPath) + + if self.chkNoLog.isChecked(): + reportMap['fileLog'] = '' + else: + with open(combinedLogPath, 'r') as f: + reportMap['fileLog'] = f.read() + + LOGDEBUG('Sending the following dictionary of values to server') + for key,val in reportMap.iteritems(): + if key=='fileLog': + LOGDEBUG(key.ljust(12) + ': ' + binary_to_hex(sha256(val))) + else: + LOGDEBUG(key.ljust(12) + ': ' + val) + + expectedResponseMap = {} + expectedResponseMap['logHash'] = binary_to_hex(sha256(reportMap['fileLog'])) + + try: + import urllib3 + http = urllib3.PoolManager() + headers = urllib3.make_headers('ArmoryBugReportWindowNotABrowser') + response = http.request('POST', BUG_REPORT_URL, reportMap, headers) + responseMap = ast.literal_eval(response._body) + + + LOGINFO('-'*50) + LOGINFO('Response JSON:') + for key,val in responseMap.iteritems(): + LOGINFO(key.ljust(12) + ': ' + str(val)) + + LOGINFO('-'*50) + LOGINFO('Expected JSON:') + for key,val in expectedResponseMap.iteritems(): + LOGINFO(key.ljust(12) + ': ' + str(val)) + + + LOGDEBUG('Connection info:') + LOGDEBUG(' status: ' + str(response.status)) + LOGDEBUG(' version: ' + str(response.version)) + LOGDEBUG(' reason: ' + str(response.reason)) + LOGDEBUG(' strict: ' + str(response.strict)) + + + if responseMap==expectedResponseMap: + LOGINFO('Server verified receipt of log file') + cemail = 'contact@bitcoinarmory.com' + QMessageBox.information(self, tr('Submitted!'), tr(""" + Your report was submitted successfully! +

    + You should receive and email shortly from our support system. + If you do not receive it, you should follow up your request + with an email to %s. If you do, please + attach the following file to your email: +

    + %s +

    + Please be aware that the team receives lots of reports, + so it may take a few days for the team to get back to + you.""") % (cemail, cemail, combinedLogPath), QMessageBox.Ok) + self.accept() + else: + raise ConnectionError('Failed to send bug report') + + except: + LOGEXCEPT('Failed:') + bugpage = 'https://bitcoinarmory.com/support/' + QMessageBox.information(self, tr('Submitted!'), tr(""" + There was a problem submitting your bug report. It is recommended + that you submit this information through our webpage instead: +

    + %s""") % (bugpage, bugpage), QMessageBox.Ok) + self.reject() + + +################################################################################ +# Hack! We need to replicate the DlgBugReport... but to be as safe as +# possible for 0.91.1, we simply duplicate the dialog and modify directly. +# TODO: There's definitely a way to make DlgBugReport more generic so that +# both these contexts can be handled by it. +class DlgInconsistentWltReport(ArmoryDialog): + + def __init__(self, parent, main, logPathList): + super(DlgInconsistentWltReport, self).__init__(parent, main) + + + QMessageBox.critical(self, tr('Inconsistent Wallet!'), tr(""" + Important: Wallet Consistency + Issues Detected! +

    + Armory now detects certain kinds of hardware errors, and one + or more of your wallets + was flagged. The consistency logs need to be analyzed by the + Armory team to determine if any further action is required. +

    + This warning will pop up every time you start Armory until + the wallet is fixed""") % (htmlColor('TextWarn')), + QMessageBox.Ok) + + + + # logPathList is [wltID, corruptFolder] pairs + self.logPathList = logPathList[:] + walletList = [self.main.walletMap[wid] for wid,folder in logPathList] + + getWltStr = lambda w: 'Wallet "%s" (%s)' % \ + (w.labelName, w.uniqueIDB58) + + if len(logPathList) == 1: + wltDispStr = getWltStr(walletList[0]) + ' is' + else: + strList = [getWltStr(w) for w in walletList] + wltDispStr = ', '.join(strList[:-1]) + ' and ' + strList[-1] + ' are ' + + lblTopDescr = QRichLabel(tr(""" + Submit Wallet Analysis Logs for + Review
    """) % htmlColor('TextWarn'), + hAlign=Qt.AlignHCenter) + + lblDescr = QRichLabel(tr(""" + Armory has detected that %s inconsistent, + possibly due to hardware errors out of our control. It strongly + recommended you submit the wallet logs to the Armory team + for review. Until you hear back from an Armory representative, + we recommend: +
      +
    • Do not delete any data in your Armory home directory
    • +
    • Do not send or receive any funds with the affected + wallet(s)
    • +
    • Create a backup of the wallet analysis logs
    • +
    + """) % (wltDispStr)) + + self.chkIncludeReg = QCheckBox(tr("""Include all log files""")) + self.chkIncludeWOW = QCheckBox(tr("""Include watch-only + @{wallet|wallets}@""", pluralList=len(walletList))) + self.chkIncludeWOW.setChecked(False) + self.chkIncludeReg.setChecked(True) + + self.btnMoreInfo = QLabelButton('Privacy Warning') + self.connect(self.btnMoreInfo, SIGNAL(CLICKED), \ + self.main.logFileTriplePrivacyWarning) + + + btnBackupLogs = QPushButton(tr("Save backup of log files")) + self.connect(btnBackupLogs, SIGNAL('clicked()'), self.doBackupLogs) + frmBackup = makeHorizFrame(['Stretch', btnBackupLogs, 'Stretch']) + + + self.lblEmail = QRichLabel(tr('Email Address:')) + self.edtEmail = QLineEdit() + self.edtEmail.setMaxLength(100) + + self.lblSubject = QRichLabel(tr('Subject:')) + self.edtSubject = QLineEdit() + self.edtSubject.setMaxLength(64) + self.edtSubject.setText("Wallet Consistency Logs") + + self.txtDescr = QTextEdit() + self.txtDescr.setFont(GETFONT('Fixed', 9)) + w,h = tightSizeNChar(self, 80) + self.txtDescr.setMinimumWidth(w) + self.txtDescr.setMinimumHeight(int(2.5*h)) + + self.btnSubmit = QPushButton(tr('Submit Data to ATI')) + self.btnCancel = QPushButton(tr('Cancel')) + self.btnbox = QDialogButtonBox() + self.btnbox.addButton(self.btnSubmit, QDialogButtonBox.AcceptRole) + self.btnbox.addButton(self.btnCancel, QDialogButtonBox.RejectRole) + self.connect(self.btnSubmit, SIGNAL(CLICKED), self.submitReport) + self.connect(self.btnCancel, SIGNAL(CLICKED), self, SLOT('reject()')) + + armoryver = getVersionString(BTCARMORY_VERSION) + lblDetect = QRichLabel( tr(""" + Detected: %s (%s) / %0.2f GB RAM / Armory version %s
    + (this data will be included with the data + submission""") % \ + (OS_NAME, OS_VARIANT[0], SystemSpecs.Memory, armoryver)) + + + layout = QGridLayout() + i = -1 + + i += 1 + layout.addWidget(lblTopDescr, i,0, 1,2) + + i += 1 + layout.addWidget(lblDescr, i,0, 1,2) + + i += 1 + layout.addWidget(frmBackup, i,0, 1,2) + + i += 1 + layout.addWidget(HLINE(), i,0, 1,2) + + + i += 1 + layout.addWidget(self.lblEmail, i,0, 1,1) + layout.addWidget(self.edtEmail, i,1, 1,1) + + i += 1 + layout.addWidget(self.lblSubject, i,0, 1,1) + layout.addWidget(self.edtSubject, i,1, 1,1) + + i += 1 + layout.addWidget(QLabel(tr("Additional Info:")), i,0, 1,2) + + i += 1 + layout.addWidget(self.txtDescr, i,0, 1,2) + + i += 1 + frmChkBtnRL = makeHorizFrame([self.chkIncludeReg, + self.chkIncludeWOW, + self.btnMoreInfo,]) + layout.addWidget(frmChkBtnRL, i,0, 1,2) + + + i += 1 + layout.addWidget(self.btnbox, i,0, 1,2) + + self.setLayout(layout) + self.setWindowTitle(tr('Send Wallet Logs to ATI')) + self.setWindowIcon(QIcon(self.main.iconfile)) + + + ############################################################################# + def submitReport(self): + + self.userAgreedToPrivacyPolicy = False + if self.main.getUserAgreeToPrivacy(True): + self.userAgreedToPrivacyPolicy = True + else: + return + + emailAddr = unicode(self.edtEmail.text()).strip() + emailLen = lenBytes(emailAddr) + + subjectText = unicode(self.edtSubject.text()).strip() + subjectLen = lenBytes(subjectText) + + description = unicode(self.txtDescr.toPlainText()).strip() + descrLen = lenBytes(description) + + + if emailLen == 0 or not '@' in emailAddr: + QMessageBox.warning(self, tr('Missing Email'), tr(""" + You must supply a valid email address so we can follow up on your + submission."""), QMessageBox.Ok) + return + + + + maxDescr = 16384 + if descrLen > maxDescr: + reply = MsgBoxCustom(MSGBOX.Warning, tr('Long Description'), tr(""" + You have exceeded the maximum size of the description that can + be submitted to our ticket system, which is %d bytes. + If you click "Continue", the last %d bytes of your description + will be removed before sending.""") % (maxDescr, descrLen-maxDescr), \ + noStr=tr('Go Back'), yesStr=tr('Continue')) + + if not reply: + return + else: + description = unicode_truncate(description, maxDescr) + + + # This is a unique-but-not-traceable ID, to simply match users to log files + uniqID = binary_to_base58(hash256(USER_HOME_DIR)[:4]) + dateStr = unixTimeToFormatStr(RightNow(), '%Y%m%d_%H%M') + osvariant = OS_VARIANT[0] if OS_MACOSX else '-'.join(OS_VARIANT) + + reportMap = {} + reportMap['uniqID'] = uniqID + reportMap['OSmajor'] = OS_NAME + reportMap['OSvariant'] = osvariant + reportMap['ArmoryVer'] = getVersionString(BTCARMORY_VERSION) + reportMap['TotalRAM'] = '%0.2f' % SystemSpecs.Memory + reportMap['isAmd64'] = str(SystemSpecs.IsX64).lower() + reportMap['userEmail'] = emailAddr + reportMap['userSubject'] = subjectText + reportMap['userDescr'] = description + reportMap['userTime'] = unixTimeToFormatStr(RightNow()) + reportMap['userTimeUTC'] = unixTimeToFormatStr(RightNowUTC()) + reportMap['agreedPrivacy'] = str(self.userAgreedToPrivacyPolicy) + + fileUploadKey = 'fileWalletLogs' + + # Create a zip file of all logs (for all dirs), and put raw into map + zpath = self.createZipfile() + with open(zpath, 'rb') as f: + reportMap[fileUploadKey] = f.read() + + LOGDEBUG('Sending the following dictionary of values to server') + for key,val in reportMap.iteritems(): + if key==fileUploadKey: + LOGDEBUG(key.ljust(12) + ': ' + binary_to_hex(sha256(val))) + else: + LOGDEBUG(key.ljust(12) + ': ' + val) + + + expectedResponseMap = {} + with open(zpath, 'rb') as f: + expectedResponseMap['fileWalletLogsHash'] = \ + binary_to_hex(sha256(f.read())) + + try: + + import urllib3 + http = urllib3.PoolManager() + headers = urllib3.make_headers('ArmoryBugReportWindowNotABrowser') + response = http.request('POST', BUG_REPORT_URL, reportMap, headers) + responseMap = ast.literal_eval(response._body) + + + LOGINFO('-'*50) + LOGINFO('Response JSON:') + for key,val in responseMap.iteritems(): + LOGINFO(key.ljust(12) + ': ' + str(val)) + + LOGINFO('-'*50) + LOGINFO('Expected JSON:') + for key,val in expectedResponseMap.iteritems(): + LOGINFO(key.ljust(12) + ': ' + str(val)) + + + LOGDEBUG('Connection info:') + LOGDEBUG(' status: ' + str(response.status)) + LOGDEBUG(' version: ' + str(response.version)) + LOGDEBUG(' reason: ' + str(response.reason)) + LOGDEBUG(' strict: ' + str(response.strict)) + + + if responseMap==expectedResponseMap: + LOGINFO('Server verified receipt of log file') + cemail = 'contact@bitcoinarmory.com' + QMessageBox.information(self, tr('Submitted!'), tr(""" + Your report was submitted successfully! +

    + You should receive and email shortly from our support system. + If you do not receive it, you should follow up your request + with an email to %s. + You should hear back from an Armory representative within + 24 hours.""") % (cemail, cemail), QMessageBox.Ok) + self.accept() + else: + raise ConnectionError('Failed to send bug report') + + except: + LOGEXCEPT('Failed:') + bugpage = 'https://bitcoinarmory.com/support/' + QMessageBox.information(self, tr('Submission Error!'), tr(""" + There was a problem submitting your data through Armory. + Please create a new support ticket using our webpage, and attach + the following file to it: +

    + %s +

    + Click below to go to the support page to open a new ticket. +

    + %s""") % (zpath, bugpage, bugpage), QMessageBox.Ok) + + try: + strOut = 'Raw response from server:\n' + strOut += response.text + LOGINFO(strOut) + except: + # Get here if response._body doesn't exist... never got that far + pass + + self.reject() + + + ############################################################################# + def createZipfile(self, zfilePath=None, forceIncludeAllData=False): + """ + If not forceIncludeAllData, then we will exclude wallet file and/or + regular logs, depending on the user's checkbox selection. For making + a user backup, we always want to include everything, regardless of + that selection. + """ + + # Should we include wallet files from logs directory? + includeWlt = self.chkIncludeWOW.isChecked() + includeReg = self.chkIncludeReg.isChecked() + + # Set to default save path if needed + if zfilePath is None: + zfilePath = os.path.join(ARMORY_HOME_DIR, 'wallet_analyze_logs.zip') + + # Remove a previous copy + if os.path.exists(zfilePath): + os.remove(zfilePath) + + LOGINFO('Creating archive: %s', zfilePath) + zfile = ZipFile(zfilePath, 'w', ZIP_DEFLATED) + + # Iterate over all log directories (usually one) + for wltID,logDir in self.logPathList: + for fn in os.listdir(logDir): + fullpath = os.path.join(logDir, fn) + + # If multiple dirs, will see duplicate armorylogs and multipliers + if not os.path.isfile(fullpath): + continue + + + if not forceIncludeAllData: + # Exclude any wallet files if the checkbox was not checked + if not includeWlt and os.path.getsize(fullpath) >= 8: + # Don't exclude based on file extension, check leading bytes + with open(fullpath, 'rb') as tempopen: + if tempopen.read(8) == '\xbaWALLET\x00': + continue + + # Exclude regular logs as well, if desired + if not includeReg and fn in ['armorylog.txt', 'armorycpplog.txt']: + continue + + + # If we got here, add file to archive + parentDir = os.path.basename(logDir) + archiveName = '%s_%s_%s' % (wltID, parentDir, fn) + LOGINFO(' Adding %s to archive' % archiveName) + zfile.write(fullpath, archiveName) + + zfile.close() + + return zfilePath + + + ############################################################################# + def doBackupLogs(self): + saveTo = self.main.getFileSave(ffilter=['Zip files (*.zip)'], + defaultFilename='wallet_analyze_logs.zip') + if not saveTo: + QMessageBox.critical(self, tr("Not saved"), tr(""" + You canceled the backup operation. No backup was made."""), + QMessageBox.Ok) + return + + try: + self.createZipfile(saveTo, forceIncludeAllData=True) + QMessageBox.information(self, tr('Success'), tr(""" + The wallet logs were successfully saved to the following + location: +

    + %s +

    + It is still important to complete the rest of this form + and submit the data to the Armory team for review!""") % \ + saveTo, QMessageBox.Ok) + + except: + LOGEXCEPT('Failed to create zip file') + QMessageBox.warning(self, tr('Save Failed'), tr("""There was an + error saving a copy of your log files"""), QMessageBox.Ok) + + + ################################################################################ class DlgNewWallet(ArmoryDialog): @@ -428,8 +1060,9 @@ def __init__(self, parent=None, main=None, initLabel=''): self.selectedImport = False # Options for creating a new wallet - lblDlgDescr = QLabel('Create a new wallet for managing your funds.\n' - 'The name and description can be changed at any time.') + lblDlgDescr = QRichLabel(tr(""" + Create a new wallet for managing your funds.
    + The name and description can be changed at any time.""")) lblDlgDescr.setWordWrap(True) self.edtName = QLineEdit() @@ -449,28 +1082,28 @@ def __init__(self, parent=None, main=None, initLabel=''): QDialogButtonBox.Cancel) - + # Advanced Encryption Options - lblComputeDescr = QLabel( \ + lblComputeDescr = QLabel(\ 'Armory will test your system\'s speed to determine the most ' 'challenging encryption settings that can be performed ' 'in a given amount of time. High settings make it much harder ' 'for someone to guess your passphrase. This is used for all ' 'encrypted wallets, but the default parameters can be changed below.\n') lblComputeDescr.setWordWrap(True) - timeDescrTip = self.main.createToolTipWidget( \ + timeDescrTip = self.main.createToolTipWidget(\ 'This is the amount of time it will take for your computer ' 'to unlock your wallet after you enter your passphrase. ' '(the actual time used will be less than the specified ' 'time, but more than one half of it). ') - - + + # Set maximum compute time self.edtComputeTime = QLineEdit() self.edtComputeTime.setText('250 ms') self.edtComputeTime.setMaxLength(12) lblComputeTime = QLabel('Target compute &time (s, ms):') - memDescrTip = self.main.createToolTipWidget( \ + memDescrTip = self.main.createToolTipWidget(\ 'This is the maximum memory that will be ' 'used as part of the encryption process. The actual value used ' 'may be lower, depending on your system\'s speed. If a ' @@ -485,30 +1118,30 @@ def __init__(self, parent=None, main=None, initLabel=''): self.edtComputeMem = QLineEdit() self.edtComputeMem.setText('32.0 MB') self.edtComputeMem.setMaxLength(12) - lblComputeMem = QLabel('Max &memory usage (kB, MB):') + lblComputeMem = QLabel('Max &memory usage (kB, MB):') lblComputeMem.setBuddy(self.edtComputeMem) - self.edtComputeTime.setMaximumWidth( tightSizeNChar(self, 20)[0] ) - self.edtComputeMem.setMaximumWidth( tightSizeNChar(self, 20)[0] ) + self.edtComputeTime.setMaximumWidth(tightSizeNChar(self, 20)[0]) + self.edtComputeMem.setMaximumWidth(tightSizeNChar(self, 20)[0]) # Fork watching-only wallet cryptoLayout = QGridLayout() - cryptoLayout.addWidget(lblComputeDescr, 0, 0, 1, 3) + cryptoLayout.addWidget(lblComputeDescr, 0, 0, 1, 3) - cryptoLayout.addWidget(timeDescrTip, 1, 0, 1, 1) - cryptoLayout.addWidget(lblComputeTime, 1, 1, 1, 1) - cryptoLayout.addWidget(self.edtComputeTime, 1, 2, 1, 1) + cryptoLayout.addWidget(timeDescrTip, 1, 0, 1, 1) + cryptoLayout.addWidget(lblComputeTime, 1, 1, 1, 1) + cryptoLayout.addWidget(self.edtComputeTime, 1, 2, 1, 1) - cryptoLayout.addWidget(memDescrTip, 2, 0, 1, 1) - cryptoLayout.addWidget(lblComputeMem, 2, 1, 1, 1) - cryptoLayout.addWidget(self.edtComputeMem, 2, 2, 1, 1) + cryptoLayout.addWidget(memDescrTip, 2, 0, 1, 1) + cryptoLayout.addWidget(lblComputeMem, 2, 1, 1, 1) + cryptoLayout.addWidget(self.edtComputeMem, 2, 2, 1, 1) self.cryptoFrame = QFrame() self.cryptoFrame.setFrameStyle(STYLE_SUNKEN) self.cryptoFrame.setLayout(cryptoLayout) self.cryptoFrame.setVisible(False) - self.chkUseCrypto = QCheckBox("Use wallet &encryption") + self.chkUseCrypto = QCheckBox("Use wallet &encryption") self.chkUseCrypto.setChecked(True) usecryptoTooltip = self.main.createToolTipWidget( 'Encryption prevents anyone who accesses your computer ' @@ -531,54 +1164,54 @@ def __init__(self, parent=None, main=None, initLabel=''): 'Anyone who gets ahold of your paper backup will be able to spend ' 'the money in your wallet, so please secure it appropriately.') - - self.btnAccept = QPushButton("Accept") - self.btnCancel = QPushButton("Cancel") + + self.btnAccept = QPushButton("Accept") + self.btnCancel = QPushButton("Cancel") self.btnAdvCrypto = QPushButton("Adv. Encrypt Options>>>") self.btnAdvCrypto.setCheckable(True) self.btnbox = QDialogButtonBox() self.btnbox.addButton(self.btnAdvCrypto, QDialogButtonBox.ActionRole) - self.btnbox.addButton(self.btnCancel, QDialogButtonBox.RejectRole) - self.btnbox.addButton(self.btnAccept, QDialogButtonBox.AcceptRole) + self.btnbox.addButton(self.btnCancel, QDialogButtonBox.RejectRole) + self.btnbox.addButton(self.btnAccept, QDialogButtonBox.AcceptRole) self.connect(self.btnAdvCrypto, SIGNAL('toggled(bool)'), \ - self.cryptoFrame, SLOT('setVisible(bool)')) - self.connect(self.btnAccept, SIGNAL('clicked()'), \ + self.cryptoFrame, SLOT('setVisible(bool)')) + self.connect(self.btnAccept, SIGNAL(CLICKED), \ self.verifyInputsBeforeAccept) - self.connect(self.btnCancel, SIGNAL('clicked()'), \ - self, SLOT('reject()')) + self.connect(self.btnCancel, SIGNAL(CLICKED), \ + self, SLOT('reject()')) self.btnImportWlt = QPushButton("Import wallet...") - self.connect( self.btnImportWlt, SIGNAL("clicked()"), \ + self.connect(self.btnImportWlt, SIGNAL("clicked()"), \ self.importButtonClicked) - + masterLayout = QGridLayout() - masterLayout.addWidget(lblDlgDescr, 1, 0, 1, 2) - #masterLayout.addWidget(self.btnImportWlt, 1, 2, 1, 1) - masterLayout.addWidget(lblName, 2, 0, 1, 1) - masterLayout.addWidget(self.edtName, 2, 1, 1, 2) - masterLayout.addWidget(lblDescr, 3, 0, 1, 2) - masterLayout.addWidget(self.edtDescr, 3, 1, 2, 2) - masterLayout.addWidget(self.chkUseCrypto, 5, 0, 1, 1) - masterLayout.addWidget(usecryptoTooltip, 5, 1, 1, 1) + masterLayout.addWidget(lblDlgDescr, 1, 0, 1, 2) + # masterLayout.addWidget(self.btnImportWlt, 1, 2, 1, 1) + masterLayout.addWidget(lblName, 2, 0, 1, 1) + masterLayout.addWidget(self.edtName, 2, 1, 1, 2) + masterLayout.addWidget(lblDescr, 3, 0, 1, 2) + masterLayout.addWidget(self.edtDescr, 3, 1, 2, 2) + masterLayout.addWidget(self.chkUseCrypto, 5, 0, 1, 1) + masterLayout.addWidget(usecryptoTooltip, 5, 1, 1, 1) masterLayout.addWidget(self.chkPrintPaper, 6, 0, 1, 1) masterLayout.addWidget(paperBackupTooltip, 6, 1, 1, 1) - masterLayout.addWidget(self.cryptoFrame, 8, 0, 3, 3) - - masterLayout.addWidget(self.btnbox, 11, 0, 1, 2) + masterLayout.addWidget(self.cryptoFrame, 8, 0, 3, 3) + + masterLayout.addWidget(self.btnbox, 11, 0, 1, 2) masterLayout.setVerticalSpacing(5) - + self.setLayout(masterLayout) self.layout().setSizeConstraint(QLayout.SetFixedSize) self.connect(self.chkUseCrypto, SIGNAL("clicked()"), \ - self.cryptoFrame, SLOT("setEnabled(bool)")) + self.cryptoFrame, SLOT("setEnabled(bool)")) self.setWindowTitle('Create Armory wallet') - self.setWindowIcon(QIcon( self.main.iconfile)) + self.setWindowIcon(QIcon(self.main.iconfile)) @@ -589,29 +1222,29 @@ def importButtonClicked(self): def verifyInputsBeforeAccept(self): ### Confirm that the name and descr are within size limits ####### - wltName = self.edtName.text() + wltName = self.edtName.text() wltDescr = self.edtDescr.toPlainText() - if len(wltName)<1: + if len(wltName) < 1: QMessageBox.warning(self, 'Invalid wallet name', \ 'You must enter a name for this wallet, up to 32 characters.', \ QMessageBox.Ok) return False - - if len(wltDescr)>256: + + if len(wltDescr) > 256: reply = QMessageBox.warning(self, 'Input too long', \ 'The wallet description is limited to 256 characters. Only the first ' '256 characters will be used.', \ QMessageBox.Ok | QMessageBox.Cancel) - if reply==QMessageBox.Ok: - self.edtDescr.setText( wltDescr[:256]) + if reply == QMessageBox.Ok: + self.edtDescr.setText(wltDescr[:256]) else: return False ### Check that the KDF inputs are well-formed #################### try: - kdfT, kdfUnit = str(self.edtComputeTime.text()).strip().split(' ') - if kdfUnit.lower()=='ms': - self.kdfSec = float(kdfT)/1000. + kdfT, kdfUnit = str(self.edtComputeTime.text()).strip().split(' ') + if kdfUnit.lower() == 'ms': + self.kdfSec = float(kdfT) / 1000. elif kdfUnit.lower() in ('s', 'sec', 'seconds'): self.kdfSec = float(kdfT) @@ -622,12 +1255,12 @@ def verifyInputsBeforeAccept(self): return False kdfM, kdfUnit = str(self.edtComputeMem.text()).split(' ') - if kdfUnit.lower()=='mb': - self.kdfBytes = round(float(kdfM)*(1024.0**2) ) - if kdfUnit.lower()=='kb': - self.kdfBytes = round(float(kdfM)*(1024.0)) + if kdfUnit.lower() == 'mb': + self.kdfBytes = round(float(kdfM) * (1024.0 ** 2)) + if kdfUnit.lower() == 'kb': + self.kdfBytes = round(float(kdfM) * (1024.0)) - if not (2**15 <= self.kdfBytes <= 2**31): + if not (2 ** 15 <= self.kdfBytes <= 2 ** 31): QMessageBox.critical(self, 'Invalid KDF Parameters', \ 'Please specify a maximum memory usage between 32 kB ' 'and 2048 MB.') @@ -640,17 +1273,17 @@ def verifyInputsBeforeAccept(self): '"250 ms" or "2.1 s". Specify memory as kB or MB, such as ' '"32 MB" or "256 kB". ', QMessageBox.Ok) return False - - + + self.accept() - - + + def getImportWltPath(self): self.importFile = QFileDialog.getOpenFileName(self, 'Import Wallet File', \ - ARMORY_HOME_DIR, 'Wallet files (*.wallet);; All files (*)') + ARMORY_HOME_DIR, 'Wallet files (*.wallet);; All files (*)') if self.importFile: self.accept() - + @@ -675,8 +1308,8 @@ def __init__(self, parent=None, main=None, noPrevEncrypt=True): self.edtPasswdOrig.setEchoMode(QLineEdit.Password) self.edtPasswdOrig.setMinimumWidth(MIN_PASSWD_WIDTH(self)) lblCurrPasswd = QLabel('Current Passphrase:') - layout.addWidget(lblCurrPasswd, 1, 0) - layout.addWidget(self.edtPasswdOrig, 1, 1) + layout.addWidget(lblCurrPasswd, 1, 0) + layout.addWidget(self.edtPasswdOrig, 1, 1) @@ -690,22 +1323,22 @@ def __init__(self, parent=None, main=None, noPrevEncrypt=True): self.edtPasswd2.setEchoMode(QLineEdit.Password) self.edtPasswd2.setMinimumWidth(MIN_PASSWD_WIDTH(self)) - layout.addWidget(lblPwd1, 2,0) - layout.addWidget(lblPwd2, 3,0) - layout.addWidget(self.edtPasswd1, 2,1) - layout.addWidget(self.edtPasswd2, 3,1) + layout.addWidget(lblPwd1, 2, 0) + layout.addWidget(lblPwd2, 3, 0) + layout.addWidget(self.edtPasswd1, 2, 1) + layout.addWidget(self.edtPasswd2, 3, 1) - self.lblMatches = QLabel(' '*20) + self.lblMatches = QLabel(' ' * 20) self.lblMatches.setTextFormat(Qt.RichText) - layout.addWidget(self.lblMatches, 4,1) + layout.addWidget(self.lblMatches, 4, 1) self.chkDisableCrypt = QCheckBox('Disable encryption for this wallet') if not noPrevEncrypt: self.connect(self.chkDisableCrypt, SIGNAL('toggled(bool)'), \ self.disablePassphraseBoxes) - layout.addWidget(self.chkDisableCrypt, 4,0) - + layout.addWidget(self.chkDisableCrypt, 4, 0) + self.btnAccept = QPushButton("Accept") self.btnCancel = QPushButton("Cancel") @@ -719,7 +1352,7 @@ def __init__(self, parent=None, main=None, noPrevEncrypt=True): else: self.setWindowTitle("Change Encryption Passphrase") - self.setWindowIcon(QIcon( self.main.iconfile)) + self.setWindowIcon(QIcon(self.main.iconfile)) self.setLayout(layout) @@ -728,16 +1361,16 @@ def __init__(self, parent=None, main=None, noPrevEncrypt=True): self.connect(self.edtPasswd2, SIGNAL('textChanged(QString)'), \ self.checkPassphrase) - self.connect(self.btnAccept, SIGNAL('clicked()'), \ + self.connect(self.btnAccept, SIGNAL(CLICKED), \ self.checkPassphraseFinal) - self.connect(self.btnCancel, SIGNAL('clicked()'), \ - self, SLOT('reject()')) + self.connect(self.btnCancel, SIGNAL(CLICKED), \ + self, SLOT('reject()')) def disablePassphraseBoxes(self, noEncrypt=True): - self.edtPasswd1.setEnabled(not noEncrypt) - self.edtPasswd2.setEnabled(not noEncrypt) + self.edtPasswd1.setEnabled(not noEncrypt) + self.edtPasswd2.setEnabled(not noEncrypt) def checkPassphrase(self): @@ -746,20 +1379,20 @@ def checkPassphrase(self): p1 = self.edtPasswd1.text() p2 = self.edtPasswd2.text() goodColor = htmlColor('TextGreen') - badColor = htmlColor('TextRed') + badColor = htmlColor('TextRed') if not isASCII(unicode(p1)) or \ not isASCII(unicode(p2)): self.lblMatches.setText('Passphrase is non-ASCII!' % badColor) return False - if not p1==p2: + if not p1 == p2: self.lblMatches.setText('Passphrases do not match!' % badColor) return False - if len(p1)<5: + if len(p1) < 5: self.lblMatches.setText('Passphrase is too short!' % badColor) return False self.lblMatches.setText('Passphrases match!' % goodColor) return True - + def checkPassphraseFinal(self): if self.chkDisableCrypt.isChecked(): @@ -772,7 +1405,7 @@ def checkPassphraseFinal(self): QMessageBox.critical(self, 'Invalid Passphrase', \ 'You entered your confirmation passphrase incorrectly!', QMessageBox.Ok) else: - self.accept() + self.accept() else: self.reject() @@ -787,10 +1420,10 @@ def __init__(self, parent=None, main=None): lblWarnImgL.setPixmap(QPixmap(':/MsgBox_warning48.png')) lblWarnImgL.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) - lblWarnTxt1 = QRichLabel( \ + lblWarnTxt1 = QRichLabel(\ '!!! DO NOT FORGET YOUR PASSPHRASE !!!', size=4) lblWarnTxt1.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) - lblWarnTxt2 = QRichLabel( \ + lblWarnTxt2 = QRichLabel(\ 'No one can help you recover you bitcoins if you forget the ' 'passphrase and don\'t have a paper backup! Your wallet and ' 'any digital backups are useless if you forget it. ' @@ -804,7 +1437,7 @@ def __init__(self, parent=None, main=None): 'Please enter your passphrase a third time to indicate that you ' 'are aware of the risks of losing your passphrase!
    ', doWrap=True) - + self.edtPasswd3 = QLineEdit() self.edtPasswd3.setEchoMode(QLineEdit.Password) self.edtPasswd3.setMinimumWidth(MIN_PASSWD_WIDTH(self)) @@ -812,16 +1445,16 @@ def __init__(self, parent=None, main=None): bbox = QDialogButtonBox() btnOk = QPushButton('Accept') btnNo = QPushButton('Cancel') - self.connect(btnOk, SIGNAL('clicked()'), self.accept) - self.connect(btnNo, SIGNAL('clicked()'), self.reject) + self.connect(btnOk, SIGNAL(CLICKED), self.accept) + self.connect(btnNo, SIGNAL(CLICKED), self.reject) bbox.addButton(btnOk, QDialogButtonBox.AcceptRole) bbox.addButton(btnNo, QDialogButtonBox.RejectRole) layout = QGridLayout() - layout.addWidget(lblWarnImgL, 0, 0, 4, 1) - layout.addWidget(lblWarnTxt1, 0, 1, 1, 1) - layout.addWidget(lblWarnTxt2, 2, 1, 1, 1) - layout.addWidget(self.edtPasswd3, 5, 1, 1, 1) - layout.addWidget(bbox, 6, 1, 1, 2) + layout.addWidget(lblWarnImgL, 0, 0, 4, 1) + layout.addWidget(lblWarnTxt1, 0, 1, 1, 1) + layout.addWidget(lblWarnTxt2, 2, 1, 1, 1) + layout.addWidget(self.edtPasswd3, 5, 1, 1, 1) + layout.addWidget(bbox, 6, 1, 1, 2) self.setLayout(layout) self.setWindowTitle('WARNING!') @@ -839,7 +1472,7 @@ def __init__(self, currName='', currDescr='', parent=None, main=None): self.edtDescr = QTextEdit() tightHeight = tightSizeNChar(self.edtDescr, 1)[1] - self.edtDescr.setMaximumHeight(tightHeight*4.2) + self.edtDescr.setMaximumHeight(tightHeight * 4.2) lblDescr = QLabel("Wallet &description:") lblDescr.setAlignment(Qt.AlignVCenter) lblDescr.setBuddy(self.edtDescr) @@ -853,13 +1486,13 @@ def __init__(self, currName='', currDescr='', parent=None, main=None): self.connect(buttonBox, SIGNAL('rejected()'), self.reject) layout = QGridLayout() - layout.addWidget(lblName, 1, 0, 1, 1) - layout.addWidget(self.edtName, 1, 1, 1, 1) - layout.addWidget(lblDescr, 2, 0, 1, 1) - layout.addWidget(self.edtDescr, 2, 1, 2, 1) - layout.addWidget(buttonBox, 4, 0, 1, 2) + layout.addWidget(lblName, 1, 0, 1, 1) + layout.addWidget(self.edtName, 1, 1, 1, 1) + layout.addWidget(lblDescr, 2, 0, 1, 1) + layout.addWidget(self.edtDescr, 2, 1, 2, 1) + layout.addWidget(buttonBox, 4, 0, 1, 2) self.setLayout(layout) - + self.setWindowTitle('Wallet Descriptions') @@ -869,16 +1502,16 @@ def accept(self, *args): UnicodeErrorBox(self) return - if len(str(self.edtName.text()).strip())==0: + if len(str(self.edtName.text()).strip()) == 0: QMessageBox.critical(self, 'Empty Name', \ 'All wallets must have a name. ', QMessageBox.Ok) return super(DlgChangeLabels, self).accept(*args) - + ################################################################################ class DlgWalletDetails(ArmoryDialog): - """ For displaying the details of a specific wallet, with options """ + """ For displaying the details of a specific wallet, with options """ ############################################################################# def __init__(self, wlt, usermode=USERMODE.Standard, parent=None, main=None): @@ -889,22 +1522,22 @@ def __init__(self, wlt, usermode=USERMODE.Standard, parent=None, main=None): self.wlt = wlt self.usermode = usermode self.wlttype, self.typestr = determineWalletType(wlt, parent) - if self.typestr=='Encrypted': - self.typestr='Encrypted (AES256)' + if self.typestr == 'Encrypted': + self.typestr = 'Encrypted (AES256)' self.labels = [wlt.labelName, wlt.labelDescr] self.passphrase = '' - self.setMinimumSize(800,400) - - w,h = relaxedSizeNChar(self,60) - viewWidth,viewHeight = w, 10*h - + self.setMinimumSize(800, 400) + + w, h = relaxedSizeNChar(self, 60) + viewWidth, viewHeight = w, 10 * h + # Address view self.wltAddrModel = WalletAddrDispModel(wlt, self) self.wltAddrProxy = WalletAddrSortProxy(self) self.wltAddrProxy.setSourceModel(self.wltAddrModel) - self.wltAddrView = QTableView() + self.wltAddrView = QTableView() self.wltAddrView.setModel(self.wltAddrProxy) self.wltAddrView.setSortingEnabled(True) @@ -915,7 +1548,7 @@ def __init__(self, wlt, usermode=USERMODE.Standard, parent=None, main=None): self.wltAddrView.setMinimumWidth(550) self.wltAddrView.setMinimumHeight(150) iWidth = tightSizeStr(self.wltAddrView, 'Imp')[0] - initialColResize(self.wltAddrView, [iWidth*1.5, 0.35, 0.4, 64, 0.2]) + initialColResize(self.wltAddrView, [iWidth * 1.5, 0.35, 0.4, 64, 0.2]) self.wltAddrView.sizeHint = lambda: QSize(700, 225) self.wltAddrView.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) @@ -923,9 +1556,9 @@ def __init__(self, wlt, usermode=USERMODE.Standard, parent=None, main=None): self.wltAddrView.setContextMenuPolicy(Qt.CustomContextMenu) self.wltAddrView.customContextMenuRequested.connect(self.showContextMenu) self.wltAddrProxy.sort(ADDRESSCOLS.ChainIdx, Qt.AscendingOrder) - + uacfv = lambda x: self.main.updateAddressCommentFromView(self.wltAddrView, self.wlt) - + self.connect(self.wltAddrView, SIGNAL('doubleClicked(QModelIndex)'), \ self.dblClickAddressView) @@ -933,7 +1566,7 @@ def __init__(self, wlt, usermode=USERMODE.Standard, parent=None, main=None): # Now add all the options buttons, dependent on the type of wallet. lbtnChangeLabels = QLabelButton('Change Wallet Labels'); - self.connect(lbtnChangeLabels, SIGNAL('clicked()'), self.changeLabels) + self.connect(lbtnChangeLabels, SIGNAL(CLICKED), self.changeLabels) if not self.wlt.watchingOnly: s = '' @@ -942,29 +1575,31 @@ def __init__(self, wlt, usermode=USERMODE.Standard, parent=None, main=None): else: s = 'Encrypt Wallet' lbtnChangeCrypto = QLabelButton(s) - self.connect(lbtnChangeCrypto, SIGNAL('clicked()'), self.changeEncryption) + self.connect(lbtnChangeCrypto, SIGNAL(CLICKED), self.changeEncryption) lbtnSendBtc = QLabelButton('Send Bitcoins') lbtnGenAddr = QLabelButton('Receive Bitcoins') lbtnImportA = QLabelButton('Import/Sweep Private Keys') lbtnDeleteA = QLabelButton('Remove Imported Address') - #lbtnSweepA = QLabelButton('Sweep Wallet/Address') + # lbtnSweepA = QLabelButton('Sweep Wallet/Address') lbtnForkWlt = QLabelButton('Create Watching-Only Copy') lbtnBackups = QLabelButton('Backup This Wallet') - lbtnRemove = QLabelButton('Delete/Remove Wallet') - - #LOGERROR('remove me!') - #fnfrag = lambda: DlgFragBackup(self, self.main, self.wlt).exec_() - #LOGERROR('remove me!') - - self.connect(lbtnSendBtc, SIGNAL('clicked()'), self.execSendBtc) - self.connect(lbtnGenAddr, SIGNAL('clicked()'), self.getNewAddress) - self.connect(lbtnBackups, SIGNAL('clicked()'), self.execBackupDlg) - #self.connect(lbtnBackups, SIGNAL('clicked()'), fnfrag) - self.connect(lbtnRemove, SIGNAL('clicked()'), self.execRemoveDlg) - self.connect(lbtnImportA, SIGNAL('clicked()'), self.execImportAddress) - self.connect(lbtnDeleteA, SIGNAL('clicked()'), self.execDeleteAddress) - self.connect(lbtnForkWlt, SIGNAL('clicked()'), self.forkOnlineWallet) + lbtnRemove = QLabelButton('Delete/Remove Wallet') + #lbtnRecover = QLabelButton('Recover Password Wallet') + + # LOGERROR('remove me!') + # fnfrag = lambda: DlgFragBackup(self, self.main, self.wlt).exec_() + # LOGERROR('remove me!') + + self.connect(lbtnSendBtc, SIGNAL(CLICKED), self.execSendBtc) + self.connect(lbtnGenAddr, SIGNAL(CLICKED), self.getNewAddress) + self.connect(lbtnBackups, SIGNAL(CLICKED), self.execBackupDlg) + # self.connect(lbtnBackups, SIGNAL(CLICKED), fnfrag) + self.connect(lbtnRemove, SIGNAL(CLICKED), self.execRemoveDlg) + self.connect(lbtnImportA, SIGNAL(CLICKED), self.execImportAddress) + self.connect(lbtnDeleteA, SIGNAL(CLICKED), self.execDeleteAddress) + self.connect(lbtnForkWlt, SIGNAL(CLICKED), self.forkOnlineWallet) + #self.connect(lbtnRecover, SIGNAL(CLICKED), self.recoverPwd) lbtnSendBtc.setToolTip('Send bitcoins to other users, or transfer ' 'between wallets') @@ -981,7 +1616,7 @@ def __init__(self, wlt, usermode=USERMODE.Standard, parent=None, main=None): lbtnDeleteA.setToolTip('Permanently delete an imported address from ' 'this wallet. You cannot delete addresses that ' 'were generated natively by this wallet.') - #lbtnSweepA .setToolTip('') + # lbtnSweepA .setToolTip('') lbtnForkWlt.setToolTip('Save a copy of this wallet that can only be used ' 'for generating addresses and monitoring incoming ' 'payments. A watching-only wallet cannot spend ' @@ -992,6 +1627,8 @@ def __init__(self, wlt, usermode=USERMODE.Standard, parent=None, main=None): lbtnRemove.setToolTip('Permanently delete this wallet, or just delete ' 'the private keys to convert it to a watching-only ' 'wallet.') + #lbtnRecover.setToolTip('Attempt to recover a lost password using ' + # 'details that you remember.') if not self.wlt.watchingOnly: lbtnChangeCrypto.setToolTip('Add/Remove/Change wallet encryption settings.') @@ -1017,12 +1654,14 @@ def createVBoxSeparator(): if hasPriv: optLayout.addWidget(lbtnBackups) if hasPriv and adv: optLayout.addWidget(lbtnForkWlt) if True: optLayout.addWidget(lbtnRemove) + # if True: optLayout.addWidget(lbtnRecover) + # Not sure yet that we want to include the password finer in here if hasPriv and adv: optLayout.addWidget(createVBoxSeparator()) if hasPriv and adv: optLayout.addWidget(lbtnImportA) if hasPriv and adv: optLayout.addWidget(lbtnDeleteA) - #if hasPriv and adv: optLayout.addWidget(lbtnSweepA) + # if hasPriv and adv: optLayout.addWidget(lbtnSweepA) optLayout.addStretch() optFrame.setLayout(optLayout) @@ -1033,18 +1672,18 @@ def createVBoxSeparator(): totalFunds = self.wlt.getBalance('Total') spendFunds = self.wlt.getBalance('Spendable') - unconfFunds= self.wlt.getBalance('Unconfirmed') - uncolor = htmlColor('MoneyNeg') if unconfFunds>0 else htmlColor('Foreground') - btccolor = htmlColor('DisableFG') if spendFunds==totalFunds else htmlColor('MoneyPos') - lblcolor = htmlColor('DisableFG') if spendFunds==totalFunds else htmlColor('Foreground') - goodColor= htmlColor('TextGreen') - - self.lblTot = QRichLabel('', doWrap=False); - self.lblSpd = QRichLabel('', doWrap=False); - self.lblUnc = QRichLabel('', doWrap=False); - - self.lblTotalFunds = QRichLabel('', doWrap=False) - self.lblSpendFunds = QRichLabel('', doWrap=False) + unconfFunds = self.wlt.getBalance('Unconfirmed') + uncolor = htmlColor('MoneyNeg') if unconfFunds > 0 else htmlColor('Foreground') + btccolor = htmlColor('DisableFG') if spendFunds == totalFunds else htmlColor('MoneyPos') + lblcolor = htmlColor('DisableFG') if spendFunds == totalFunds else htmlColor('Foreground') + goodColor = htmlColor('TextGreen') + + self.lblTot = QRichLabel('', doWrap=False); + self.lblSpd = QRichLabel('', doWrap=False); + self.lblUnc = QRichLabel('', doWrap=False); + + self.lblTotalFunds = QRichLabel('', doWrap=False) + self.lblSpendFunds = QRichLabel('', doWrap=False) self.lblUnconfFunds = QRichLabel('', doWrap=False) self.lblTotalFunds.setAlignment(Qt.AlignRight | Qt.AlignVCenter) self.lblSpendFunds.setAlignment(Qt.AlignRight | Qt.AlignVCenter) @@ -1059,13 +1698,13 @@ def createVBoxSeparator(): self.lblBTC2 = QRichLabel('', doWrap=False) self.lblBTC3 = QRichLabel('', doWrap=False) - ttipTot = self.main.createToolTipWidget( \ + ttipTot = self.main.createToolTipWidget(\ 'Total funds if all current transactions are confirmed. ' 'Value appears gray when it is the same as your spendable funds.') - ttipSpd = self.main.createToolTipWidget( \ + ttipSpd = self.main.createToolTipWidget(\ 'Funds that can be spent right now') - ttipUcn = self.main.createToolTipWidget( \ - 'Funds that have less than 6 confirmations' ) + ttipUcn = self.main.createToolTipWidget(\ + 'Funds that have less than 6 confirmations') self.setSummaryBalances() @@ -1073,54 +1712,54 @@ def createVBoxSeparator(): frmTotals = QFrame() frmTotals.setFrameStyle(STYLE_NONE) frmTotalsLayout = QGridLayout() - frmTotalsLayout.addWidget(self.lblTot, 0,0) - frmTotalsLayout.addWidget(self.lblSpd, 1,0) - frmTotalsLayout.addWidget(self.lblUnc, 2,0) + frmTotalsLayout.addWidget(self.lblTot, 0, 0) + frmTotalsLayout.addWidget(self.lblSpd, 1, 0) + frmTotalsLayout.addWidget(self.lblUnc, 2, 0) - frmTotalsLayout.addWidget(self.lblTotalFunds, 0,1) - frmTotalsLayout.addWidget(self.lblSpendFunds, 1,1) - frmTotalsLayout.addWidget(self.lblUnconfFunds, 2,1) + frmTotalsLayout.addWidget(self.lblTotalFunds, 0, 1) + frmTotalsLayout.addWidget(self.lblSpendFunds, 1, 1) + frmTotalsLayout.addWidget(self.lblUnconfFunds, 2, 1) - frmTotalsLayout.addWidget(self.lblBTC1, 0,2) - frmTotalsLayout.addWidget(self.lblBTC2, 1,2) - frmTotalsLayout.addWidget(self.lblBTC3, 2,2) + frmTotalsLayout.addWidget(self.lblBTC1, 0, 2) + frmTotalsLayout.addWidget(self.lblBTC2, 1, 2) + frmTotalsLayout.addWidget(self.lblBTC3, 2, 2) - frmTotalsLayout.addWidget(ttipTot, 0,3) - frmTotalsLayout.addWidget(ttipSpd, 1,3) - frmTotalsLayout.addWidget(ttipUcn, 2,3) + frmTotalsLayout.addWidget(ttipTot, 0, 3) + frmTotalsLayout.addWidget(ttipSpd, 1, 3) + frmTotalsLayout.addWidget(ttipUcn, 2, 3) frmTotals.setLayout(frmTotalsLayout) lblWltAddr = QRichLabel('Addresses in Wallet:', doWrap=False) - self.chkHideEmpty = QCheckBox('Hide Empty') + self.chkHideEmpty = QCheckBox('Hide Empty') self.chkHideChange = QCheckBox('Hide Change') self.chkHideUnused = QCheckBox('Hide Unused') self.chkHideEmpty.setChecked(False) - self.chkHideChange.setChecked(self.main.usermode==USERMODE.Standard) - self.chkHideUnused.setChecked(self.wlt.highestUsedChainIndex>25) + self.chkHideChange.setChecked(self.main.usermode == USERMODE.Standard) + self.chkHideUnused.setChecked(self.wlt.highestUsedChainIndex > 25) - self.connect(self.chkHideEmpty, SIGNAL('clicked()'), self.doFilterAddr) - self.connect(self.chkHideChange, SIGNAL('clicked()'), self.doFilterAddr) - self.connect(self.chkHideUnused, SIGNAL('clicked()'), self.doFilterAddr) + self.connect(self.chkHideEmpty, SIGNAL(CLICKED), self.doFilterAddr) + self.connect(self.chkHideChange, SIGNAL(CLICKED), self.doFilterAddr) + self.connect(self.chkHideUnused, SIGNAL(CLICKED), self.doFilterAddr) headerFrm = makeHorizFrame([ lblWltAddr, \ - 'Stretch', \ + STRETCH, \ self.chkHideEmpty, \ self.chkHideChange, \ - self.chkHideUnused] ) + self.chkHideUnused]) btnGoBack = QPushButton('<<< Go Back') - self.connect(btnGoBack, SIGNAL('clicked()'), self.accept) - bottomFrm = makeHorizFrame([btnGoBack, 'Stretch', frmTotals]) + self.connect(btnGoBack, SIGNAL(CLICKED), self.accept) + bottomFrm = makeHorizFrame([btnGoBack, STRETCH, frmTotals]) layout = QGridLayout() - layout.addWidget(self.frm, 0, 0) - layout.addWidget(headerFrm, 1, 0) - layout.addWidget(self.wltAddrView, 2, 0) - layout.addWidget(bottomFrm, 3, 0) + layout.addWidget(self.frm, 0, 0) + layout.addWidget(headerFrm, 1, 0) + layout.addWidget(self.wltAddrView, 2, 0) + layout.addWidget(bottomFrm, 3, 0) - #layout.addWidget(QLabel("Available Actions:"), 0, 4) - layout.addWidget(optFrame, 0, 1, 4, 1) + # layout.addWidget(QLabel("Available Actions:"), 0, 4) + layout.addWidget(optFrame, 0, 1, 4, 1) layout.setRowStretch(0, 0) layout.setRowStretch(1, 0) layout.setRowStretch(2, 1) @@ -1135,10 +1774,10 @@ def createVBoxSeparator(): hexgeom = self.main.settings.get('WltPropGeometry') tblgeom = self.main.settings.get('WltPropAddrCols') - if len(hexgeom)>0: + if len(hexgeom) > 0: geom = QByteArray.fromHex(hexgeom) self.restoreGeometry(geom) - if len(tblgeom)>0: + if len(tblgeom) > 0: restoreTableView(self.wltAddrView, tblgeom) def remindBackup(): @@ -1162,17 +1801,17 @@ def remindBackup(): 'Read more about Armory backups', None, yesStr='Ok', \ dnaaStartChk=True) self.main.setWltSetting(wlt.uniqueIDB58, 'DNAA_RemindBackup', result[1]) - - + + wltType = determineWalletType(wlt, main)[0] chkLoad = (self.main.getSettingOrSetDefault('Load_Count', 1) % 5 == 0) - chkType = not wltType in (WLTTYPES.Offline, WLTTYPES.WatchOnly) + chkType = not wltType in (WLTTYPES.Offline, WLTTYPES.WatchOnly) chkDNAA = not self.main.getWltSetting(wlt.uniqueIDB58, 'DNAA_RemindBackup') chkDont = not self.main.getSettingOrSetDefault('DNAA_AllBackupWarn', False) if chkLoad and chkType and chkDNAA and chkDont: from twisted.internet import reactor - reactor.callLater(1,remindBackup) + reactor.callLater(1, remindBackup) lbtnBackups.setText('Backup This Wallet' \ % htmlColor('TextWarn')) @@ -1187,31 +1826,31 @@ def doFilterAddr(self): def setSummaryBalances(self): totalFunds = self.wlt.getBalance('Total') spendFunds = self.wlt.getBalance('Spendable') - unconfFunds= self.wlt.getBalance('Unconfirmed') - uncolor = htmlColor('MoneyNeg') if unconfFunds>0 else htmlColor('Foreground') - btccolor = htmlColor('DisableFG') if spendFunds==totalFunds else htmlColor('MoneyPos') - lblcolor = htmlColor('DisableFG') if spendFunds==totalFunds else htmlColor('Foreground') - goodColor= htmlColor('TextGreen') + unconfFunds = self.wlt.getBalance('Unconfirmed') + uncolor = htmlColor('MoneyNeg') if unconfFunds > 0 else htmlColor('Foreground') + btccolor = htmlColor('DisableFG') if spendFunds == totalFunds else htmlColor('MoneyPos') + lblcolor = htmlColor('DisableFG') if spendFunds == totalFunds else htmlColor('Foreground') + goodColor = htmlColor('TextGreen') - self.lblTot.setText('Maximum Funds:'%lblcolor) + self.lblTot.setText('Maximum Funds:' % lblcolor) self.lblSpd.setText('Spendable Funds:') self.lblUnc.setText('Unconfirmed:') - #if self.main.blkMode in (BLOCKCHAINMODE.Offline, BLOCKCHAINMODE.Rescanning): - if TheBDM.getBDMState() in ('Uninitialized', 'Offline','Scanning'): - totStr = '-'*12 - spdStr = '-'*12 - ucnStr = '-'*12 + # if self.main.blkMode in (BLOCKCHAINMODE.Offline, BLOCKCHAINMODE.Rescanning): + if TheBDM.getBDMState() in ('Uninitialized', 'Offline', 'Scanning'): + totStr = '-' * 12 + spdStr = '-' * 12 + ucnStr = '-' * 12 else: - totStr = '%s' % (btccolor, coin2str(totalFunds)) + totStr = '%s' % (btccolor, coin2str(totalFunds)) spdStr = '%s' % (goodColor, coin2str(spendFunds)) - ucnStr = '%s' % (uncolor, coin2str(unconfFunds)) + ucnStr = '%s' % (uncolor, coin2str(unconfFunds)) self.lblTotalFunds.setText(totStr) self.lblSpendFunds.setText(spdStr) self.lblUnconfFunds.setText(ucnStr) - self.lblBTC1.setText('BTC'%lblcolor) + self.lblBTC1.setText('BTC' % lblcolor) self.lblBTC2.setText('BTC') self.lblBTC3.setText('BTC') @@ -1235,28 +1874,28 @@ def accept(self, *args): def reject(self, *args): self.saveGeometrySettings() super(DlgWalletDetails, self).reject(*args) - + ############################################################################# def showContextMenu(self, pos): menu = QMenu(self.wltAddrView) - std = (self.main.usermode==USERMODE.Standard) - adv = (self.main.usermode==USERMODE.Advanced) - dev = (self.main.usermode==USERMODE.Expert) - - if True: actionCopyAddr = menu.addAction("Copy Address") - if True: actionShowQRCode = menu.addAction("Display Address QR Code") - if True: actionBlkChnInfo = menu.addAction("View Address on www.blockchain.info") - if True: actionReqPayment = menu.addAction("Request Payment to this Address") + std = (self.main.usermode == USERMODE.Standard) + adv = (self.main.usermode == USERMODE.Advanced) + dev = (self.main.usermode == USERMODE.Expert) + + if True: actionCopyAddr = menu.addAction("Copy Address") + if True: actionShowQRCode = menu.addAction("Display Address QR Code") + if True: actionBlkChnInfo = menu.addAction("View Address on www.blockchain.info") + if True: actionReqPayment = menu.addAction("Request Payment to this Address") if dev: actionCopyHash160 = menu.addAction("Copy Hash160 (hex)") if True: actionCopyComment = menu.addAction("Copy Comment") if True: actionCopyBalance = menu.addAction("Copy Balance") idx = self.wltAddrView.selectedIndexes()[0] action = menu.exec_(QCursor.pos()) - + addr = str(self.wltAddrView.model().index(idx.row(), ADDRESSCOLS.Address).data().toString()).strip() - if action==actionCopyAddr: + if action == actionCopyAddr: s = self.wltAddrView.model().index(idx.row(), ADDRESSCOLS.Address).data().toString() - elif action==actionBlkChnInfo: + elif action == actionBlkChnInfo: try: import webbrowser blkchnURL = 'https://blockchain.info/address/%s' % addr @@ -1268,18 +1907,18 @@ def showContextMenu(self, pos): 'the following URL into your browser: ' '

    %s' % blkchnURL, QMessageBox.Ok) return - elif action==actionShowQRCode: + elif action == actionShowQRCode: wltstr = 'Wallet: %s (%s)' % (self.wlt.labelName, self.wlt.uniqueIDB58) - DlgQRCodeDisplay(self, self.main, addr, addr, wltstr ).exec_() + DlgQRCodeDisplay(self, self.main, addr, addr, wltstr).exec_() return - elif action==actionReqPayment: - DlgRequestPayment(self, self.main, addr).exec_() + elif action == actionReqPayment: + DlgRequestPayment(self, self.main, addr).exec_() return - elif dev and action==actionCopyHash160: - s = binary_to_hex(addrStr_to_hash160(addr)) - elif action==actionCopyComment: + elif dev and action == actionCopyHash160: + s = binary_to_hex(addrStr_to_hash160(addr)[1]) + elif action == actionCopyComment: s = self.wltAddrView.model().index(idx.row(), ADDRESSCOLS.Comment).data().toString() - elif action==actionCopyBalance: + elif action == actionCopyBalance: s = self.wltAddrView.model().index(idx.row(), ADDRESSCOLS.Balance).data().toString() else: return @@ -1291,11 +1930,13 @@ def showContextMenu(self, pos): ############################################################################# def dblClickAddressView(self, index): model = index.model() - if index.column()==ADDRESSCOLS.Comment: + if index.column() == ADDRESSCOLS.Comment: self.main.updateAddressCommentFromView(self.wltAddrView, self.wlt) else: addrStr = str(index.model().index(index.row(), ADDRESSCOLS.Address).data().toString()) - dlg = DlgAddressInfo(self.wlt, addrStr_to_hash160(addrStr), self, self.main) + atype, addr160 = addrStr_to_hash160(addrStr, False) + + dlg = DlgAddressInfo(self.wlt, addr160, self, self.main) dlg.exec_() @@ -1305,7 +1946,7 @@ def changeLabels(self): if dlgLabels.exec_(): # Make sure to use methods like this which not only update in memory, # but guarantees the file is updated, too - newName = str(dlgLabels.edtName.text())[:32] + newName = str(dlgLabels.edtName.text())[:32] newDescr = str(dlgLabels.edtDescr.toPlainText())[:256] self.wlt.setWalletLabels(newName, newDescr) @@ -1319,21 +1960,28 @@ def changeEncryption(self): if dlgCrypt.exec_(): self.disableEncryption = dlgCrypt.chkDisableCrypt.isChecked() newPassphrase = SecureBinaryData(str(dlgCrypt.edtPasswd1.text())) + dlgCrypt.edtPasswd1.clear() + dlgCrypt.edtPasswd2.clear() if self.wlt.useEncryption: origPassphrase = SecureBinaryData(str(dlgCrypt.edtPasswdOrig.text())) + dlgCrypt.edtPasswdOrig.clear() if self.wlt.verifyPassphrase(origPassphrase): - self.wlt.unlock(securePassphrase=origPassphrase) + unlockProgress = DlgProgress(self, self.main, HBar=1, + Title="Unlocking Wallet") + unlockProgress.exec_(self.wlt.unlock, securePassphrase=origPassphrase) else: # Even if the wallet is already unlocked, enter pwd again to change it QMessageBox.critical(self, 'Invalid Passphrase', \ 'Previous passphrase is not correct! Could not unlock wallet.', \ QMessageBox.Ok) - - + + if self.disableEncryption: - self.wlt.changeWalletEncryption(None, None) - #self.accept() + unlockProgress = DlgProgress(self, self.main, HBar=1, + Title="Changing Encryption") + unlockProgress.exec_(self.wlt.changeWalletEncryption) + # self.accept() self.labelValues[WLTFIELDS.Secure].setText('No Encryption') self.labelValues[WLTFIELDS.Secure].setText('') self.labelValues[WLTFIELDS.Secure].setText('') @@ -1341,19 +1989,23 @@ def changeEncryption(self): if not self.wlt.useEncryption: kdfParams = self.wlt.computeSystemSpecificKdfParams(0.2) self.wlt.changeKdfParams(*kdfParams) - self.wlt.changeWalletEncryption(securePassphrase=newPassphrase) + + unlockProgress = DlgProgress(self, self.main, HBar=2, + Title="Changing Encryption") + unlockProgress.exec_(self.wlt.changeWalletEncryption, + securePassphrase=newPassphrase) self.labelValues[WLTFIELDS.Secure].setText('Encrypted (AES256)') - #self.accept() - + # self.accept() + def getNewAddress(self): if showRecvCoinsWarningIfNecessary(self.wlt, self.main): DlgNewAddressDisp(self.wlt, self, self.main).exec_() self.wltAddrView.reset() - + def execSendBtc(self): - #if self.main.blkMode == BLOCKCHAINMODE.Offline: + # if self.main.blkMode == BLOCKCHAINMODE.Offline: if TheBDM.getBDMState() in ('Offline', 'Uninitialized'): QMessageBox.warning(self, 'Offline Mode', \ 'Armory is currently running in offline mode, and has no ' @@ -1372,14 +2024,15 @@ def execSendBtc(self): 'balance appears on the main window, then try again.', \ QMessageBox.Ok) return - dlgSend = DlgSendBitcoins(self.wlt, self, self.main) - dlgSend.exec_() + + self.accept() + DlgSendBitcoins(self.wlt, self, self.main, onlyOfflineWallets=False).exec_() self.wltAddrModel.reset() - + def changeKdf(self): - """ + """ This is a low-priority feature. I mean, the PyBtcWallet class has this feature implemented, but I don't have a GUI for it """ @@ -1387,7 +2040,7 @@ def changeKdf(self): def execBackupDlg(self): - if self.main.usermode==USERMODE.Expert: + if self.main.usermode == USERMODE.Expert: DlgBackupCenter(self, self.main, self.wlt).exec_() else: DlgSimpleBackup(self, self.main, self.wlt).exec_() @@ -1401,21 +2054,21 @@ def execPrintDlg(self): if not self.wlt.addrMap['ROOT'].hasPrivKey(): QMessageBox.warning(self, 'Move along...', \ 'This wallet does not contain any private keys. Nothing to backup!', QMessageBox.Ok) - return + return OpenPaperBackupWindow('Single', self, self.main, self.wlt) - + def execRemoveDlg(self): dlg = DlgRemoveWallet(self.wlt, self, self.main) if dlg.exec_(): - pass # not sure that I don't handle everything in the dialog itself + pass # not sure that I don't handle everything in the dialog itself def execKeyList(self): if self.wlt.useEncryption and self.wlt.isLocked: dlg = DlgUnlockWallet(self.wlt, self, self.main, 'Unlock Private Keys') if not dlg.exec_(): - if self.main.usermode==USERMODE.Expert: + if self.main.usermode == USERMODE.Expert: QMessageBox.warning(self, 'Unlock Failed', \ 'Wallet was not be unlocked. The public keys and addresses ' 'will still be shown, but private keys will not be available ' @@ -1426,23 +2079,26 @@ def execKeyList(self): 'Wallet could not be unlocked to display individual keys.', \ QMessageBox.Ok) return - + dlg = DlgShowKeyList(self.wlt, self, self.main) dlg.exec_() def execDeleteAddress(self): selectedList = self.wltAddrView.selectedIndexes() - if len(selectedList)==0: + if len(selectedList) == 0: QMessageBox.warning(self, 'No Selection', \ 'You must select an address to remove!', \ QMessageBox.Ok) return - + row = selectedList[0].row() addrStr = str(self.wltAddrView.model().index(row, ADDRESSCOLS.Address).data().toString()) - addr160 = addrStr_to_hash160(addrStr) - if self.wlt.addrMap[addr160].chainIndex==-2: - dlg = DlgRemoveAddress(self.wlt, addr160, self, self.main) + atype, addr160 = addrStr_to_hash160(addrStr) + if atype==P2SHBYTE: + LOGWARN('Deleting P2SH address: %s' % addrStr) + + if self.wlt.addrMap[addr160].chainIndex == -2: + dlg = DlgRemoveAddress(self.wlt, addr160, self, self.main) dlg.exec_() else: QMessageBox.warning(self, 'Invalid Selection', \ @@ -1453,27 +2109,27 @@ def execDeleteAddress(self): def execImportAddress(self): - - #if TheBDM.getBDMState()=='Scanning': - #QMessageBox.warning(self, 'Armory Not Ready', - #'Armory is currently in the process of scanning the blockchain ' - #'for your existing wallets. This operation must finish before ' - #'you can import or sweep private keys. ' - #'

    ' - #'Try again after your balances and transaction history appear ' - #'in the main window.', QMessageBox.Ok) - #return + + # if TheBDM.getBDMState()=='Scanning': + # QMessageBox.warning(self, 'Armory Not Ready', + # 'Armory is currently in the process of scanning the blockchain ' + # 'for your existing wallets. This operation must finish before ' + # 'you can import or sweep private keys. ' + # '

    ' + # 'Try again after your balances and transaction history appear ' + # 'in the main window.', QMessageBox.Ok) + # return if not self.main.getSettingOrSetDefault('DNAA_ImportWarning', False): result = MsgBoxWithDNAA(MSGBOX.Warning, \ tr("""Imported Address Warning"""), tr(""" - Armory supports importing of external private keys into your - wallet but imported addresses are not automatically - protected by your backups. If you do not plan to use the - address again, it is recommended that you "Sweep" the private + Armory supports importing of external private keys into your + wallet but imported addresses are not automatically + protected by your backups. If you do not plan to use the + address again, it is recommended that you "Sweep" the private key instead of importing it. -

    - Individual private keys, including imported ones, can be +

    + Individual private keys, including imported ones, can be backed up using the "Export Key Lists" option in the wallet backup window."""), None) self.main.writeSetting('DNAA_ImportWarning', result[1]) @@ -1495,78 +2151,81 @@ def saveWalletCopy(self): if self.wlt.watchingOnly: fn = 'armory_%s.watchonly.wallet' % self.wlt.uniqueIDB58 savePath = self.main.getFileSave(defaultFilename=fn) - if len(savePath)>0: + if len(savePath) > 0: self.wlt.writeFreshWalletFile(savePath) - self.main.statusBar().showMessage( \ + self.main.statusBar().showMessage(\ 'Successfully copied wallet to ' + savePath, 10000) - - + + def forkOnlineWallet(self): currPath = self.wlt.walletPath pieces = os.path.splitext(currPath) currPath = pieces[0] + '.watchonly' + pieces[1] - - saveLoc = self.main.getFileSave('Save Watching-Only Copy',\ + + saveLoc = self.main.getFileSave('Save Watching-Only Copy', \ defaultFilename=currPath) if not saveLoc.endswith('.wallet'): saveLoc += '.wallet' self.wlt.forkOnlineWallet(saveLoc, self.wlt.labelName, \ '(Watching-Only) ' + self.wlt.labelDescr) - - - - # A possible way to remove an existing layout - #def setLayout(self, layout): - #self.clearLayout() - #QWidget.setLayout(self, layout) +# def recoverPwd(self): +# passwordFinder = PasswordFinder(wallet=self.wlt) + + + + + # A possible way to remove an existing layout + # def setLayout(self, layout): + # self.clearLayout() + # QWidget.setLayout(self, layout) # - #def clearLayout(self): - #if self.layout() is not None: - #old_layout = self.layout() - #for i in reversed(range(old_layout.count())): - #old_layout.itemAt(i).widget().setParent(None) - #import sip - #sip.delete(old_layout) + # def clearLayout(self): + # if self.layout() is not None: + # old_layout = self.layout() + # for i in reversed(range(old_layout.count())): + # old_layout.itemAt(i).widget().setParent(None) + # import sip + # sip.delete(old_layout) ############################################################################# def setWltDetailsFrame(self): - dispCrypto = self.wlt.useEncryption and (self.usermode==USERMODE.Advanced or \ - self.usermode==USERMODE.Expert) + dispCrypto = self.wlt.useEncryption and (self.usermode == USERMODE.Advanced or \ + self.usermode == USERMODE.Expert) self.wltID = self.wlt.uniqueIDB58 if dispCrypto: mem = self.wlt.kdf.getMemoryReqtBytes() - kdfmemstr = str(mem/1024)+' kB' - if mem >= 1024*1024: - kdfmemstr = str(mem/(1024*1024))+' MB' - - - tooltips = [[]]*10 - + kdfmemstr = str(mem / 1024) + ' kB' + if mem >= 1024 * 1024: + kdfmemstr = str(mem / (1024 * 1024)) + ' MB' + + + tooltips = [[]] * 10 + tooltips[WLTFIELDS.Name] = self.main.createToolTipWidget( 'This is the name stored with the wallet file. Click on the ' '"Change Labels" button on the right side of this ' - 'window to change this field' ) - + 'window to change this field') + tooltips[WLTFIELDS.Descr] = self.main.createToolTipWidget( 'This is the description of the wallet stored in the wallet file. ' 'Press the "Change Labels" button on the right side of this ' - 'window to change this field' ) - + 'window to change this field') + tooltips[WLTFIELDS.WltID] = self.main.createToolTipWidget( 'This is a unique identifier for this wallet, based on the root key. ' 'No other wallet can have the same ID ' 'unless it is a copy of this one, regardless of whether ' 'the name and description match.') - + tooltips[WLTFIELDS.NumAddr] = self.main.createToolTipWidget( 'This is the number of addresses *used* by this wallet so far. ' 'If you recently restored this wallet and you do not see all the ' 'funds you were expecting, click on this field to increase it.') - - if self.typestr=='Offline': + + if self.typestr == 'Offline': tooltips[WLTFIELDS.Secure] = self.main.createToolTipWidget( 'Offline: This is a "Watching-Only" wallet that you have identified ' 'belongs to you, but you cannot spend any of the wallet funds ' @@ -1574,18 +2233,18 @@ def setWltDetailsFrame(self): 'is usually stored on an internet-connected computer, to manage ' 'incoming transactions, but the private keys needed ' 'to spend the money are stored on an offline computer.') - elif self.typestr=='Watching-Only': + elif self.typestr == 'Watching-Only': tooltips[WLTFIELDS.Secure] = self.main.createToolTipWidget( 'Watching-Only: You can only watch addresses in this wallet ' 'but cannot spend any of the funds.') - elif self.typestr=='No Encryption': + elif self.typestr == 'No Encryption': tooltips[WLTFIELDS.Secure] = self.main.createToolTipWidget( 'No Encryption: This wallet contains private keys, and does not require ' 'a passphrase to spend funds available to this wallet. If someone ' 'else obtains a copy of this wallet, they can also spend your funds! ' '(You can click the "Change Encryption" button on the right side of this ' 'window to enabled encryption)') - elif self.typestr=='Encrypted (AES256)': + elif self.typestr == 'Encrypted (AES256)': tooltips[WLTFIELDS.Secure] = self.main.createToolTipWidget( 'This wallet contains the private keys needed to spend this wallet\'s ' 'funds, but they are encrypted on your harddrive. The wallet must be ' @@ -1596,57 +2255,57 @@ def setWltDetailsFrame(self): tooltips[WLTFIELDS.BelongsTo] = self.main.createToolTipWidget( 'Declare who owns this wallet. If you click on the field and select ' '"This wallet is mine", it\'s balance will be included in your total ' - 'Armory Balance in the main window' ) - + 'Armory Balance in the main window') + tooltips[WLTFIELDS.Time] = self.main.createToolTipWidget( 'This is exactly how long it takes your computer to unlock your ' 'wallet after you have entered your passphrase. If someone got ' 'ahold of your wallet, this is approximately how long it would take ' 'them to for each guess of your passphrase.') - + tooltips[WLTFIELDS.Mem] = self.main.createToolTipWidget( 'This is the amount of memory required to unlock your wallet. ' 'Memory values above 64 kB pretty much guarantee that GPU-acceleration ' 'will be useless for guessing your passphrase') - + tooltips[WLTFIELDS.Version] = self.main.createToolTipWidget( 'Wallets created with different versions of Armory, may have ' 'different wallet versions. Not all functionality may be ' 'available with all wallet versions. Creating a new wallet will ' 'always create the latest version.') - labelNames = [[]]*10 - labelNames[WLTFIELDS.Name] = QLabel('Wallet Name:') - labelNames[WLTFIELDS.Descr] = QLabel('Description:') - - labelNames[WLTFIELDS.WltID] = QLabel('Wallet ID:') - labelNames[WLTFIELDS.NumAddr] = QLabel('Addresses Used:') - labelNames[WLTFIELDS.Secure] = QLabel('Security:') - labelNames[WLTFIELDS.Version] = QLabel('Version:') + labelNames = [[]] * 10 + labelNames[WLTFIELDS.Name] = QLabel('Wallet Name:') + labelNames[WLTFIELDS.Descr] = QLabel('Description:') + + labelNames[WLTFIELDS.WltID] = QLabel('Wallet ID:') + labelNames[WLTFIELDS.NumAddr] = QLabel('Addresses Used:') + labelNames[WLTFIELDS.Secure] = QLabel('Security:') + labelNames[WLTFIELDS.Version] = QLabel('Version:') labelNames[WLTFIELDS.BelongsTo] = QLabel('Belongs to:') - - + + # TODO: Add wallet path/location to this! - + if dispCrypto: - labelNames[WLTFIELDS.Time] = QLabel('Unlock Time:') - labelNames[WLTFIELDS.Mem] = QLabel('Unlock Memory:') - - self.labelValues = [[]]*10 - self.labelValues[WLTFIELDS.Name] = QLabel(self.wlt.labelName) - self.labelValues[WLTFIELDS.Descr] = QLabel(self.wlt.labelDescr) - - self.labelValues[WLTFIELDS.WltID] = QLabel(self.wlt.uniqueIDB58) - self.labelValues[WLTFIELDS.Secure] = QLabel(self.typestr) + labelNames[WLTFIELDS.Time] = QLabel('Unlock Time:') + labelNames[WLTFIELDS.Mem] = QLabel('Unlock Memory:') + + self.labelValues = [[]] * 10 + self.labelValues[WLTFIELDS.Name] = QLabel(self.wlt.labelName) + self.labelValues[WLTFIELDS.Descr] = QLabel(self.wlt.labelDescr) + + self.labelValues[WLTFIELDS.WltID] = QLabel(self.wlt.uniqueIDB58) + self.labelValues[WLTFIELDS.Secure] = QLabel(self.typestr) self.labelValues[WLTFIELDS.BelongsTo] = QLabel('') - self.labelValues[WLTFIELDS.Version] = QLabel(getVersionString(self.wlt.version)) + self.labelValues[WLTFIELDS.Version] = QLabel(getVersionString(self.wlt.version)) - topUsed = max(self.wlt.highestUsedChainIndex,0) + topUsed = max(self.wlt.highestUsedChainIndex, 0) self.labelValues[WLTFIELDS.NumAddr] = QLabelButton('%d' % topUsed) self.labelValues[WLTFIELDS.NumAddr].setAlignment(Qt.AlignLeft | Qt.AlignVCenter) - opendlgkeypool = lambda: DlgKeypoolSettings(self.wlt,self,self.main).exec_() - self.connect(self.labelValues[WLTFIELDS.NumAddr], SIGNAL('clicked()'), opendlgkeypool) + opendlgkeypool = lambda: DlgKeypoolSettings(self.wlt, self, self.main).exec_() + self.connect(self.labelValues[WLTFIELDS.NumAddr], SIGNAL(CLICKED), opendlgkeypool) # Set the owner appropriately if self.wlt.watchingOnly: @@ -1655,125 +2314,125 @@ def setWltDetailsFrame(self): self.labelValues[WLTFIELDS.BelongsTo].setAlignment(Qt.AlignLeft | Qt.AlignVCenter) else: owner = self.main.getWltSetting(self.wltID, 'BelongsTo') - if owner=='': + if owner == '': self.labelValues[WLTFIELDS.BelongsTo] = QLabelButton('Someone else...') self.labelValues[WLTFIELDS.BelongsTo].setAlignment(Qt.AlignLeft | Qt.AlignVCenter) else: self.labelValues[WLTFIELDS.BelongsTo] = QLabelButton(owner) self.labelValues[WLTFIELDS.BelongsTo].setAlignment(Qt.AlignLeft | Qt.AlignVCenter) - self.connect(self.labelValues[WLTFIELDS.BelongsTo], SIGNAL('clicked()'), \ + self.connect(self.labelValues[WLTFIELDS.BelongsTo], SIGNAL(CLICKED), \ self.execSetOwner) - - - + + + if dispCrypto: - self.labelValues[WLTFIELDS.Time] = QLabelButton('Click to Test') - self.labelValues[WLTFIELDS.Mem] = QLabel(kdfmemstr) + self.labelValues[WLTFIELDS.Time] = QLabelButton('Click to Test') + self.labelValues[WLTFIELDS.Mem] = QLabel(kdfmemstr) for ttip in tooltips: try: ttip.setAlignment(Qt.AlignRight | Qt.AlignTop) - w,h = relaxedSizeStr(ttip, '(?)') - ttip.setMaximumSize(w,h) + w, h = relaxedSizeStr(ttip, '(?)') + ttip.setMaximumSize(w, h) except AttributeError: pass - + for lbl in labelNames: try: lbl.setTextFormat(Qt.RichText) - lbl.setText( '' + lbl.text() + '') + lbl.setText('' + lbl.text() + '') lbl.setContentsMargins(0, 0, 0, 0) - w,h = tightSizeStr(lbl, '9'*16) - lbl.setMaximumSize(w,h) + w, h = tightSizeStr(lbl, '9' * 16) + lbl.setMaximumSize(w, h) except AttributeError: pass - - - for i,lbl in enumerate(self.labelValues): - if i==WLTFIELDS.BelongsTo: + + + for i, lbl in enumerate(self.labelValues): + if i == WLTFIELDS.BelongsTo: lbl.setContentsMargins(10, 0, 10, 0) continue try: - lbl.setText( '' + lbl.text() + '') + lbl.setText('' + lbl.text() + '') lbl.setContentsMargins(10, 0, 10, 0) - #lbl.setTextInteractionFlags(Qt.TextSelectableByMouse | \ - #Qt.TextSelectableByKeyboard) + # lbl.setTextInteractionFlags(Qt.TextSelectableByMouse | \ + # Qt.TextSelectableByKeyboard) except AttributeError: pass - + # Not sure why this has to be connected downhere... it didn't work above it if dispCrypto: self.labelValues[WLTFIELDS.Time].setAlignment(Qt.AlignLeft | Qt.AlignVCenter) - self.connect(self.labelValues[WLTFIELDS.Time], SIGNAL('clicked()'), self.testKdfTime) + self.connect(self.labelValues[WLTFIELDS.Time], SIGNAL(CLICKED), self.testKdfTime) labelNames[WLTFIELDS.Descr].setAlignment(Qt.AlignLeft | Qt.AlignTop) self.labelValues[WLTFIELDS.Descr].setWordWrap(True) self.labelValues[WLTFIELDS.Descr].setAlignment(Qt.AlignLeft | Qt.AlignTop) - - lblEmpty = QLabel(' '*20) - + + lblEmpty = QLabel(' ' * 20) + layout = QGridLayout() - layout.addWidget(tooltips[WLTFIELDS.WltID], 0, 0); - layout.addWidget(labelNames[WLTFIELDS.WltID], 0, 1); - layout.addWidget(self.labelValues[WLTFIELDS.WltID], 0, 2) + layout.addWidget(tooltips[WLTFIELDS.WltID], 0, 0); + layout.addWidget(labelNames[WLTFIELDS.WltID], 0, 1); + layout.addWidget(self.labelValues[WLTFIELDS.WltID], 0, 2) + + layout.addWidget(tooltips[WLTFIELDS.Name], 1, 0); + layout.addWidget(labelNames[WLTFIELDS.Name], 1, 1); + layout.addWidget(self.labelValues[WLTFIELDS.Name], 1, 2) + + layout.addWidget(tooltips[WLTFIELDS.Descr], 2, 0); + layout.addWidget(labelNames[WLTFIELDS.Descr], 2, 1); + layout.addWidget(self.labelValues[WLTFIELDS.Descr], 2, 2, 4, 1) + + layout.addWidget(tooltips[WLTFIELDS.Version], 0, 3); + layout.addWidget(labelNames[WLTFIELDS.Version], 0, 4); + layout.addWidget(self.labelValues[WLTFIELDS.Version], 0, 5) + + i = 0 + if self.main.usermode == USERMODE.Expert: + i += 1 + layout.addWidget(tooltips[WLTFIELDS.NumAddr], i, 3) + layout.addWidget(labelNames[WLTFIELDS.NumAddr], i, 4) + layout.addWidget(self.labelValues[WLTFIELDS.NumAddr], i, 5) + + i += 1 + layout.addWidget(tooltips[WLTFIELDS.Secure], i, 3); + layout.addWidget(labelNames[WLTFIELDS.Secure], i, 4); + layout.addWidget(self.labelValues[WLTFIELDS.Secure], i, 5) - layout.addWidget(tooltips[WLTFIELDS.Name], 1, 0); - layout.addWidget(labelNames[WLTFIELDS.Name], 1, 1); - layout.addWidget(self.labelValues[WLTFIELDS.Name], 1, 2) - - layout.addWidget(tooltips[WLTFIELDS.Descr], 2, 0); - layout.addWidget(labelNames[WLTFIELDS.Descr], 2, 1); - layout.addWidget(self.labelValues[WLTFIELDS.Descr], 2, 2, 4, 1) - - layout.addWidget(tooltips[WLTFIELDS.Version], 0, 3); - layout.addWidget(labelNames[WLTFIELDS.Version], 0, 4); - layout.addWidget(self.labelValues[WLTFIELDS.Version], 0, 5) - - i=0 - if self.main.usermode==USERMODE.Expert: - i+=1 - layout.addWidget(tooltips[WLTFIELDS.NumAddr], i, 3) - layout.addWidget(labelNames[WLTFIELDS.NumAddr], i, 4) - layout.addWidget(self.labelValues[WLTFIELDS.NumAddr], i, 5) - - i+=1 - layout.addWidget(tooltips[WLTFIELDS.Secure], i, 3); - layout.addWidget(labelNames[WLTFIELDS.Secure], i, 4); - layout.addWidget(self.labelValues[WLTFIELDS.Secure], i, 5) - if self.wlt.watchingOnly: - i+=1 - layout.addWidget(tooltips[WLTFIELDS.BelongsTo], i, 3); - layout.addWidget(labelNames[WLTFIELDS.BelongsTo], i, 4); - layout.addWidget(self.labelValues[WLTFIELDS.BelongsTo], i, 5) - - + i += 1 + layout.addWidget(tooltips[WLTFIELDS.BelongsTo], i, 3); + layout.addWidget(labelNames[WLTFIELDS.BelongsTo], i, 4); + layout.addWidget(self.labelValues[WLTFIELDS.BelongsTo], i, 5) + + if dispCrypto: - i+=1 - layout.addWidget(tooltips[WLTFIELDS.Time], i, 3); - layout.addWidget(labelNames[WLTFIELDS.Time], i, 4); - layout.addWidget(self.labelValues[WLTFIELDS.Time], i, 5) - - i+=1 - layout.addWidget(tooltips[WLTFIELDS.Mem], i, 3); - layout.addWidget(labelNames[WLTFIELDS.Mem], i, 4); - layout.addWidget(self.labelValues[WLTFIELDS.Mem], i, 5) + i += 1 + layout.addWidget(tooltips[WLTFIELDS.Time], i, 3); + layout.addWidget(labelNames[WLTFIELDS.Time], i, 4); + layout.addWidget(self.labelValues[WLTFIELDS.Time], i, 5) + + i += 1 + layout.addWidget(tooltips[WLTFIELDS.Mem], i, 3); + layout.addWidget(labelNames[WLTFIELDS.Mem], i, 4); + layout.addWidget(self.labelValues[WLTFIELDS.Mem], i, 5) + - self.frm = QFrame() self.frm.setFrameStyle(STYLE_SUNKEN) self.frm.setLayout(layout) - - + + def testKdfTime(self): kdftimestr = "%0.3f sec" % self.wlt.testKdfComputeTime() self.labelValues[WLTFIELDS.Time].setText(kdftimestr) - + def execSetOwner(self): dlg = self.dlgChangeOwner(self.wltID, self, self.main) @@ -1785,18 +2444,18 @@ def execSetOwner(self): self.labelValues[WLTFIELDS.BelongsTo].setAlignment(Qt.AlignLeft | Qt.AlignVCenter) self.labelValues[WLTFIELDS.Secure].setText('Offline') else: - owner = unicode(dlg.edtOwnerString.text()) + owner = unicode(dlg.edtOwnerString.text()) self.main.setWltSetting(self.wltID, 'IsMine', False) self.main.setWltSetting(self.wltID, 'BelongsTo', owner) - - if len(owner)>0: + + if len(owner) > 0: self.labelValues[WLTFIELDS.BelongsTo].setText(owner) else: self.labelValues[WLTFIELDS.BelongsTo].setText('Someone else') self.labelValues[WLTFIELDS.Secure].setText('Watching-Only') self.labelValues[WLTFIELDS.BelongsTo].setAlignment(Qt.AlignLeft | Qt.AlignVCenter) self.labelValues[WLTFIELDS.Secure].setAlignment(Qt.AlignLeft | Qt.AlignVCenter) - + class dlgChangeOwner(ArmoryDialog): @@ -1806,7 +2465,7 @@ def __init__(self, wltID, parent=None, main=None): layout = QGridLayout() self.chkIsMine = QCheckBox('This wallet is mine') - self.edtOwnerString = QLineEdit() + self.edtOwnerString = QLineEdit() if parent.main.getWltSetting(wltID, 'IsMine'): lblDescr = QLabel( 'The funds in this wallet are currently identified as ' @@ -1816,45 +2475,45 @@ def __init__(self, wltID, parent=None, main=None): 'If you do not actually own this wallet, or do not wish ' 'for its funds to be considered part of your balance, ' 'uncheck the box below. Optionally, you can include the ' - 'name of the person or organization that does own it.' ) + 'name of the person or organization that does own it.') lblDescr.setWordWrap(True) - layout.addWidget(lblDescr, 0, 0, 1, 2) - layout.addWidget(self.chkIsMine, 1, 0) + layout.addWidget(lblDescr, 0, 0, 1, 2) + layout.addWidget(self.chkIsMine, 1, 0) self.chkIsMine.setChecked(True) self.edtOwnerString.setEnabled(False) else: owner = parent.main.getWltSetting(wltID, 'BelongsTo') - if owner=='': - owner='someone else' + if owner == '': + owner = 'someone else' else: self.edtOwnerString.setText(owner) lblDescr = QLabel( 'The funds in this wallet are currently identified as ' - 'belonging to '+owner+'. If these funds are actually ' + 'belonging to ' + owner + '. If these funds are actually ' 'yours, and you would like the funds included in your balance in ' - 'the main window, please check the box below.\n\n' ) + 'the main window, please check the box below.\n\n') lblDescr.setWordWrap(True) - layout.addWidget(lblDescr, 0, 0, 1, 2) - layout.addWidget(self.chkIsMine, 1, 0) + layout.addWidget(lblDescr, 0, 0, 1, 2) + layout.addWidget(self.chkIsMine, 1, 0) ttip = self.main.createToolTipWidget( 'You might choose this option if you keep a full ' 'wallet on a non-internet-connected computer, and use this ' 'watching-only wallet on this computer to generate addresses ' 'and monitor incoming transactions.') - layout.addWidget(ttip, 1, 1) + layout.addWidget(ttip, 1, 1) slot = lambda b: self.edtOwnerString.setEnabled(not b) self.connect(self.chkIsMine, SIGNAL('toggled(bool)'), slot) - - layout.addWidget(QLabel('Wallet owner (optional):'), 3, 0) - layout.addWidget(self.edtOwnerString, 3, 1) + + layout.addWidget(QLabel('Wallet owner (optional):'), 3, 0) + layout.addWidget(self.edtOwnerString, 3, 1) bbox = QDialogButtonBox(QDialogButtonBox.Ok | \ QDialogButtonBox.Cancel) self.connect(bbox, SIGNAL('accepted()'), self.accept) self.connect(bbox, SIGNAL('rejected()'), self.reject) - layout.addWidget(bbox, 4, 0) + layout.addWidget(bbox, 4, 0) self.setLayout(layout) self.setWindowTitle('Set Wallet Owner') @@ -1862,23 +2521,23 @@ def __init__(self, wltID, parent=None, main=None): def showRecvCoinsWarningIfNecessary(wlt, main): numTimesOnline = main.getSettingOrSetDefault("SyncSuccessCount", 0) - if numTimesOnline < 1 and not TheBDM.getBDMState()=='Offline': + if numTimesOnline < 1 and not TheBDM.getBDMState() == 'Offline': result = QMessageBox.warning(main, tr('Careful!'), tr(""" - Armory is not online yet, and will eventually need to be online to + Armory is not online yet, and will eventually need to be online to access any funds sent to your wallet. Please do not - receive Bitcoins to your Armory wallets until you have successfully + receive Bitcoins to your Armory wallets until you have successfully gotten online at least one time.

    Armory is still beta software, and some users report difficulty - ever getting online. + ever getting online.

    Do you wish to continue?"""), QMessageBox.Cancel | QMessageBox.Ok) - if not result==QMessageBox.Ok: + if not result == QMessageBox.Ok: return False wlttype = determineWalletType(wlt, main)[0] - notMyWallet = (wlttype==WLTTYPES.WatchOnly) - offlineWallet = (wlttype==WLTTYPES.Offline) + notMyWallet = (wlttype == WLTTYPES.WatchOnly) + offlineWallet = (wlttype == WLTTYPES.Offline) dnaaPropName = 'Wallet_%s_%s' % (wlt.uniqueIDB58, 'DNAA_RecvOther') dnaaThisWallet = main.getSettingOrSetDefault(dnaaPropName, False) if notMyWallet and not dnaaThisWallet: @@ -1922,8 +2581,8 @@ def __init__(self, wlt, parent=None, main=None): self.wlt = wlt self.addressesWereGenerated = False - - self.lblDescr = QRichLabel( \ + + self.lblDescr = QRichLabel(\ 'Armory pre-computes a pool of addresses beyond the last address ' 'you have used, and keeps them in your wallet to "look-ahead." One ' 'reason it does this is in case you have restored this wallet from ' @@ -1936,15 +2595,15 @@ def __init__(self, wlt, parent=None, main=None): 'rare, but it can happen. You may extend the keypool manually, ' 'below.') - self.lblAddrUsed = QRichLabel('Addresses used: ', doWrap=False) - self.lblAddrComp = QRichLabel('Addresses computed: ', doWrap=False) - self.lblAddrUsedVal = QRichLabel('%d' % max(0,self.wlt.highestUsedChainIndex)) + self.lblAddrUsed = QRichLabel('Addresses used: ', doWrap=False) + self.lblAddrComp = QRichLabel('Addresses computed: ', doWrap=False) + self.lblAddrUsedVal = QRichLabel('%d' % max(0, self.wlt.highestUsedChainIndex)) self.lblAddrCompVal = QRichLabel('%d' % self.wlt.lastComputedChainIndex) self.lblNumAddr = QRichLabel('Compute this many more addresses: ') self.edtNumAddr = QLineEdit() self.edtNumAddr.setText('100') - self.edtNumAddr.setMaximumWidth( relaxedSizeStr(self,'9999999')[0]) + self.edtNumAddr.setMaximumWidth(relaxedSizeStr(self, '9999999')[0]) self.lblWarnSpeed = QRichLabel( 'Address computation is very slow. It may take up to one minute ' @@ -1955,20 +2614,20 @@ def __init__(self, wlt, parent=None, main=None): buttonBox = QDialogButtonBox() self.btnAccept = QPushButton("Compute") self.btnReject = QPushButton("Done") - self.connect(self.btnAccept, SIGNAL('clicked()'), self.clickCompute) - self.connect(self.btnReject, SIGNAL('clicked()'), self.reject) + self.connect(self.btnAccept, SIGNAL(CLICKED), self.clickCompute) + self.connect(self.btnReject, SIGNAL(CLICKED), self.reject) buttonBox.addButton(self.btnAccept, QDialogButtonBox.AcceptRole) buttonBox.addButton(self.btnReject, QDialogButtonBox.RejectRole) - frmLbl = makeVertFrame([self.lblAddrUsed, self.lblAddrComp ]) - frmVal = makeVertFrame([self.lblAddrUsedVal, self.lblAddrCompVal]) - subFrm1 = makeHorizFrame(['Stretch', frmLbl, frmVal, 'Stretch'], STYLE_SUNKEN) - - subFrm2 = makeHorizFrame(['Stretch', \ + frmLbl = makeVertFrame([self.lblAddrUsed, self.lblAddrComp ]) + frmVal = makeVertFrame([self.lblAddrUsedVal, self.lblAddrCompVal]) + subFrm1 = makeHorizFrame([STRETCH, frmLbl, frmVal, STRETCH], STYLE_SUNKEN) + + subFrm2 = makeHorizFrame([STRETCH, \ self.lblNumAddr, \ self.edtNumAddr, \ - 'Stretch'], STYLE_SUNKEN) + STRETCH], STYLE_SUNKEN) layout = QVBoxLayout() layout.addWidget(self.lblDescr) @@ -1983,24 +2642,24 @@ def __init__(self, wlt, parent=None, main=None): ############################################################################# def reject(self): - if self.addressesWereGenerated and not TheBDM.getBDMState() in ('Offline','Uninitialized'): + if self.addressesWereGenerated and not TheBDM.getBDMState() in ('Offline', 'Uninitialized'): QMessageBox.warning(self, 'Rescan Required', \ 'New addresses have been generated for your wallet, but their ' 'balances are not yet reflected on the main screen. You must ' 'initiate a blockchain rescan before this happens. Press the ' 'button on the dashboard to do a rescan, or simply restart Armory', \ QMessageBox.Ok) - + super(DlgKeypoolSettings, self).reject() ############################################################################# def clickCompute(self): - #if TheBDM.getBDMState()=='Scanning': - #QMessageBox.warning(self, 'Armory is Busy', \ - #'Armory is in the middle of a scan, and cannot add addresses to ' - #'any of its wallets until the scan is finished. Please wait until ' - #'the dashboard says that Armory is "online."', QMessageBox.Ok) - #return + # if TheBDM.getBDMState()=='Scanning': + # QMessageBox.warning(self, 'Armory is Busy', \ + # 'Armory is in the middle of a scan, and cannot add addresses to ' + # 'any of its wallets until the scan is finished. Please wait until ' + # 'the dashboard says that Armory is "online."', QMessageBox.Ok) + # return err = False @@ -2009,30 +2668,37 @@ def clickCompute(self): except: err = True - if err or naddr<1: + if err or naddr < 1: QMessageBox.critical(self, 'Invalid input', \ 'The value you entered is invalid. Please enter a positive ' 'number of addresses to generate.', QMessageBox.Ok) return - - if naddr>=1000: + + if naddr >= 1000: confirm = QMessageBox.warning(self, 'Are you sure?', \ 'You have entered that you want to compute %d more addresses ' 'for this wallet. This operation will take a very long time, ' 'and Armory will become unresponsive until the computation is ' 'finished. Armory estimates it will take about %d minutes. ' - '

    Do you want to continue?' % (naddr, int(naddr/750.)), \ + '

    Do you want to continue?' % (naddr, int(naddr / 750.)), \ QMessageBox.Yes | QMessageBox.No) - - if not confirm==QMessageBox.Yes: + + if not confirm == QMessageBox.Yes: return cred = htmlColor('TextRed') - self.lblAddrCompVal.setText('Calculating...' % cred) - + self.lblAddrCompVal.setText('Calculating...' % \ + cred) + def doit(): - currPool = self.wlt.lastComputedChainIndex - self.wlt.highestUsedChainIndex - self.wlt.fillAddressPool(currPool+naddr, isActuallyNew=False) + currPool = self.wlt.lastComputedChainIndex - \ + self.wlt.highestUsedChainIndex + fillAddressPoolProgress = DlgProgress(self, self.main, HBar=1, + Title='Computing New Addresses') + fillAddressPoolProgress.exec_( \ + self.wlt.fillAddressPool, currPool + naddr, + isActuallyNew=False) + self.lblAddrCompVal.setText('%d' % \ (cred, self.wlt.lastComputedChainIndex)) self.addressesWereGenerated = True @@ -2052,25 +2718,25 @@ class DlgNewAddressDisp(ArmoryDialog): def __init__(self, wlt, parent=None, main=None): super(DlgNewAddressDisp, self).__init__(parent, main) - self.wlt = wlt + self.wlt = wlt self.addr = wlt.getNextUnusedAddress() addrStr = self.addr.getAddrStr() - wlttype = determineWalletType( self.wlt, self.main)[0] - notMyWallet = (wlttype==WLTTYPES.WatchOnly) - offlineWallet = (wlttype==WLTTYPES.Offline) + wlttype = determineWalletType(self.wlt, self.main)[0] + notMyWallet = (wlttype == WLTTYPES.WatchOnly) + offlineWallet = (wlttype == WLTTYPES.Offline) - lblDescr = QLabel( \ + lblDescr = QLabel(\ 'The following address can be used to receive bitcoins:') self.edtNewAddr = QLineEdit() self.edtNewAddr.setReadOnly(True) self.edtNewAddr.setText(addrStr) self.edtNewAddr.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) btnClipboard = QPushButton('Copy to Clipboard') - #lbtnClipboard.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) + # lbtnClipboard.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.lblIsCopied = QLabel(' or ') self.lblIsCopied.setTextFormat(Qt.RichText) - self.connect(btnClipboard, SIGNAL('clicked()'), self.setClipboard) + self.connect(btnClipboard, SIGNAL(CLICKED), self.setClipboard) def openPaymentRequest(): msgTxt = str(self.edtComm.toPlainText()) @@ -2079,32 +2745,32 @@ def openPaymentRequest(): dlg.exec_() btnLink = QPushButton('Create Clickable Link') - self.connect(btnLink, SIGNAL('clicked()'), openPaymentRequest) + self.connect(btnLink, SIGNAL(CLICKED), openPaymentRequest) - - tooltip1 = self.main.createToolTipWidget( \ + + tooltip1 = self.main.createToolTipWidget(\ 'You can securely use this address as many times as you want. ' 'However, all people to whom you give this address will ' 'be able to see the number and amount of bitcoins ever ' 'sent to it. Therefore, using a new address for each transaction ' 'improves overall privacy, but there is no security issues ' - 'with reusing any address.' ) + 'with reusing any address.') frmNewAddr = QFrame() frmNewAddr.setFrameStyle(STYLE_RAISED) frmNewAddrLayout = QGridLayout() - frmNewAddrLayout.addWidget(lblDescr, 0,0, 1,2) - frmNewAddrLayout.addWidget(self.edtNewAddr, 1,0, 1,1) - frmNewAddrLayout.addWidget(tooltip1, 1,1, 1,1) + frmNewAddrLayout.addWidget(lblDescr, 0, 0, 1, 2) + frmNewAddrLayout.addWidget(self.edtNewAddr, 1, 0, 1, 1) + frmNewAddrLayout.addWidget(tooltip1, 1, 1, 1, 1) if not notMyWallet: palette = QPalette() - palette.setColor( QPalette.Base, Colors.TblWltMine ) + palette.setColor(QPalette.Base, Colors.TblWltMine) boldFont = self.edtNewAddr.font() boldFont.setWeight(QFont.Bold) self.edtNewAddr.setFont(boldFont) - self.edtNewAddr.setPalette( palette ); - self.edtNewAddr.setAutoFillBackground( True ); + self.edtNewAddr.setPalette(palette); + self.edtNewAddr.setAutoFillBackground(True); frmCopy = QFrame() frmCopy.setFrameShape(QFrame.NoFrame) @@ -2118,42 +2784,42 @@ def openPaymentRequest(): frmNewAddrLayout.addWidget(frmCopy, 2, 0, 1, 2) frmNewAddr.setLayout(frmNewAddrLayout) - - lblCommDescr = QLabel( \ + + lblCommDescr = QLabel(\ '(Optional) Add a label to this address, which will ' 'be shown with any relevant transactions in the ' '"Transactions" tab.') lblCommDescr.setWordWrap(True) self.edtComm = QTextEdit() tightHeight = tightSizeNChar(self.edtComm, 1)[1] - self.edtComm.setMaximumHeight(tightHeight*3.2) + self.edtComm.setMaximumHeight(tightHeight * 3.2) frmComment = QFrame() frmComment.setFrameStyle(STYLE_RAISED) frmCommentLayout = QGridLayout() - frmCommentLayout.addWidget(lblCommDescr, 0,0, 1,2) - frmCommentLayout.addWidget(self.edtComm, 1,0, 2,2) + frmCommentLayout.addWidget(lblCommDescr, 0, 0, 1, 2) + frmCommentLayout.addWidget(self.edtComm, 1, 0, 2, 2) frmComment.setLayout(frmCommentLayout) - - lblRecvWlt = QRichLabel( 'Bitcoins sent to this address will ' + + lblRecvWlt = QRichLabel('Bitcoins sent to this address will ' 'appear in the wallet:', doWrap=False) - + lblRecvWlt.setWordWrap(True) lblRecvWlt.setAlignment(Qt.AlignHCenter | Qt.AlignTop) - lblRecvWlt.setMinimumWidth( tightSizeStr(lblRecvWlt, lblRecvWlt.text())[0]) + lblRecvWlt.setMinimumWidth(tightSizeStr(lblRecvWlt, lblRecvWlt.text())[0]) - lblRecvWltID = QLabel( \ + lblRecvWltID = QLabel(\ '"%s" (%s)' % (wlt.labelName, wlt.uniqueIDB58)) lblRecvWltID.setWordWrap(True) lblRecvWltID.setTextFormat(Qt.RichText) lblRecvWltID.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) - + buttonBox = QDialogButtonBox() - self.btnDone = QPushButton("Done") - self.connect(self.btnDone, SIGNAL('clicked()'), self.acceptNewAddr) - buttonBox.addButton(self.btnDone, QDialogButtonBox.AcceptRole) + self.btnDone = QPushButton("Done") + self.connect(self.btnDone, SIGNAL(CLICKED), self.acceptNewAddr) + buttonBox.addButton(self.btnDone, QDialogButtonBox.AcceptRole) @@ -2172,36 +2838,41 @@ def openPaymentRequest(): qrcode = QRCodeWidget(addrStr, parent=self) smLabel = QRichLabel('%s' % addrStr) smLabel.setAlignment(Qt.AlignHCenter | Qt.AlignTop) - frmQRsub2 = makeHorizFrame( ['Stretch', qrcode, 'Stretch' ]) - frmQRsub3 = makeHorizFrame( ['Stretch', smLabel, 'Stretch' ]) - frmQR = makeVertFrame( ['Stretch', qrdescr, frmQRsub2, frmQRsub3, 'Stretch' ], STYLE_SUNKEN) - - layout=QGridLayout() - layout.addWidget(frmNewAddr, 0, 0, 1, 1) - layout.addWidget(frmComment, 2, 0, 1, 1) - layout.addWidget(frmWlt, 3, 0, 1, 1) - layout.addWidget(buttonBox, 4, 0, 1, 2) - layout.addWidget(frmQR, 0, 1, 4, 1) - - self.setLayout(layout) + frmQRsub2 = makeHorizFrame([STRETCH, qrcode, STRETCH ]) + frmQRsub3 = makeHorizFrame([STRETCH, smLabel, STRETCH ]) + frmQR = makeVertFrame([STRETCH, qrdescr, frmQRsub2, frmQRsub3, STRETCH ], STYLE_SUNKEN) + + layout = QGridLayout() + layout.addWidget(frmNewAddr, 0, 0, 1, 1) + layout.addWidget(frmComment, 2, 0, 1, 1) + layout.addWidget(frmWlt, 3, 0, 1, 1) + layout.addWidget(buttonBox, 4, 0, 1, 2) + layout.addWidget(frmQR, 0, 1, 4, 1) + + self.setLayout(layout) self.setWindowTitle('New Receiving Address') self.setFocus() + from twisted.internet import reactor + + if TheBDM.getBDMState()=='BlockchainReady': + reactor.callLater(0.1, TheBDM.saveScrAddrHistories) + try: self.parent.wltAddrModel.reset() except AttributeError: # Sometimes this is called from a dialog that doesn't have an addr model pass - + def acceptNewAddr(self): comm = str(self.edtComm.toPlainText()) - if len(comm)>0: + if len(comm) > 0: self.wlt.setComment(self.addr.getAddr160(), comm) self.accept() def rejectNewAddr(self): - #self.wlt.rewindHighestIndex(1) + # self.wlt.rewindHighestIndex(1) try: self.parent.reject() except AttributeError: @@ -2215,7 +2886,7 @@ def setClipboard(self): self.lblIsCopied.setText('Copied!') - + ############################################################################# @@ -2228,22 +2899,19 @@ def __init__(self, wlt, parent=None, main=None): lblImportLbl = QRichLabel('Enter:') - self.radioImportOne = QRadioButton('One Key') - self.radioImportMany = QRadioButton('Multiple Keys') + self.radioImportOne = QRadioButton('One Key') + self.radioImportMany = QRadioButton('Multiple Keys') btngrp = QButtonGroup(self) btngrp.addButton(self.radioImportOne) btngrp.addButton(self.radioImportMany) btngrp.setExclusive(True) self.radioImportOne.setChecked(True) - self.connect(self.radioImportOne, SIGNAL('clicked()'), self.clickImportCount) - self.connect(self.radioImportMany, SIGNAL('clicked()'), self.clickImportCount) + self.connect(self.radioImportOne, SIGNAL(CLICKED), self.clickImportCount) + self.connect(self.radioImportMany, SIGNAL(CLICKED), self.clickImportCount) frmTop = makeHorizFrame([lblImportLbl, self.radioImportOne, \ - self.radioImportMany, 'Stretch']) + self.radioImportMany, STRETCH]) self.stackedImport = QStackedWidget() - stkOneLayout = QVBoxLayout() - stkManyLayout = QVBoxLayout() - # Set up the single-key import widget lblDescrOne = QRichLabel('The key can either be imported into your wallet, ' @@ -2257,8 +2925,8 @@ def __init__(self, wlt, parent=None, main=None): lblPrivOne = QRichLabel('Private Key') self.edtPrivData = QLineEdit() - self.edtPrivData.setMinimumWidth( tightSizeStr(self.edtPrivData, 'X'*80)[0]) - privTooltip = self.main.createToolTipWidget( \ + self.edtPrivData.setMinimumWidth(tightSizeStr(self.edtPrivData, 'X' * 60)[0]) + privTooltip = self.main.createToolTipWidget(\ 'Supported formats are any hexadecimal or Base58 ' 'representation of a 32-byte private key (with or ' 'without checksums), and mini-private-key format ' @@ -2267,37 +2935,54 @@ def __init__(self, wlt, parent=None, main=None): 'supported by Armory.') frmMid1 = makeHorizFrame([lblPrivOne, self.edtPrivData, privTooltip]) - stkOne = makeVertFrame([HLINE(),lblDescrOne, frmMid1, 'Stretch']) + stkOne = makeVertFrame([HLINE(), lblDescrOne, frmMid1, STRETCH]) self.stackedImport.addWidget(stkOne) - + # Set up the multi-key import widget - lblDescrMany = QRichLabel( \ + lblDescrMany = QRichLabel(\ 'Enter a list of private keys to be "swept" or imported. ' 'All standard private-key formats are supported. ') lblPrivMany = QRichLabel('Private Key List') lblPrivMany.setAlignment(Qt.AlignTop) - ttipPrivMany = self.main.createToolTipWidget( \ + ttipPrivMany = self.main.createToolTipWidget(\ 'One private key per line, in any standard format. ' 'Data may be copied directly from file the "Backup ' 'Individual Keys" dialog (all text on a line preceding ' 'the key data, separated by a colon, will be ignored).') self.txtPrivBulk = QTextEdit() - w,h = tightSizeStr(self.edtPrivData, 'X'*70) + w, h = tightSizeStr(self.edtPrivData, 'X' * 70) self.txtPrivBulk.setMinimumWidth(w) - self.txtPrivBulk.setMinimumHeight( 2.2*h) - self.txtPrivBulk.setMaximumHeight( 4.2*h) + self.txtPrivBulk.setMinimumHeight(2.2 * h) + self.txtPrivBulk.setMaximumHeight(4.2 * h) frmMid = makeHorizFrame([lblPrivMany, self.txtPrivBulk, ttipPrivMany]) - stkMany = makeVertFrame([HLINE(),lblDescrMany, frmMid]) + stkMany = makeVertFrame([HLINE(), lblDescrMany, frmMid]) self.stackedImport.addWidget(stkMany) + + self.chkUseSP = QCheckBox(tr(""" + This is from a backup with SecurePrint\xe2\x84\xa2""")) + self.edtSecurePrint = QLineEdit() + self.edtSecurePrint.setFont(GETFONT('Fixed',9)) + self.edtSecurePrint.setEnabled(False) + w, h = relaxedSizeStr(self.edtSecurePrint, 'X' * 12) + self.edtSecurePrint.setMaximumWidth(w) + + def toggleSP(): + if self.chkUseSP.isChecked(): + self.edtSecurePrint.setEnabled(True) + else: + self.edtSecurePrint.setEnabled(False) + self.chkUseSP.stateChanged.connect(toggleSP) + frmSP = makeHorizFrame([self.chkUseSP, self.edtSecurePrint, 'Stretch']) + #frmSP.setFrameStyle(STYLE_PLAIN) # Set up the Import/Sweep select frame - ## Import option - self.radioSweep = QRadioButton('Sweep any funds owned by these addresses ' + # # Import option + self.radioSweep = QRadioButton('Sweep any funds owned by these addresses ' 'into your wallet\n' 'Select this option if someone else gave you this key') self.radioImport = QRadioButton('Import these addresses to your wallet\n' @@ -2305,30 +2990,30 @@ def __init__(self, wlt, parent=None, main=None): 'that no one else has access to this key') - ## Sweep option (only available when online) - if TheBDM.getBDMState()=='BlockchainReady': - self.radioSweep = QRadioButton('Sweep any funds owned by this address ' + # # Sweep option (only available when online) + if TheBDM.getBDMState() == 'BlockchainReady': + 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: - if TheBDM.getBDMState() in ('Offline','Uninitialized'): - self.radioSweep = QRadioButton('Sweep any funds owned by this address ' + if TheBDM.getBDMState() in ('Offline', 'Uninitialized'): + self.radioSweep = QRadioButton('Sweep any funds owned by this address ' 'into your wallet\n' '(Not available in offline mode)') - elif TheBDM.getBDMState()=='Scanning': - self.radioSweep = QRadioButton(tr(""" + elif TheBDM.getBDMState() == 'Scanning': + self.radioSweep = QRadioButton(tr(""" Sweep any funds owned by this address into your wallet""")) self.radioImport.setChecked(True) self.radioSweep.setEnabled(False) - sweepTooltip = self.main.createToolTipWidget( \ + sweepTooltip = self.main.createToolTipWidget(\ '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 ' 'itself. You should use this option for Casascius physical bitcoins.') - importTooltip = self.main.createToolTipWidget( \ + importTooltip = self.main.createToolTipWidget(\ 'This option will make the key part of your wallet, meaning that it ' 'can be used to securely receive future payments. Never select this ' 'option for private keys that other people may have access to.') @@ -2341,28 +3026,29 @@ def __init__(self, wlt, parent=None, main=None): btngrp.setExclusive(True) frmWarn = QFrame() - frmWarn.setFrameStyle(QFrame.Box|QFrame.Plain) + frmWarn.setFrameStyle(QFrame.Box | QFrame.Plain) frmWarnLayout = QGridLayout() - frmWarnLayout.addWidget(self.radioSweep, 0,0, 1,1) - frmWarnLayout.addWidget(self.radioImport, 1,0, 1,1) - frmWarnLayout.addWidget(sweepTooltip, 0,1, 1,1) - frmWarnLayout.addWidget(importTooltip, 1,1, 1,1) + frmWarnLayout.addWidget(self.radioSweep, 0, 0, 1, 1) + frmWarnLayout.addWidget(self.radioImport, 1, 0, 1, 1) + frmWarnLayout.addWidget(sweepTooltip, 0, 1, 1, 1) + frmWarnLayout.addWidget(importTooltip, 1, 1, 1, 1) frmWarn.setLayout(frmWarnLayout) - + buttonbox = QDialogButtonBox(QDialogButtonBox.Ok | \ QDialogButtonBox.Cancel) self.connect(buttonbox, SIGNAL('accepted()'), self.okayClicked) self.connect(buttonbox, SIGNAL('rejected()'), self.reject) - - + + layout = QVBoxLayout() layout.addWidget(frmTop) layout.addWidget(self.stackedImport) + layout.addWidget(frmSP) layout.addWidget(frmWarn) layout.addWidget(buttonbox) @@ -2383,19 +3069,48 @@ def clickImportCount(self): ############################################################################# def okayClicked(self): + pwd = None + if self.chkUseSP.isChecked(): + SECPRINT = HardcodedKeyMaskParams() + pwd = str(self.edtSecurePrint.text()).strip() + self.edtSecurePrint.setText("") + + if len(pwd) < 9: + QMessageBox.critical(self, 'Invalid Code', tr(""" + You didn't enter a full SecurePrint\xe2\x84\xa2 code. This + code is needed to decrypt your backup. If this backup is + actually unencrypted and there is no code, then choose the + appropriate backup type from the drop-down box"""), QMessageBox.Ok) + return + + if not SECPRINT['FUNC_CHKPWD'](pwd): + QMessageBox.critical(self, 'Bad Encryption Code', tr(""" + The SecurePrint\xe2\x84\xa2 code you entered has an error + in it. Note that the code is case-sensitive. Please verify + you entered it correctly and try again."""), QMessageBox.Ok) + return + if self.radioImportOne.isChecked(): - self.processUserString() + self.processUserString(pwd) else: - self.processMultiKey() + self.processMultiKey(pwd) ############################################################################# - def processUserString(self): - theStr = str(self.edtPrivData.text()).strip().replace(' ','') - binKeyData, addr160, addrStr = '','','' - + def processUserString(self, pwd=None): + theStr = str(self.edtPrivData.text()).strip().replace(' ', '') + binKeyData, addr160, addrStr = '', '', '' + try: binKeyData, keyType = parsePrivateKeyData(theStr) + + if pwd: + SECPRINT = HardcodedKeyMaskParams() + maskKey = SECPRINT['FUNC_KDF'](pwd) + SBDbinKeyData = SECPRINT['FUNC_UNMASK'](SecureBinaryData(binKeyData), ekey=maskKey) + binKeyData = SBDbinKeyData.toBinStr() + SBDbinKeyData.destroy() + if binary_to_int(binKeyData, BIGENDIAN) >= SECP256K1_ORDER: QMessageBox.critical(self, 'Invalid Private Key', \ 'The private key you have entered is actually not valid ' @@ -2436,7 +3151,7 @@ def processUserString(self): 'Please check that you entered it correctly', QMessageBox.Ok) LOGEXCEPT('Error processing the private key data') return - + if not 'mini' in keyType.lower(): @@ -2445,10 +3160,10 @@ def processUserString(self): 'the following Bitcoin address:\n\n\t' + addrStr + '\n\nIs this the correct address?', QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) - if reply==QMessageBox.Cancel: - return + if reply == QMessageBox.Cancel: + return else: - if reply==QMessageBox.No: + if reply == QMessageBox.No: binKeyData = binary_switchEndian(binKeyData) addr160 = convertKeyDataToAddress(privKey=binKeyData) addrStr = hash160_to_addrStr(addr160) @@ -2458,8 +3173,8 @@ def processUserString(self): 'reversed, the following address is obtained:\n\n\t ' + addrStr + '\n\nIs this the correct address?', \ QMessageBox.Yes | QMessageBox.No) - if reply==QMessageBox.No: - binKeyData='' + if reply == QMessageBox.No: + binKeyData = '' return @@ -2474,37 +3189,37 @@ def processUserString(self): 'negative if you have to pay a fee for the transfer)\n\n' 'Do you still want to sweep this key?', \ QMessageBox.Yes | QMessageBox.Cancel) - if not result==QMessageBox.Yes: + if not result == QMessageBox.Yes: return else: wltID = self.main.getWalletForAddr160(addr160) - if not wltID=='': + if not wltID == '': addr = self.main.walletMap[wltID].addrMap[addr160] - typ = 'Imported' if addr.chainIndex==-2 else 'Permanent' + typ = 'Imported' if addr.chainIndex == -2 else 'Permanent' msg = ('The key you entered is already part of another wallet you ' 'are maintaining:' '

    ' 'Address: ' + addrStr + '
    ' 'Wallet ID: ' + wltID + '
    ' 'Wallet Name: ' + self.main.walletMap[wltID].labelName + '
    ' - 'Address Type: ' + typ + + 'Address Type: ' + typ + '

    ' 'The sweep operation will simply move bitcoins out of the wallet ' 'above into this wallet. If the network charges a fee for this ' 'transaction, you balance will be reduced by that much.') result = QMessageBox.warning(self, 'Duplicate Address', msg, \ QMessageBox.Ok | QMessageBox.Cancel) - if not result==QMessageBox.Ok: + if not result == QMessageBox.Ok: return - - #if not TheBDM.getBDMState()=='BlockchainReady': - #reply = QMessageBox.critical(self, 'Cannot Sweep Address', \ - #'You need access to the Bitcoin network and the blockchain in order ' - #'to find the balance of this address and sweep its funds. ', \ - #QMessageBox.Ok) - #return + + # if not TheBDM.getBDMState()=='BlockchainReady': + # reply = QMessageBox.critical(self, 'Cannot Sweep Address', \ + # 'You need access to the Bitcoin network and the blockchain in order ' + # 'to find the balance of this address and sweep its funds. ', \ + # QMessageBox.Ok) + # return # Create the address object for the addr to be swept sweepAddr = PyBtcAddress().createFromPlainKeyData(SecureBinaryData(binKeyData)) @@ -2524,19 +3239,19 @@ def processUserString(self): return wltID = self.main.getWalletForAddr160(addr160) - if not wltID=='': + if not wltID == '': addr = self.main.walletMap[wltID].addrMap[addr160] - typ = 'Imported' if addr.chainIndex==-2 else 'Permanent' + typ = 'Imported' if addr.chainIndex == -2 else 'Permanent' msg = tr('The key you entered is already part of another wallet you own:' '

    ' 'Address: ' + addrStr + '
    ' 'Wallet ID: ' + wltID + '
    ' 'Wallet Name: ' + self.main.walletMap[wltID].labelName + '
    ' - 'Address Type: ' + typ + + 'Address Type: ' + typ + '

    ' 'Armory cannot properly display balances or create transactions ' 'when the same address is in multiple wallets at once. ') - if typ=='Imported': + if typ == 'Imported': QMessageBox.critical(self, 'Duplicate Addresses', \ msg + 'To import this address to this wallet, please remove it from the ' 'other wallet, then try the import operation again.', QMessageBox.Ok) @@ -2547,25 +3262,25 @@ def processUserString(self): 'imported to any other wallet. The import operation cannot ' 'continue.', QMessageBox.Ok) return - + if self.wlt.useEncryption and self.wlt.isLocked: dlg = DlgUnlockWallet(self.wlt, self, self.main, 'Encrypt New Address') if not dlg.exec_(): reply = QMessageBox.critical(self, 'Wallet is locked', 'New private key data cannot be imported unless the wallet is ' - 'unlocked. Please try again when you have the passphrase.',\ + 'unlocked. Please try again when you have the passphrase.', \ QMessageBox.Ok) return - self.wlt.importExternalAddressData( privKey=SecureBinaryData(binKeyData)) - self.main.statusBar().showMessage( 'Successful import of address ' \ + self.wlt.importExternalAddressData(privKey=SecureBinaryData(binKeyData)) + self.main.statusBar().showMessage('Successful import of address ' \ + addrStr + ' into wallet ' + self.wlt.uniqueIDB58, 10000) ####################################################################### - if TheBDM.getBDMState()=='BlockchainReady': + if TheBDM.getBDMState() == 'BlockchainReady': nblk = TheBDM.numBlocksToRescan(self.wlt.cppWallet, wait=True) - if nblk<2016: + if nblk < 2016: self.wlt.syncWithBlockchain(0) QMessageBox.information(self, 'Import Successful', \ 'The address was imported into your wallet successfully, and ' @@ -2591,10 +3306,10 @@ def processUserString(self): else: LOGINFO('User requested no rescan after import. Should be dirty.') self.main.setDashboardDetails() - + ####################################################################### - elif TheBDM.getBDMState()=='Scanning': - warnMsg = ( \ + elif TheBDM.getBDMState() == 'Scanning': + warnMsg = (\ 'The address was imported successfully, but your wallet balance ' 'will be incorrect until the global transaction history is ' 'searched for previous transactions. Armory is currently in the ' @@ -2608,7 +3323,7 @@ def processUserString(self): self.main.walletListChanged() - + try: self.parent.wltAddrModel.reset() except: @@ -2618,12 +3333,16 @@ def processUserString(self): ############################################################################# - def processMultiKey(self): + def processMultiKey(self, pwd=None): thisWltID = self.wlt.uniqueIDB58 inputText = str(self.txtPrivBulk.toPlainText()) - inputLines = [s.strip().replace(' ','') for s in inputText.split('\n')] - binKeyData, addr160, addrStr = '','','' + inputLines = [s.strip().replace(' ', '') for s in inputText.split('\n')] + binKeyData, addr160, addrStr = '', '', '' + + if pwd: + SECPRINT = HardcodedKeyMaskParams() + maskKey = SECPRINT['FUNC_KDF'](pwd) privKeyList = [] addrSet = set() @@ -2635,6 +3354,8 @@ def processMultiKey(self): try: nLines += 1 binKeyData = SecureBinaryData(parsePrivateKeyData(lineend)[0]) + if pwd: binKeyData = SECPRINT['FUNC_UNMASK'](binKeyData, ekey=maskKey) + addr160 = convertKeyDataToAddress(privKey=binKeyData.toBinStr()) if not addr160 in addrSet: addrSet.add(addr160) @@ -2644,26 +3365,26 @@ def processMultiKey(self): LOGWARN('Key line skipped, probably not a private key (key not shown for security)') continue - if len(privKeyList)==0: - if nLines>1: + if len(privKeyList) == 0: + if nLines > 1: QMessageBox.critical(self, 'Invalid Data', \ - 'No valid private key data was entered.', QMessageBox.Ok ) + 'No valid private key data was entered.', QMessageBox.Ok) return - #privKeyList now contains: + # privKeyList now contains: # [ [A160, AddrStr, Priv], - # [A160, AddrStr, Priv], + # [A160, AddrStr, Priv], # [A160, AddrStr, Priv], ... ] - # Determine if any addresses are already part of some wallets + # Determine if any addresses are already part of some wallets addr_to_wltID = lambda a: self.main.getWalletForAddr160(a) allWltList = [ [addr_to_wltID(k[0]), k[1]] for k in privKeyList] # allWltList is now [ [WltID, AddrStr], [WltID, AddrStr], ... ] - + if self.radioSweep.isChecked(): ##### SWEEPING ##### - dupeWltList = filter(lambda a: len(a[0])>0, allWltList) - if len(dupeWltList)>0: + dupeWltList = filter(lambda a: len(a[0]) > 0, allWltList) + if len(dupeWltList) > 0: reply = QMessageBox.critical(self, 'Duplicate Addresses!', \ 'You are attempting to sweep %d addresses, but %d of them ' 'are already part of existing wallets. That means that some or ' @@ -2672,18 +3393,18 @@ def processMultiKey(self): 'Would you like to continue anyway?' % \ (len(allWltList), len(dupeWltList)), \ QMessageBox.Ok | QMessageBox.Cancel) - if reply==QMessageBox.Cancel: + if reply == QMessageBox.Cancel: return - - + + cppWlt = Cpp.BtcWallet() - for addr160,addrStr,SecurePriv in privKeyList: + for addr160, addrStr, SecurePriv in privKeyList: cppWlt.addScrAddress_1_(Hash160ToScrAddr(addr160)) - + # If we got here, let's go ahead and sweep! addrList = [] - for addr160,addrStr,SecurePriv in privKeyList: + for addr160, addrStr, SecurePriv in privKeyList: pyAddr = PyBtcAddress().createFromPlainKeyData(SecurePriv) addrList.append(pyAddr) @@ -2697,26 +3418,26 @@ def processMultiKey(self): # allWltList is [ [WltID, AddrStr], [WltID, AddrStr], ... ] # Warn about addresses that would be duplicates. - # Addresses already in the selected wallet will simply be skipped, no - # need to do anything about that -- only addresses that would appear in + # Addresses already in the selected wallet will simply be skipped, no + # need to do anything about that -- only addresses that would appear in # two wlts if we were to continue. - dupeWltList = filter(lambda a: (len(a[0])>0 and a[0]!=thisWltID), allWltList) - if len(dupeWltList)>0: + dupeWltList = filter(lambda a: (len(a[0]) > 0 and a[0] != thisWltID), allWltList) + if len(dupeWltList) > 0: dupeAddrStrList = [d[1] for d in dupeWltList] dlg = DlgDuplicateAddr(dupeAddrStrList, self, self.main) if not dlg.exec_(): return - + privKeyList = filter(lambda x: (x[1] not in dupeAddrStrList), privKeyList) - + # Confirm import addrStrList = [k[1] for k in privKeyList] dlg = DlgConfirmBulkImport(addrStrList, thisWltID, self, self.main) if not dlg.exec_(): return - + if self.wlt.useEncryption and self.wlt.isLocked: # Target wallet is encrypted... unlockdlg = DlgUnlockWallet(self.wlt, self, self.main, 'Unlock Wallet to Import') @@ -2725,46 +3446,45 @@ def processMultiKey(self): 'Cannot import private keys without unlocking wallet!', \ QMessageBox.Ok) return - - + + nTotal = 0 nImport = 0 nAlready = 0 - nError = 0 - for addr160,addrStr,sbdKey in privKeyList: - nTotal += 1 + nError = 0 + for addr160, addrStr, sbdKey in privKeyList: + nTotal += 1 try: - prevPartOfWallet = self.main.getWalletForAddr160(addr160) - if not self.main.getWalletForAddr160(addr160)==thisWltID: + if not self.main.getWalletForAddr160(addr160) == thisWltID: self.wlt.importExternalAddressData(privKey=sbdKey) nImport += 1 else: nAlready += 1 - except Exception,msg: - #print '***ERROR importing:', addrStr - #print ' Error Msg:', msg - #nError += 1 + except Exception, msg: + # print '***ERROR importing:', addrStr + # print ' Error Msg:', msg + # nError += 1 LOGERROR('Problem importing: %s: %s', addrStr, msg) raise - - if nAlready==nTotal: + + if nAlready == nTotal: MsgBoxCustom(MSGBOX.Warning, 'Nothing Imported!', 'All addresses ' 'chosen to be imported are already part of this wallet. ' 'Nothing was imported.') return - elif nImport==0 and nTotal>0: - MsgBoxCustom(MSGBOX.Error, 'Error!', tr( """ - Failed: No addresses could be imported. - Please check the logfile (ArmoryQt.exe.log) or the console output + elif nImport == 0 and nTotal > 0: + MsgBoxCustom(MSGBOX.Error, 'Error!', tr(""" + Failed: No addresses could be imported. + Please check the logfile (ArmoryQt.exe.log) or the console output for information about why it failed (and email support@bitcoinarmory.com for help fixing the problem). """)) return else: if nError == 0: - if nAlready>0: + if nAlready > 0: MsgBoxCustom(MSGBOX.Good, 'Success!', \ - 'Success: %d private keys were imported into your wallet. ' + 'Success: %d private keys were imported into your wallet. ' '

    ' 'The other %d private keys were skipped, because they were ' 'already part of your wallet.' % (nImport, nAlready)) @@ -2773,17 +3493,17 @@ def processMultiKey(self): 'Success: %d private keys were imported into your wallet.' % nImport) else: MsgBoxCustom(MSGBOX.Warning, 'Partial Success!', \ - '%d private keys were imported into your wallet, but there was ' + '%d private keys were imported into your wallet, but there were ' 'also %d addresses that could not be imported (see console ' 'or log file for more information). It is safe to try this ' 'operation again: all addresses previously imported will be ' - 'skipped. %s' % (nImport, nError, restartMsg)) - + 'skipped.' % (nImport, nError)) + ####################################################################### - if TheBDM.getBDMState()=='BlockchainReady': + if TheBDM.getBDMState() == 'BlockchainReady': nblk = TheBDM.numBlocksToRescan(self.wlt.cppWallet, wait=True) - if nblk<2016: + if nblk < 2016: self.wlt.syncWithBlockchain(0) QMessageBox.information(self, 'Import Successful', \ 'The addresses were imported into your wallet successfully, and ' @@ -2803,16 +3523,16 @@ def processMultiKey(self): 'you to stay in online mode, but your balances may be incorrect ' 'until you press the rescan button on the dashboard, or restart ' 'Armory', QMessageBox.Yes | QMessageBox.No) - if doRescanNow==QMessageBox.Yes: + if doRescanNow == QMessageBox.Yes: LOGINFO('User requested rescan immediately after import') self.main.startRescanBlockchain() else: LOGINFO('User requested no rescan after import. Should be dirty.') self.main.setDashboardDetails() - + ####################################################################### - elif TheBDM.getBDMState()=='Scanning': - warnMsg = tr( \ + elif TheBDM.getBDMState() == 'Scanning': + warnMsg = tr(\ 'The addresses were imported successfully, but your wallet balance ' 'will be incorrect until the global transaction history is ' 'searched for previous transactions. Armory is currently in the ' @@ -2822,7 +3542,7 @@ def processMultiKey(self): QMessageBox.Ok) self.main.startRescanBlockchain() self.main.setDashboardDetails() - + try: self.main.walletListChanged() @@ -2835,7 +3555,7 @@ def processMultiKey(self): pass self.accept() - + ############################################################################# class DlgVerifySweep(ArmoryDialog): @@ -2843,21 +3563,21 @@ def __init__(self, inputStr, outputStr, outVal, fee, parent=None, main=None): super(DlgVerifySweep, self).__init__(parent, main) - lblQuestion = QRichLabel( \ + lblQuestion = QRichLabel(\ 'You are about to sweep all funds from the specified address ' 'to your wallet. Please confirm the action:') - - outStr = coin2str(outVal,maxZeros=2) - feeStr = ('') if (fee==0) else ('(Fee: %s)' % coin2str(fee,maxZeros=0)) + + outStr = coin2str(outVal, maxZeros=2) + feeStr = ('') if (fee == 0) else ('(Fee: %s)' % coin2str(fee, maxZeros=0)) frm = QFrame() frm.setFrameStyle(STYLE_RAISED) frmLayout = QGridLayout() - #frmLayout.addWidget(QRichLabel('Funds will be swept...'), 0,0, 1,2) - frmLayout.addWidget(QRichLabel(' From ' + inputStr, doWrap=False), 1,0, 1,2) - frmLayout.addWidget(QRichLabel(' To ' + outputStr, doWrap=False), 2,0, 1,2) - frmLayout.addWidget(QRichLabel(' Total %s BTC %s'%(outStr,feeStr), doWrap=False), 3,0, 1,2) + # frmLayout.addWidget(QRichLabel('Funds will be swept...'), 0,0, 1,2) + frmLayout.addWidget(QRichLabel(' From ' + inputStr, doWrap=False), 1, 0, 1, 2) + frmLayout.addWidget(QRichLabel(' To ' + outputStr, doWrap=False), 2, 0, 1, 2) + frmLayout.addWidget(QRichLabel(' Total %s BTC %s' % (outStr, feeStr), doWrap=False), 3, 0, 1, 2) frm.setLayout(frmLayout) lblFinalConfirm = QLabel('Are you sure you want to execute this transaction?') @@ -2873,7 +3593,7 @@ def __init__(self, inputStr, outputStr, outVal, fee, parent=None, main=None): layout = QHBoxLayout() layout.addWidget(lblWarnImg) - layout.addWidget(makeLayoutFrame('Vert',[lblQuestion, frm, lblFinalConfirm, bbox])) + layout.addWidget(makeLayoutFrame(VERTICAL, [lblQuestion, frm, lblFinalConfirm, bbox])) self.setLayout(layout) self.setWindowTitle('Confirm Sweep') @@ -2881,375 +3601,42 @@ def __init__(self, inputStr, outputStr, outVal, fee, parent=None, main=None): -############################################################################# -#### Migration no longer is supported -- not until the new wallet format -#### is created that supports compressed public keys -# -#class DlgMigrateSatoshiWallet(ArmoryDialog): -# def __init__(self, parent=None, main=None): -# super(DlgMigrateSatoshiWallet, self).__init__(parent, main) -# -# -# lblDescr = QRichLabel( \ -# 'Specify the location of your regular Bitcoin wallet (wallet.dat) ' -# 'to be migrated into an Armory wallet. All private ' -# 'keys will be imported, giving you full access to those addresses, as ' -# 'if Armory had generated them natively.' -# '

    ' -# 'NOTE: 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 (typically ' + -# os.path.join(BTC_HOME_DIR, 'wallet.dat') + ')', doWrap=False) -# ttipWlt = self.main.createToolTipWidget(\ -# 'This is the wallet file used by the standard Bitcoin client from ' -# 'bitcoin.org. It contains all the information needed for Armory to ' -# 'know how to access the bitcoins maintained by that program') -# self.txtWalletPath = QLineEdit() -# -# -# -# self.chkAllKeys = QCheckBox('Include Address Pool (unused keys)') -# ttipAllKeys = self.main.createToolTipWidget( \ -# 'The wallet.dat file typically ' -# 'holds a pool of 100 addresses beyond the ones you ever used. ' -# 'These are the next 100 addresses to be used by the main Bitcoin ' -# 'client for the next 100 transactions. ' -# 'If you are planning to switch to Armory exclusively, you will not ' -# 'need these addresses') -# -# self.chkAllKeys.setChecked(True) -# if self.main.usermode in (USERMODE.Standard,): -# self.chkAllKeys.setVisible(False) -# self.ttipAllKeys.setVisible(False) -# -# 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 -# lblWltDest = QRichLabel('Migrate Addresses to which Wallet?', doWrap=False) -# 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, wltID) )) -# self.wltidlist.append(wltID) -# self.lstWallets.setCurrentRow(0) -# self.connect(self.lstWallets, SIGNAL('currentRowChanged(int)'), self.wltChange) -# -# -# self.lblDescrNew = QRichLabel( '' ) -# self.lblDescrNew.setAlignment(Qt.AlignTop) -# self.wltChange(0) -# -# -# dlgLayout = QVBoxLayout() -# dlgLayout.addWidget(lblDescr) -# dlgLayout.addWidget(HLINE()) -# dlgLayout.addWidget(makeHorizFrame([lblSatoshiWlt, ttipWlt, 'Stretch'])) -# dlgLayout.addWidget(makeHorizFrame([self.txtWalletPath, btnGetFilename])) -# dlgLayout.addWidget(makeHorizFrame([self.chkAllKeys, ttipAllKeys, 'Stretch'])) -# dlgLayout.addWidget(HLINE()) -# dlgLayout.addWidget(makeHorizFrame([lblWltDest, 'Stretch'])) -# dlgLayout.addWidget(makeHorizFrame([self.lstWallets, self.lblDescrNew,'Stretch'])) -# dlgLayout.addWidget(HLINE()) -# dlgLayout.addWidget(buttonBox) -# -# self.setLayout(dlgLayout) -# -# self.setMinimumWidth(500) -# self.setWindowTitle('Migrate Bitcoin-Qt Wallet') -# self.setWindowIcon(QIcon( self.main.iconfile)) -# -# -# def getSatoshiFilename(self): -# # Temporarily reset the "LastDir" to where the default wallet.dat is -# prevLastDir = self.main.settings.get('LastDirectory') -# self.main.writeSetting('LastDirectory', BTC_HOME_DIR) -# satoshiWltFile = self.main.getFileLoad('Load Bitcoin Wallet File', \ -# ['Bitcoin Wallets (*.dat)']) -# self.main.writeSetting('LastDirectory', prevLastDir) -# if len(str(satoshiWltFile))>0: -# self.txtWalletPath.setText(satoshiWltFile) -# -# -# def wltChange(self, row): -# if row==0: -# self.lblDescrNew.setText( \ -# 'If your wallet.dat is encrypted, the new Armory wallet will also ' -# 'be encrypted, and with the same passphrase. If your wallet.dat ' -# 'is not encrypted, neither will the new Armory wallet. Encryption ' -# 'can always be added, changed or removed on any wallet.') -# else: -# self.lblDescrNew.setText( '' ) -# -# 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] -# -# -# -# # Critical to avoid wallet corruption!! -# base,fn = os.path.split(satoshiWltFile) -# nm,ext = os.path.splitext(fn) -# satoshiWltFileCopy = os.path.join(ARMORY_HOME_DIR, nm+'_temp_copy'+ext) -# shutil.copy(satoshiWltFile, satoshiWltFileCopy) -# if not os.path.exists(satoshiWltFileCopy): -# raise FileExistsError, 'There was an error creating a copy of wallet.dat' -# -# # KeyList is [addrB58, privKey, usedYet, acctName] -# # This block will not only decrypt the Satoshi wallet, but also catch -# # if the user specified a wallet.dat for a different network! -# keyList = [] -# satoshiPassphrase = None -# satoshiWltIsEncrypted = checkSatoshiEncrypted(satoshiWltFileCopy) -# -# if not satoshiWltIsEncrypted: -# try: -# keyList = extractSatoshiKeys(satoshiWltFileCopy) -# except NetworkIDError: -# QMessageBox.critical(self, 'Wrong Network!', \ -# 'The specified wallet.dat file is for a different network! ' -# '(you are on the ' + NETWORKS[ADDRBYTE] + ')', \ -# QMessageBox.Ok) -# LOGERROR('Wallet is for the wrong network!') -# return -# else: -# correctPassphrase = False -# firstAsk = True -# while not correctPassphrase: -# # Loop until we get a valid passphrase -# redText = '' -# if not firstAsk: -# redText = 'Incorrect passphrase.

    ' % htmlColor('TextRed') -# firstAsk = False -# -# dlg = DlgGenericGetPassword( \ -# redText + 'The wallet.dat file you specified is encrypted. ' -# 'Please provide the passphrase to decrypt it.', self, self.main) -# -# if not dlg.exec_(): -# return -# else: -# satoshiPassphrase = SecureBinaryData(str(dlg.edtPasswd.text())) -# try: -# keyList = extractSatoshiKeys(satoshiWltFileCopy, satoshiPassphrase) -# correctPassphrase = True -# except EncryptionError: -# pass -# except NetworkIDError: -# QMessageBox.critical(self, 'Wrong Network!', \ -# 'The specified wallet.dat file is for a different network! ' -# '(you are on the ' + NETWORKS[ADDRBYTE] + ')', \ -# QMessageBox.Ok) -# LOGERROR('Wallet is for the wrong network!') -# return -# -# # We're done accessing the file, delete the -# os.remove(satoshiWltFileCopy) -# -# 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) -# -# if len(dupeWltList)>0: -# dlg = DlgDuplicateAddr([d[1].ljust(40)+d[0] for d in dupeWltList], self, self.main) -# didAccept = dlg.exec_() -# if not didAccept or dlg.doCancel: -# return -# -# if dlg.newOnly: -# dupeAddrList = [a[1] for a in dupeWltList] -# keyList = filter(lambda x: (x[0] not in dupeAddrList), keyList) -# elif dlg.takeAll: -# pass # we already have duplicates in the list, leave them -# -# -# # Confirm import -# addrList = [k[0].ljust(40)+k[3] for k in keyList] -# dlg = DlgConfirmBulkImport(addrList, toWalletID, self, self.main) -# if not dlg.exec_(): -# return -# -# # Okay, let's do it! Get a wallet, unlock it if necessary, create if desired -# wlt = None -# if toWalletID==None: -# lblShort = 'Migrated wallet.dat' -# lblLong = 'Wallet created to hold addresses from the regular Bitcoin wallet.dat.' -# -# if not satoshiPassphrase: -# wlt = PyBtcWallet().createNewWallet( \ -# withEncrypt=False, \ -# shortLabel=lblShort, \ -# longLabel=lblLong) -# -# else: -# lblLong += ' (encrypted using same passphrase as the original wallet)' -# wlt = PyBtcWallet().createNewWallet( \ -# withEncrypt=True, \ -# securePassphrase=satoshiPassphrase, \ -# shortLabel=lblShort, \ -# longLabel=lblLong) -# wlt.unlock(securePassphrase=satoshiPassphrase) -# -# -# else: -# wlt = self.main.walletMap[toWalletID] -# if wlt.useEncryption and wlt.isLocked: -# # Target wallet is encrypted... -# unlockdlg = DlgUnlockWallet(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 -# -# -# self.nImport = 0 -# self.nError = 0 -# def finallyDoMigrate(): -# for i,key4 in enumerate(keyList): -# addrB58, sbdKey, isUsed, addrName = key4[:] -# try: -# a160 = addrStr_to_hash160(addrB58) -# wlt.importExternalAddressData(privKey=sbdKey) -# cmt = 'Imported #%03d'%i -# if len(addrName)>0: -# cmt += ': %s' % addrName -# wlt.setComment(a160, cmt) -# self.nImport += 1 -# except Exception,msg: -# LOGERROR('Problem importing: %s: %s', addrB58, msg) -# self.nError += 1 -# -# -# DlgExecLongProcess(finallyDoMigrate, "Migrating Bitcoin-Qt Wallet", self, self.main).exec_() -# -# -# if self.nImport==0: -# MsgBoxCustom(MSGBOX.Error,'Error!', 'Failed: No addresses could be imported. ' -# 'Please check the logfile (ArmoryQt.exe.log) or the console output ' -# 'for information about why it failed (and email support@bitcoinarmory.com.' -# 'for help fixing the problem).') -# else: -# if self.nError == 0: -# MsgBoxCustom(MSGBOX.Good, 'Success!', \ -# 'Success: %d private keys were imported into your wallet.' % self.nImport) -# else: -# MsgBoxCustom(MSGBOX.Warning, 'Partial Success!', \ -# '%d private keys were imported into your wallet, but there was ' -# 'also %d addresses that could not be imported (see console ' -# 'or log file for more information). It is safe to try this ' -# 'operation again: all addresses previously imported will be ' -# 'skipped. %s' % (self.nImport, self.nError, restartMsg)) -# -# if self.main.blkMode==BLOCKCHAINMODE.Full: -# ########################################################################## -# warnMsg = ( \ -# 'Would you like to rescan the blockchain for all the addresses you ' -# 'just migrated? This operation can take between 5 seconds to 3 minutes ' -# 'depending on your system. If you skip this operation, it will be ' -# 'performed the next time you restart Armory. Wallet balances may ' -# 'be incorrect until then.') -# waitMsg = 'Searching the global transaction history' -# -# self.main.isDirty() = True -# if self.main.BDM_SyncCppWallet_Confirm(wlt.cppWallet, warnMsg, waitMsg): -# self.main.safeAddWallet(wlt.cppWallet) -# #TheBDM.registerWallet(wlt.cppWallet) -# #wlt.syncWithBlockchain(0) -# else: -# ########################################################################## -# -# self.main.addWalletToApplication(wlt, walletIsNew=False) -# -# self.main.walletListChanged() -# self.accept() -# -# - - - - - - - ############################################################################## class DlgConfirmBulkImport(ArmoryDialog): def __init__(self, addrList, wltID, parent=None, main=None): super(DlgConfirmBulkImport, self).__init__(parent, main) - self.wltID = wltID + self.wltID = wltID - if len(addrList)==0: + if len(addrList) == 0: QMessageBox.warning(self, 'No Addresses to Import', \ 'There are no addresses to import!', QMessageBox.Ok) self.reject() - + walletDescr = 'a new wallet' - if not wltID==None: + if not wltID == None: wlt = self.main.walletMap[wltID] walletDescr = 'wallet, %s (%s)' % (wltID, wlt.labelName) - lblDescr = QRichLabel( \ + lblDescr = QRichLabel(\ 'You are about to import %d addresses into %s.

    ' 'The following is a list of addresses to be imported:' % \ (len(addrList), walletDescr)) - fnt = GETFONT('Fixed',10) - w,h = tightSizeNChar(fnt, 100) + fnt = GETFONT('Fixed', 10) + w, h = tightSizeNChar(fnt, 100) txtDispAddr = QTextEdit() txtDispAddr.setFont(fnt) txtDispAddr.setReadOnly(True) - txtDispAddr.setMinimumWidth( min(w, 700)) - txtDispAddr.setMinimumHeight(16.2*h) - txtDispAddr.setText( '\n'.join(addrList) ) + txtDispAddr.setMinimumWidth(min(w, 700)) + 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) + 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) @@ -3260,7 +3647,7 @@ def __init__(self, addrList, wltID, parent=None, main=None): self.setLayout(dlgLayout) self.setWindowTitle('Confirm Import') - self.setWindowIcon(QIcon( self.main.iconfile)) + self.setWindowIcon(QIcon(self.main.iconfile)) ############################################################################# @@ -3268,40 +3655,40 @@ class DlgDuplicateAddr(ArmoryDialog): def __init__(self, addrList, wlt, parent=None, main=None): super(DlgDuplicateAddr, self).__init__(parent, main) - self.wlt = wlt + self.wlt = wlt self.doCancel = True - self.newOnly = False + self.newOnly = False - if len(addrList)==0: + if len(addrList) == 0: QMessageBox.warning(self, 'No Addresses to Import', \ 'There are no addresses to import!', QMessageBox.Ok) self.reject() - lblDescr = QRichLabel( \ + lblDescr = QRichLabel(\ 'Duplicate addresses detected! The following ' 'addresses already exist in other Armory wallets:' % htmlColor('TextWarn')) - fnt = GETFONT('Fixed',8) - w,h = tightSizeNChar(fnt, 50) + 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) ) + txtDispAddr.setMinimumHeight(8.2 * h) + txtDispAddr.setText('\n'.join(addrList)) - lblWarn = QRichLabel( \ + lblWarn = QRichLabel(\ 'Duplicate addresses cannot be imported. If you continue, ' 'the addresses above will be ignored, and only new addresses ' 'will be imported to this wallet.') buttonBox = QDialogButtonBox() self.btnContinue = QPushButton("Continue") - self.btnCancel = QPushButton("Cancel") - self.connect(self.btnContinue, SIGNAL('clicked()'), self.accept) - self.connect(self.btnCancel, SIGNAL('clicked()'), self.reject) + self.btnCancel = QPushButton("Cancel") + self.connect(self.btnContinue, SIGNAL(CLICKED), self.accept) + self.connect(self.btnCancel, SIGNAL(CLICKED), self.reject) buttonBox.addButton(self.btnContinue, QDialogButtonBox.AcceptRole) - buttonBox.addButton(self.btnCancel, QDialogButtonBox.RejectRole) + buttonBox.addButton(self.btnCancel, QDialogButtonBox.RejectRole) dlgLayout = QVBoxLayout() dlgLayout.addWidget(lblDescr) @@ -3320,8 +3707,8 @@ class DlgAddressInfo(ArmoryDialog): def __init__(self, wlt, addr160, parent=None, main=None, mode=None): super(DlgAddressInfo, self).__init__(parent, main) - self.wlt = wlt - self.addr = self.wlt.getAddrByHash160(addr160) + self.wlt = wlt + self.addr = self.wlt.getAddrByHash160(addr160) self.addrLedger = wlt.getAddrTxLedger(addr160) @@ -3331,12 +3718,12 @@ def __init__(self, wlt, addr160, parent=None, main=None, mode=None): self.mode = mode - if mode==None: - if main==None: + if mode == None: + if main == None: self.mode = USERMODE.Standard else: self.mode = self.main.usermode - + dlgLayout = QGridLayout() cppAddr = self.wlt.cppWallet.getScrAddrObjByKey(Hash160ToScrAddr(addr160)) @@ -3344,7 +3731,7 @@ def __init__(self, wlt, addr160, parent=None, main=None, mode=None): lblDescr = QLabel('Information for address: ' + addrStr) - + frmInfo = QFrame() frmInfo.setFrameStyle(STYLE_RAISED) frmInfoLayout = QGridLayout() @@ -3353,92 +3740,93 @@ def __init__(self, wlt, addr160, parent=None, main=None, mode=None): # Hash160 if mode in (USERMODE.Advanced, USERMODE.Expert): - bin25 = base58_to_binary(addrStr) + bin25 = base58_to_binary(addrStr) lbls.append([]) - lbls[-1].append( self.main.createToolTipWidget( \ + lbls[-1].append(self.main.createToolTipWidget(\ 'This is the computer-readable form of the address')) - lbls[-1].append( QRichLabel('Public Key Hash') ) + lbls[-1].append(QRichLabel('Public Key Hash')) h160Str = binary_to_hex(bin25[1:-4]) - if mode==USERMODE.Expert: + if mode == USERMODE.Expert: network = binary_to_hex(bin25[:1 ]) hash160 = binary_to_hex(bin25[ 1:-4 ]) addrChk = binary_to_hex(bin25[ -4:]) h160Str += '%s (Network: %s / Checksum: %s)' % (hash160, network, addrChk) - lbls[-1].append( QLabel(h160Str)) + lbls[-1].append(QLabel(h160Str)) lbls.append([]) - lbls[-1].append( QLabel('')) - lbls[-1].append( QRichLabel('Address:')) - lbls[-1].append( QLabel( addrStr)) + lbls[-1].append(QLabel('')) + lbls[-1].append(QRichLabel('Address:')) + lbls[-1].append(QLabel(addrStr)) lbls.append([]) - lbls[-1].append( self.main.createToolTipWidget( + lbls[-1].append(self.main.createToolTipWidget( 'Address type is either Imported or Permanent. ' - 'Permanent ' + 'Permanent ' 'addresses are part of base wallet, and are protected by printed ' 'paper backups, regardless of when the backup was performed. ' 'Imported addresses are only protected by digital backups, or manually ' 'printing the individual keys list, and only if it was backed up ' 'after the keys were imported.')) - - lbls[-1].append( QRichLabel('Address Type:')) - if self.addr.chainIndex==-2: - lbls[-1].append( QLabel('Imported') ) + + lbls[-1].append(QRichLabel('Address Type:')) + if self.addr.chainIndex == -2: + lbls[-1].append(QLabel('Imported')) else: - lbls[-1].append( QLabel('Permanent') ) + lbls[-1].append(QLabel('Permanent')) # Current Balance of address lbls.append([]) - lbls[-1].append( self.main.createToolTipWidget( + lbls[-1].append(self.main.createToolTipWidget( 'This is the current spendable balance of this address, ' 'not including zero-confirmation transactions from others.')) - lbls[-1].append( QRichLabel('Current Balance') ) - balStr = coin2str(cppAddr.getSpendableBalance(), maxZeros=1) - if cppAddr.getSpendableBalance()>0: + lbls[-1].append(QRichLabel('Current Balance')) + balCoin = cppAddr.getSpendableBalance(self.main.currBlockNum, IGNOREZC) + balStr = coin2str(balCoin, maxZeros=1) + if balCoin > 0: goodColor = htmlColor('MoneyPos') - lbls[-1].append( QRichLabel( \ - '' + balStr.strip() + ' BTC' )) - else: - lbls[-1].append( QRichLabel( balStr.strip() + ' BTC')) + lbls[-1].append(QRichLabel(\ + '' + balStr.strip() + ' BTC')) + else: + lbls[-1].append(QRichLabel(balStr.strip() + ' BTC')) # Number of transactions - txHashes = set() + txHashes = set() for le in self.addrLedger: txHashes.add(le.getTxHash()) - + lbls.append([]) - lbls[-1].append( self.main.createToolTipWidget( + lbls[-1].append(self.main.createToolTipWidget( 'The total number of transactions in which this address was involved')) - lbls[-1].append( QRichLabel('Transaction Count:') ) - lbls[-1].append( QLabel(str(len(txHashes)))) - + lbls[-1].append(QRichLabel('Transaction Count:')) + lbls[-1].append(QLabel(str(len(txHashes)))) + for i in range(len(lbls)): - for j in range(1,3): - lbls[i][j].setTextInteractionFlags( Qt.TextSelectableByMouse | \ + for j in range(1, 3): + lbls[i][j].setTextInteractionFlags(Qt.TextSelectableByMouse | \ Qt.TextSelectableByKeyboard) for j in range(3): - if (i,j)==(0,2): - frmInfoLayout.addWidget(lbls[i][j], i,j, 1,2) + if (i, j) == (0, 2): + frmInfoLayout.addWidget(lbls[i][j], i, j, 1, 2) else: - frmInfoLayout.addWidget(lbls[i][j], i,j, 1,1) + frmInfoLayout.addWidget(lbls[i][j], i, j, 1, 1) qrcode = QRCodeWidget(addrStr, 80, parent=self) qrlbl = QRichLabel('Double-click to inflate') frmqr = makeVertFrame([qrcode, qrlbl]) - frmInfoLayout.addWidget(frmqr, 0,4, len(lbls),1) + frmInfoLayout.addWidget(frmqr, 0, 4, len(lbls), 1) frmInfo.setLayout(frmInfoLayout) - dlgLayout.addWidget(frmInfo, 0,0, 1,1) + dlgLayout.addWidget(frmInfo, 0, 0, 1, 1) - ### Set up the address ledger + # ## Set up the address ledger self.ledgerModel = LedgerDispModelSimple(self.ledgerTable, self, self.main) self.ledgerView = QTableView() self.ledgerView.setModel(self.ledgerModel) @@ -3462,7 +3850,7 @@ def __init__(self, wlt, addr160, parent=None, main=None, mode=None): dateWidth = tightSizeStr(self.ledgerView, '_9999-Dec-99 99:99pm__')[0] initialColResize(self.ledgerView, [20, 0, dateWidth, 72, 0, 0.45, 0.3]) - ttipLedger = self.main.createToolTipWidget( \ + ttipLedger = self.main.createToolTipWidget(\ 'Unlike the wallet-level ledger, this table shows every ' 'transaction input and output as a separate entry. ' 'Therefore, there may be multiple entries for a single transaction, ' @@ -3470,21 +3858,21 @@ def __init__(self, wlt, addr160, parent=None, main=None, mode=None): 'the change-back-to-self address).') lblLedger = QLabel('All Address Activity:') - lblstrip = makeLayoutFrame('Horiz', [lblLedger, ttipLedger, 'Stretch']) - frmLedger = makeLayoutFrame('Vert', [lblstrip, self.ledgerView]) - dlgLayout.addWidget(frmLedger, 1,0, 1,1) + lblstrip = makeLayoutFrame(HORIZONTAL, [lblLedger, ttipLedger, STRETCH]) + frmLedger = makeLayoutFrame(VERTICAL, [lblstrip, self.ledgerView]) + dlgLayout.addWidget(frmLedger, 1, 0, 1, 1) # Now add the right-hand-side option buttons lbtnCopyAddr = QLabelButton('Copy Address to Clipboard') lbtnViewKeys = QLabelButton('View Address Keys') - #lbtnSweepA = QLabelButton('Sweep Address') - lbtnDelete = QLabelButton('Delete Address') + # lbtnSweepA = QLabelButton('Sweep Address') + lbtnDelete = QLabelButton('Delete Address') - self.connect(lbtnCopyAddr, SIGNAL('clicked()'), self.copyAddr) - self.connect(lbtnViewKeys, SIGNAL('clicked()'), self.viewKeys) - #self.connect(lbtnSweepA, SIGNAL('clicked()'), self.sweepAddr) - self.connect(lbtnDelete, SIGNAL('clicked()'), self.deleteAddr) + self.connect(lbtnCopyAddr, SIGNAL(CLICKED), self.copyAddr) + self.connect(lbtnViewKeys, SIGNAL(CLICKED), self.viewKeys) + # self.connect(lbtnSweepA, SIGNAL(CLICKED), self.sweepAddr) + self.connect(lbtnDelete, SIGNAL(CLICKED), self.deleteAddr) optFrame = QFrame() optFrame.setFrameStyle(STYLE_SUNKEN) @@ -3497,7 +3885,7 @@ def __init__(self, wlt, addr160, parent=None, main=None, mode=None): self.lblCopied = QRichLabel('') self.lblCopied.setMinimumHeight(tightSizeNChar(self.lblCopied, 1)[1]) - self.lblLedgerWarning = QRichLabel( \ + self.lblLedgerWarning = QRichLabel(\ 'NOTE: The ledger shows each transaction input and ' 'output for this address. There are typically many ' 'inputs and outputs for each transaction, therefore the entries ' @@ -3509,8 +3897,8 @@ def __init__(self, wlt, addr160, parent=None, main=None, mode=None): if True: optLayout.addWidget(lbtnCopyAddr) if adv: optLayout.addWidget(lbtnViewKeys) - #if not watch: optLayout.addWidget(lbtnSweepA) - #if adv: optLayout.addWidget(lbtnDelete) + # if not watch: optLayout.addWidget(lbtnSweepA) + # if adv: optLayout.addWidget(lbtnDelete) if True: optLayout.addStretch() if True: optLayout.addWidget(self.lblCopied) @@ -3519,14 +3907,14 @@ def __init__(self, wlt, addr160, parent=None, main=None, mode=None): optLayout.addStretch() optFrame.setLayout(optLayout) - - rightFrm = makeLayoutFrame('Vert', [QLabel('Available Actions:'), optFrame]) - dlgLayout.addWidget(rightFrm, 0,1, 2,1) + + rightFrm = makeLayoutFrame(VERTICAL, [QLabel('Available Actions:'), optFrame]) + dlgLayout.addWidget(rightFrm, 0, 1, 2, 1) btnGoBack = QPushButton('<<< Go Back') - self.connect(btnGoBack, SIGNAL('clicked()'), self.reject) - bottomStrip = makeLayoutFrame('Horiz', [btnGoBack, 'Stretch']) - dlgLayout.addWidget(bottomStrip, 2,0, 1,2) + self.connect(btnGoBack, SIGNAL(CLICKED), self.reject) + bottomStrip = makeLayoutFrame(HORIZONTAL, [btnGoBack, STRETCH]) + dlgLayout.addWidget(bottomStrip, 2, 0, 1, 2) self.setLayout(dlgLayout) self.setWindowTitle('Address Information') @@ -3553,13 +3941,13 @@ def viewKeys(self): addr = self.addr.copy() dlg = DlgShowKeys(addr, self, self.main) dlg.exec_() - + def sweepAddr(self): # This is broken, and I don't feel like fixing it because it's not very # useful. Maybe some time in the future it will be resolved. return - """ + """ if self.wlt.useEncryption and self.wlt.isLocked: unlockdlg = DlgUnlockWallet(self.wlt, self, self.main, 'Sweep Address') if not unlockdlg.exec_(): @@ -3573,7 +3961,7 @@ def sweepAddr(self): ####################################################################### # This is the part that may take a while. Verify user will wait! - # The sync/confirm call guarantees that the next sync call will + # The sync/confirm call guarantees that the next sync call will # return instantaneously with the correct answer. This only stops # being true when more addresses or wallets are imported. if not self.main.BDM_SyncAddressList_Confirm(addrToSweep): @@ -3603,13 +3991,13 @@ def sweepAddr(self): 'If you believe that your entire wallet has been compromised, ' 'you should instead send all the funds from this wallet to another address ' 'or wallet.', QMessageBox.Ok) - + # Finally, if we got here, we're ready to broadcast! dispIn = 'address %s' % addrToSweep.getAddrStr() dispOut = 'wallet "%s" (%s) ' % (self.wlt.labelName, self.wlt.uniqueIDB58) if DlgVerifySweep(dispIn, dispOut, outVal, fee).exec_(): self.main.broadcastTransaction(finishedTx, dryRun=False) - """ + """ def deleteAddr(self): pass @@ -3621,88 +4009,88 @@ class DlgShowKeys(ArmoryDialog): def __init__(self, addr, parent=None, main=None): super(DlgShowKeys, self).__init__(parent, main) - self.addr = addr + self.addr = addr + - lblWarn = QRichLabel('') plainPriv = False - if addr.binPrivKey32_Plain.getSize()>0: + if addr.binPrivKey32_Plain.getSize() > 0: plainPriv = True - lblWarn = QRichLabel( \ + lblWarn = QRichLabel(\ 'Warning: the unencrypted private keys ' 'for this address are shown below. They are "private" because ' 'anyone who obtains them can spend the money held ' 'by this address. Please protect this information the ' 'same as you protect your wallet.' % htmlColor('TextWarn')) - warnFrm = makeLayoutFrame('Horiz', [lblWarn]) + warnFrm = makeLayoutFrame(HORIZONTAL, [lblWarn]) endianness = self.main.getSettingOrSetDefault('PrefEndian', BIGENDIAN) - estr = 'BE' if endianness==BIGENDIAN else 'LE' + estr = 'BE' if endianness == BIGENDIAN else 'LE' def formatBinData(binStr, endian=LITTLEENDIAN): binHex = binary_to_hex(binStr) - if endian!=LITTLEENDIAN: - binHex = hex_switchEndian(binHex) - binHexPieces = [binHex[i:i+8] for i in range(0, len(binHex), 8)] + if endian != LITTLEENDIAN: + binHex = hex_switchEndian(binHex) + binHexPieces = [binHex[i:i + 8] for i in range(0, len(binHex), 8)] return ' '.join(binHexPieces) lblDescr = QRichLabel('Key Data for address: %s' % self.addr.getAddrStr()) lbls = [] - #lbls.append([]) - #lbls[-1].append(QLabel('')) - #lbls[-1].append(QRichLabel('Address:')) - #lbls[-1].append(QLabel(addr.getAddrStr())) + # lbls.append([]) + # lbls[-1].append(QLabel('')) + # lbls[-1].append(QRichLabel('Address:')) + # lbls[-1].append(QLabel(addr.getAddrStr())) lbls.append([]) binKey = self.addr.binPrivKey32_Plain.toBinStr() - lbls[-1].append(self.main.createToolTipWidget( \ + lbls[-1].append(self.main.createToolTipWidget(\ 'The raw form of the private key for this address. It is ' '32-bytes of randomly generated data')) lbls[-1].append(QRichLabel('Private Key (hex,%s):' % estr)) if not addr.hasPrivKey(): lbls[-1].append(QRichLabel('[[ No Private Key in Watching-Only Wallet ]]')) elif plainPriv: - lbls[-1].append( QLabel( formatBinData(binKey) ) ) + lbls[-1].append(QLabel(formatBinData(binKey))) else: lbls[-1].append(QRichLabel('[[ ENCRYPTED ]]')) if plainPriv: lbls.append([]) - lbls[-1].append(self.main.createToolTipWidget( \ + lbls[-1].append(self.main.createToolTipWidget(\ 'This is a more compact form of the private key, and includes ' 'a checksum for error detection.')) lbls[-1].append(QRichLabel('Private Key (Base58):')) b58Key = encodePrivKeyBase58(binKey) - lbls[-1].append( QLabel(' '.join([b58Key[i:i+6] for i in range(0, len(b58Key), 6)]))) - - + lbls[-1].append(QLabel(' '.join([b58Key[i:i + 6] for i in range(0, len(b58Key), 6)]))) + + lbls.append([]) - lbls[-1].append(self.main.createToolTipWidget( \ + lbls[-1].append(self.main.createToolTipWidget(\ 'The raw public key data. This is the X-coordinate of ' 'the Elliptic-curve public key point.')) lbls[-1].append(QRichLabel('Public Key X (%s):' % estr)) - lbls[-1].append(QRichLabel(formatBinData(self.addr.binPublicKey65.toBinStr()[1:1+32]))) - + lbls[-1].append(QRichLabel(formatBinData(self.addr.binPublicKey65.toBinStr()[1:1 + 32]))) + lbls.append([]) - lbls[-1].append(self.main.createToolTipWidget( \ + lbls[-1].append(self.main.createToolTipWidget(\ 'The raw public key data. This is the Y-coordinate of ' 'the Elliptic-curve public key point.')) lbls[-1].append(QRichLabel('Public Key Y (%s):' % estr)) - lbls[-1].append(QRichLabel(formatBinData(self.addr.binPublicKey65.toBinStr()[1+32:1+32+32]))) + lbls[-1].append(QRichLabel(formatBinData(self.addr.binPublicKey65.toBinStr()[1 + 32:1 + 32 + 32]))) - bin25 = base58_to_binary(self.addr.getAddrStr()) + bin25 = base58_to_binary(self.addr.getAddrStr()) network = binary_to_hex(bin25[:1 ]) hash160 = binary_to_hex(bin25[ 1:-4 ]) addrChk = binary_to_hex(bin25[ -4:]) h160Str = '%s (Network: %s / Checksum: %s)' % (hash160, network, addrChk) lbls.append([]) - lbls[-1].append(self.main.createToolTipWidget( \ + lbls[-1].append(self.main.createToolTipWidget(\ 'This is the hexadecimal version if the address string')) lbls[-1].append(QRichLabel('Public Key Hash:')) lbls[-1].append(QLabel(h160Str)) @@ -3713,7 +4101,7 @@ def formatBinData(binStr, endian=LITTLEENDIAN): # Now set the label properties and jam them into an information frame - for row,lbl3 in enumerate(lbls): + for row, lbl3 in enumerate(lbls): lbl3[1].setFont(GETFONT('Var')) lbl3[2].setFont(GETFONT('Fixed')) lbl3[2].setTextInteractionFlags(Qt.TextSelectableByMouse | \ @@ -3723,20 +4111,20 @@ def formatBinData(binStr, endian=LITTLEENDIAN): for j in range(3): frmKeyDataLayout.addWidget(lbl3[j], row, j) - + frmKeyData.setLayout(frmKeyDataLayout) bbox = QDialogButtonBox(QDialogButtonBox.Ok) self.connect(bbox, SIGNAL('accepted()'), self.accept) - + dlgLayout = QVBoxLayout() dlgLayout.addWidget(lblWarn) dlgLayout.addWidget(lblDescr) dlgLayout.addWidget(frmKeyData) dlgLayout.addWidget(bbox) - - + + self.setLayout(dlgLayout) self.setWindowTitle('Address Key Information') @@ -3745,27 +4133,27 @@ class DlgEULA(ArmoryDialog): def __init__(self, parent=None, main=None): super(DlgEULA, self).__init__(parent, main) - txtWidth,txtHeight = tightSizeNChar(self, 110) + txtWidth, txtHeight = tightSizeNChar(self, 110) txtLicense = QTextEdit() - txtLicense.sizeHint = lambda: QSize(txtWidth, 14*txtHeight) + txtLicense.sizeHint = lambda: QSize(txtWidth, 14 * txtHeight) txtLicense.setReadOnly(True) - txtLicense.setCurrentFont(GETFONT('Fixed',8)) + txtLicense.setCurrentFont(GETFONT('Fixed', 8)) from LICENSE import licenseText txtLicense.setText(licenseText()) - + self.chkAgree = QCheckBox('I agree to all the terms of the license above') self.btnCancel = QPushButton("Cancel") self.btnAccept = QPushButton("Accept") self.btnAccept.setEnabled(False) - self.connect(self.btnCancel, SIGNAL('clicked()'), self.reject) - self.connect(self.btnAccept, SIGNAL('clicked()'), self.accept) - self.connect(self.chkAgree, SIGNAL('toggled(bool)'), self.toggleChkBox) - btnBox = makeHorizFrame(['Stretch', self.btnCancel, self.btnAccept]) + self.connect(self.btnCancel, SIGNAL(CLICKED), self.reject) + self.connect(self.btnAccept, SIGNAL(CLICKED), self.accept) + self.connect(self.chkAgree, SIGNAL('toggled(bool)'), self.toggleChkBox) + btnBox = makeHorizFrame([STRETCH, self.btnCancel, self.btnAccept]) - lblPleaseAgree = QRichLabel( \ + lblPleaseAgree = QRichLabel(\ 'Armory Bitcoin Client is licensed under the Affero General ' 'Public License, Version 3 (AGPLv3)' '

    ' @@ -3779,8 +4167,8 @@ def __init__(self, parent=None, main=None): dlgLayout = QVBoxLayout() - frmChk = makeHorizFrame([self.chkAgree, 'Stretch']) - frmBtn = makeHorizFrame(['Stretch', self.btnCancel, self.btnAccept]) + frmChk = makeHorizFrame([self.chkAgree, STRETCH]) + frmBtn = makeHorizFrame([STRETCH, self.btnCancel, self.btnAccept]) frmAll = makeVertFrame([lblPleaseAgree, txtLicense, frmChk, frmBtn]) dlgLayout.addWidget(frmAll) @@ -3793,9 +4181,9 @@ def reject(self): self.main.abortLoad = True LOGERROR('User did not accept the EULA') super(DlgEULA, self).reject() - + def accept(self): - self.main.writeSetting('Agreed_to_EULA', True) + self.main.writeSetting('Agreed_to_EULA', True) super(DlgEULA, self).accept() def toggleChkBox(self, isEnabled): @@ -3816,10 +4204,10 @@ def __init__(self, parent=None, main=None): lblWelcome = QRichLabel('Welcome to Armory!') lblWelcome.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) lblWelcome.setFont(GETFONT('Var', 14)) - lblSlogan = QRichLabel('The most advanced Bitcoin Client on Earth!') + lblSlogan = QRichLabel('The most advanced Bitcoin Client on Earth!') lblSlogan.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) - lblDescr = QRichLabel( \ + lblDescr = QRichLabel(\ 'You are about to use the most secure and feature-rich Bitcoin client ' 'software available! But please remember, this software ' 'is still Beta - Armory developers will not be held responsible ' @@ -3829,63 +4217,63 @@ def __init__(self, parent=None, main=None): 'frequently ' 'asked questions.') lblDescr.setOpenExternalLinks(True) - - lblContact = QRichLabel( \ + + lblContact = QRichLabel(\ 'If you find this software useful, please consider pressing ' 'the "Donate" button on your next transaction!') - spacer = lambda: QSpacerItem(20,20, QSizePolicy.Fixed, QSizePolicy.Expanding) + spacer = lambda: QSpacerItem(20, 20, QSizePolicy.Fixed, QSizePolicy.Expanding) - frmText = makeLayoutFrame('Vert', [lblWelcome, spacer(), \ - lblDescr, spacer(), \ + frmText = makeLayoutFrame(VERTICAL, [lblWelcome, spacer(), \ + lblDescr, spacer(), \ lblContact ]) - + self.chkDnaaIntroDlg = QCheckBox('Do not show this window again') self.requestCreate = False self.requestImport = False buttonBox = QDialogButtonBox() - frmIcon = makeLayoutFrame('Vert', [lblInfoImg, 'Stretch']) + frmIcon = makeLayoutFrame(VERTICAL, [lblInfoImg, STRETCH]) frmIcon.setMaximumWidth(60) - if len(self.main.walletMap)==0: + if len(self.main.walletMap) == 0: self.btnCreate = QPushButton("Create Your First Wallet!") self.btnImport = QPushButton("Import Existing Wallet") self.btnCancel = QPushButton("Skip") - self.connect(self.btnCreate, SIGNAL('clicked()'), self.createClicked) - self.connect(self.btnImport, SIGNAL('clicked()'), self.importClicked) - self.connect(self.btnCancel, SIGNAL('clicked()'), self.reject) + self.connect(self.btnCreate, SIGNAL(CLICKED), self.createClicked) + self.connect(self.btnImport, SIGNAL(CLICKED), self.importClicked) + self.connect(self.btnCancel, SIGNAL(CLICKED), self.reject) buttonBox.addButton(self.btnCreate, QDialogButtonBox.AcceptRole) buttonBox.addButton(self.btnImport, QDialogButtonBox.AcceptRole) buttonBox.addButton(self.btnCancel, QDialogButtonBox.RejectRole) self.chkDnaaIntroDlg.setVisible(False) - frmBtn = makeLayoutFrame('Horiz', [self.chkDnaaIntroDlg, \ + frmBtn = makeLayoutFrame(HORIZONTAL, [self.chkDnaaIntroDlg, \ self.btnCancel, \ - 'Stretch', \ + STRETCH, \ self.btnImport, \ self.btnCreate]) else: self.btnOkay = QPushButton("OK!") - self.connect(self.btnOkay, SIGNAL('clicked()'), self.accept) + self.connect(self.btnOkay, SIGNAL(CLICKED), self.accept) buttonBox.addButton(self.btnOkay, QDialogButtonBox.AcceptRole) - frmBtn = makeLayoutFrame('Horiz', [self.chkDnaaIntroDlg, \ - 'Stretch', \ + frmBtn = makeLayoutFrame(HORIZONTAL, [self.chkDnaaIntroDlg, \ + STRETCH, \ self.btnOkay]) - + dlgLayout = QGridLayout() - dlgLayout.addWidget(frmIcon, 0, 0, 1, 1) - dlgLayout.addWidget(frmText, 0, 1, 1, 1) - dlgLayout.addWidget(frmBtn, 1, 0, 1, 2) - + dlgLayout.addWidget(frmIcon, 0, 0, 1, 1) + dlgLayout.addWidget(frmText, 0, 1, 1, 1) + dlgLayout.addWidget(frmBtn, 1, 0, 1, 2) + self.setLayout(dlgLayout) self.setWindowTitle('Greetings!') - self.setWindowIcon(QIcon( self.main.iconfile)) + self.setWindowIcon(QIcon(self.main.iconfile)) self.setMinimumWidth(750) - + def createClicked(self): self.requestCreate = True @@ -3900,9 +4288,6 @@ def sizeHint(self): - - - ############################################################################# class DlgImportPaperWallet(ArmoryDialog): @@ -3910,18 +4295,14 @@ class DlgImportPaperWallet(ArmoryDialog): def __init__(self, parent=None, main=None): super(DlgImportPaperWallet, self).__init__(parent, main) - self.wltDataLines = [[]]*4 - self.prevChars = ['']*4 + self.wltDataLines = [[]] * 4 + self.prevChars = [''] * 4 - ROOT0, ROOT1, CHAIN0, CHAIN1 = range(4) - self.lineEdits = [QLineEdit() for i in range(4)] - self.prevChars = ['' for i in range(4)] - - for i,edt in enumerate(self.lineEdits): + for i, edt in enumerate(self.lineEdits): # I screwed up the ref/copy, this loop only connected the last one... - #theSlot = lambda: self.autoSpacerFunction(i) - #self.connect(edt, SIGNAL('textChanged(QString)'), theSlot) - edt.setMinimumWidth( tightSizeNChar(edt, 50)[0] ) + # theSlot = lambda: self.autoSpacerFunction(i) + # self.connect(edt, SIGNAL('textChanged(QString)'), theSlot) + edt.setMinimumWidth(tightSizeNChar(edt, 50)[0]) # Just do it manually because it's guaranteed to work! slot = lambda: self.autoSpacerFunction(0) @@ -3947,12 +4328,12 @@ def __init__(self, parent=None, main=None): self.labels[2].setText('Chain Code:') self.labels[3].setText('') - lblDescr1 = QLabel( + lblDescr1 = QLabel( 'Enter the characters exactly as they are printed on the ' 'paper-backup page. Alternatively, you can scan the QR ' 'code from another application, then copy&paste into the ' 'entry boxes below.') - lblDescr2 = QLabel( + lblDescr2 = QLabel( 'The data can be entered with or without ' 'spaces, and up to ' 'one character per line will be corrected automatically.') @@ -3963,80 +4344,80 @@ def __init__(self, parent=None, main=None): layout = QGridLayout() layout.addWidget(lblDescr1, 0, 0, 1, 2) layout.addWidget(lblDescr2, 1, 0, 1, 2) - for i,edt in enumerate(self.lineEdits): - layout.addWidget( self.labels[i], i+2, 0) - layout.addWidget( self.lineEdits[i], i+2, 1) + for i, edt in enumerate(self.lineEdits): + layout.addWidget(self.labels[i], i + 2, 0) + layout.addWidget(self.lineEdits[i], i + 2, 1) self.chkEncrypt = QCheckBox('Encrypt Wallet') self.chkEncrypt.setChecked(True) bottomFrm = makeHorizFrame([self.chkEncrypt, buttonbox]) - layout.addWidget(bottomFrm, 6, 0, 1, 2) + layout.addWidget(bottomFrm, 6, 0, 1, 2) layout.setVerticalSpacing(10) self.setLayout(layout) - + self.setWindowTitle('Recover Wallet from Paper Backup') - self.setWindowIcon(QIcon( self.main.iconfile)) + self.setWindowIcon(QIcon(self.main.iconfile)) def autoSpacerFunction(self, i): currStr = str(self.lineEdits[i].text()) - rawStr = currStr.replace(' ','') + rawStr = currStr.replace(' ', '') if len(rawStr) > 36: rawStr = rawStr[:36] - if len(rawStr)==36: - quads = [rawStr[j:j+4] for j in range(0,36, 4)] + if len(rawStr) == 36: + quads = [rawStr[j:j + 4] for j in range(0, 36, 4)] self.lineEdits[i].setText(' '.join(quads)) - - + + def verifyUserInput(self): def englishNumberList(nums): nums = map(str, nums) - if len(nums)==1: + if len(nums) == 1: return nums[0] return ', '.join(nums[:-1]) + ' and ' + nums[-1] errorLines = [] for i in range(4): - hasError=False + hasError = False try: data, err = readSixteenEasyBytes(str(self.lineEdits[i].text())) except (KeyError, TypeError): data, err = ('', 'Exception') - - if data=='': + + if data == '': reply = QMessageBox.critical(self, 'Verify Wallet ID', \ - 'There is an error on line ' + str(i+1) + ' of the data you ' + 'There is an error on line ' + str(i + 1) + ' of the data you ' 'entered, which could not be fixed automatically. Please ' 'double-check that you entered the text exactly as it appears ' 'on the wallet-backup page.', \ QMessageBox.Ok) LOGERROR('Error in wallet restore field') - self.labels[i].setText(''+str(self.labels[i].text())+'') + self.labels[i].setText('' + str(self.labels[i].text()) + '') return - if err=='Fixed_1' or err=='No_Checksum': - errorLines += [i+1] + if err == 'Fixed_1' or err == 'No_Checksum': + errorLines += [i + 1] self.wltDataLines[i] = data if errorLines: - pluralChar = '' if len(errorLines)==1 else 's' - article = ' an' if len(errorLines)==1 else '' + pluralChar = '' if len(errorLines) == 1 else 's' + article = ' an' if len(errorLines) == 1 else '' QMessageBox.question(self, 'Errors Corrected!', \ - 'Detected' + article +' error' + pluralChar + ' on line' + + 'Detected' + article + ' error' + pluralChar + ' on line' + pluralChar + ' ' + englishNumberList(errorLines) + - ' in the data you entered. Armory attempted to fix the ' + + ' in the data you entered. Armory attempted to fix the ' + 'error' + pluralChar + ' but it is not always right. Be sure ' 'to verify the "Wallet Unique ID" closely on the next window.', \ QMessageBox.Ok) - + # If we got here, the data is valid, let's create the wallet and accept the dlg privKey = ''.join(self.wltDataLines[:2]) - chain = ''.join(self.wltDataLines[2:]) - - root = PyBtcAddress().createFromPlainKeyData(SecureBinaryData(privKey)) + chain = ''.join(self.wltDataLines[2:]) + + root = PyBtcAddress().createFromPlainKeyData(SecureBinaryData(privKey)) root.chaincode = SecureBinaryData(chain) first = root.extendAddressChain() newWltID = binary_to_base58((ADDRBYTE + first.getAddr160()[:5])[::-1]) @@ -4048,16 +4429,16 @@ def englishNumberList(nums): 'Nothing to do...', QMessageBox.Ok) self.reject() return - - - + + + reply = QMessageBox.question(self, 'Verify Wallet ID', \ 'The data you entered corresponds to a wallet with a wallet ID: \n\n \t' + - newWltID + '\n\nDoes this ID match the "Wallet Unique ID" ' + newWltID + '\n\nDoes this ID match the "Wallet Unique ID" ' 'printed on your paper backup? If not, click "No" and reenter ' 'key and chain-code data again.', \ QMessageBox.Yes | QMessageBox.No) - if reply==QMessageBox.No: + if reply == QMessageBox.No: return passwd = [] @@ -4073,32 +4454,34 @@ def englishNumberList(nums): return if passwd: - self.newWallet = PyBtcWallet().createNewWallet( \ + self.newWallet = PyBtcWallet().createNewWallet(\ plainRootKey=SecureBinaryData(privKey), \ chaincode=SecureBinaryData(chain), \ - shortLabel='PaperBackup - %s'%newWltID, \ + shortLabel='PaperBackup - %s' % newWltID, \ withEncrypt=True, \ securePassphrase=passwd, \ kdfTargSec=0.25, \ - kdfMaxMem=32*1024*1024, \ + kdfMaxMem=32 * 1024 * 1024, \ isActuallyNew=False, \ doRegisterWithBDM=False) else: - self.newWallet = PyBtcWallet().createNewWallet( \ + self.newWallet = PyBtcWallet().createNewWallet(\ plainRootKey=SecureBinaryData(privKey), \ chaincode=SecureBinaryData(chain), \ - shortLabel='PaperBackup - %s'%newWltID, \ - withEncrypt=False,\ + shortLabel='PaperBackup - %s' % newWltID, \ + withEncrypt=False, \ isActuallyNew=False, \ doRegisterWithBDM=False) def fillAddrPoolAndAccept(): - self.newWallet.fillAddressPool() + progressBar = DlgProgress(self, self.main, None, HBar=1, + Title="Computing New Addresses") + progressBar.exec_(self.newWallet.fillAddressPool) self.accept() # Will pop up a little "please wait..." window while filling addr pool DlgExecLongProcess(fillAddrPoolAndAccept, "Recovering wallet...", self, self.main).exec_() - + @@ -4112,7 +4495,7 @@ def __init__(self, currcomment='', ctype='', parent=None, main=None): self.setWindowTitle('Modify Comment') - self.setWindowIcon(QIcon( self.main.iconfile)) + self.setWindowIcon(QIcon(self.main.iconfile)) buttonbox = QDialogButtonBox(QDialogButtonBox.Ok | \ QDialogButtonBox.Cancel) @@ -4121,21 +4504,21 @@ def __init__(self, currcomment='', ctype='', parent=None, main=None): layout = QGridLayout() lbl = None - if ctype and currcomment: lbl = QLabel('Change %s Comment:'%ctype) + if ctype and currcomment: lbl = QLabel('Change %s Comment:' % ctype) if not ctype and currcomment: lbl = QLabel('Change Comment:') - if ctype and not currcomment: lbl = QLabel('Add %s Comment:'%ctype) + if ctype and not currcomment: lbl = QLabel('Add %s Comment:' % ctype) if not ctype and not currcomment: lbl = QLabel('Add Comment:') self.edtComment = QLineEdit() self.edtComment.setText(currcomment) - h,w = relaxedSizeNChar(self, 50) - self.edtComment.setMinimumSize(h,w) - layout.addWidget(lbl, 0,0) - layout.addWidget(self.edtComment, 1,0) - layout.addWidget(buttonbox, 2,0) + h, w = relaxedSizeNChar(self, 50) + self.edtComment.setMinimumSize(h, w) + layout.addWidget(lbl, 0, 0) + layout.addWidget(self.edtComment, 1, 0) + layout.addWidget(buttonbox, 2, 0) self.setLayout(layout) ############################################################################# - def accept(self): + def accept(self): if not isASCII(unicode(self.edtComment.text())): UnicodeErrorBox(self) return @@ -4145,19 +4528,20 @@ def accept(self): +################################################################################ class DlgRemoveWallet(ArmoryDialog): def __init__(self, wlt, parent=None, main=None): super(DlgRemoveWallet, self).__init__(parent, main) - + wltID = wlt.uniqueIDB58 wltName = wlt.labelName wltDescr = wlt.labelDescr - lblWarning = QLabel( '!!! WARNING !!!\n\n') + lblWarning = QLabel('!!! WARNING !!!\n\n') lblWarning.setTextFormat(Qt.RichText) lblWarning.setAlignment(Qt.AlignHCenter) - lblWarning2 = QLabel( 'You have requested that the following wallet ' + lblWarning2 = QLabel('You have requested that the following wallet ' 'be removed from Armory:') lblWarning.setTextFormat(Qt.RichText) lblWarning.setWordWrap(True) @@ -4177,16 +4561,16 @@ def __init__(self, wlt, parent=None, main=None): # TODO: This should not *ever* require a blockchain scan, because all - # current wallets should already be registered and up-to-date. + # current wallets should already be registered and up-to-date. # But I should verify that this is actually the case. wltEmpty = True - if TheBDM.getBDMState()=='BlockchainReady': + if TheBDM.getBDMState() == 'BlockchainReady': wlt.syncWithBlockchain() bal = wlt.getBalance('Full') lbls.append([]) lbls[3].append(QLabel('Current Balance (w/ unconfirmed):')) - if bal>0: - lbls[3].append(QLabel(''+coin2str(bal, maxZeros=1).strip()+' BTC')) + if bal > 0: + lbls[3].append(QLabel('' + coin2str(bal, maxZeros=1).strip() + ' BTC')) lbls[3][-1].setTextFormat(Qt.RichText) wltEmpty = False else: @@ -4203,20 +4587,20 @@ def __init__(self, wlt, parent=None, main=None): # Add the warning text and images to the top of the dialog layout = QGridLayout() - layout.addWidget(lblWarning, 0, 1, 1, 1) + layout.addWidget(lblWarning, 0, 1, 1, 1) layout.addWidget(lblWarning2, 1, 1, 1, 1) - layout.addWidget(lblWarnImg, 0, 0, 2, 1) - layout.addWidget(lblWarnImg2, 0, 2, 2, 1) + layout.addWidget(lblWarnImg, 0, 0, 2, 1) + layout.addWidget(lblWarnImg2, 0, 2, 2, 1) frmInfo = QFrame() - frmInfo.setFrameStyle(QFrame.Box|QFrame.Plain) + frmInfo.setFrameStyle(QFrame.Box | QFrame.Plain) frmInfo.setLineWidth(3) frmInfoLayout = QGridLayout() for i in range(len(lbls)): - lbls[i][0].setText(''+lbls[i][0].text()+'') + lbls[i][0].setText('' + lbls[i][0].text() + '') lbls[i][0].setTextFormat(Qt.RichText) - frmInfoLayout.addWidget(lbls[i][0], i, 0) - frmInfoLayout.addWidget(lbls[i][1], i, 1, 1, 2) + frmInfoLayout.addWidget(lbls[i][0], i, 0) + frmInfoLayout.addWidget(lbls[i][1], i, 1, 1, 2) frmInfo.setLayout(frmInfoLayout) layout.addWidget(frmInfo, 2, 0, 2, 3) @@ -4233,74 +4617,77 @@ def __init__(self, wlt, parent=None, main=None): self.radioExclude = QRadioButton('Add this wallet to the "ignore list"') self.radioExclude.setEnabled(False) - self.radioDelete = QRadioButton('Permanently delete this wallet') - self.radioWatch = QRadioButton('Delete private keys only, make watching-only') + self.radioDelete = QRadioButton('Permanently delete this wallet') + self.radioWatch = QRadioButton('Delete private keys only, make watching-only') # Make sure that there can only be one selection btngrp = QButtonGroup(self) btngrp.addButton(self.radioExclude) btngrp.addButton(self.radioDelete) - if not self.main.usermode==USERMODE.Standard: + if not self.main.usermode == USERMODE.Standard: btngrp.addButton(self.radioWatch) btngrp.setExclusive(True) - ttipExclude = self.main.createToolTipWidget( \ - '[DISABLED] This will not delete any files, but will add this ' - 'wallet to the "ignore list." This means that Armory ' - 'will no longer show this wallet in the main screen ' - 'and none of its funds will be added to your balance. ' - 'You can re-include this wallet in Armory at a later ' - 'time by selecting the "Excluded Wallets..." option ' - 'in the "Wallets" menu.') - ttipDelete = self.main.createToolTipWidget( \ - 'This will delete the wallet file, removing ' - 'all its private keys from your settings directory. ' - 'If you intend to keep using addresses from this ' - 'wallet, do not select this option unless the wallet ' - 'is backed up elsewhere.') - ttipWatch = self.main.createToolTipWidget( \ - 'This will delete the private keys from your wallet, ' - 'leaving you with a watching-only wallet, which can be ' - 'used to generate addresses and monitor incoming ' - 'payments. This option would be used if you created ' - 'the wallet on this computer in order to transfer ' - 'it to a different computer or device and want to ' - 'remove the private data from this system for security.') - - - self.chkPrintBackup = QCheckBox('Print a paper backup of this wallet before deleting') + ttipExclude = self.main.createToolTipWidget(\ + '[DISABLED] This will not delete any files, but will add this ' + 'wallet to the "ignore list." This means that Armory ' + 'will no longer show this wallet in the main screen ' + 'and none of its funds will be added to your balance. ' + 'You can re-include this wallet in Armory at a later ' + 'time by selecting the "Excluded Wallets..." option ' + 'in the "Wallets" menu.') + ttipDelete = self.main.createToolTipWidget(\ + 'This will delete the wallet file, removing ' + 'all its private keys from your settings directory. ' + 'If you intend to keep using addresses from this ' + 'wallet, do not select this option unless the wallet ' + 'is backed up elsewhere.') + ttipWatch = self.main.createToolTipWidget(\ + 'This will delete the private keys from your wallet, ' + 'leaving you with a watching-only wallet, which can be ' + 'used to generate addresses and monitor incoming ' + 'payments. This option would be used if you created ' + 'the wallet on this computer in order to transfer ' + 'it to a different computer or device and want to ' + 'remove the private data from this system for security.') + + + self.chkPrintBackup = QCheckBox(tr(""" + Print a paper backup of this wallet before deleting""")) if wlt.watchingOnly: - ttipDelete = self.main.createToolTipWidget('This will delete the wallet file from your system. ' - 'Since this is a watching-only wallet, no private keys ' - 'will be deleted.') - ttipWatch = self.main.createToolTipWidget('This wallet is already a watching-only wallet ' - 'so this option is pointless') + ttipDelete = self.main.createToolTipWidget(tr(""" + This will delete the wallet file from your system. + Since this is a watching-only wallet, no private keys + will be deleted.""")) + ttipWatch = self.main.createToolTipWidget(tr(""" + This wallet is already a watching-only wallet so this option + is pointless""")) self.radioWatch.setEnabled(False) self.chkPrintBackup.setEnabled(False) - + self.frm = [] rdoFrm = QFrame() rdoFrm.setFrameStyle(STYLE_RAISED) rdoLayout = QGridLayout() - + startRow = 0 - for rdo,ttip in [(self.radioExclude, ttipExclude), \ - (self.radioDelete, ttipDelete), \ - (self.radioWatch, ttipWatch)]: + for rdo, ttip in [(self.radioExclude, ttipExclude), \ + (self.radioDelete, ttipDelete), \ + (self.radioWatch, ttipWatch)]: self.frm.append(QFrame()) - #self.frm[-1].setFrameStyle(STYLE_SUNKEN) + # self.frm[-1].setFrameStyle(STYLE_SUNKEN) self.frm[-1].setFrameStyle(QFrame.NoFrame) frmLayout = QHBoxLayout() frmLayout.addWidget(rdo) - ttip.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + ttip.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) frmLayout.addWidget(ttip) frmLayout.addStretch() self.frm[-1].setLayout(frmLayout) rdoLayout.addWidget(self.frm[-1], startRow, 0, 1, 3) - startRow +=1 + startRow += 1 self.radioDelete.setChecked(True) @@ -4311,28 +4698,30 @@ def __init__(self, wlt, parent=None, main=None): if wlt.watchingOnly: self.frm[-1].setVisible(False) - - - printTtip = self.main.createToolTipWidget( \ - 'If this box is checked, you will have the ability to print off an ' - 'unencrypted version of your wallet before it is deleted. If ' - 'printing is unsuccessful, please press *CANCEL* on the print dialog ' - 'to prevent the delete operation from continuing') - printFrm = makeLayoutFrame('Horiz', [self.chkPrintBackup, printTtip, 'Stretch']) - startRow +=1 - layout.addWidget( printFrm, startRow, 0, 1, 3) + + + printTtip = self.main.createToolTipWidget(tr(""" + If this box is checked, you will have the ability to print off an + unencrypted version of your wallet before it is deleted. If + printing is unsuccessful, please press *CANCEL* on the print dialog + to prevent the delete operation from continuing""")) + printFrm = makeLayoutFrame(HORIZONTAL, [self.chkPrintBackup, \ + printTtip, \ + 'Stretch']) + startRow += 1 + layout.addWidget(printFrm, startRow, 0, 1, 3) if wlt.watchingOnly: printFrm.setVisible(False) - + rmWalletSlot = lambda: self.removeWallet(wlt) - startRow +=1 + startRow += 1 self.btnDelete = QPushButton("Delete") self.btnCancel = QPushButton("Cancel") - self.connect(self.btnDelete, SIGNAL('clicked()'), rmWalletSlot) - self.connect(self.btnCancel, SIGNAL('clicked()'), self.reject) + self.connect(self.btnDelete, SIGNAL(CLICKED), rmWalletSlot) + self.connect(self.btnCancel, SIGNAL(CLICKED), self.reject) buttonBox = QDialogButtonBox() buttonBox.addButton(self.btnDelete, QDialogButtonBox.AcceptRole) buttonBox.addButton(self.btnCancel, QDialogButtonBox.RejectRole) @@ -4341,39 +4730,39 @@ def __init__(self, wlt, parent=None, main=None): self.setLayout(layout) self.setWindowTitle('Delete Wallet Options') - + def removeWallet(self, wlt): - # Open the print dialog. If they hit cancel at any time, then + # Open the print dialog. If they hit cancel at any time, then # we go back to the primary wallet-remove dialog without any other action - if self.chkPrintBackup.isChecked(): + if self.chkPrintBackup.isChecked(): if not OpenPaperBackupWindow('Single', self, self.main, self.wlt, \ tr('Unlock Paper Backup')): - QMessageBox.warning(self, 'Operation Aborted', \ - 'You requested a paper backup before deleting the wallet, but ' - 'clicked "Cancel" on the backup printing window. So, the delete ' - 'operation was canceled as well.', QMessageBox.Ok) + QMessageBox.warning(self, tr('Operation Aborted'), tr(""" + You requested a paper backup before deleting the wallet, but + clicked "Cancel" on the backup printing window. So, the delete + operation was canceled as well."""), QMessageBox.Ok) return - - + + # If they only want to exclude the wallet, we will add it to the excluded # list and remove it from the application. The wallet files will remain # in the settings directory but will be ignored by Armory wltID = wlt.uniqueIDB58 if self.radioExclude.isChecked(): - reply = QMessageBox.warning(self, 'Verify Intentions', \ - 'Are you sure you want to remove this wallet from your Armory ' - 'dashboard? The wallet file will not be deleted, but you will ' - 'no longer have access to the wallet or its funds unless you ' - 're-enable it through the "Wallets"->"Excluded Wallets" menu. ', \ + reply = QMessageBox.warning(self, tr('Verify Intentions'), tr(""" + Are you sure you want to remove this wallet from your Armory + dashboard? The wallet file will not be deleted, but you will + no longer have access to the wallet or its funds unless you + re-enable it through the "Wallets"->"Excluded Wallets" menu."""), \ QMessageBox.Yes | QMessageBox.Cancel) - if reply==QMessageBox.Yes: + if reply == QMessageBox.Yes: self.main.removeWalletFromApplication(wltID) self.main.settings.extend('Excluded_Wallets', wlt.walletPath) - self.main.statusBar().showMessage( \ - 'Wallet '+wltID+' was added to the ignore list.', 20000) + self.main.statusBar().showMessage(\ + 'Wallet ' + wltID + ' was added to the ignore list.', 20000) self.main.accept() self.accept() else: @@ -4398,9 +4787,9 @@ def removeWallet(self, wlt): 'of this wallet elsewhere, such as a paper backup or on an offline ' 'computer with the full wallet. ', QMessageBox.Yes | QMessageBox.Cancel) - if reply==QMessageBox.Yes: + if reply == QMessageBox.Yes: - thepath = wlt.getWalletPath() + thepath = wlt.getWalletPath() thepathBackup = wlt.getWalletPath('backup') if self.radioWatch.isChecked(): @@ -4414,15 +4803,15 @@ def removeWallet(self, wlt): os.remove(thepath) os.remove(thepathBackup) self.main.walletMap[wltID] = newWlt - self.main.statusBar().showMessage( \ + self.main.statusBar().showMessage(\ 'Wallet %s was replaced with a watching-only wallet.' % wltID, 10000) elif self.radioDelete.isChecked(): LOGINFO('***Completely deleting wallet') os.remove(thepath) os.remove(thepathBackup) - self.main.removeWalletFromApplication(wltID) - self.main.statusBar().showMessage( \ - 'Wallet '+wltID+' was deleted!', 10000) + self.main.removeWalletFromApplication(wltID) + self.main.statusBar().showMessage(\ + 'Wallet ' + wltID + ' was deleted!', 10000) self.parent.accept() self.accept() @@ -4430,28 +4819,29 @@ def removeWallet(self, wlt): self.reject() +################################################################################ class DlgRemoveAddress(ArmoryDialog): def __init__(self, wlt, addr160, parent=None, main=None): super(DlgRemoveAddress, self).__init__(parent, main) - + if not wlt.hasAddr(addr160): - raise WalletAddressError, 'Address does not exist in wallet!' + raise WalletAddressError('Address does not exist in wallet!') - if not wlt.getAddrByHash160(addr160).chainIndex==-2: - raise WalletAddressError, ('Cannot delete regular chained addresses! ' + if not wlt.getAddrByHash160(addr160).chainIndex == -2: + raise WalletAddressError('Cannot delete regular chained addresses! ' 'Can only delete imported addresses.') - self.wlt = wlt - self.addr = wlt.addrMap[addr160] - self.comm = wlt.getCommentForAddress(addr160) + self.wlt = wlt + self.addr = wlt.addrMap[addr160] + self.comm = wlt.getCommentForAddress(addr160) - lblWarning = QLabel( '!!! WARNING !!!\n\n') + lblWarning = QLabel('!!! WARNING !!!\n\n') lblWarning.setTextFormat(Qt.RichText) lblWarning.setAlignment(Qt.AlignHCenter) - lblWarning2 = QLabel( 'You have requested that the following address ' + lblWarning2 = QLabel('You have requested that the following address ' 'be deleted from your wallet:') lblWarning.setTextFormat(Qt.RichText) lblWarning.setWordWrap(True) @@ -4470,13 +4860,13 @@ def __init__(self, wlt, addr160, parent=None, main=None): lbls[-1].append(QLabel('"%s" (%s)' % (wlt.labelName, wlt.uniqueIDB58))) addrEmpty = True - if TheBDM.getBDMState()=='BlockchainReady': - #wlt.syncWithBlockchain() + if TheBDM.getBDMState() == 'BlockchainReady': + # wlt.syncWithBlockchain() bal = wlt.getAddrBalance(addr160, 'Full') lbls.append([]) lbls[-1].append(QLabel('Address Balance (w/ unconfirmed):')) - if bal>0: - lbls[-1].append(QLabel(''+coin2str(bal, maxZeros=1)+' BTC')) + if bal > 0: + lbls[-1].append(QLabel('' + coin2str(bal, maxZeros=1) + ' BTC')) lbls[-1][-1].setTextFormat(Qt.RichText) addrEmpty = False else: @@ -4493,25 +4883,25 @@ def __init__(self, wlt, addr160, parent=None, main=None): # Add the warning text and images to the top of the dialog layout = QGridLayout() - layout.addWidget(lblWarning, 0, 1, 1, 1) + layout.addWidget(lblWarning, 0, 1, 1, 1) layout.addWidget(lblWarning2, 1, 1, 1, 1) - layout.addWidget(lblWarnImg, 0, 0, 2, 1) - layout.addWidget(lblWarnImg2, 0, 2, 2, 1) + layout.addWidget(lblWarnImg, 0, 0, 2, 1) + layout.addWidget(lblWarnImg2, 0, 2, 2, 1) frmInfo = QFrame() - frmInfo.setFrameStyle(QFrame.Box|QFrame.Plain) + frmInfo.setFrameStyle(QFrame.Box | QFrame.Plain) frmInfo.setLineWidth(3) frmInfoLayout = QGridLayout() for i in range(len(lbls)): - lbls[i][0].setText(''+lbls[i][0].text()+'') + lbls[i][0].setText('' + lbls[i][0].text() + '') lbls[i][0].setTextFormat(Qt.RichText) - frmInfoLayout.addWidget(lbls[i][0], i, 0) - frmInfoLayout.addWidget(lbls[i][1], i, 1, 1, 2) + frmInfoLayout.addWidget(lbls[i][0], i, 0) + frmInfoLayout.addWidget(lbls[i][1], i, 1, 1, 2) frmInfo.setLayout(frmInfoLayout) layout.addWidget(frmInfo, 2, 0, 2, 3) - lblDelete = QLabel( \ + lblDelete = QLabel(\ 'Do you want to delete this address? No other addresses in this ' 'wallet will be affected.') lblDelete.setWordWrap(True) @@ -4522,7 +4912,7 @@ def __init__(self, wlt, addr160, parent=None, main=None): QDialogButtonBox.Cancel) self.connect(bbox, SIGNAL('accepted()'), self.removeAddress) self.connect(bbox, SIGNAL('rejected()'), self.reject) - layout.addWidget(bbox, 5,0,1,3) + layout.addWidget(bbox, 5, 0, 1, 3) self.setLayout(layout) self.setWindowTitle('Confirm Delete Address') @@ -4537,11 +4927,11 @@ def removeAddress(self): 'inaccessible.\n\n ' 'If you are maintaining an external copy of this address ' 'please ignore this warning\n\n' - 'Are you absolutely sure you want to delete ' + + 'Are you absolutely sure you want to delete ' + self.addr.getAddrStr() + '?', \ QMessageBox.Yes | QMessageBox.Cancel) - if reply==QMessageBox.Yes: + if reply == QMessageBox.Yes: self.wlt.deleteImportedAddress(self.addr.getAddr160()) try: self.parent.wltAddrModel.reset() @@ -4549,202 +4939,163 @@ def removeAddress(self): except AttributeError: pass self.accept() - + else: self.reject() - - +################################################################################ class DlgWalletSelect(ArmoryDialog): - def __init__(self, parent=None, main=None, title='Select Wallet:', \ - descr='', firstSelect=None, onlyMyWallets=False, \ - wltIDList=None, atLeast=0): + def __init__(self, parent=None, main=None, title='Select Wallet:', + descr='', + firstSelect=None, + onlyMyWallets=False, + wltIDList=None, + atLeast=0): super(DlgWalletSelect, self).__init__(parent, main) - self.lstWallets = QListWidget() + self.balAtLeast = atLeast - if self.main and len(self.main.walletMap)==0: + if self.main and len(self.main.walletMap) == 0: QMessageBox.critical(self, 'No Wallets!', \ 'There are no wallets to select from. Please create or import ' 'a wallet first.', QMessageBox.Ok) self.accept() return - - if wltIDList==None: - wltIDList = list(self.main.walletIDList) - - self.rowList = [] - - selectedRow = 0 - self.selectedID = None - nrows = 0 - if len(wltIDList)>0: - self.selectedID = wltIDList[0] - for r,wltID in enumerate(wltIDList): - wlt = self.main.walletMap[wltID] - wlttype = determineWalletType(self.main.walletMap[wltID], self.main)[0] - if onlyMyWallets and wlttype==WLTTYPES.WatchOnly: - continue - self.lstWallets.addItem(QListWidgetItem(wlt.labelName)) - self.rowList.append([wltID]) - - if wltID==firstSelect: - selectedRow = nrows - self.selectedID = wltID - nrows += 1 - - self.lstWallets.setCurrentRow(selectedRow) - - self.connect(self.lstWallets, SIGNAL('currentRowChanged(int)'), self.showWalletInfo) - self.lstWallets.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - - self.connect(self.lstWallets, SIGNAL('itemDoubleClicked(QListWidgetItem *)'), self.dblclick) + if wltIDList == None: + wltIDList = list(self.main.walletIDList) # Start the layout layout = QVBoxLayout() - - if descr: - layout.addWidget(makeHorizFrame([QRichLabel(descr)], STYLE_SUNKEN)) - layout.addWidget(QRichLabel(''+title+'')) - - - lbls = [] - lbls.append( QLabel("Wallet ID:") ) - lbls.append( QLabel("Name:")) - lbls.append( QLabel("Description:")) - lbls.append( QLabel("Spendable Balance:")) - - for i in range(len(lbls)): - lbls[i].setAlignment(Qt.AlignLeft | Qt.AlignTop) - lbls[i].setTextFormat(Qt.RichText) - lbls[i].setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum) - lbls[i].setText(''+str(lbls[i].text())+'') - - self.dispID = QLabel() - self.dispName = QLabel() - self.dispDescr = QLabel() - self.dispBal = QLabel() - - self.dispBal.setTextFormat(Qt.RichText) - self.dispDescr.setWordWrap(True) - - - frm = QFrame() - frm.setFrameStyle(STYLE_SUNKEN) - frmLayout = QGridLayout() - for i in range(len(lbls)): - frmLayout.addWidget(lbls[i], i, 0, 1, 1) - - self.dispID.setAlignment(Qt.AlignLeft | Qt.AlignTop) - self.dispName.setAlignment(Qt.AlignLeft | Qt.AlignTop) - self.dispDescr.setAlignment(Qt.AlignLeft | Qt.AlignTop) - self.dispBal.setAlignment(Qt.AlignLeft | Qt.AlignTop) - self.dispDescr.setMinimumWidth( tightSizeNChar(self.dispDescr, 40)[0]) - frmLayout.addWidget(self.dispID, 0, 2, 1, 1) - frmLayout.addWidget(self.dispName, 1, 2, 1, 1) - frmLayout.addWidget(self.dispDescr, 2, 2, 1, 1) - frmLayout.addWidget(self.dispBal, 3, 2, 1, 1) - #for i in range(len(displays)): - #displays[i].setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum) - #frmLayout.addWidget(displays[i], i, 1, 1, 1) - - frmLayout.addItem(QSpacerItem(20, 10, QSizePolicy.Fixed, QSizePolicy.Expanding), 0, 1, 3, 1) - frm.setLayout(frmLayout) - - + # Expect to set selectedId + wltFrame = SelectWalletFrame(self, main, HORIZONTAL, firstSelect, onlyMyWallets, + wltIDList, atLeast, self.selectWallet) + layout.addWidget(wltFrame) + self.selectedID = wltFrame.selectedID buttonBox = QDialogButtonBox() btnAccept = QPushButton('OK') btnCancel = QPushButton('Cancel') - self.connect(btnAccept, SIGNAL('clicked()'), self.accept) - self.connect(btnCancel, SIGNAL('clicked()'), self.reject) + self.connect(btnAccept, SIGNAL(CLICKED), self.accept) + self.connect(btnCancel, SIGNAL(CLICKED), self.reject) buttonBox.addButton(btnAccept, QDialogButtonBox.AcceptRole) buttonBox.addButton(btnCancel, QDialogButtonBox.RejectRole) - layout.addWidget(makeHorizFrame([self.lstWallets, frm])) layout.addWidget(buttonBox) layout.setSpacing(15) self.setLayout(layout) - if not self.selectedID==None: - self.showWalletInfo() - self.setWindowTitle('Select Wallet') + def selectWallet(self, wlt, isDoubleClick=False): + self.selectedID = wlt.uniqueIDB58 + if isDoubleClick: + self.accept() +############################################################################# +def excludeChange(outputPairs, wlt): + """ + NOTE: this method works ONLY because we always generate a new address + whenever creating a change-output, which means it must have a + higher chainIndex than all other addresses. If you did something + creative with this tx, this may not actually work. + """ + maxChainIndex = -5 + nonChangeOutputPairs = [] + currentMaxChainPair = None + for pair in outputPairs: + addr160 = scrAddr_to_hash160(pair[0])[1] + addr = wlt.getAddrByHash160(addr160) + # this logic excludes the pair with the maximum chainIndex from the + # returned list + if addr: + if addr.chainIndex > maxChainIndex: + maxChainIndex = addr.chainIndex + if currentMaxChainPair: + nonChangeOutputPairs.append(currentMaxChainPair) + currentMaxChainPair = pair + else: + nonChangeOutputPairs.append(pair) + return nonChangeOutputPairs - def showWalletInfo(self, i=0): - currRow = self.lstWallets.currentRow() - wltID = self.rowList[currRow][0] - wlt = self.main.walletMap[wltID] - self.dispID.setText(wltID) - self.dispName.setText(wlt.labelName) - self.dispDescr.setText(wlt.labelDescr) - self.selectedID=wltID - - if not TheBDM.getBDMState()=='BlockchainReady': - self.dispBal.setText('-'*12) - return - - bal = wlt.getBalance('Spendable') - balStr = coin2str(wlt.getBalance('Spendable'), maxZeros=1) - if bal<=self.balAtLeast: - self.dispBal.setText('%s' % balStr) - else: - self.dispBal.setText(''+balStr+'') - - - def dblclick(self, *args): - currRow = self.lstWallets.currentRow() - self.selectedID = self.rowList[currRow][0] - self.accept() - - - - - - +################################################################################ class DlgConfirmSend(ArmoryDialog): - def __init__(self, wlt, recipValPairs, fee, parent=None, main=None, sendNow=False, changeBehave=None): + def __init__(self, wlt, scraddrValuePairs, fee, parent=None, main=None, sendNow=False, changeBehave=None): super(DlgConfirmSend, self).__init__(parent, main) - - self.wlt = wlt - layout = QGridLayout() - - lblInfoImg = QLabel() lblInfoImg.setPixmap(QPixmap(':/MsgBox_info48.png')) lblInfoImg.setAlignment(Qt.AlignHCenter | Qt.AlignTop) - - totalSend = sum([rv[1] for rv in recipValPairs]) + fee + + sendPairs = [] + returnPairs = [] + for pair in scraddrValuePairs: + if not wlt.hasAddr(scrAddr_to_hash160(pair[0])[1]): + sendPairs.append(pair) + else: + returnPairs.append(pair) + + # if we do not know the change behavior then we have to + # guess that the highest chain index is the change + # and exclude it from the returnPairs list + # and not in expert mode (because in expert mode the change could be anywhere + if changeBehave == None and returnPairs > 0: + returnPairs = excludeChange(returnPairs, wlt) + + sendPairs.extend(returnPairs) + + # If there are multiple outputs coming back to wallet + # assume that the one with the highest index is change. + lblMsg = QRichLabel('') + totalSend = sum([sv[1] for sv in sendPairs]) + fee sumStr = coin2str(totalSend, maxZeros=1) - - lblMsg = QRichLabel( - 'You are about to spend %s BTC from wallet "%s" (%s). You ' - 'specified the following distribution:' % (sumStr, wlt.labelName, wlt.uniqueIDB58)) - + if len(returnPairs) > 0: + if changeBehave == None and self.main.usermode == USERMODE.Expert: + lblMsg.setText(tr(""" + This transaction will spend %s BTC from wallet + "%s" (%s). +

    Note: Starred entries in the below list are + going to the same wallet from which they came, and thus have + no effect on your overall balance. When using Expert usermode + features, Armory cannot always distinguish the starred outputs + from the change address.""") % \ + (sumStr, wlt.labelName, wlt.uniqueIDB58)) + else: + lblMsg.setText(tr(""" + This transaction will spend %s BTC from wallet + "%s" (%s). +

    Note: Any starred outputs are are going to the + same wallet from which they came, and will have no effect on + the wallet's overall balance.""") % \ + (sumStr, wlt.labelName, wlt.uniqueIDB58)) + else: + lblMsg.setText(tr(""" + This transaction will spend %s BTC from wallet + "%s" (%s). Here are the outputs:""") % \ + (sumStr, wlt.labelName, wlt.uniqueIDB58)) recipLbls = [] ffixBold = GETFONT('Fixed') ffixBold.setWeight(QFont.Bold) - for rv in recipValPairs: - addrPrint = (hash160_to_addrStr(rv[0]) + ' : ').ljust(37) - recipLbls.append(QLabel( addrPrint + coin2str(rv[1], rJust=True, maxZeros=4))) + for sv in sendPairs: + if sv in returnPairs: + addrPrint = ('*' + scrAddr_to_addrStr(sv[0]) + ' : ').ljust(38) + else: + addrPrint = (scrAddr_to_addrStr(sv[0]) + ' : ').ljust(38) + recipLbls.append(QLabel(addrPrint + coin2str(sv[1], rJust=True, maxZeros=4))) recipLbls[-1].setFont(ffixBold) - if fee>0: - recipLbls.append(QSpacerItem(10,10)) - recipLbls.append(QLabel( 'Transaction Fee : '.ljust(37) + + if fee > 0: + recipLbls.append(QSpacerItem(10, 10)) + recipLbls.append(QLabel('Transaction Fee : '.ljust(38) + coin2str(fee, rJust=True, maxZeros=4))) recipLbls[-1].setFont(GETFONT('Fixed')) recipLbls.append(HLINE(QFrame.Sunken)) - recipLbls.append(QLabel( 'Total bitcoins : '.ljust(37) + + recipLbls.append(QLabel('Total bitcoins Sent: '.ljust(38) + coin2str(totalSend, rJust=True, maxZeros=4))) recipLbls[-1].setFont(GETFONT('Fixed')) @@ -4759,9509 +5110,8833 @@ def __init__(self, wlt, recipValPairs, fee, parent=None, main=None, sendNow=Fals # Acknowledge if the user has selected a non-std change location lblSpecialChange = QRichLabel('') - if self.main.usermode==USERMODE.Expert and changeBehave: - chngAddr160 = changeBehave[0] - chngAddrStr = hash160_to_addrStr(chngAddr160) + if self.main.usermode == USERMODE.Expert and changeBehave: + chngScrAddr = changeBehave[0] + if len(chngScrAddr) > 0: + chngAddrStr = scrAddr_to_addrStr(chngScrAddr) + atype, chngAddr160 = addrStr_to_hash160(chngAddrStr) + if atype == P2SHBYTE: + LOGWARN('P2SH Change address received') chngBehaveStr = changeBehave[1] - if chngBehaveStr=='Feedback': + if chngBehaveStr == 'Feedback': lblSpecialChange.setText('*Change will be sent back to first input address') - elif chngBehaveStr=='Specify': - wltID = self.main.getWalletForAddr160(changeBehave[0]) + elif chngBehaveStr == 'Specify': + wltID = self.main.getWalletForAddr160(chngAddr160) msg = '*Change will be sent to %s...' % chngAddrStr[:12] if wltID: - msg += ' (Wallet: %s)'%wltID + msg += ' (Wallet: %s)' % wltID lblSpecialChange.setText(msg) - elif chngBehaveStr=='NoChange': + elif chngBehaveStr == NO_CHANGE: lblSpecialChange.setText('(This transaction is exact -- there are no change outputs)') - + self.btnCancel = QPushButton("Cancel") - self.connect(self.btnAccept, SIGNAL('clicked()'), self.accept) - self.connect(self.btnCancel, SIGNAL('clicked()'), self.reject) + 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) - frmTable = makeLayoutFrame('Vert', recipLbls, STYLE_RAISED) - frmRight = makeVertFrame( [ lblMsg, \ + frmTable = makeLayoutFrame(VERTICAL, recipLbls, STYLE_RAISED) + frmRight = makeVertFrame([ lblMsg, \ 'Space(20)', \ frmTable, \ lblSpecialChange, \ 'Space(10)', \ lblLastConfirm, \ 'Space(10)', \ - buttonBox ] ) + buttonBox ]) + + frmAll = makeHorizFrame([ lblInfoImg, frmRight ]) - frmAll = makeHorizFrame( [ lblInfoImg, frmRight ] ) - layout.addWidget(frmAll) self.setLayout(layout) self.setMinimumWidth(350) self.setWindowTitle('Confirm Transaction') - - +################################################################################ class DlgSendBitcoins(ArmoryDialog): - COLS = enum('LblAddr','Addr','AddrBook', 'LblWltID', 'LblAmt','Btc','LblUnit','BtnMax', 'LblComm','Comm') - def __init__(self, wlt, parent=None, main=None, prefill=None): + def __init__(self, wlt, parent=None, main=None, prefill=None, wltIDList=None, onlyOfflineWallets=False): super(DlgSendBitcoins, self).__init__(parent, main) - self.maxHeight = tightSizeNChar(GETFONT('var'), 1)[1]+8 - - self.wlt = wlt - self.wltID = wlt.uniqueIDB58 - - txFee = self.main.getSettingOrSetDefault('Default_Fee', MIN_TX_FEE) - - self.widgetTable = [] - - self.scrollRecipArea = QScrollArea() - lblRecip = QRichLabel('Enter Recipients:') - lblRecip.setAlignment(Qt.AlignLeft | Qt.AlignBottom) - - self.freeOfErrors = True - - feetip = self.main.createToolTipWidget( \ - 'Transaction fees go to users who contribute computing power to ' - 'keep the Bitcoin network secure, and in return they get your transaction ' - 'included in the blockchain faster. Most transactions ' - 'do not require a fee but it is recommended anyway ' - 'since it guarantees quick processing for less than $0.01 USD and ' - 'helps the network.') - - self.edtFeeAmt = QLineEdit() - self.edtFeeAmt.setFont(GETFONT('Fixed')) - self.edtFeeAmt.setMaximumWidth(tightSizeNChar(self.edtFeeAmt, 12)[0]) - self.edtFeeAmt.setMaximumHeight(self.maxHeight) - self.edtFeeAmt.setAlignment(Qt.AlignRight) - self.edtFeeAmt.setText(coin2str(txFee, maxZeros=1).strip()) - - spacer = QSpacerItem(20, 1) + layout = QVBoxLayout() + self.frame = SendBitcoinsFrame(self, main, 'Send Bitcoins',\ + wlt, prefill, wltIDList, onlyOfflineWallets=onlyOfflineWallets,\ + sendCallback=self.createTxAndBroadcast,\ + createUnsignedTxCallback=self.createUnsignedTxDPAndDisplay) + layout.addWidget(self.frame) + self.setLayout(layout) + # Update the any controls based on the initial wallet selection + self.frame.fireWalletChange() - # Create the wallet summary display - lbls = [] - lbls.append( QRichLabel("Wallet ID:", doWrap=False) ) - lbls.append( QRichLabel("Name:", doWrap=False)) - lbls.append( QRichLabel("Description:", doWrap=False)) - lbls.append( QRichLabel("Spendable BTC:", doWrap=False)) + ############################################################################# + def createUnsignedTxDPAndDisplay(self, txdp): + self.accept() + dlg = DlgOfflineTxCreated(self.frame.wlt, txdp, self.parent, self.main) + dlg.exec_() - for i in range(len(lbls)): - lbls[i].setAlignment(Qt.AlignLeft | Qt.AlignTop) - lbls[i].setText(''+str(lbls[i].text())+'') - - self.lblSummaryID = QRichLabel('') - self.lblSummaryName = QRichLabel('') - self.lblSummaryDescr = QRichLabel('') - self.lblSummaryDescr.setWordWrap(True) - self.lblSummaryBal = QMoneyLabel(0) - - # Format balance if necessary - if not TheBDM.getBDMState()=='BlockchainReady': - self.lblSummaryBal.setText('(available when online)', color='DisableFG') - else: - bal = wlt.getBalance('Spendable') - if bal==0: - self.lblSummaryBal.setValueText(0, wBold=True) - else: - self.lblSummaryBal.setValueText(bal, maxZeros=1, wBold=True) + ############################################################################# + def createTxAndBroadcast(self): + self.accept() - - self.frmWalletInfo = QFrame() - self.frmWalletInfo.setFrameStyle(STYLE_SUNKEN) - frmLayout = QGridLayout() - for i in range(len(lbls)): - frmLayout.addWidget(lbls[i], i, 0, 1, 1) - - self.lblSummaryID.setAlignment(Qt.AlignLeft | Qt.AlignTop) - self.lblSummaryName.setAlignment(Qt.AlignLeft | Qt.AlignTop) - self.lblSummaryDescr.setAlignment(Qt.AlignLeft | Qt.AlignTop) - self.lblSummaryDescr.setMinimumWidth( tightSizeNChar(self.lblSummaryDescr, 30)[0]) - frmLayout.addWidget(self.lblSummaryID, 0, 2, 1, 1) - frmLayout.addWidget(self.lblSummaryName, 1, 2, 1, 1) - frmLayout.addWidget(self.lblSummaryDescr, 2, 2, 1, 1) - frmLayout.addWidget(self.lblSummaryBal, 3, 2, 1, 1) - frmLayout.addItem(QSpacerItem(20, 10, QSizePolicy.Fixed, QSizePolicy.Expanding), 0, 1, 3, 1) - self.frmWalletInfo.setLayout(frmLayout) + ############################################################################# + def saveGeometrySettings(self): + self.main.writeSetting('SendBtcGeometry', str(self.saveGeometry().toHex())) - - lblNoSend = QRichLabel('') - btnUnsigned = QRichLabel('') - ttipUnsigned = QRichLabel('') - - if not TheBDM.getBDMState()=='BlockchainReady': - lblNoSend.setText( - '' - 'You can sign this transaction, but you do not have ' - 'access to the Bitcoin network in order to broadcast ' - 'it. However, you can create the transaction that ' - 'you want to send, and then broadcast it from a computer ' - 'that is connected to the network.' % htmlColor('TextWarn')) - elif wlt.watchingOnly: - lblNoSend = QRichLabel( \ - '' - 'This is an "offline" wallet, which means that the ' - 'private keys needed to send bitcoins are not on this computer. ' - 'However, you can create the transaction you would like to ' - 'spend, then Armory will provide you with a file that can be ' - 'signed by the computer that does have the private ' - 'keys.' % htmlColor('TextWarn')) - - btnSend = QPushButton('Send!') - - txFrm = makeLayoutFrame('Horiz', [QLabel('Transaction Fee:'), \ - self.edtFeeAmt, \ - feetip, \ - 'stretch', \ - btnSend]) - - if not wlt.watchingOnly: - ttipUnsigned = self.main.createToolTipWidget( \ - 'If you would like to create the transaction but not sign it yet, ' - 'you can click this button to save it to a file.') - self.connect(btnSend, SIGNAL('clicked()'), self.createTxAndBroadcast) - else: - ttipUnsigned = self.main.createToolTipWidget( \ - 'After clicking this button, you will be given directions for ' - 'completing this transaction.') - btnSend.setToolTip('This is a watching-only wallet! ' - 'You cannot use it to send bitcoins!') - #btnSend.setEnabled(False) - btnSend.setEnabled(True) - btnSend.setText("Create Unsigned Transaction") - self.connect(btnSend, SIGNAL('clicked()'), self.createOfflineTxDPAndDisplay) - - - btnUnsigned = QPushButton('Create Unsigned Transaction') - self.connect(btnUnsigned, SIGNAL('clicked()'), self.createOfflineTxDPAndDisplay) - - if not TheBDM.getBDMState()=='BlockchainReady': - btnSend.setToolTip('You are currently not connected to the Bitcoin network, ' - 'so you cannot initiate any transactions.') - btnSend.setEnabled(False) - btnUnsigned.setEnabled(False) - + ############################################################################# + def closeEvent(self, event): + self.saveGeometrySettings() + super(DlgSendBitcoins, self).closeEvent(event) + ############################################################################# + def accept(self, *args): + self.saveGeometrySettings() + super(DlgSendBitcoins, self).accept(*args) - def addDonation(): - self.addOneRecipient(ARMORY_DONATION_ADDR, ONE_BTC, \ - 'Donation to Armory Developers. Thank you for your generosity!', \ - label='Armory Donation Address') - - - btnDonate = QPushButton("Donate to Armory Developers!") - ttipDonate = self.main.createToolTipWidget( \ - 'Making this software was a lot of work. You can give back ' - 'by adding a small donation to go to the Armory developers. ' - 'You will have the ability to change the donation amount ' - 'before finalizing the transaction.') - self.connect(btnDonate, SIGNAL("clicked()"), self.addDonation) + ############################################################################# + def reject(self, *args): + self.saveGeometrySettings() + super(DlgSendBitcoins, self).reject(*args) - btnEnterURI = QPushButton('Manually Enter "bitcoin:" Link') - ttipEnterURI = self.main.createToolTipWidget( \ - 'Armory does not always succeed at registering itself to handle ' - 'URL links from webpages and email. ' - 'Click this button to copy a link directly into Armory') - self.connect(btnEnterURI, SIGNAL("clicked()"), self.clickEnterURI) +################################################################################ +class DlgOfflineTxCreated(ArmoryDialog): + def __init__(self, wlt, txdp, parent=None, main=None): + super(DlgOfflineTxCreated, self).__init__(parent, main) + layout = QVBoxLayout() + reviewOfflineTxFrame = ReviewOfflineTxFrame(self, main, "Review Offline Transaction") + reviewOfflineTxFrame.setWallet(wlt) + reviewOfflineTxFrame.setTxDp(txdp) + continueButton = QPushButton('Continue') + self.connect(continueButton, SIGNAL(CLICKED), self.signBroadcastTx) + doneButton = QPushButton('Done') + self.connect(doneButton, SIGNAL(CLICKED), self.accept) + + ttipDone = self.main.createToolTipWidget(\ + 'By clicking Done you will exit end the offline transaction process for now. ' + 'When you are ready to sign and/or broadcast the transaction, click the Offline ' + 'Transactions button in the main window, then click the Sign and/or ' + 'Broadcast Transaction button in the Select Offline Action dialog.') + + ttipContinue = self.main.createToolTipWidget(\ + 'By clicking Continue you will continue to the next step in the offline ' + 'transaction process to sign and/or broadcast the transaction.') + + bottomStrip = makeLayoutFrame(HORIZONTAL, [doneButton, ttipDone, STRETCH, continueButton, ttipContinue]) + frame = makeLayoutFrame(VERTICAL, [reviewOfflineTxFrame, bottomStrip]) + layout.addWidget(frame) + self.setLayout(layout) + self.setWindowTitle('Review Offline Transaction') + self.setWindowIcon(QIcon(self.main.iconfile)) - if USE_TESTNET: - btnDonate.setVisible(False) - ttipDonate.setVisible(False) + def signBroadcastTx(self): + self.accept() + DlgSignBroadcastOfflineTx(self.parent,self.main).exec_() - ######################################################################## - # In Expert usermode, allow the user to modify source addresses - frmCoinControl = QFrame() - self.sourceAddrList = None - self.altBalance = None - if self.main.usermode == USERMODE.Expert: - self.lblCoinCtrl = QRichLabel('Source: All addresses') - self.btnCoinCtrl = QPushButton('Coin Control') - self.connect(self.btnCoinCtrl, SIGNAL('clicked()'), self.doCoinCtrl) - frmCoinControl = makeHorizFrame([self.lblCoinCtrl, self.btnCoinCtrl]) +################################################################################ +class DlgOfflineSelect(ArmoryDialog): + def __init__(self, parent=None, main=None): + super(DlgOfflineSelect, self).__init__(parent, main) - ######################################################################## - # In Expert usermode, allow the user to modify the change address - frmChangeAddr = QFrame() - if self.main.usermode == USERMODE.Expert: - self.chkDefaultChangeAddr = QCheckBox('Use an existing address for change') - self.radioFeedback = QRadioButton('Send change to first input address') - self.radioSpecify = QRadioButton('Specify a change address') - self.lblChangeAddr = QRichLabel('Send Change To:') - self.edtChangeAddr = QLineEdit() - self.btnChangeAddr = createAddrBookButton(self, self.edtChangeAddr, \ - self.wlt.uniqueIDB58, 'Send change to') - self.chkRememberChng = QCheckBox('Remember for future transactions') - self.vertLine = VLINE() - - self.ttipSendChange = self.main.createToolTipWidget( \ - 'Most transactions end up with oversized inputs and Armory will send ' - 'the change to the next address in this wallet. You may change this ' - 'behavior by checking this box.') - self.ttipFeedback = self.main.createToolTipWidget( \ - 'Guarantees that no new addresses will be created to receive ' - 'change. This reduces anonymity, but is useful if you ' - 'created this wallet solely for managing imported addresses, ' - 'and want to keep all funds within existing addresses.') - self.ttipSpecify = self.main.createToolTipWidget( \ - 'You can specify any valid Bitcoin address for the change. ' - 'NOTE: If the address you specify is not in this wallet, ' - 'Armory will not be able to distinguish the outputs when it shows ' - 'up in your ledger. The change will look like a second recipient, ' - 'and the total debit to your wallet will be equal to the amount ' - 'you sent to the recipient plus the change.') - - - # Make sure that there can only be one selection - btngrp = QButtonGroup(self) - btngrp.addButton(self.radioFeedback) - btngrp.addButton(self.radioSpecify) - btngrp.setExclusive(True) - - def toggleSpecify(b): - self.lblChangeAddr.setVisible(b) - self.edtChangeAddr.setVisible(b) - self.btnChangeAddr.setVisible(b) - - def toggleChngAddr(b): - self.radioFeedback.setVisible(b) - self.radioSpecify.setVisible(b) - self.ttipFeedback.setVisible(b) - self.ttipSpecify.setVisible(b) - self.chkRememberChng.setVisible(b) - self.vertLine.setVisible(b) - if not self.radioFeedback.isChecked() and not self.radioSpecify.isChecked(): - self.radioFeedback.setChecked(True) - toggleSpecify(b and self.radioSpecify.isChecked()) - - - self.connect(self.chkDefaultChangeAddr, SIGNAL('toggled(bool)'), toggleChngAddr) - self.connect(self.radioSpecify, SIGNAL('toggled(bool)'), toggleSpecify) - - # Pre-set values based on settings - - chngBehave = self.main.getWltSetting(self.wltID, 'ChangeBehavior') - chngAddr = self.main.getWltSetting(self.wltID, 'ChangeAddr') - if chngBehave == 'Feedback': - self.chkDefaultChangeAddr.setChecked(True) - self.radioFeedback.setChecked(True) - self.radioSpecify.setChecked(False) - toggleChngAddr(True) - self.chkRememberChng.setChecked(True) - elif chngBehave == 'Specify': - self.chkDefaultChangeAddr.setChecked(True) - self.radioFeedback.setChecked(False) - self.radioSpecify.setChecked(True) - toggleChngAddr(True) - if checkAddrStrValid(chngAddr): - self.edtChangeAddr.setText(chngAddr) - self.edtChangeAddr.setCursorPosition(0) - self.chkRememberChng.setChecked(True) - else: - # Other option is "NewAddr" but in case there's an error, should run - # this branch by default - self.chkDefaultChangeAddr.setChecked(False) - self.radioFeedback.setChecked(False) - self.radioSpecify.setChecked(False) - toggleChngAddr(False) - - if( self.chkDefaultChangeAddr.isChecked() and \ - not self.radioFeedback.isChecked() and \ - not self.radioSpecify.isChecked()): - self.radioFeedback.setChecked(True) - - frmChngLayout = QGridLayout() - i=0; - frmChngLayout.addWidget(self.chkDefaultChangeAddr, i,0, 1,6) - frmChngLayout.addWidget(self.ttipSendChange, i,6, 1,2) - i+=1 - frmChngLayout.addWidget(self.radioFeedback, i,1, 1,5) - frmChngLayout.addWidget(self.ttipFeedback, i,6, 1,2) - i+=1 - frmChngLayout.addWidget(self.radioSpecify, i,1, 1,5) - frmChngLayout.addWidget(self.ttipSpecify, i,6, 1,2) - i+=1 - frmChngLayout.addWidget(self.lblChangeAddr, i,1, 1,2) - frmChngLayout.addWidget(self.edtChangeAddr, i,3, 1,4) - frmChngLayout.addWidget(self.btnChangeAddr, i,7, 1,1) - i+=1 - frmChngLayout.addWidget(self.chkRememberChng, i,1, 1,7) - - frmChngLayout.addWidget(self.vertLine, 1,0, i-1,1) - - frmChangeAddr.setLayout(frmChngLayout) - frmChangeAddr.setFrameStyle(STYLE_SUNKEN) - - - - frmUnsigned = makeHorizFrame([btnUnsigned, ttipUnsigned]) - frmDonate = makeHorizFrame([btnDonate, ttipDonate]) - frmEnterURI = makeHorizFrame([btnEnterURI, ttipEnterURI]) - + self.do_review = False + self.do_create = False + self.do_broadc = False + lblDescr = QRichLabel(\ + 'In order to execute an offline transaction, three steps must ' + 'be followed:

    ' + '\t(1) Online Computer: Create the unsigned transaction
    ' + '\t(2) Offline Computer: Get the transaction signed
    ' + '\t(3) Online Computer: Broadcast the signed transaction

    ' + 'You must create the transaction using a watch-only wallet on an online ' + 'system, but watch-only wallets cannot sign it. Only the offline system ' + 'can create a valid signature. The easiest way to execute all three steps ' + 'is to use a USB key to move the data between computers.

    ' + 'All the data saved to the removable medium during all three steps are ' + 'completely safe and do not reveal any private information that would benefit an ' + 'attacker trying to steal your funds. However, this transaction data does ' + 'reveal some addresses in your wallet, and may represent a breach of ' + 'privacy if not protected.') - frmNoSend = makeLayoutFrame('Horiz', [lblNoSend], STYLE_SUNKEN) - if not wlt.watchingOnly: - frmNoSend.setVisible(False) - if self.main.usermode==USERMODE.Standard: - btnUnsigned.setVisible(False) - ttipUnsigned.setVisible(False) - btnEnterURI.setVisible(False) - ttipEnterURI.setVisible(False) + btnCreate = QPushButton('Create New Offline Transaction') + broadcastButton = QPushButton('Sign and/or Broadcast Transaction') + if not TheBDM.getBDMState() == 'BlockchainReady': + btnCreate.setEnabled(False) + if len(self.main.walletMap) == 0: + broadcastButton = QPushButton('No wallets available!') + broadcastButton.setEnabled(False) + else: + broadcastButton = QPushButton('Sign Offline Transaction') + else: + if len(self.main.getWatchingOnlyWallets()) == 0: + btnCreate = QPushButton('No watching-only-wallets available!') + btnCreate.setEnabled(False) + if len(self.main.walletMap) == 0 and self.main.netMode == NETWORKMODE.Full: + broadcastButton = QPushButton('Broadcast Signed Transaction') - frmBottomLeft = makeVertFrame( [self.frmWalletInfo, \ - frmUnsigned, \ - frmEnterURI, \ - frmDonate, \ - 'Stretch', \ - frmCoinControl, \ - frmChangeAddr], STYLE_SUNKEN ) + btnCancel = QPushButton('<<< Go Back') - lblSend = QRichLabel('Sending from Wallet:') - lblSend.setAlignment(Qt.AlignLeft | Qt.AlignBottom) + def create(): + self.do_create = True; self.accept() + def broadc(): + self.do_broadc = True; self.accept() + self.connect(btnCreate, SIGNAL(CLICKED), create) + self.connect(broadcastButton, SIGNAL(CLICKED), broadc) + self.connect(btnCancel, SIGNAL(CLICKED), self.reject) - layout = QGridLayout() + lblCreate = QRichLabel(tr(""" + Create a transaction from an Offline/Watching-Only wallet + to be signed by the computer with the full wallet """)) - layout.addWidget(lblSend, 0,0, 1,1) - layout.addWidget(frmBottomLeft, 1,0, 2,1) + lblReview = QRichLabel(tr(""" + Review an unsigned transaction and sign it if you have + the private keys needed for it """)) - layout.addWidget(lblRecip, 0,1, 1,1) - layout.addWidget(self.scrollRecipArea, 1,1, 1,1) - layout.addWidget(txFrm, 2,1, 1,1) + lblBroadc = QRichLabel(tr(""" + Send a pre-signed transaction to the Bitcoin network to finalize it""")) - layout.setRowStretch(0,0) - layout.setRowStretch(1,1) - layout.setRowStretch(2,0) - self.setLayout(layout) + lblBroadc.setMinimumWidth(tightSizeNChar(lblBroadc, 45)[0]) - self.makeRecipFrame(1) - self.setWindowTitle('Send Bitcoins') - self.setMinimumHeight(self.maxHeight*20) + frmOptions = QFrame() + frmOptions.setFrameStyle(STYLE_PLAIN) + frmOptionsLayout = QGridLayout() + frmOptionsLayout.addWidget(btnCreate, 0, 0) + frmOptionsLayout.addWidget(lblCreate, 0, 2) + frmOptionsLayout.addWidget(HLINE(), 1, 0, 1, 3) + frmOptionsLayout.addWidget(broadcastButton, 2, 0, 3, 1) + frmOptionsLayout.addWidget(lblReview, 2, 2) + frmOptionsLayout.addWidget(HLINE(), 3, 2, 1, 1) + frmOptionsLayout.addWidget(lblBroadc, 4, 2) + + frmOptionsLayout.addItem(QSpacerItem(20, 20), 0, 1, 3, 1) + frmOptions.setLayout(frmOptionsLayout) - loadCount = self.main.settings.get('Load_Count') - alreadyDonated = self.main.getSettingOrSetDefault('DonateAlready', False) - lastPestering = self.main.getSettingOrSetDefault('DonateLastPester', 0) - donateFreq = self.main.getSettingOrSetDefault('DonateFreq', 20) - dnaaDonate = self.main.getSettingOrSetDefault('DonateDNAA', False) + frmDescr = makeLayoutFrame(HORIZONTAL, ['Space(10)', lblDescr, 'Space(10)'], \ + STYLE_SUNKEN) + frmCancel = makeLayoutFrame(HORIZONTAL, [btnCancel, STRETCH]) + dlgLayout = QGridLayout() + dlgLayout.addWidget(frmDescr, 0, 0, 1, 1) + dlgLayout.addWidget(frmOptions, 1, 0, 1, 1) + dlgLayout.addWidget(frmCancel, 2, 0, 1, 1) - if prefill: - get = lambda s: prefill[s] if prefill.has_key(s) else '' - addr160 = addrStr_to_hash160(get('address')) - amount = get('amount') - message = get('message') - label = get('label') - self.addOneRecipient(addr160, amount, message, label) - - elif not self.main==None and loadCount%donateFreq==(donateFreq-1) and \ - not loadCount==lastPestering and not dnaaDonate and \ - wlt.getBalance('Spendable') > 5*ONE_BTC and not USE_TESTNET: - result = MsgBoxWithDNAA(MSGBOX.Question, 'Please donate!', \ - 'Armory is the result of over 3,000 hours of development ' - 'and dozens of late nights bug-hunting and testing. Yet, this software ' - 'has been given to you for free to benefit the greater Bitcoin ' - 'community! ' - '

    However, continued development may not be possible without ' - 'donations. If you are satisfied with this software, please consider ' - 'donating what you think this software would be worth as a commercial ' - 'application.' - '

    Are you willing to donate to the Armory developers? If you ' - 'select "Yes," a donation field will be added to your ' - 'next transaction. You will have the opportunity to remove or change ' - 'the amount before sending the transaction.', None) - self.main.writeSetting('DonateLastPester', loadCount) - - if result[0]==True: - self.addDonation() - self.makeRecipFrame(2) - - if result[1]==True: - self.main.writeSetting('DonateDNAA', True) - - hexgeom = self.main.settings.get('SendBtcGeometry') - if len(hexgeom)>0: - geom = QByteArray.fromHex(hexgeom) - self.restoreGeometry(geom) - + self.setLayout(dlgLayout) + self.setWindowTitle('Select Offline Action') + self.setWindowIcon(QIcon(self.main.iconfile)) - if TheBDM.getBDMState()=='BlockchainReady' and not wlt.watchingOnly: - btnSend.setDefault(True) - else: - btnUnsigned.setDefault(True) - - self.setWalletSummary() +################################################################################ +class DlgSignBroadcastOfflineTx(ArmoryDialog): + """ + We will make the assumption that this dialog is used ONLY for outgoing + transactions from your wallet. This simplifies the logic if we don't + have to identify input senders/values, and handle the cases where those + may not be specified + """ + def __init__(self, parent=None, main=None): + super(DlgSignBroadcastOfflineTx, self).__init__(parent, main) + self.setWindowTitle('Review Offline Transaction') + self.setWindowIcon(QIcon(self.main.iconfile)) - ############################################################################# - def saveGeometrySettings(self): - self.main.writeSetting('SendBtcGeometry', str(self.saveGeometry().toHex())) + signBroadcastOfflineTxFrame = SignBroadcastOfflineTxFrame( + self, main, "Sign or Broadcast Transaction") - ############################################################################# - def closeEvent(self, event): - self.saveGeometrySettings() - super(DlgSendBitcoins, self).closeEvent(event) + doneButton = QPushButton('Done') + self.connect(doneButton, SIGNAL(CLICKED), self.accept) + doneForm = makeLayoutFrame(HORIZONTAL, [STRETCH, doneButton]) + dlgLayout = QVBoxLayout() + dlgLayout.addWidget(signBroadcastOfflineTxFrame) + dlgLayout.addWidget(doneForm) + self.setLayout(dlgLayout) + signBroadcastOfflineTxFrame.processTxDP() - ############################################################################# - def accept(self, *args): - self.saveGeometrySettings() - super(DlgSendBitcoins, self).accept(*args) +################################################################################ +class DlgShowKeyList(ArmoryDialog): + def __init__(self, wlt, parent=None, main=None): + super(DlgShowKeyList, self).__init__(parent, main) - ############################################################################# - def reject(self, *args): - self.saveGeometrySettings() - super(DlgSendBitcoins, self).reject(*args) + self.wlt = wlt - ############################################################################# - def createOfflineTxDPAndDisplay(self): - self.txValues = [] - self.origRVPairs = [] - self.comments = [] - txdp = self.validateInputsGetTxDP() - if not txdp: - return - - changePair = (self.change160, self.selectedBehavior) - dlg = DlgConfirmSend(self.wlt, self.origRVPairs, self.txValues[1], self, \ - self.main, False, changePair) - if dlg.exec_(): - dlg = DlgOfflineTxCreated(self.wlt, txdp, self, self.main) - dlg.exec_() - self.accept() + self.havePriv = ((not self.wlt.useEncryption) or not (self.wlt.isLocked)) + wltType = determineWalletType(self.wlt, self.main)[0] + if wltType in (WLTTYPES.Offline, WLTTYPES.WatchOnly): + self.havePriv = False - ############################################################################# - def createTxAndBroadcast(self): - self.txValues = [] - self.origRVPairs = [] - self.comments = [] - txdp = self.validateInputsGetTxDP() - if not txdp: - return + # NOTE/WARNING: We have to make copies (in RAM) of the unencrypted + # keys, or else we will have to type in our address + # every 10s if we want to modify the key list. This + # isn't likely a big problem, but it's not ideal, + # either. Not much I can do about, though... + # (at least: once this dialog is closed, all the + # garbage should be collected...) + self.addrCopies = [] + for addr in self.wlt.getLinearAddrList(withAddrPool=True): + self.addrCopies.append(addr.copy()) + self.rootKeyCopy = self.wlt.addrMap['ROOT'].copy() - if not self.txValues: - QMessageBox.critical(self, 'Tx Construction Failed', \ - 'Unknown error trying to create transaction', \ - QMessageBox.Ok) - - - changePair = (self.change160, self.selectedBehavior) - dlg = DlgConfirmSend(self.wlt, self.origRVPairs, self.txValues[1], self, \ - self.main, True, changePair) - if dlg.exec_(): - try: - if self.wlt.isLocked: - unlockdlg = DlgUnlockWallet(self.wlt, self, self.main, 'Send Transaction') - if not unlockdlg.exec_(): - QMessageBox.critical(self, 'Wallet is Locked', \ - 'Cannot sign transaction while your wallet is locked. ', \ - QMessageBox.Ok) - return - - - commentStr = '' - if len(self.comments)==1: - commentStr = self.comments[0] - else: - for i in range(len(self.comments)): - amt = self.origRVPairs[i][1] - if len(self.comments[i].strip())>0: - commentStr += '%s (%s); ' % (self.comments[i], coin2str_approx(amt).strip()) - + backupVersion = BACKUP_TYPE_135A + testChain = DeriveChaincodeFromRootKey(self.rootKeyCopy.binPrivKey32_Plain) + self.needChaincode = (not testChain == self.rootKeyCopy.chaincode) + if not self.needChaincode: + backupVersion = BACKUP_TYPE_135C - txdp = self.wlt.signTxDistProposal(txdp) - finalTx = txdp.prepareFinalTx() - if len(commentStr)>0: - self.wlt.setComment(finalTx.getHash(), commentStr) - self.main.broadcastTransaction(finalTx) - self.accept() - try: - self.parent.accept() - except: - pass - except: - LOGEXCEPT('Problem sending transaction!') - # TODO: not sure what errors to catch here, yet... - raise + self.strDescrReg = (\ + 'The textbox below shows all keys that are part of this wallet, ' + 'which includes both permanent keys and imported keys. If you ' + 'simply want to backup your wallet and you have no imported keys ' + 'then all data below is reproducible from a plain paper backup.' + '

    ' + 'If you have imported addresses to backup, and/or you ' + 'would like to export your private keys to another ' + 'wallet service or application, then you can save this data ' + 'to disk, or copy&paste it into the other application.') + self.strDescrWarn = (\ + '

    ' + 'Warning: The text box below contains ' + 'the plaintext (unencrypted) private keys for each of ' + 'the addresses in this wallet. This information can be used ' + 'to spend the money associated with those addresses, so please ' + 'protect it like you protect the rest of your wallet.') + self.lblDescr = QRichLabel('') + self.lblDescr.setAlignment(Qt.AlignLeft | Qt.AlignTop) - ############################################################################# - def updateAddrField(self, idx, col, color): - palette = QPalette() - palette.setColor( QPalette.Base, color) - self.widgetTable[idx][col].setPalette( palette ); - self.widgetTable[idx][col].setAutoFillBackground( True ); - try: - addrtext = str(self.widgetTable[idx][self.COLS.Addr].text()) - wid = self.main.getWalletForAddr160(addrStr_to_hash160(addrtext)) - if wid: - wlt = self.main.walletMap[wid] - dispStr = '%s (%s)' % (wlt.labelName, wlt.uniqueIDB58) - self.widgetTable[idx][self.COLS.LblWltID].setVisible(True) - self.widgetTable[idx][self.COLS.LblWltID].setText(dispStr, color='TextBlue') - else: - self.widgetTable[idx][self.COLS.LblWltID].setVisible(False) - except: - self.widgetTable[idx][self.COLS.LblWltID].setVisible(False) - LOGEXCEPT("Addr string invalid") + txtFont = GETFONT('Fixed', 8) + self.txtBox = QTextEdit() + self.txtBox.setReadOnly(True) + self.txtBox.setFont(txtFont) + w, h = tightSizeNChar(txtFont, 110) + self.txtBox.setFont(txtFont) + self.txtBox.setMinimumWidth(w) + self.txtBox.setMaximumWidth(w) + self.txtBox.setMinimumHeight(h * 3.2) + self.txtBox.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - ############################################################################# - def validateInputsGetTxDP(self): - COLS = self.COLS - self.freeOfErrors = True - addrBytes = [] - for i in range(len(self.widgetTable)): - # Verify validity of address strings - addrStr = str(self.widgetTable[i][COLS.Addr].text()).strip() - self.widgetTable[i][COLS.Addr].setText(addrStr) # overwrite w/ stripped - addrIsValid = False - try: - addrBytes.append(checkAddrType(base58_to_binary(addrStr))) - addrIsValid = (addrBytes[i]==ADDRBYTE) - except ValueError: - addrBytes.append(-1) + # Create a list of checkboxes and then some ID word to identify what + # to put there + self.chkList = {} + self.chkList['AddrStr'] = QCheckBox('Address String') + self.chkList['PubKeyHash'] = QCheckBox('Hash160') + self.chkList['PrivCrypt'] = QCheckBox('Private Key (Encrypted)') + self.chkList['PrivHexBE'] = QCheckBox('Private Key (Plain Hex)') + self.chkList['PrivB58'] = QCheckBox('Private Key (Plain Base58)') + self.chkList['PubKey'] = QCheckBox('Public Key (BE)') + self.chkList['ChainIndex'] = QCheckBox('Chain Index') - - if not addrIsValid: - self.freeOfErrors = False - self.updateAddrField(i, COLS.Addr, Colors.SlightRed) - - - numChkFail = sum([1 if b==-1 or b!=ADDRBYTE else 0 for b in addrBytes]) - if not self.freeOfErrors: - QMessageBox.critical(self, 'Invalid Address', \ - 'You have entered %d invalid addresses. The errors have been ' - 'highlighted on the entry screen.' % (numChkFail), \ - QMessageBox.Ok) - - for i in range(len(self.widgetTable)): - if addrBytes[i]!=-1 and addrBytes[i]!=ADDRBYTE: - net = 'Unknown Network' - if NETWORKS.has_key(addrBytes[i]): - net = NETWORKS[addrBytes[i]] - QMessageBox.warning(self, 'Wrong Network!', \ - 'Address %d is for the wrong network! You are on the %s ' - 'and the address you supplied is for the the ' - '%s!' % (i+1, NETWORKS[ADDRBYTE], net), QMessageBox.Ok) - return False + self.chkList['AddrStr' ].setChecked(True) + self.chkList['PubKeyHash'].setChecked(False) + self.chkList['PrivB58' ].setChecked(self.havePriv) + self.chkList['PrivCrypt' ].setChecked(False) + self.chkList['PrivHexBE' ].setChecked(self.havePriv) + self.chkList['PubKey' ].setChecked(not self.havePriv) + self.chkList['ChainIndex'].setChecked(False) + namelist = ['AddrStr', 'PubKeyHash', 'PrivB58', 'PrivCrypt', \ + 'PrivHexBE', 'PubKey', 'ChainIndex'] - # Construct recipValuePairs and check that all metrics check out - recipValuePairs = [] - totalSend = 0 - for i in range(len(self.widgetTable)): - try: - recipStr = str(self.widgetTable[i][COLS.Addr].text()).strip() - valueStr = str(self.widgetTable[i][COLS.Btc].text()).strip() - value = str2coin(valueStr, negAllowed=False) - if value==0: - QMessageBox.critical(self, 'Zero Amount', \ - 'You cannot send 0 BTC to any recipients.
    Please enter ' - 'a positive amount for recipient %d.' % (i+1), QMessageBox.Ok) - return False - - except NegativeValueError: - QMessageBox.critical(self, 'Negative Value', \ - 'You have specified a negative amount for recipient %d.
    Only ' - 'positive values are allowed!.' % (i+1), QMessageBox.Ok) - return False - except TooMuchPrecisionError: - QMessageBox.critical(self, 'Too much precision', \ - 'Bitcoins can only be specified down to 8 decimal places. ' - 'The smallest value that can be sent is 0.0000 0001 BTC. ' - 'Please enter a new amount for recipient %d.' % (i+1), QMessageBox.Ok) - return False - except ValueError: - QMessageBox.critical(self, 'Missing recipient amount', \ - 'You did not specify an amount to send!', QMessageBox.Ok) - return False - except: - QMessageBox.critical(self, 'Invalid Value String', \ - 'The amount you specified ' - 'to send to address %d is invalid (%s).' % (i+1,valueStr), QMessageBox.Ok) - LOGERROR('Invalid amount specified: "%s"', valueStr) - return False + for name in self.chkList.keys(): + self.connect(self.chkList[name], SIGNAL('toggled(bool)'), \ + self.rewriteList) - totalSend += value - recip160 = addrStr_to_hash160(recipStr) - recipValuePairs.append( (recip160, value) ) - self.comments.append(str(self.widgetTable[i][COLS.Comm].text())) - try: - feeStr = str(self.edtFeeAmt.text()) - fee = str2coin(feeStr, negAllowed=False) - except NegativeValueError: - QMessageBox.critical(self, 'Negative Value', \ - 'You must enter a positive value for the fee.', QMessageBox.Ok) - return False - except TooMuchPrecisionError: - QMessageBox.critical(self, 'Too much precision', \ - 'Bitcoins can only be specified down to 8 decimal places. ' - 'The smallest meaning Bitcoin amount is 0.0000 0001 BTC. ' - 'Please enter a fee of at least 0.0000 0001', QMessageBox.Ok) - return False - except: - QMessageBox.critical(self, 'Invalid Fee String', \ - 'The fee you specified is invalid. A standard fee is 0.0001 BTC, ' - 'though some transactions may succeed with zero fee.', QMessageBox.Ok) - LOGERROR('Invalid fee specified: "%s"', feeStr) - return False - - - bal = self.getUsableBalance() - if totalSend+fee > bal: - valTry = coin2str(totalSend+fee, maxZeros=2).strip() - valMax = coin2str(bal, maxZeros=2).strip() - if self.altBalance==None: - QMessageBox.critical(self, 'Insufficient Funds', \ - 'You just tried to send %s BTC, including fee, but you only ' - 'have %s BTC (spendable) in this wallet!' % (valTry, valMax), QMessageBox.Ok) - else: - QMessageBox.critical(self, 'Insufficient Funds', \ - 'You just tried to send %s BTC, including fee, but you only ' - 'have %s BTC with this coin control selection!' % (valTry, valMax), QMessageBox.Ok) - return False - + self.chkImportedOnly = QCheckBox('Imported Addresses Only') + self.chkWithAddrPool = QCheckBox('Include Unused (Address Pool)') + self.chkDispRootKey = QCheckBox('Include Paper Backup Root') + self.chkOmitSpaces = QCheckBox('Omit spaces in key data') + self.chkDispRootKey.setChecked(True) + self.connect(self.chkImportedOnly, SIGNAL('toggled(bool)'), self.rewriteList) + self.connect(self.chkWithAddrPool, SIGNAL('toggled(bool)'), self.rewriteList) + self.connect(self.chkDispRootKey, SIGNAL('toggled(bool)'), self.rewriteList) + self.connect(self.chkOmitSpaces, SIGNAL('toggled(bool)'), self.rewriteList) + # self.chkCSV = QCheckBox('Display in CSV format') - # Get unspent outs for this wallet: - utxoList = self.getUsableTxOutList() - utxoSelect = PySelectCoins(utxoList, totalSend, fee) - - - - # TODO: I should use a while loop/iteration to make sure that the fee - # change does not actually induce another, higher fee (which - # is extraordinarily unlikely... I even set up the SelectCoins - # algorithm to try to leave some room in the tx so that the fee - # will not change the I/Os). Despite this, I will concede - # the extremely rare situation where this would happen, I think - # it will be okay to send a slightly sub-standard fee. - minFeeRec = calcMinSuggestedFees(utxoSelect, totalSend, fee) - if fee bal: - # Need to adjust this based on overrideMin flag - self.edtFeeAmt.setText(coin2str(minFeeRec[1], maxZeros=1).strip()) - QMessageBox.warning(self, 'Insufficient Balance', \ - 'You have specified a valid amount to send, but the required ' - 'transaction fee causes this transaction to exceed your balance. ' - 'In order to send this transaction, you will be required to ' - 'pay a fee of ' + coin2str(minFeeRec[1], maxZeros=0).strip() + ' BTC. ' - '

    ' - 'Please go back and adjust the value of your transaction, not ' - 'to exceed a total of ' + coin2str(bal-minFeeRec[1], maxZeros=0).strip() + - ' BTC (the necessary fee has been entered into the form, so you ' - 'can use the "MAX" button to enter the remaining balance for a ' - 'recipient).', QMessageBox.Ok) - return - + if not self.havePriv: + self.chkDispRootKey.setChecked(False) + self.chkDispRootKey.setEnabled(False) - extraMsg = '' - feeStr = coin2str(fee, maxZeros=0).strip() - minRecStr = coin2str(minFeeRec[1], maxZeros=0).strip() - msgBtns = QMessageBox.Yes | QMessageBox.Cancel + std = (self.main.usermode == USERMODE.Standard) + adv = (self.main.usermode == USERMODE.Advanced) + dev = (self.main.usermode == USERMODE.Expert) + if std: + self.chkList['PubKeyHash'].setVisible(False) + self.chkList['PrivCrypt' ].setVisible(False) + self.chkList['ChainIndex'].setVisible(False) + elif adv: + self.chkList['PubKeyHash'].setVisible(False) + self.chkList['ChainIndex'].setVisible(False) - reply = QMessageBox.warning(self, 'Insufficient Fee', \ - 'The fee you have specified (%s BTC) is insufficient for the size ' - 'and priority of your transaction. You must include at least ' - '%s BTC to send this transaction. \n\nDo you agree to the fee of %s BTC? ' % \ - (feeStr, minRecStr, minRecStr), msgBtns) - if reply == QMessageBox.Cancel: - return False - if reply == QMessageBox.No: - pass - elif reply==QMessageBox.Yes: - fee = long(minFeeRec[1]) - utxoSelect = PySelectCoins(utxoSelect, totalSend, fee) - - if len(utxoSelect)==0: - QMessageBox.critical(self, 'Coin Selection Error', \ - 'SelectCoins returned a list of size zero. This is problematic ' - 'and probably not your fault.', QMessageBox.Ok) - return - + # We actually just want to remove these entirely + # (either we need to display all data needed for decryption, + # besides passphrase, or we shouldn't show any of it) + self.chkList['PrivCrypt' ].setVisible(False) - ### IF we got here, everything is good to go... - # Just need to get a change address and then construct the tx - totalTxSelect = sum([u.getValue() for u in utxoSelect]) - totalChange = totalTxSelect - (totalSend + fee) - - self.origRVPairs = list(recipValuePairs) - self.change160 = '' - self.selectedBehavior = '' - if totalChange>0: - self.change160 = self.determineChangeAddr(utxoSelect) - LOGINFO('Change address behavior: %s', self.selectedBehavior) - if not self.change160: - return - recipValuePairs.append( [self.change160, totalChange]) - else: - if self.main.usermode==USERMODE.Expert and self.chkDefaultChangeAddr.isChecked(): - self.selectedBehavior = 'NoChange' - - # Anonymize the outputs - random.shuffle(recipValuePairs) - txdp = PyTxDistProposal().createFromTxOutSelection( utxoSelect, \ - recipValuePairs) + chkBoxList = [self.chkList[n] for n in namelist] + chkBoxList.append('Line') + chkBoxList.append(self.chkImportedOnly) + chkBoxList.append(self.chkWithAddrPool) + chkBoxList.append(self.chkDispRootKey) - self.txValues = [totalSend, fee, totalChange] - return txdp + frmChks = makeLayoutFrame(VERTICAL, chkBoxList, STYLE_SUNKEN) - - ############################################################################# - def getUsableBalance(self): - if self.altBalance==None: - return self.wlt.getBalance('Spendable') - else: - return self.altBalance + btnGoBack = QPushButton('<<< Go Back') + btnSaveFile = QPushButton('Save to File...') + btnCopyClip = QPushButton('Copy to Clipboard') + self.lblCopied = QRichLabel('') - - ############################################################################# - def getUsableTxOutList(self): - if self.altBalance==None: - return list(self.wlt.getTxOutList('Spendable')) - else: - utxoList = [] - for a160 in self.sourceAddrList: - # Trying to avoid a swig bug involving iteration over vector<> types - utxos = self.wlt.getAddrTxOutList(a160) - for i in range(len(utxos)): - utxoList.append(PyUnspentTxOut().createFromCppUtxo(utxos[i])) - return utxoList + self.connect(btnGoBack, SIGNAL(CLICKED), self.accept) + self.connect(btnSaveFile, SIGNAL(CLICKED), self.saveToFile) + self.connect(btnCopyClip, SIGNAL(CLICKED), self.copyToClipboard) + frmGoBack = makeLayoutFrame(HORIZONTAL, [btnGoBack, \ + STRETCH, \ + self.chkOmitSpaces, \ + STRETCH, \ + self.lblCopied, \ + btnCopyClip, \ + btnSaveFile]) + frmDescr = makeLayoutFrame(HORIZONTAL, [self.lblDescr], STYLE_SUNKEN) - ############################################################################# - def determineChangeAddr(self, utxoList): - changeAddr160 = '' - self.selectedBehavior = 'NewAddr' - addrStr = '' - if not self.main.usermode==USERMODE.Expert: - changeAddr160 = self.wlt.getNextUnusedAddress().getAddr160() - self.wlt.setComment(changeAddr160, CHANGE_ADDR_DESCR_STRING) - else: - if not self.chkDefaultChangeAddr.isChecked(): - changeAddr160 = self.wlt.getNextUnusedAddress().getAddr160() - self.wlt.setComment(changeAddr160, CHANGE_ADDR_DESCR_STRING) - # If generate new address, remove previously-remembered behavior - self.main.setWltSetting(self.wltID, 'ChangeBehavior', self.selectedBehavior) - else: - if self.radioFeedback.isChecked(): - changeAddr160 = CheckHash160(utxoList[0].getRecipientScrAddr()) - self.selectedBehavior = 'Feedback' - elif self.radioSpecify.isChecked(): - addrStr = str(self.edtChangeAddr.text()).strip() - if not checkAddrStrValid(addrStr): - QMessageBox.warning(self, 'Invalid Address', \ - 'You specified an invalid change address ' - 'for this transcation.', QMessageBox.Ok) - return '',False - changeAddr160 = addrStr_to_hash160(addrStr) - self.selectedBehavior = 'Specify' - - if self.main.usermode==USERMODE.Expert and self.chkRememberChng.isChecked(): - self.main.setWltSetting(self.wltID, 'ChangeBehavior', self.selectedBehavior) - if self.selectedBehavior=='Specify' and len(addrStr)>0: - self.main.setWltSetting(self.wltID, 'ChangeAddr', addrStr) - else: - self.main.setWltSetting(self.wltID, 'ChangeBehavior', 'NewAddr') - - return changeAddr160 - - - - ############################################################################# - def addDonation(self, amt=ONE_BTC): - COLS = self.COLS - lastIsEmpty = True - for col in (COLS.Addr, COLS.Btc, COLS.Comm): - if len(str(self.widgetTable[-1][col].text()))>0: - lastIsEmpty = False - - if not lastIsEmpty: - self.makeRecipFrame( len(self.widgetTable)+1 ) + if not self.havePriv or (self.wlt.useEncryption and self.wlt.isLocked): + self.chkList['PrivHexBE'].setEnabled(False) + self.chkList['PrivHexBE'].setChecked(False) + self.chkList['PrivB58' ].setEnabled(False) + self.chkList['PrivB58' ].setChecked(False) - self.widgetTable[-1][self.COLS.Addr].setText(ARMORY_DONATION_ADDR) - self.widgetTable[-1][self.COLS.Btc].setText(coin2str(amt, maxZeros=2).strip()) - self.widgetTable[-1][self.COLS.Comm].setText(\ - 'Donation to Armory developers. Thank you for your generosity!') + dlgLayout = QGridLayout() + dlgLayout.addWidget(frmDescr, 0, 0, 1, 1) + dlgLayout.addWidget(frmChks, 0, 1, 1, 1) + dlgLayout.addWidget(self.txtBox, 1, 0, 1, 2) + dlgLayout.addWidget(frmGoBack, 2, 0, 1, 2) + dlgLayout.setRowStretch(0, 0) + dlgLayout.setRowStretch(1, 1) + dlgLayout.setRowStretch(2, 0) - ############################################################################# - def clickEnterURI(self): - dlg = DlgUriCopyAndPaste(self,self.main) - dlg.exec_() + self.setLayout(dlgLayout) + self.rewriteList() + self.setWindowTitle('All Wallet Keys') - if len(dlg.uriDict)>0: - COLS = self.COLS - lastIsEmpty = True - for col in (COLS.Addr, COLS.Btc, COLS.Comm): - if len(str(self.widgetTable[-1][col].text()))>0: - lastIsEmpty = False - - if not lastIsEmpty: - self.makeRecipFrame( len(self.widgetTable)+1 ) - - self.widgetTable[-1][self.COLS.Addr].setText(dlg.uriDict['address']) - if dlg.uriDict.has_key('amount'): - amtStr = coin2str(dlg.uriDict['amount'], maxZeros=1).strip() - self.widgetTable[-1][self.COLS.Btc].setText( amtStr) + def rewriteList(self, *args): + """ + Write out all the wallet data + """ + whitespace = '' if self.chkOmitSpaces.isChecked() else ' ' - - haveLbl = dlg.uriDict.has_key('label') - haveMsg = dlg.uriDict.has_key('message') - - dispComment = '' - if haveLbl and haveMsg: - dispComment = dlg.uriDict['label'] + ': ' + dlg.uriDict['message'] - elif not haveLbl and haveMsg: - dispComment = dlg.uriDict['message'] - elif haveLbl and not haveMsg: - dispComment = dlg.uriDict['label'] - - self.widgetTable[-1][self.COLS.Comm].setText(dispComment) - + def fmtBin(s, nB=4, sw=False): + h = binary_to_hex(s) + if sw: + h = hex_switchEndian(h) + return whitespace.join([h[i:i + nB] for i in range(0, len(h), nB)]) - ############################################################################# - def addOneRecipient(self, addr160, amt, msg, label=''): - if len(label)>0: - self.wlt.setComment(addr160, label) - - COLS = self.COLS - lastIsEmpty = True - for col in (COLS.Addr, COLS.Btc, COLS.Comm): - if len(str(self.widgetTable[-1][col].text()))>0: - lastIsEmpty = False - - if not lastIsEmpty: - self.makeRecipFrame( len(self.widgetTable)+1 ) + L = [] + L.append('Created: ' + unixTimeToFormatStr(RightNow(), self.main.getPreferredDateFormat())) + L.append('Wallet ID: ' + self.wlt.uniqueIDB58) + L.append('Wallet Name: ' + self.wlt.labelName) + L.append('') - if amt: - amt = coin2str(amt, maxZeros=2).strip() + if self.chkDispRootKey.isChecked(): + binPriv0 = self.rootKeyCopy.binPrivKey32_Plain.toBinStr()[:16] + binPriv1 = self.rootKeyCopy.binPrivKey32_Plain.toBinStr()[16:] + binChain0 = self.rootKeyCopy.chaincode.toBinStr()[:16] + binChain1 = self.rootKeyCopy.chaincode.toBinStr()[16:] + binPriv0Chk = computeChecksum(binPriv0, nBytes=2) + binPriv1Chk = computeChecksum(binPriv1, nBytes=2) + binChain0Chk = computeChecksum(binChain0, nBytes=2) + binChain1Chk = computeChecksum(binChain1, nBytes=2) - self.widgetTable[-1][self.COLS.Addr].setText(hash160_to_addrStr(addr160)) - self.widgetTable[-1][self.COLS.Addr].setCursorPosition(0) - self.widgetTable[-1][self.COLS.Btc].setText(amt) - self.widgetTable[-1][self.COLS.Btc].setCursorPosition(0) - self.widgetTable[-1][self.COLS.Comm].setText(msg) - self.widgetTable[-1][self.COLS.Comm].setCursorPosition(0) + binPriv0 = binary_to_easyType16(binPriv0 + binPriv0Chk) + binPriv1 = binary_to_easyType16(binPriv1 + binPriv1Chk) + binChain0 = binary_to_easyType16(binChain0 + binChain0Chk) + binChain1 = binary_to_easyType16(binChain1 + binChain1Chk) + L.append('-' * 80) + L.append('The following is the same information contained on your paper backup.') + L.append('All NON-imported addresses in your wallet are backed up by this data.') + L.append('') + L.append('Root Key: ' + ' '.join([binPriv0[i:i + 4] for i in range(0, 36, 4)])) + L.append(' ' + ' '.join([binPriv1[i:i + 4] for i in range(0, 36, 4)])) + if self.needChaincode: + L.append('Chain Code: ' + ' '.join([binChain0[i:i + 4] for i in range(0, 36, 4)])) + L.append(' ' + ' '.join([binChain1[i:i + 4] for i in range(0, 36, 4)])) + L.append('-' * 80) + L.append('') + # Cleanup all that sensitive data laying around in RAM + binPriv0, binPriv1 = None, None + binChain0, binChain1 = None, None + binPriv0Chk, binPriv1Chk = None, None + binChain0Chk, binChain1Chk = None, None + self.havePriv = False + topChain = self.wlt.highestUsedChainIndex + extraLbl = '' + # c = ',' if self.chkCSV.isChecked() else '' + for addr in self.addrCopies: - ##################################################################### - def setMaximum(self, targWidget): - nRecip = len(self.widgetTable) - totalOther = 0 - r=0 - try: - bal = self.getUsableBalance() - txFee = str2coin(str(self.edtFeeAmt.text())) - while r topChain: + extraLbl = ' (Unused/Address Pool)' + else: + if addr.chainIndex > topChain: continue - amtStr = str(self.widgetTable[r][self.COLS.Btc].text()).strip() - if len(amtStr)>0: - totalOther += str2coin(amtStr) - r+=1 - - except: - QMessageBox.warning(self, 'Invalid Input', \ - 'Cannot compute the maximum amount ' - 'because there is an error in the amount ' - 'for recipient %d.' % (r+1,), QMessageBox.Ok) - return - - - maxStr = coin2str( (bal - (txFee + totalOther)), maxZeros=0 ) - if bal < txFee+totalOther: - QMessageBox.warning(self, 'Insufficient funds', \ - 'You have specified more than your spendable balance to ' - 'the other recipients and the transaction fee. Therefore, the ' - 'maximum amount for this recipient would actually be negative.', \ - QMessageBox.Ok) - return - - targWidget.setText(maxStr.strip()) - - - ##################################################################### - def createSetMaxButton(self, targWidget): - newBtn = QPushButton('MAX') - newBtn.setMaximumWidth( relaxedSizeStr(self, 'MAX')[0]) - newBtn.setToolTip( 'Fills in the maximum spendable amount minus ' - 'the amounts specified for other recipients ' - 'and the transaction fee ') - funcSetMax = lambda: self.setMaximum( targWidget ) - self.connect( newBtn, SIGNAL('clicked()'), funcSetMax ) - return newBtn - - - ##################################################################### - def makeRecipFrame(self, nRecip): - prevNRecip = len(self.widgetTable) - nRecip = max(nRecip, 1) - inputs = [] - for i in range(nRecip): - if i1: - self.lblCoinCtrl.setText('Source: %d addresses' % nAddr) - - self.setWalletSummary() - - - ############################################################################# - def setWalletSummary(self): - useAllAddr = (self.altBalance==None) - fullBal = self.wlt.getBalance('Spendable') - if useAllAddr: - self.lblSummaryID.setText(self.wlt.uniqueIDB58) - self.lblSummaryName.setText(self.wlt.labelName) - self.lblSummaryDescr.setText(self.wlt.labelDescr) - if fullBal==0: - self.lblSummaryBal.setText('0.0', color='TextRed', bold=True) + # Imported Addresses + if self.chkImportedOnly.isChecked(): + if not addr.chainIndex == -2: + continue else: - self.lblSummaryBal.setValueText(fullBal, wBold=True) - else: - self.lblSummaryID.setText(self.wlt.uniqueIDB58 + '*') - self.lblSummaryName.setText(self.wlt.labelName + '*') - self.lblSummaryDescr.setText('*Coin Control Subset*', color='TextBlue', bold=True) - self.lblSummaryBal.setText(coin2str(self.altBalance, maxZeros=0), color='TextBlue') - rawValTxt = str(self.lblSummaryBal.text()) - self.lblSummaryBal.setText(rawValTxt + ' (of %s)' % \ - (htmlColor('DisableFG'), coin2str(fullBal, maxZeros=0))) - - if not TheBDM.getBDMState()=='BlockchainReady': - self.lblSummaryBal.setText('(available when online)', color='DisableFG') - + if addr.chainIndex == -2: + extraLbl = ' (Imported)' -################################################################################ -class DlgOfflineTxCreated(ArmoryDialog): - def __init__(self, wlt, txdp, parent=None, main=None): - super(DlgOfflineTxCreated, self).__init__(parent, main) + if self.chkList['AddrStr' ].isChecked(): + L.append(addr.getAddrStr() + extraLbl) + if self.chkList['PubKeyHash'].isChecked(): + L.append(' Hash160 : ' + fmtBin(addr.getAddr160())) + if self.chkList['PrivB58' ].isChecked(): + pB58 = encodePrivKeyBase58(addr.binPrivKey32_Plain.toBinStr()) + pB58Stretch = whitespace.join([pB58[i:i + 6] for i in range(0, len(pB58), 6)]) + L.append(' PrivBase58: ' + pB58Stretch) + self.havePriv = True + if self.chkList['PrivCrypt' ].isChecked(): + L.append(' PrivCrypt : ' + fmtBin(addr.binPrivKey32_Encr.toBinStr())) + if self.chkList['PrivHexBE' ].isChecked(): + L.append(' PrivHexBE : ' + fmtBin(addr.binPrivKey32_Plain.toBinStr())) + self.havePriv = True + if self.chkList['PubKey' ].isChecked(): + L.append(' PublicX : ' + fmtBin(addr.binPublicKey65.toBinStr()[1:33 ])) + L.append(' PublicY : ' + fmtBin(addr.binPublicKey65.toBinStr()[ 33:])) + if self.chkList['ChainIndex'].isChecked(): + L.append(' ChainIndex: ' + str(addr.chainIndex)) - self.txdp = txdp - self.wlt = wlt - - canSign = False - lblDescr = QRichLabel('') - if determineWalletType(wlt, self.main)[0]==WLTTYPES.Offline: - lblDescr.setText( - 'The block of data shown below is the complete transaction you just ' - 'requested, but is invalid because it does not contain the appropriate ' - 'signatures. You must ' - 'take this data to the computer holding the private keys for this ' - 'wallet to get the necessary signatures, then bring back the completed ' - 'transaction to be broadcast to the Bitcoin network.' - '

    ' - 'Use the "Save as file..." button ' - 'to copy the *.unsigned.tx file to a USB key or other removable media. ' - 'Take the file to the offline computer, and use the ' - '"Offline Transactions" dialog to load the transaction data and sign it ' - '(the filename suffix will be changed to *.signed.tx).' - '

    ' - 'On the next screen, you will be able to load the signed transaction, ' - 'and then broadcast it if all signatures are valid. In fact, the final, ' - 'signed transaction can be finalized from any ' - 'computer that is running Armory and connected to the Bitcoin network.') - elif determineWalletType(wlt, self.main)[0]==WLTTYPES.WatchOnly: - lblDescr.setText( \ - 'The chunk of data shown below is the complete transaction you just ' - 'requested, but without the signatures needed to be valid. ' - '

    ' - 'In order to complete this transaction, you need to send this ' - 'chunk of data (the proposed transaction) to the party who holds the ' - 'full version of this wallet. They can load this data into Armory ' - 'and broadcast the transaction if they chose to sign it. ') + self.txtBox.setText('\n'.join(L)) + if self.havePriv: + self.lblDescr.setText(self.strDescrReg + self.strDescrWarn) else: - canSign = True - lblDescr.setText( - 'You have chosen to create the previous transaction but not sign ' - 'it or broadcast it, yet. Below, you can save the unsigned ' - 'transaction to file, or copy&paste from the text box. ' - 'In some cases, you may actually want the transaction signed ' - 'but not broadcast yet. On the "Next Step" you can choose to sign ' - 'the transaction without broadcasting it.') - - - ttipBip0010 = self.main.createToolTipWidget( \ - 'The serialization used in this block of data is based on BIP 0010, ' - 'which is a standard proposed by the core Armory Developer (Alan Reiner) ' - 'for simple execution of offline transactions and multi-signature ' - 'transactions. Any other client software that implements BIP 0010 ' - 'will be able to recognize ' - 'this block of data, and take appropriate action without having Armory ' - 'software available. Technical details of BIP 0010 can be found at: ' - 'https://en.bitcoin.it/wiki/BIP_0010') - - - ttipDataIsSafe = self.main.createToolTipWidget( \ - 'There is no security-sensitive information in this data below, so ' - 'it is perfectly safe to copy-and-paste it into an ' - 'email message, or save it to a borrowed USB key.') - - btnSave = QPushButton('Save as file...') - self.connect(btnSave, SIGNAL('clicked()'), self.doSaveFile) - ttipSave = self.main.createToolTipWidget( \ - 'Save this data to a USB key or other device, to be transferred to ' - 'a computer that contains the private keys for this wallet.') - - btnCopy = QPushButton('Copy to clipboard') - self.connect(btnCopy, SIGNAL('clicked()'), self.copyAsciiTxDP) - self.lblCopied = QRichLabel(' ') - self.lblCopied.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) + self.lblDescr.setText(self.strDescrReg) - ttipCopy = self.main.createToolTipWidget( \ - 'Copy the transaction data to the clipboard, so that it can be ' - 'pasted into an email or a text document.') - - lblInstruct = QRichLabel('Instructions for completing this transaction:') - lblUTX = QRichLabel('Transaction Data \t (Unsigned ID: %s)' % txdp.uniqueB58) - w,h = tightSizeStr(GETFONT('Fixed',8),'0'*90)[0], int(12*8.2) - - frmUTX = makeLayoutFrame('Horiz', [ttipDataIsSafe, lblUTX]) - frmUpper = makeLayoutFrame('Horiz', [lblDescr], STYLE_SUNKEN) - - # Wow, I just cannot get the txtEdits to be the right size without - # forcing them very explicitly - self.txtTxDP = QTextEdit() - self.txtTxDP.setFont( GETFONT('Fixed',8) ) - self.txtTxDP.setMinimumWidth(w) - self.txtTxDP.setMinimumHeight(h) - #self.txtTxDP.setMaximumWidth(w) - self.txtTxDP.setMaximumHeight(h) - self.txtTxDP.setText(txdp.serializeAscii()) - self.txtTxDP.setReadOnly(True) - self.txtTxDP.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) - - - lblNextStep = QRichLabel( \ - 'Once you have signed the transaction, you can go onto the next ' - 'step. Alternatively, you can close this window, and complete the ' - 'last step at a later time, using the "Offline Transactions" button ' - 'on the main window') - btnNextStep = QPushButton('Next Step >>>') - maxBtnWidth = 1.5*relaxedSizeStr(btnNextStep, 'Next Step >>>')[0] - btnNextStep.setMaximumWidth(maxBtnWidth) - self.connect(btnNextStep, SIGNAL('clicked()'), self.doNextStep) - - nextStepStrip = makeLayoutFrame('Horiz', [lblNextStep, btnNextStep], \ - STYLE_SUNKEN) - - btnLater = QPushButton("Close Window") - self.connect(btnLater, SIGNAL('clicked()'), self.reject) - bottomStrip = makeLayoutFrame('Horiz', [btnLater, 'Stretch']) - - - frmLower = QFrame() - frmLower.setFrameStyle(STYLE_RAISED) - frmLowerLayout = QGridLayout() - - frmLowerLayout.addWidget(frmUTX, 0,0, 1,3) - frmLowerLayout.addWidget(self.txtTxDP, 1,0, 3,1) - frmLowerLayout.addWidget(btnSave, 1,1, 1,1) - frmLowerLayout.addWidget(ttipSave, 1,2, 1,1) - frmLowerLayout.addWidget(btnCopy, 2,1, 1,1) - frmLowerLayout.addWidget(ttipCopy, 2,2, 1,1) - frmLowerLayout.addWidget(self.lblCopied,3,1, 1,2) - frmLowerLayout.setColumnStretch(0, 1) - frmLowerLayout.setColumnStretch(1, 0) - frmLowerLayout.setColumnStretch(2, 0) - frmLowerLayout.setColumnStretch(3, 0) - frmLowerLayout.setRowStretch(0, 0) - frmLowerLayout.setRowStretch(1, 0) - frmLowerLayout.setRowStretch(2, 0) - frmLowerLayout.setRowStretch(3, 0) - - frmLowerLayout.addWidget(nextStepStrip, 4,0, 1,3) - frmLower.setLayout(frmLowerLayout) - - - frmAll = makeLayoutFrame('Vert', [lblInstruct, \ - frmUpper, \ - 'Space(5)', \ - frmLower, \ - nextStepStrip,\ - bottomStrip]) - frmAll.layout().setStretch(0, 0) - frmAll.layout().setStretch(1, 0) - frmAll.layout().setStretch(2, 0) - frmAll.layout().setStretch(3, 2) - frmAll.layout().setStretch(4, 1) - frmAll.layout().setStretch(5, 0) + def saveToFile(self): + if self.havePriv: + if not self.main.getSettingOrSetDefault('DNAA_WarnPrintKeys', False): + result = MsgBoxWithDNAA(MSGBOX.Warning, title='Plaintext Private Keys', \ + msg='REMEMBER: The data you ' + 'are about to save contains private keys. Please make sure ' + 'that only trusted persons will have access to this file.' + '

    Are you sure you want to continue?', \ + dnaaMsg=None, wCancel=True) + if not result[0]: + return + self.main.writeSetting('DNAA_WarnPrintKeys', result[1]) - dlgLayout = QGridLayout() - dlgLayout.addWidget(frmAll) + wltID = self.wlt.uniqueIDB58 + fn = self.main.getFileSave(title='Save Key List', \ + ffilter=['Text Files (*.txt)'], \ + defaultFilename='keylist_%s_.txt' % wltID) + if len(fn) > 0: + fileobj = open(fn, 'w') + fileobj.write(str(self.txtBox.toPlainText())) + fileobj.close() - self.setLayout(dlgLayout) - self.setWindowTitle("Offline Transaction") - self.setWindowIcon(QIcon( self.main.iconfile)) - def copyAsciiTxDP(self): - clipb = QApplication.clipboard() - clipb.clear() - clipb.setText(self.txtTxDP.toPlainText()) - self.lblCopied.setText('Copied!') - def copyAsciiTxDPS(self): + def copyToClipboard(self): clipb = QApplication.clipboard() clipb.clear() - clipb.setText(self.txtSigned.toPlainText()) - self.lblCopiedS.setText('Copied!') - - def doSaveFile(self): - """ Save the Unsigned-Tx block of data """ - dpid = self.txdp.uniqueB58 - suffix = ('' if OS_WINDOWS else '.unsigned.tx') - toSave = self.main.getFileSave( \ - 'Save Unsigned Transaction', \ - ['Armory Transactions (*.unsigned.tx)'], \ - 'armory_%s_%s' % (dpid, suffix)) - # In windows, we get all these superfluous file suffixes - toSave = toSave.replace('unsigned.tx.unsigned.tx', 'unsigned.tx') - toSave = toSave.replace('unsigned.tx.unsigned.tx', 'unsigned.tx') - LOGINFO('Saving unsigned tx file: %s', toSave) - try: - theFile = open(toSave, 'w') - theFile.write(self.txtTxDP.toPlainText()) - theFile.close() - except IOError: - LOGEXCEPT('Failed to save file: %s', toSave) - pass - - def doSaveFileS(self): - """ Save the Signed-Tx block of data """ - dpid = self.txdp.uniqueB58 - suffix = ('' if OS_WINDOWS else '.signed.tx') - toSave = self.main.getFileSave( 'Save Signed Transaction', \ - ['Armory Transactions (*.signed.tx)'], \ - 'armory_%s_' % (dpid,suffix)) - # In windows, we get all these superfluous file suffixes - toSave = toSave.replace('signed.tx.signed.tx', 'signed.tx') - toSave = toSave.replace('signed.tx.signed.tx', 'signed.tx') - LOGINFO('Saving signed tx file: %s', toSave) - try: - theFile = open(toSave, 'w') - theFile.write(self.txtSigned.toPlainText()) - theFile.close() - except IOError: - LOGEXCEPT('Failed to save file: %s', toSave) - pass + clipb.setText(str(self.txtBox.toPlainText())) + self.lblCopied.setText('Copied!') - def txtSignedFirstClick(self): - if not self.txtSignedCleared: - self.txtSignedCleared = True - self.txtSigned.setText('') - self.txtSigned.setTextColor(Colors.Black) - - txt = str(self.txtSigned.toPlainText()) - a,b = '', '' - lblText = '' - if not txt.startswith('-----BEGIN-TRANSACTION'): - self.lblRight.setText('') - self.btnReady.setEnabled(False) - return - try: - txdpSigned = PyTxDistProposal().unserializeAscii(str(self.txtSigned.toPlainText())) - enoughSigs = txdpSigned.checkTxHasEnoughSignatures() - sigsValid = txdpSigned.checkTxHasEnoughSignatures(alsoVerify=True) - if not enoughSigs: - self.lblRight.setText(a + 'Not Signed!' + b) - self.btnReady.setEnabled(False) - return - if not sigsValid: - self.lblRight.setText(a + 'Invalid Signature!' + b) - self.btnReady.setEnabled(False) - return - except: - # One of the rare few times I ever catch-all exception - self.lblRight.setText(a + 'Unrecognized Input!' + b) - self.btnReady.setEnabled(False) - LOGWARN('Unrecognized TxDP input!') - return + def cleanup(self): + self.rootKeyCopy.binPrivKey32_Plain.destroy() + for addr in self.addrCopies: + addr.binPrivKey32_Plain.destroy() + self.rootKeyCopy = None + self.addrCopies = None - self.lblRight.setText('Signature Valid!') - self.btnReady.setEnabled(True) - - def execLoadSig(self): - self.txtSignedFirstClick() - fn = self.main.getFileLoad( title = 'Load Signed Transaction', \ - ffilter=['Signed Transactions (*.signed.tx)']) - fileobj = open(fn, 'r') - txdpStr = fileobj.read() - fileobj.close() - self.txtSigned.setText(txdpStr) - self.txtSignedFirstClick() - - def execBroadcast(self): - txdpSigned = PyTxDistProposal().unserializeAscii(str(self.txtSigned.toPlainText())) - finalTx = txdpSigned.getBroadcastTxIfReady() - if not txdpSigned.uniqueB58==self.txdp.uniqueB58: - QMessageBox.critical(self, 'Wrong Transaction!', \ - 'The transaction you loaded is valid, but is a different transaction ' - 'than the one you started with. Please go back check that you copied ' - 'or loaded the correct transaction.', QMessageBox.Ok) - return + def accept(self): + self.cleanup() + super(DlgShowKeyList, self).accept() - if finalTx==None: - QMessageBox.critical(self, 'Error', \ - 'There was an error finalizing the transaction. Double-check ' - 'that the correct data was loaded into the text box', QMessageBox.Ok) - return - + def reject(self): + self.cleanup() + super(DlgShowKeyList, self).reject() - self.main.broadcastTransaction(finalTx) - self.accept() - try: - self.parent.accept() - except: - pass - def signTxdp(self): - try: - strUnsign = str(self.txtTxDP.toPlainText()) - txdpUnsign = PyTxDistProposal().unserializeAscii(strUnsign) - except: - LOGEXCEPT('Could not unserialize TxDP') - raise +################################################################################ +class DlgTxFeeOptions(ArmoryDialog): + def __init__(self, wlt, parent=None, main=None): + super(DlgTxFeeOptions, self).__init__(parent, main) - if self.wlt.useEncryption and self.wlt.isLocked: - dlg = DlgUnlockWallet(self.wlt, self.parent, self.main, 'Sign Transaction') - if not dlg.exec_(): - QMessageBox.warning(self, 'Unlock Error', \ - 'Cannot sign this transaction while your wallet is locked. ' - 'Please enter the correct passphrase to finish signing.', \ - QMessageBox.Ok) - return + lblDescr = QLabel(\ + 'Transaction fees go to people who contribute processing power to ' + 'the Bitcoin network to process transactions and keep it secure.') + lblDescr2 = QLabel(\ + 'Nearly all transactions are guaranteed to be ' + 'processed if a fee of 0.0005 BTC is included (less than $0.01 USD). You ' + 'will be prompted for confirmation if a higher fee amount is required for ' + 'your transaction.') - - txdpSign = self.wlt.signTxDistProposal(txdpUnsign) - self.txtSigned.setText(txdpSign.serializeAscii()) - self.btnSaveS.setEnabled(True) - self.btnCopyS.setEnabled(True) - - - def doNextStep(self): - DlgReviewOfflineTx(self, self.main).exec_() - self.accept() - +################################################################################ +class DlgAddressProperties(ArmoryDialog): + def __init__(self, wlt, parent=None, main=None): + super(DlgAddressProperties, self).__init__(parent, main) ################################################################################ -class DlgOfflineSelect(ArmoryDialog): - def __init__(self, parent=None, main=None): - super(DlgOfflineSelect, self).__init__(parent, main) +def extractTxInfo(pytx, rcvTime=None): + pytxdp = None + if isinstance(pytx, PyTxDistProposal): + pytxdp = pytx + pytx = pytxdp.pytxObj.copy() + txHash = pytx.getHash() + txOutToList, sumTxOut, txinFromList, sumTxIn, txTime, txBlk, txIdx = [None] * 7 - self.do_review = False - self.do_create = False - self.do_broadc = False - lblDescr = QRichLabel( \ - 'In order to execute an offline transaction, three steps must ' - 'be followed:

    ' - '\t(1) Online Computer: Create the unsigned transaction
    ' - '\t(2) Offline Computer: Get the transaction signed
    ' - '\t(3) Online Computer: Broadcast the signed transaction

    ' - 'You must create the transaction using a watch-only wallet on an online ' - 'system, but watch-only wallets cannot sign it. Only the offline system ' - 'can create a valid signature. The easiest way to execute all three steps ' - 'is to use a USB key to move the data between computers.

    ' - 'All the data saved to the removable medium during all three steps are ' - 'completely safe and do not reveal any private information that would benefit an ' - 'attacker trying to steal your funds. However, this transaction data does ' - 'reveal some addresses in your wallet, and may represent a breach of ' - 'privacy if not protected.') + txOutToList = pytx.makeRecipientsList() + sumTxOut = sum([t[1] for t in txOutToList]) - btnCreate = QPushButton('Create New Offline Transaction') - btnReview = QPushButton('Sign and/or Broadcast Transaction') - if not TheBDM.getBDMState()=='BlockchainReady': - btnCreate.setEnabled(False) - if len(self.main.walletMap)==0: - btnReview = QPushButton('No wallets available!') - btnReview.setEnabled(False) + txcpp = Tx() + if TheBDM.getBDMState() == 'BlockchainReady': + txcpp = TheBDM.getTxByHash(txHash) + if txcpp.isInitialized(): + hgt = txcpp.getBlockHeight() + if hgt < TheBDM.getTopBlockHeight(): + headref = TheBDM.getHeaderByHeight(hgt) + txTime = unixTimeToFormatStr(headref.getTimestamp()) + txBlk = headref.getBlockHeight() + txIdx = txcpp.getBlockTxIndex() else: - btnReview = QPushButton('Sign Offline Transaction') - elif len(self.main.walletMap)==0 and self.main.netMode==NETWORKMODE.Full: - btnReview = QPushButton('Broadcast Signed Transaction') - - btnCancel = QPushButton('<<< Go Back') - - def create(): - self.do_create = True; self.accept() - def review(): - self.do_review = True; self.accept() - def broadc(): - self.do_broadc = True; self.accept() - - self.connect(btnCreate, SIGNAL('clicked()'), create) - self.connect(btnReview, SIGNAL('clicked()'), review) - self.connect(btnCancel, SIGNAL('clicked()'), self.reject) - - lblCreate = QRichLabel( tr(""" - Create a transaction from an Offline/Watching-Only wallet - to be signed by the computer with the full wallet """)) - - lblReview = QRichLabel( tr(""" - Review an unsigned transaction and sign it if you have - the private keys needed for it """)) - - lblBroadc = QRichLabel( tr( """ - Send a pre-signed transaction to the Bitcoin network to finalize it""")) - - lblBroadc.setMinimumWidth( tightSizeNChar(lblBroadc, 45)[0] ) - - - frmOptions = QFrame() - frmOptions.setFrameStyle(STYLE_PLAIN) - frmOptionsLayout = QGridLayout() - frmOptionsLayout.addWidget(btnCreate, 0,0) - frmOptionsLayout.addWidget(lblCreate, 0,2) - frmOptionsLayout.addWidget(HLINE(), 1,0, 1,3) - frmOptionsLayout.addWidget(btnReview, 2,0, 3,1) - frmOptionsLayout.addWidget(lblReview, 2,2) - frmOptionsLayout.addWidget(HLINE(), 3,2, 1,1) - frmOptionsLayout.addWidget(lblBroadc, 4,2) + if rcvTime == None: + txTime = 'Unknown' + elif rcvTime == -1: + txTime = '[[Not broadcast yet]]' + elif isinstance(rcvTime, basestring): + txTime = rcvTime + else: + txTime = unixTimeToFormatStr(rcvTime) + txBlk = UINT32_MAX + txIdx = -1 + txinFromList = [] + if TheBDM.getBDMState() == 'BlockchainReady' and txcpp.isInitialized(): + # Use BDM to get all the info about the TxOut being spent + # Recip, value, block-that-incl-tx, tx-that-incl-txout, txOut-index + haveAllInput = True + for i in range(txcpp.getNumTxIn()): + txinFromList.append([]) + cppTxin = txcpp.getTxInCopy(i) + prevTxHash = cppTxin.getOutPoint().getTxHash() + if TheBDM.getTxByHash(prevTxHash).isInitialized(): + prevTx = TheBDM.getPrevTx(cppTxin) + prevTxOut = prevTx.getTxOutCopy(cppTxin.getOutPoint().getTxOutIndex()) + txinFromList[-1].append(TheBDM.getSenderScrAddr(cppTxin)) + txinFromList[-1].append(TheBDM.getSentValue(cppTxin)) + if prevTx.isInitialized(): + txinFromList[-1].append(prevTx.getBlockHeight()) + txinFromList[-1].append(prevTx.getThisHash()) + txinFromList[-1].append(prevTxOut.getIndex()) + else: + LOGERROR('How did we get a bad parent pointer? (extractTxInfo)') + prevTxOut.pprint() + txinFromList[-1].append('') + txinFromList[-1].append('') + txinFromList[-1].append('') + else: + haveAllInput = False + txin = PyTxIn().unserialize(cppTxin.serialize()) + scraddr = addrStr_to_scrAddr(TxInExtractAddrStrIfAvail(txin)) + txinFromList[-1].append(scraddr) + txinFromList[-1].append('') + txinFromList[-1].append('') + txinFromList[-1].append('') + txinFromList[-1].append('') + elif not pytxdp is None: + haveAllInput = True + for i, txin in enumerate(pytxdp.pytxObj.inputs): + txinFromList.append([]) + txinFromList[-1].append(script_to_scrAddr(pytxdp.txOutScripts[i])) + txinFromList[-1].append(pytxdp.inputValues[i]) + txinFromList[-1].append('') + txinFromList[-1].append('') + txinFromList[-1].append('') + else: # BDM is not initialized + haveAllInput = False + for i, txin in enumerate(pytx.inputs): + scraddr = addrStr_to_scrAddr(TxInExtractAddrStrIfAvail(txin)) + txinFromList.append([]) + txinFromList[-1].append(scraddr) + txinFromList[-1].append('') + txinFromList[-1].append('') + txinFromList[-1].append('') + txinFromList[-1].append('') + if haveAllInput: + sumTxIn = sum([x[1] for x in txinFromList]) + else: + sumTxIn = None + return [txHash, txOutToList, sumTxOut, txinFromList, sumTxIn, txTime, txBlk, txIdx] +################################################################################ +class DlgDispTxInfo(ArmoryDialog): + def __init__(self, pytx, wlt=None, parent=None, main=None, mode=None, \ + precomputeIdxGray=None, precomputeAmt=None, txtime=None): + """ + This got freakin' complicated, because I'm trying to handle + wallet/nowallet, BDM/noBDM and Std/Adv/Dev all at once. - frmOptionsLayout.addItem(QSpacerItem(20,20), 0,1, 3,1) - frmOptions.setLayout(frmOptionsLayout) + We can override the user mode as an input argument, in case a std + user decides they want to see the tx in adv/dev mode + """ + super(DlgDispTxInfo, self).__init__(parent, main) + self.mode = mode - frmDescr = makeLayoutFrame('Horiz', ['Space(10)', lblDescr, 'Space(10)'], \ - STYLE_SUNKEN) - frmCancel = makeLayoutFrame('Horiz', [btnCancel, 'Stretch']) - dlgLayout = QGridLayout() - dlgLayout.addWidget(frmDescr, 0,0, 1,1 ) - dlgLayout.addWidget(frmOptions, 1,0, 1,1 ) - dlgLayout.addWidget(frmCancel, 2,0, 1,1 ) - + FIELDS = enum('Hash', 'OutList', 'SumOut', 'InList', 'SumIn', 'Time', 'Blk', 'Idx') + data = extractTxInfo(pytx, txtime) - self.setLayout(dlgLayout) - self.setWindowTitle('Select Offline Action') - self.setWindowIcon(QIcon(self.main.iconfile)) - - + # If this is actually a TxDP in here... + pytxdp = None + if isinstance(pytx, PyTxDistProposal): + pytxdp = pytx + pytx = pytxdp.pytxObj.copy() -################################################################################ -class DlgReviewOfflineTx(ArmoryDialog): - """ - We will make the assumption that this dialog is used ONLY for outgoing - transactions from your wallet. This simplifies the logic if we don't - have to identify input senders/values, and handle the cases where those - may not be specified - """ - def __init__(self, parent=None, main=None): - super(DlgReviewOfflineTx, self).__init__(parent, main) + self.pytx = pytx.copy() - self.wlt = None - self.sentToSelfWarn = False - self.fileLoaded = None + if self.mode == None: + self.mode = self.main.usermode + txHash = data[FIELDS.Hash] - lblDescr = QRichLabel( \ - 'Copy or load a transaction from file into the text box below. ' - 'If the transaction is unsigned and you have the correct wallet, ' - 'you will have the opportunity to sign it. If it is already signed ' - 'you will have the opportunity to broadcast it to ' - 'the Bitcoin network to make it final.') + haveWallet = (wlt != None) + haveBDM = TheBDM.getBDMState() == 'BlockchainReady' + # Should try to identify what is change and what's not + wltLE = None + IsNonStandard = False + fee = None + txAmt = data[FIELDS.SumOut] + # Collect our own outputs only, and ID non-std tx + svPairSelf = [] + svPairOther = [] + indicesSelf = [] + indicesOther = [] + indicesMakeGray = [] + idx = 0 + for scrType, amt, script, msInfo in data[FIELDS.OutList]: + if scrType in CPP_TXOUT_HAS_ADDRSTR: + addrStr = script_to_addrStr(script) + addr160 = addrStr_to_hash160(addrStr)[1] + scrAddr = script_to_scrAddr(script) + if haveWallet and wlt.hasAddr(addr160): + svPairSelf.append([scrAddr, amt]) + indicesSelf.append(idx) + else: + svPairOther.append([scrAddr, amt]) + indicesOther.append(idx) + else: + # This isn't actually true: P2Pool outputs get flagged as non-std... + IsNonStandard = True + idx += 1 - w,h = tightSizeStr(GETFONT('Fixed',8),'0'*90)[0], int(12*8.2) - self.txtTxDP = QTextEdit() - self.txtTxDP.setFont( GETFONT('Fixed',8) ) - self.txtTxDP.sizeHint = lambda: QSize(w,h) - self.txtTxDP.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + txdir = None + changeIndex = None + svPairDisp = None + if haveBDM and haveWallet and data[FIELDS.SumOut] and data[FIELDS.SumIn]: + fee = data[FIELDS.SumOut] - data[FIELDS.SumIn] + ldgr = wlt.getTxLedger() + for le in ldgr: + if le.getTxHash() == txHash: + wltLE = le + txAmt = le.getValue() - self.btnSign = QPushButton('Sign') - self.btnBroadcast = QPushButton('Broadcast') - self.btnSave = QPushButton('Save file...') - self.btnLoad = QPushButton('Load file...') - self.btnCopy = QPushButton('Copy Text') - self.btnCopyHex = QPushButton('Copy Final Tx (Hex)') - self.lblCopied = QRichLabel('') - self.lblCopied.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) + # If we found the LE for this tx, then we can display much + # more useful information... like ignoring change outputs, + if le.isSentToSelf(): + txdir = 'Sent-to-Self' + svPairDisp = [] + if len(self.pytx.outputs)==1: + txAmt = fee + triplet = data[FIELDS.OutList][0] + scrAddr = script_to_scrAddr(triplet[2]) + svPairDisp.append([scrAddr, triplet[1]]) + else: + txAmt, changeIndex = determineSentToSelfAmt(le, wlt) + for i, triplet in enumerate(data[FIELDS.OutList]): + if not i == changeIndex: + scrAddr = script_to_scrAddr(triplet[2]) + svPairDisp.append([scrAddr, triplet[1]]) + else: + indicesMakeGray.append(i) + else: + if le.getValue() > 0: + txdir = 'Received' + svPairDisp = svPairSelf + indicesMakeGray.extend(indicesOther) + if le.getValue() < 0: + txdir = 'Sent' + svPairDisp = svPairOther + indicesMakeGray.extend(indicesSelf) + break - self.btnSign.setEnabled(False) - self.btnBroadcast.setEnabled(False) - self.connect(self.txtTxDP, SIGNAL('textChanged()'), self.processTxDP) - + # If this is a TxDP, the above calculation probably didn't do its job + # It is possible, but it's also possible that this Tx has nothing to + # do with our wallet, which is not the focus of the above loop/conditions + # So we choose to pass in the amount we already computed based on extra + # information available in the TxDP structure + if precomputeAmt: + txAmt = precomputeAmt - self.connect(self.btnSign, SIGNAL('clicked()'), self.signTx) - self.connect(self.btnBroadcast, SIGNAL('clicked()'), self.broadTx) - self.connect(self.btnSave, SIGNAL('clicked()'), self.saveTx) - self.connect(self.btnLoad, SIGNAL('clicked()'), self.loadTx) - self.connect(self.btnCopy, SIGNAL('clicked()'), self.copyTx) - self.connect(self.btnCopyHex, SIGNAL('clicked()'), self.copyTxHex) - self.lblStatus = QRichLabel('') - self.lblStatus.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) - wStat, hStat = relaxedSizeStr(self.lblStatus, 'Signature is Invalid!') - self.lblStatus.setMinimumWidth( int(wStat*1.2) ) - self.lblStatus.setMinimumHeight( int(hStat*1.2) ) + # This is incorrectly flagging P2Pool outputs as non-std! + # if IsNonStandard: + # # TODO: Need to do something with this non-std tx! + # print '***Non-std transaction!' + # QMessageBox.critical(self, 'Non-Standard Transaction', \ + # 'This is a non-standard transaction, which cannot be ' + # 'interpretted by this program. DO NOT ASSUME that you ' + # 'own these bitcoins, even if you see your address in ' + # 'any part of the transaction. Only an expert can tell ' + # 'you if and how these coins can be redeemed! \n\n' + # 'If you would like more information, please copy the ' + # 'information on the next window into an email and send ' + # 'it to alan.reiner@gmail.com.', QMessageBox.Ok) - btnGoBack = QPushButton('<<< Go Back') - self.connect(btnGoBack, SIGNAL('clicked()'), self.accept) - frmGoBack = makeLayoutFrame('Horiz', [btnGoBack, 'Stretch']) - - frmDescr = makeLayoutFrame('Horiz', [lblDescr], STYLE_RAISED) + layout = QGridLayout() + lblDescr = QLabel('Transaction Information:') - # Finally, let's make small info frame, with a button to link to the - # full info - self.infoLbls = [] + layout.addWidget(lblDescr, 0, 0, 1, 1) - - ### - self.infoLbls.append([]) - self.infoLbls[-1].append( self.main.createToolTipWidget( \ - 'This is wallet from which the offline transaction spends bitcoins')) - self.infoLbls[-1].append( QRichLabel('Wallet:')) - self.infoLbls[-1].append( QRichLabel('')) - - ### - self.infoLbls.append([]) - self.infoLbls[-1].append( self.main.createToolTipWidget('The name of the wallet')) - self.infoLbls[-1].append( QRichLabel('Wallet Label:')) - self.infoLbls[-1].append( QRichLabel('')) - - ### - self.infoLbls.append([]) - self.infoLbls[-1].append( self.main.createToolTipWidget( \ - 'A unique string that identifies an unsigned transaction. ' - 'This is different than the ID that the transaction will have when ' - 'it is finally broadcast, because the broadcast ID cannot be ' - 'calculated without all the signatures')) - self.infoLbls[-1].append( QRichLabel('Pre-Broadcast ID:')) - self.infoLbls[-1].append( QRichLabel('')) - - ### - self.infoLbls.append([]) - self.infoLbls[-1].append( self.main.createToolTipWidget( \ - 'Net effect on this wallet\'s balance')) - self.infoLbls[-1].append( QRichLabel('Transaction Amount:')) - self.infoLbls[-1].append( QRichLabel('')) - - - ### - #self.infoLbls.append([]) - #self.infoLbls[-1].append( self.main.createToolTipWidget( \ - #'Total number of transaction recipients, excluding change transactions.')) - #self.infoLbls[-1].append( QRichLabel('# Recipients:')) - #self.infoLbls[-1].append( QRichLabel('')) - - ### - #self.infoLbls.append([]) - #self.infoLbls[-1].append( self.main.createToolTipWidget( \ - #'Click for more details about this transaction')) - #self.infoLbls[-1].append( QRichLabel('More Info:')) - #self.infoLbls[-1].append( QLabelButton('Click Here') ) - - - self.moreInfo = QLabelButton('Click here for more
    information about
    this transaction') - self.connect(self.moreInfo, SIGNAL('clicked()'), self.execMoreTxInfo) - frmMoreInfo = makeLayoutFrame('Horiz', [self.moreInfo], STYLE_SUNKEN) - frmMoreInfo.setMinimumHeight( tightSizeStr(self.moreInfo, 'Any String')[1]*5) - - expert = (self.main.usermode==USERMODE.Expert) - frmBtn = makeLayoutFrame('Vert', [ self.btnSign, \ - self.btnBroadcast, \ - self.btnSave, \ - self.btnLoad, \ - self.btnCopy, \ - self.btnCopyHex if expert else QRichLabel(''), \ - self.lblCopied, \ - HLINE(), \ - self.lblStatus, \ - HLINE(), \ - 'Stretch', \ - frmMoreInfo]) - - frmBtn.setMaximumWidth(tightSizeNChar(QPushButton(''), 30)[0]) + frm = QFrame() + frm.setFrameStyle(STYLE_RAISED) + frmLayout = QGridLayout() + lbls = [] - frmInfoLayout = QGridLayout() - for r in range(len(self.infoLbls)): - for c in range(len(self.infoLbls[r])): - frmInfoLayout.addWidget( self.infoLbls[r][c], r,c, 1,1 ) - frmInfo = QFrame() - frmInfo.setFrameStyle(STYLE_SUNKEN) - frmInfo.setLayout(frmInfoLayout) - frmBottom = QFrame() - frmBottom.setFrameStyle(STYLE_SUNKEN) - frmBottomLayout = QGridLayout() - frmBottomLayout.addWidget(self.txtTxDP, 0,0, 1,1) - frmBottomLayout.addWidget(frmBtn, 0,1, 2,1) - frmBottomLayout.addWidget(frmInfo, 1,0, 1,1) - #frmBottomLayout.addWidget(frmMoreInfo, 1,1, 1,1) - frmBottom.setLayout(frmBottomLayout) + # Show the transaction ID, with the user's preferred endianness + # I hate BE, but block-explorer requires it so it's probably a better default + endianness = self.main.getSettingOrSetDefault('PrefEndian', BIGENDIAN) + estr = '' + if self.mode in (USERMODE.Advanced, USERMODE.Expert): + estr = ' (BE)' if endianness == BIGENDIAN else ' (LE)' - dlgLayout = QVBoxLayout() - dlgLayout.addWidget(frmDescr) - dlgLayout.addWidget(frmBottom) - dlgLayout.addWidget(frmGoBack) + lbls.append([]) + lbls[-1].append(self.main.createToolTipWidget('Unique identifier for this transaction')) + lbls[-1].append(QLabel('Transaction ID' + estr + ':')) - self.setLayout(dlgLayout) - self.processTxDP() - - self.setWindowTitle('Review Offline Transaction') - self.setWindowIcon(QIcon( self.main.iconfile)) - + # Want to display the hash of the Tx if we have a valid one: + # A TxDP does not have a valid hash until it's completely signed, though + longTxt = '[[ Transaction ID cannot be determined without all signatures ]]' + w, h = relaxedSizeStr(QRichLabel(''), longTxt) - def processTxDP(self): - # TODO: it wouldn't be TOO hard to modify this dialog to take - # arbitrary hex-serialized transactions for broadcast... - # but it's not trivial either (for instance, I assume - # that we have inputs values, etc) - self.wlt = None - self.leValue = None - self.txdpObj = None - self.idxSelf = [] - self.idxOther = [] - self.lblStatus.setText('') - self.lblCopied.setText('') - self.enoughSigs = False - self.sigsValid = False - self.txdpReadable = False - - txdpStr = str(self.txtTxDP.toPlainText()) - try: - self.txdpObj = PyTxDistProposal().unserializeAscii(txdpStr) - self.enoughSigs = self.txdpObj.checkTxHasEnoughSignatures() - self.sigsValid = self.txdpObj.checkTxHasEnoughSignatures(alsoVerify=True) - self.txdpReadable = True - except BadAddressError: - QMessageBox.critical(self, 'Inconsistent Data!', \ - 'This transaction contains inconsistent information. This ' - 'is probably not your fault...', QMessageBox.Ok) - self.txdpObj = None - self.txdpReadable = False - except NetworkIDError: - QMessageBox.critical(self, 'Wrong Network!', \ - 'This transaction is actually for a different network! ' - 'Did you load the correct transaction?', QMessageBox.Ok) - self.txdpObj = None - self.txdpReadable = False - except (UnserializeError, IndexError, ValueError): - self.txdpObj = None - self.txdpReadable = False - - if not self.enoughSigs or not self.sigsValid or not self.txdpReadable: - self.btnBroadcast.setEnabled(False) - else: - if self.main.netMode==NETWORKMODE.Full: - self.btnBroadcast.setEnabled(True) - else: - self.btnBroadcast.setEnabled(False) - self.btnBroadcast.setToolTip('No connection to Bitcoin network!') - - self.btnSave.setEnabled(True) - self.btnCopyHex.setEnabled(False) - if not self.txdpReadable: - if len(txdpStr)>0: - self.lblStatus.setText('Unrecognized!') + tempPyTx = self.pytx.copy() + if pytxdp: + finalTx = pytxdp.getBroadcastTxIfReady() + if finalTx: + tempPyTx = finalTx.copy() else: - self.lblStatus.setText('') - self.btnSign.setEnabled(False) - self.btnBroadcast.setEnabled(False) - self.btnSave.setEnabled(False) - self.makeReviewFrame() - return - elif not self.enoughSigs: - if not self.main.getSettingOrSetDefault('DNAA_ReviewOfflineTx', False): - result = MsgBoxWithDNAA(MSGBOX.Warning, title='Offline Warning', \ - msg='Please review your transaction carefully before ' - 'signing and broadcasting it! The extra security of ' - 'using offline wallets is lost if you do ' - 'not confirm the transaction is correct!', dnaaMsg=None) - self.main.writeSetting('DNAA_ReviewOfflineTx', result[1]) - self.lblStatus.setText('Unsigned') - self.btnSign.setEnabled(True) - self.btnBroadcast.setEnabled(False) - elif not self.sigsValid: - self.lblStatus.setText('Bad Signature!') - self.btnSign.setEnabled(True) - self.btnBroadcast.setEnabled(False) - else: - self.lblStatus.setText('All Signatures Valid!') - self.btnSign.setEnabled(False) - self.btnCopyHex.setEnabled(True) - - - # NOTE: We assume this is an OUTGOING transaction. When I pull in the - # multi-sig code, I will have to either make a different dialog, - # or add some logic to this one - FIELDS = enum('Hash','OutList','SumOut','InList','SumIn', 'Time', 'Blk', 'Idx') - data = extractTxInfo(self.txdpObj, -1) - - # Collect the input wallets (hopefully just one of them) - fromWlts = set() - for recip,amt,a,b,c in data[FIELDS.InList]: - wltID = self.main.getWalletForAddr160(recip) - if not wltID=='': - fromWlts.add(wltID) - - if len(fromWlts)>1: - QMessageBox.warning(self, 'Multiple Input Wallets', \ - 'Somehow, you have obtained a transaction that actually pulls from more ' - 'than one wallet. The support for handling multi-wallet signatures is ' - 'not currently implemented (this also could have happened if you imported ' - 'the same private key into two different wallets).' ,QMessageBox.Ok) - self.makeReviewFrame() - return - elif len(fromWlts)==0: - QMessageBox.warning(self, 'Unrelated Transaction', \ - 'This transaction appears to have no relationship to any of the wallets ' - 'stored on this computer. Did you load the correct transaction?', \ - QMessageBox.Ok) - self.makeReviewFrame() - return - - spendWltID = fromWlts.pop() - self.wlt = self.main.walletMap[spendWltID] - + tempPyTx = None + lbls[-1].append(QRichLabel('' + '[[ Transaction ID cannot be determined without all signatures ]]' + '')) - toWlts = set() - myOutSum = 0 - theirOutSum = 0 - rvPairs = [] - idx = 0 - for scrType, amt, recip in data[FIELDS.OutList]: - wltID = self.main.getWalletForAddr160(recip) - if wltID==spendWltID: - toWlts.add(wltID) - myOutSum += amt - self.idxSelf.append(idx) + if tempPyTx: + if endianness == BIGENDIAN: + lbls[-1].append(QLabel(binary_to_hex(tempPyTx.getHash(), endOut=BIGENDIAN))) else: - rvPairs.append( [recip, amt] ) - theirOutSum += amt - self.idxOther.append(idx) - idx += 1 + lbls[-1].append(QLabel(binary_to_hex(tempPyTx.getHash(), endOut=LITTLEENDIAN))) - + lbls[-1][-1].setMinimumWidth(w) - myInSum = data[FIELDS.SumIn] # because we assume all are ours + if self.mode in (USERMODE.Expert,): + # Add protocol version and locktime to the display + lbls.append([]) + lbls[-1].append(self.main.createToolTipWidget('Bitcoin Protocol Version Number')) + lbls[-1].append(QLabel('Tx Version:')) + lbls[-1].append(QLabel(str(self.pytx.version))) - if myInSum == None: - fee = None - else: - fee = myInSum - data[FIELDS.SumOut] - - self.leValue = theirOutSum - self.makeReviewFrame() - - - ############################################################################ - def makeReviewFrame(self): - - - ### - if self.txdpObj==None: - self.infoLbls[0][2].setText('') - self.infoLbls[1][2].setText('') - self.infoLbls[2][2].setText('') - self.infoLbls[3][2].setText('') - #self.infoLbls[4][2].setText('') - #self.infoLbls[5][0].setVisible(False) - #self.infoLbls[5][1].setVisible(False) - #self.infoLbls[5][2].setVisible(False) - #self.moreInfo.setVisible(False) - else: - ##### 0 - - ##### 1 - if self.wlt: - self.infoLbls[0][2].setText(self.wlt.uniqueIDB58) - self.infoLbls[1][2].setText(self.wlt.labelName) - else: - self.infoLbls[0][2].setText('[[ Unrelated ]]') - self.infoLbls[1][2].setText('') - - ##### 2 - self.infoLbls[2][2].setText(self.txdpObj.uniqueB58) - - ##### 3 - if self.leValue: - self.infoLbls[3][2].setText(coin2str(self.leValue, maxZeros=0).strip() + ' BTC') + lbls.append([]) + lbls[-1].append(self.main.createToolTipWidget( + 'The time at which this transaction becomes valid.')) + lbls[-1].append(QLabel('Lock-Time:')) + if self.pytx.lockTime == 0: + lbls[-1].append(QLabel('Immediate (0)')) + elif self.pytx.lockTime < 500000000: + lbls[-1].append(QLabel('Block %d' % self.pytx.lockTime)) else: - self.infoLbls[3][2].setText('') - - ##### 4 - #if self.idxOther: - #self.infoLbls[4][2].setText(str(len(self.idxOther))) - #else: - #self.infoLbls[4][2].setText('') + lbls[-1].append(QLabel(unixTimeToFormatStr(self.pytx.lockTime))) - ##### 5 - #self.infoLbls[5][0].setVisible(True) - #self.infoLbls[5][1].setVisible(True) - #self.infoLbls[5][2].setVisible(True) - self.moreInfo.setVisible(True) + lbls.append([]) + lbls[-1].append(self.main.createToolTipWidget('Comment stored for this transaction in this wallet')) + lbls[-1].append(QLabel('User Comment:')) + if wlt.getComment(txHash): + lbls[-1].append(QRichLabel(wlt.getComment(txHash))) + else: + lbls[-1].append(QRichLabel('[None]')) + if not data[FIELDS.Time] == None: + lbls.append([]) + if data[FIELDS.Blk] >= 2 ** 32 - 1: + lbls[-1].append(self.main.createToolTipWidget( + 'The time that you computer first saw this transaction')) + else: + lbls[-1].append(self.main.createToolTipWidget( + 'All transactions are eventually included in a "block." The ' + 'time shown here is the time that the block entered the "blockchain."')) + lbls[-1].append(QLabel('Transaction Time:')) + lbls[-1].append(QLabel(data[FIELDS.Time])) + if not data[FIELDS.Blk] == None: + nConf = 0 + if data[FIELDS.Blk] >= 2 ** 32 - 1: + lbls.append([]) + lbls[-1].append(self.main.createToolTipWidget( + 'This transaction has not yet been included in a block. ' + 'It usually takes 5-20 minutes for a transaction to get ' + 'included in a block after the user hits the "Send" button.')) + lbls[-1].append(QLabel('Block Number:')) + lbls[-1].append(QRichLabel('Not in the blockchain yet')) + else: + idxStr = '' + if not data[FIELDS.Idx] == None and self.mode == USERMODE.Expert: + idxStr = ' (Tx #%d)' % data[FIELDS.Idx] + lbls.append([]) + lbls[-1].append(self.main.createToolTipWidget( + 'Every transaction is eventually included in a "block" which ' + 'is where the transaction is permanently recorded. A new block ' + 'is produced approximately every 10 minutes.')) + lbls[-1].append(QLabel('Included in Block:')) + lbls[-1].append(QRichLabel(str(data[FIELDS.Blk]) + idxStr)) + if TheBDM.getBDMState() == 'BlockchainReady': + nConf = TheBDM.getTopBlockHeight() - data[FIELDS.Blk] + 1 + lbls.append([]) + lbls[-1].append(self.main.createToolTipWidget( + 'The number of blocks that have been produced since ' + 'this transaction entered the blockchain. A transaciton ' + 'with 6 more confirmations is nearly impossible to reverse.')) + lbls[-1].append(QLabel('Confirmations:')) + lbls[-1].append(QRichLabel(str(nConf))) - def execMoreTxInfo(self): - - if not self.txdpObj: - self.processTxDP() - if not self.txdpObj: - QMessageBox.warning(self, 'Invalid Transaction', \ - 'Transaction data is invalid and cannot be shown!', QMessageBox.Ok) - return - dlgTxInfo = DlgDispTxInfo(self.txdpObj, self.wlt, self.parent, self.main, \ - precomputeIdxGray=self.idxSelf, precomputeAmt=-self.leValue, txtime=-1) - dlgTxInfo.exec_() + if svPairDisp == None and precomputeAmt == None: + # Couldn't determine recip/change outputs + lbls.append([]) + lbls[-1].append(self.main.createToolTipWidget( + 'Most transactions have at least a recipient output and a ' + 'returned-change output. You do not have enough enough information ' + 'to determine which is which, and so this fields shows the sum ' + 'of all outputs.')) + lbls[-1].append(QLabel('Sum of Outputs:')) + lbls[-1].append(QLabel(coin2str(txAmt, maxZeros=1).strip() + ' BTC')) + else: + lbls.append([]) + lbls[-1].append(self.main.createToolTipWidget( + 'Bitcoins were either sent or received, or sent-to-self')) + lbls[-1].append(QLabel('Transaction Direction:')) + lbls[-1].append(QRichLabel(txdir)) + lbls.append([]) + lbls[-1].append(self.main.createToolTipWidget( + 'The value shown here is the net effect on your ' + 'wallet, including transaction fee.')) + lbls[-1].append(QLabel('Transaction Amount:')) + lbls[-1].append(QRichLabel(coin2str(txAmt, maxZeros=1).strip() + ' BTC')) + if txAmt < 0: + lbls[-1][-1].setText('' + lbls[-1][-1].text() + ' ') + elif txAmt > 0: + lbls[-1][-1].setText('' + lbls[-1][-1].text() + ' ') - def signTx(self): - if not self.txdpObj: - QMessageBox.critical(self, 'Cannot Sign', \ - 'This transaction is not relevant to any of your wallets.' - 'Did you load the correct transaction?', QMessageBox.Ok) - return + if not data[FIELDS.SumIn] == None: + fee = data[FIELDS.SumIn] - data[FIELDS.SumOut] + lbls.append([]) + lbls[-1].append(self.main.createToolTipWidget( + 'Transaction fees go to users supplying the Bitcoin network with ' + 'computing power for processing transactions and maintaining security.')) + lbls[-1].append(QLabel('Tx Fee Paid:')) + lbls[-1].append(QLabel(coin2str(fee, maxZeros=0).strip() + ' BTC')) - if self.txdpObj==None: - QMessageBox.warning(self, 'Not Signable', \ - 'This is not a valid transaction, and thus it cannot ' - 'be signed. ', QMessageBox.Ok) - return - elif self.enoughSigs and self.sigsValid: - QMessageBox.warning(self, 'Already Signed', \ - 'This transaction has already been signed!', QMessageBox.Ok) - return - if self.wlt.watchingOnly: - QMessageBox.warning(self, 'No Private Keys!', \ - 'This transaction refers one of your wallets, but that wallet ' - 'is a watching-only wallet. Therefore, private keys are ' - 'not available to sign this transaction.', \ - QMessageBox.Ok) - return - # We should provide the same confirmation dialog here, as we do when - # sending a regular (online) transaction. But the DlgConfirmSend was - # not really designed - #self.txValues = [totalSend, fee, totalChange] - #self.origRVPairs = list(recipValuePairs) - txdp = self.txdpObj - rvpairsOther = [] - rvpairsMine = [] - outInfo = txdp.pytxObj.makeRecipientsList() - theFee = sum(txdp.inputValues) - sum([info[1] for info in outInfo]) - for info in outInfo: - if not info[0] in (TXOUT_SCRIPT_COINBASE, TXOUT_SCRIPT_STANDARD): - rvpairsOther.append( ['Non-Standard Output', info[1]]) - continue + lastRow = 0 + for row, lbl3 in enumerate(lbls): + lastRow = row + for i in range(3): + lbl3[i].setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + lbl3[i].setTextInteractionFlags(Qt.TextSelectableByMouse | \ + Qt.TextSelectableByKeyboard) + frmLayout.addWidget(lbl3[0], row, 0, 1, 1) + frmLayout.addWidget(lbl3[1], row, 1, 1, 1) + frmLayout.addWidget(lbl3[2], row, 3, 1, 2) - if self.wlt.hasAddr(info[2]): - rvpairsMine.append( [info[2], info[1]]) - else: - rvpairsOther.append( [info[2], info[1]]) - - - if len(rvpairsMine)==0 and len(rvpairsOther)>1: - QMessageBox.warning(self, 'Missing Change', \ - 'This transaction has %d recipients, and none of them ' - 'are addresses in this wallet (for receiving change). ' - 'This can happen if you specified a custom change address ' - 'for this transaction, or sometimes happens solely by ' - 'chance with a multi-recipient transaction. It could also ' - 'be the result of someone tampering with the transaction. ' - '

    The transaction is valid and ready to be signed. Please ' - 'verify the recipient and amounts carefully before ' - 'confirming the transaction on the next screen.' % len(rvpairsOther), \ - QMessageBox.Ok) - dlg = DlgConfirmSend(self.wlt, rvpairsOther, theFee, self, self.main) - if not dlg.exec_(): - return - + spacer = QSpacerItem(20, 20) + frmLayout.addItem(spacer, 0, 2, len(lbls), 1) + # Show the list of recipients, if possible + numShow = 3 + rlbls = [] + if svPairDisp is not None: + numRV = len(svPairDisp) + for i, sv in enumerate(svPairDisp): + rlbls.append([]) + if i == 0: + rlbls[-1].append(self.main.createToolTipWidget( + 'All outputs of the transaction excluding change-' + 'back-to-sender outputs. If this list does not look ' + 'correct, it is possible that the change-output was ' + 'detected incorrectly -- please check the complete ' + 'input/output list below.')) + rlbls[-1].append(QLabel('Recipients:')) + else: + rlbls[-1].extend([QLabel(), QLabel()]) - if self.wlt.useEncryption and self.wlt.isLocked: - dlg = DlgUnlockWallet(self.wlt, self.parent, self.main, 'Sign Transaction') - if not dlg.exec_(): - QMessageBox.warning(self, 'Wallet is Locked', \ - 'Cannot sign transaction while your wallet is locked!', \ - QMessageBox.Ok) - return + rlbls[-1].append(QLabel(scrAddr_to_addrStr(sv[0]))) + if numRV > 1: + rlbls[-1].append(QLabel(coin2str(sv[1], maxZeros=1) + ' BTC')) + else: + rlbls[-1].append(QLabel('')) + ffixBold = GETFONT('Fixed', 10) + ffixBold.setWeight(QFont.Bold) + rlbls[-1][-1].setFont(ffixBold) - newTxdp = self.wlt.signTxDistProposal(self.txdpObj) - self.wlt.advanceHighestIndex() - self.txtTxDP.setText(newTxdp.serializeAscii()) - self.txdpObj = newTxdp + if numRV > numShow and i == numShow - 2: + moreStr = '[%d more recipients]' % (numRV - numShow + 1) + rlbls.append([]) + rlbls[-1].extend([QLabel(), QLabel(), QLabel(moreStr), QLabel()]) + break - if not self.fileLoaded==None: - self.saveTxAuto() + # ## + for i, lbl4 in enumerate(rlbls): + for j in range(4): + lbl4[j].setTextInteractionFlags(Qt.TextSelectableByMouse | \ + Qt.TextSelectableByKeyboard) + row = lastRow + 1 + i + frmLayout.addWidget(lbl4[0], row, 0, 1, 1) + frmLayout.addWidget(lbl4[1], row, 1, 1, 1) + frmLayout.addWidget(lbl4[2], row, 3, 1, 1) + frmLayout.addWidget(lbl4[3], row, 4, 1, 1) - def broadTx(self): - if self.main.netMode == NETWORKMODE.Disconnected: - QMessageBox.warning(self, 'No Internet!', \ - 'Armory lost its connection to Bitcoin-Qt, and cannot ' - 'broadcast any transactions until it is reconnected. ' - 'Please verify that Bitcoin-Qt (or bitcoind) is open ' - 'and synchronized with the network.', QMessageBox.Ok) - return - elif self.main.netMode == NETWORKMODE.Offline: - QMessageBox.warning(self, 'No Internet!', \ - 'You do not currently have a connection to the Bitcoin network. ' - 'If this does not seem correct, verify that Bitcoin-Qt is open ' - 'and synchronized with the network.', QMessageBox.Ok) - return + # TxIns/Senders + wWlt = relaxedSizeStr(GETFONT('Var'), 'A' * 10)[0] + wAddr = relaxedSizeStr(GETFONT('Var'), 'A' * 31)[0] + wAmt = relaxedSizeStr(GETFONT('Fixed'), 'A' * 20)[0] + if pytxdp: + self.txInModel = TxInDispModel(pytxdp, data[FIELDS.InList], self.main) + else: + self.txInModel = TxInDispModel(pytx, data[FIELDS.InList], self.main) + self.txInView = QTableView() + self.txInView.setModel(self.txInModel) + self.txInView.setSelectionBehavior(QTableView.SelectRows) + self.txInView.setSelectionMode(QTableView.SingleSelection) + self.txInView.horizontalHeader().setStretchLastSection(True) + self.txInView.verticalHeader().setDefaultSectionSize(20) + self.txInView.verticalHeader().hide() + w, h = tightSizeNChar(self.txInView, 1) + self.txInView.setMinimumHeight(2 * (1.4 * h)) + self.txInView.setMaximumHeight(5 * (1.4 * h)) + self.txInView.hideColumn(TXINCOLS.OutPt) + self.txInView.hideColumn(TXINCOLS.OutIdx) + self.txInView.hideColumn(TXINCOLS.Script) + + if self.mode == USERMODE.Standard: + initialColResize(self.txInView, [wWlt, wAddr, wAmt, 0, 0, 0, 0, 0, 0]) + self.txInView.hideColumn(TXINCOLS.FromBlk) + self.txInView.hideColumn(TXINCOLS.ScrType) + self.txInView.hideColumn(TXINCOLS.Sequence) + # self.txInView.setSelectionMode(QTableView.NoSelection) + elif self.mode == USERMODE.Advanced: + initialColResize(self.txInView, [0.8 * wWlt, 0.6 * wAddr, wAmt, 0, 0, 0, 0.2, 0, 0]) + self.txInView.hideColumn(TXINCOLS.FromBlk) + self.txInView.hideColumn(TXINCOLS.Sequence) + # self.txInView.setSelectionMode(QTableView.NoSelection) + elif self.mode == USERMODE.Expert: + self.txInView.resizeColumnsToContents() - try: - finalTx = self.txdpObj.prepareFinalTx() - except: - QMessageBox.warning(self, 'Error', \ - 'There was an error processing this transaction, for reasons ' - 'that are probably not your fault...', QMessageBox.Ok) - return + self.txInView.setContextMenuPolicy(Qt.CustomContextMenu) + self.txInView.customContextMenuRequested.connect(self.showContextMenuTxIn) - reply = QMessageBox.warning(self, 'Confirmation', \ - 'Are you sure that you want to broadcast this transaction?', \ - QMessageBox.Yes | QMessageBox.No) + # List of TxOuts/Recipients + if not precomputeIdxGray == None: + indicesMakeGray = precomputeIdxGray[:] + self.txOutModel = TxOutDispModel(self.pytx, self.main, idxGray=indicesMakeGray) + self.txOutView = QTableView() + self.txOutView.setModel(self.txOutModel) + self.txOutView.setSelectionBehavior(QTableView.SelectRows) + self.txOutView.setSelectionMode(QTableView.SingleSelection) + self.txOutView.verticalHeader().setDefaultSectionSize(20) + self.txOutView.verticalHeader().hide() + self.txOutView.setMinimumHeight(2 * (1.3 * h)) + self.txOutView.setMaximumHeight(5 * (1.3 * h)) + initialColResize(self.txOutView, [wWlt, 0.8 * wAddr, wAmt, 0.25, 0]) + self.txOutView.hideColumn(TXOUTCOLS.Script) + if self.mode == USERMODE.Standard: + self.txOutView.hideColumn(TXOUTCOLS.ScrType) + initialColResize(self.txOutView, [wWlt, wAddr, 0.25, 0, 0]) + self.txOutView.horizontalHeader().setStretchLastSection(True) + # self.txOutView.setSelectionMode(QTableView.NoSelection) + elif self.mode == USERMODE.Advanced: + initialColResize(self.txOutView, [0.8 * wWlt, 0.6 * wAddr, wAmt, 0.25, 0]) + # self.txOutView.setSelectionMode(QTableView.NoSelection) + elif self.mode == USERMODE.Expert: + initialColResize(self.txOutView, [wWlt, wAddr, wAmt, 0.25, 0]) + # self.txOutView.resizeColumnsToContents() - if reply==QMessageBox.Yes: - self.main.broadcastTransaction(finalTx) - if self.fileLoaded and os.path.exists(self.fileLoaded): - try: - #pcs = self.fileLoaded.split('.') - #newFileName = '.'.join(pcs[:-2]) + '.DONE.' + '.'.join(pcs[-2:]) - shutil.move(self.fileLoaded, self.fileLoaded.replace('signed','SENT')) - except: - QMessageBox.critical(self, 'File Remove Error', \ - 'The file could not be deleted. If you want to delete ' - 'it, please do so manually. The file was loaded from: ' - '

    %s: ' % self.fileLoaded, QMessageBox.Ok) - - try: - self.parent.accept() - except: - pass - self.accept() + self.txOutView.setContextMenuPolicy(Qt.CustomContextMenu) + self.txOutView.customContextMenuRequested.connect(self.showContextMenuTxOut) + self.lblTxioInfo = QRichLabel('') + self.lblTxioInfo.setMinimumWidth(tightSizeNChar(self.lblTxioInfo, 30)[0]) + self.connect(self.txInView, SIGNAL('clicked(QModelIndex)'), \ + lambda: self.dispTxioInfo('In')) + self.connect(self.txOutView, SIGNAL('clicked(QModelIndex)'), \ + lambda: self.dispTxioInfo('Out')) - def saveTxAuto(self): - if not self.txdpReadable: - QMessageBox.warning(self, 'Formatting Error', \ - 'The transaction data was not in a format recognized by ' - 'Armory.') - return - + # scrFrm = QFrame() + # scrFrm.setFrameStyle(STYLE_SUNKEN) + # scrFrmLayout = Q - if not self.fileLoaded==None and self.enoughSigs and self.sigsValid: - newSaveFile = self.fileLoaded.replace('unsigned', 'signed') - print newSaveFile - f = open(newSaveFile, 'w') - f.write(str(self.txtTxDP.toPlainText())) - f.close() - if not newSaveFile==self.fileLoaded: - os.remove(self.fileLoaded) - self.fileLoaded = newSaveFile - QMessageBox.information(self, 'Transaction Saved!',\ - 'Your transaction has been saved to the following location:' - '\n\n%s\n\nIt can now be broadcast from any computer running ' - 'Armory in online mode.' % newSaveFile, QMessageBox.Ok) - return - def saveTx(self): - if not self.txdpReadable: - QMessageBox.warning(self, 'Formatting Error', \ - 'The transaction data was not in a format recognized by ' - 'Armory.') - return - - - # The strange windows branching is because PyQt in Windows automatically - # adds the ffilter suffix to the default filename, where as it needs to - # be explicitly added in PyQt in Linux. Not sure why this behavior exists. - defaultFilename = '' - if not self.txdpObj==None: - if self.enoughSigs and self.sigsValid: - suffix = '' if OS_WINDOWS else '.signed.tx' - defaultFilename = 'armory_%s_%s' % (self.txdpObj.uniqueB58, suffix) - ffilt = 'Transactions (*.signed.tx *.unsigned.tx)' - else: - suffix = '' if OS_WINDOWS else '.unsigned.tx' - defaultFilename = 'armory_%s_%s' % (self.txdpObj.uniqueB58, suffix) - ffilt = 'Transactions (*.unsigned.tx *.signed.tx)' - filename = self.main.getFileSave('Save Transaction', \ - [ffilt], \ - defaultFilename) - - filename = filename.replace('unsigned.tx.unsigned.tx', 'unsigned.tx') - filename = filename.replace('unsigned.tx.unsigned.tx', 'unsigned.tx') - filename = filename.replace('signed.tx.signed.tx', 'signed.tx') - filename = filename.replace('signed.tx.signed.tx', 'signed.tx') - filename = filename.replace('unsigned.tx.signed.tx', 'signed.tx') - if len(str(filename))>0: - LOGINFO('Saving transaction file: %s', filename) - f = open(filename, 'w') - f.write(str(self.txtTxDP.toPlainText())) - f.close() + self.scriptArea = QScrollArea() + self.scriptArea.setWidget(self.lblTxioInfo) + self.scriptFrm = makeLayoutFrame(HORIZONTAL, [self.scriptArea]) + # self.scriptFrm.setMaximumWidth(150) + self.scriptArea.setMaximumWidth(200) + self.frmIOList = QFrame() + self.frmIOList.setFrameStyle(STYLE_SUNKEN) + frmIOListLayout = QGridLayout() - def loadTx(self): - filename = self.main.getFileLoad('Load Transaction', \ - ['Transactions (*.signed.tx *.unsigned.tx)']) - - if len(str(filename))>0: - LOGINFO('Selected transaction file to load: %s', filename) - print filename - f = open(filename, 'r') - self.txtTxDP.setText(f.read()) - f.close() - self.fileLoaded = filename - print self.fileLoaded + lblInputs = QLabel('Transaction Inputs (Sending addresses):') + ttipText = ('All transactions require previous transaction outputs as ' + 'inputs. ') + if not haveBDM: + ttipText += ('Since the blockchain is not available, not all input ' + 'information is available. You need to view this ' + 'transaction on a system with an internet connection ' + '(and blockchain) if you want to see the complete information.') + else: + ttipText += ('Each input is like an $X bill. Usually there are more inputs ' + 'than necessary for the transaction, and there will be an extra ' + 'output returning change to the sender') + ttipInputs = self.main.createToolTipWidget(ttipText) + lblOutputs = QLabel('Transaction Outputs (Receiving addresses):') + ttipOutputs = self.main.createToolTipWidget( + 'Shows all outputs, including other recipients ' + 'of the same transaction, and change-back-to-sender outputs ' + '(change outputs are displayed in light gray).') - def copyTx(self): - clipb = QApplication.clipboard() - clipb.clear() - clipb.setText(str(self.txtTxDP.toPlainText())) - self.lblCopied.setText('Copied!') - def copyTxHex(self): - clipb = QApplication.clipboard() - clipb.clear() - clipb.setText(binary_to_hex(self.txdpObj.prepareFinalTx().serialize())) - self.lblCopied.setText('Copied!') + inStrip = makeLayoutFrame(HORIZONTAL, [lblInputs, ttipInputs, STRETCH]) + outStrip = makeLayoutFrame(HORIZONTAL, [lblOutputs, ttipOutputs, STRETCH]) + frmIOListLayout.addWidget(inStrip, 0, 0, 1, 1) + frmIOListLayout.addWidget(self.txInView, 1, 0, 1, 1) + frmIOListLayout.addWidget(outStrip, 2, 0, 1, 1) + frmIOListLayout.addWidget(self.txOutView, 3, 0, 1, 1) + # frmIOListLayout.addWidget(self.lblTxioInfo, 0,1, 4,1) + self.frmIOList.setLayout(frmIOListLayout) -################################################################################ -class DlgShowKeyList(ArmoryDialog): - def __init__(self, wlt, parent=None, main=None): - super(DlgShowKeyList, self).__init__(parent, main) - self.wlt = wlt + self.btnIOList = QPushButton('') + self.btnCopy = QPushButton('Copy Raw Tx') + self.lblCopied = QRichLabel('') + self.btnOk = QPushButton('OK') + self.btnIOList.setCheckable(True) + self.connect(self.btnIOList, SIGNAL(CLICKED), self.extraInfoClicked) + self.connect(self.btnOk, SIGNAL(CLICKED), self.accept) + self.connect(self.btnCopy, SIGNAL(CLICKED), self.copyRawTx) - self.havePriv = ((not self.wlt.useEncryption) or not (self.wlt.isLocked)) + btnStrip = makeLayoutFrame(HORIZONTAL, [self.btnIOList, self.btnCopy, self.lblCopied, STRETCH, self.btnOk]) + if not self.mode == USERMODE.Expert: + self.btnCopy.setVisible(False) - wltType = determineWalletType(self.wlt, self.main)[0] - if wltType in (WLTTYPES.Offline, WLTTYPES.WatchOnly): - self.havePriv = False + if self.mode == USERMODE.Standard: + self.btnIOList.setChecked(False) + else: + self.btnIOList.setChecked(True) + self.extraInfoClicked() - # NOTE/WARNING: We have to make copies (in RAM) of the unencrypted - # keys, or else we will have to type in our address - # every 10s if we want to modify the key list. This - # isn't likely a big problem, but it's not ideal, - # either. Not much I can do about, though... - # (at least: once this dialog is closed, all the - # garbage should be collected...) - self.addrCopies = [] - for addr in self.wlt.getLinearAddrList(withAddrPool=True): - self.addrCopies.append(addr.copy()) - self.rootKeyCopy = self.wlt.addrMap['ROOT'].copy() - backupVersion = '1.35a' - testChain = DeriveChaincodeFromRootKey(self.rootKeyCopy.binPrivKey32_Plain) - self.needChaincode = (not testChain == self.rootKeyCopy.chaincode) - if not self.needChaincode: - backupVersion = '1.35c' + frm.setLayout(frmLayout) + layout.addWidget(frm, 2, 0, 1, 1) + layout.addWidget(self.scriptArea, 2, 1, 1, 1) + layout.addWidget(self.frmIOList, 3, 0, 1, 2) + layout.addWidget(btnStrip, 4, 0, 1, 2) - self.strDescrReg = ( \ - 'The textbox below shows all keys that are part of this wallet, ' - 'which includes both permanent keys and imported keys. If you ' - 'simply want to backup your wallet and you have no imported keys ' - 'then all data below is reproducible from a plain paper backup.' - '

    ' - 'If you have imported addresses to backup, and/or you ' - 'would like to export your private keys to another ' - 'wallet service or application, then you can save this data ' - 'to disk, or copy&paste it into the other application.') - self.strDescrWarn = ( \ - '

    ' - 'Warning: The text box below contains ' - 'the plaintext (unencrypted) private keys for each of ' - 'the addresses in this wallet. This information can be used ' - 'to spend the money associated with those addresses, so please ' - 'protect it like you protect the rest of your wallet.') + # bbox = QDialogButtonBox(QDialogButtonBox.Ok) + # self.connect(bbox, SIGNAL('accepted()'), self.accept) + # layout.addWidget(bbox, 6,0, 1,1) - self.lblDescr = QRichLabel('') - self.lblDescr.setAlignment(Qt.AlignLeft |Qt.AlignTop) + self.setLayout(layout) + self.layout().setSizeConstraint(QLayout.SetFixedSize) + self.setWindowTitle('Transaction Info') - txtFont = GETFONT('Fixed', 8) - self.txtBox = QTextEdit() - self.txtBox.setReadOnly(True) - self.txtBox.setFont(txtFont) - w,h = tightSizeNChar(txtFont, 110) - self.txtBox.setFont(txtFont) - self.txtBox.setMinimumWidth(w) - self.txtBox.setMaximumWidth(w) - self.txtBox.setMinimumHeight(h*3.2) - self.txtBox.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - - # Create a list of checkboxes and then some ID word to identify what - # to put there - self.chkList = {} - self.chkList['AddrStr'] = QCheckBox('Address String') - self.chkList['PubKeyHash']= QCheckBox('Hash160') - self.chkList['PrivCrypt'] = QCheckBox('Private Key (Encrypted)') - self.chkList['PrivHexBE'] = QCheckBox('Private Key (Plain Hex)') - self.chkList['PrivB58'] = QCheckBox('Private Key (Plain Base58)') - self.chkList['PubKey'] = QCheckBox('Public Key (BE)') - self.chkList['ChainIndex']= QCheckBox('Chain Index') - self.chkList['AddrStr' ].setChecked(True ) - self.chkList['PubKeyHash'].setChecked(False) - self.chkList['PrivB58' ].setChecked(self.havePriv) - self.chkList['PrivCrypt' ].setChecked(False) - self.chkList['PrivHexBE' ].setChecked(self.havePriv) - self.chkList['PubKey' ].setChecked(not self.havePriv) - self.chkList['ChainIndex'].setChecked(False) + def extraInfoClicked(self): + if self.btnIOList.isChecked(): + self.frmIOList.setVisible(True) + self.btnCopy.setVisible(True) + self.lblCopied.setVisible(True) + self.scriptArea.setVisible(self.mode == USERMODE.Expert) + self.btnIOList.setText('<<< Less Info') + else: + self.frmIOList.setVisible(False) + self.scriptArea.setVisible(False) + self.btnCopy.setVisible(False) + self.lblCopied.setVisible(False) + self.btnIOList.setText('Advanced >>>') - namelist = ['AddrStr','PubKeyHash','PrivB58','PrivCrypt', \ - 'PrivHexBE', 'PubKey', 'ChainIndex'] + def dispTxioInfo(self, InOrOut): + hexScript = None + headStr = None + if InOrOut == 'In': + selection = self.txInView.selectedIndexes() + if len(selection) == 0: + return + row = selection[0].row() + hexScript = str(self.txInView.model().index(row, TXINCOLS.Script).data().toString()) + headStr = 'TxIn Script:' + elif InOrOut == 'Out': + selection = self.txOutView.selectedIndexes() + if len(selection) == 0: + return + row = selection[0].row() + hexScript = str(self.txOutView.model().index(row, TXOUTCOLS.Script).data().toString()) + headStr = 'TxOut Script:' - for name in self.chkList.keys(): - self.connect(self.chkList[name], SIGNAL('toggled(bool)'), \ - self.rewriteList) + if hexScript: + binScript = hex_to_binary(hexScript) + addrStr = None + scrType = getTxOutScriptType(binScript) + if scrType in CPP_TXOUT_HAS_ADDRSTR: + addrStr = script_to_addrStr(binScript) - self.chkImportedOnly = QCheckBox('Imported Addresses Only') - self.chkWithAddrPool = QCheckBox('Include Unused (Address Pool)') - self.chkDispRootKey = QCheckBox('Include Paper Backup Root') - self.chkOmitSpaces = QCheckBox('Omit spaces in key data') - self.chkDispRootKey.setChecked(True) - self.connect(self.chkImportedOnly, SIGNAL('toggled(bool)'), self.rewriteList) - self.connect(self.chkWithAddrPool, SIGNAL('toggled(bool)'), self.rewriteList) - self.connect(self.chkDispRootKey, SIGNAL('toggled(bool)'), self.rewriteList) - self.connect(self.chkOmitSpaces, SIGNAL('toggled(bool)'), self.rewriteList) - #self.chkCSV = QCheckBox('Display in CSV format') + oplist = convertScriptToOpStrings(hex_to_binary(hexScript)) + opprint = [] + prevOpIsPushData = False + for op in oplist: - if not self.havePriv: - self.chkDispRootKey.setChecked(False) - self.chkDispRootKey.setEnabled(False) - + if addrStr is None or not prevOpIsPushData: + opprint.append(op) + else: + opprint.append(op + ' (%s)' % addrStr) + prevOpIsPushData = False - std = (self.main.usermode==USERMODE.Standard) - adv = (self.main.usermode==USERMODE.Advanced) - dev = (self.main.usermode==USERMODE.Expert) - if std: - self.chkList['PubKeyHash'].setVisible(False) - self.chkList['PrivCrypt' ].setVisible(False) - self.chkList['ChainIndex'].setVisible(False) - elif adv: - self.chkList['PubKeyHash'].setVisible(False) - self.chkList['ChainIndex'].setVisible(False) + if 'pushdata' in op.lower(): + prevOpIsPushData = True - # We actually just want to remove these entirely - # (either we need to display all data needed for decryption, - # besides passphrase, or we shouldn't show any of it) - self.chkList['PrivCrypt' ].setVisible(False) + lblScript = QRichLabel('') + lblScript.setText('Script:

    ' + '
    '.join(opprint)) + lblScript.setWordWrap(False) + lblScript.setTextInteractionFlags(Qt.TextSelectableByMouse | \ + Qt.TextSelectableByKeyboard) - chkBoxList = [self.chkList[n] for n in namelist] - chkBoxList.append('Line') - chkBoxList.append(self.chkImportedOnly) - chkBoxList.append(self.chkWithAddrPool) - chkBoxList.append(self.chkDispRootKey) + self.scriptArea.setWidget(makeLayoutFrame(VERTICAL, [lblScript])) + self.scriptArea.setMaximumWidth(200) - frmChks = makeLayoutFrame('Vert', chkBoxList, STYLE_SUNKEN) + def copyRawTx(self): + clipb = QApplication.clipboard() + clipb.clear() + clipb.setText(binary_to_hex(self.pytx.serialize())) + self.lblCopied.setText('Copied to Clipboard!') - btnGoBack = QPushButton('<<< Go Back') - btnSaveFile = QPushButton('Save to File...') - btnCopyClip = QPushButton('Copy to Clipboard') - self.lblCopied = QRichLabel('') - self.connect(btnGoBack, SIGNAL('clicked()'), self.accept) - self.connect(btnSaveFile, SIGNAL('clicked()'), self.saveToFile) - self.connect(btnCopyClip, SIGNAL('clicked()'), self.copyToClipboard) - frmGoBack = makeLayoutFrame('Horiz', [btnGoBack, \ - 'Stretch', \ - self.chkOmitSpaces, \ - 'Stretch', \ - self.lblCopied, \ - btnCopyClip, \ - btnSaveFile]) + ############################################################################# + def showContextMenuTxIn(self, pos): + menu = QMenu(self.txInView) + std = (self.main.usermode == USERMODE.Standard) + adv = (self.main.usermode == USERMODE.Advanced) + dev = (self.main.usermode == USERMODE.Expert) - frmDescr = makeLayoutFrame('Horiz', [self.lblDescr], STYLE_SUNKEN) + if True: actCopySender = menu.addAction("Copy Sender Address") + if True: actCopyWltID = menu.addAction("Copy Wallet ID") + if True: actCopyAmount = menu.addAction("Copy Amount") + if dev: actCopyOutPt = menu.addAction("Copy Outpoint") + if dev: actCopyScript = menu.addAction("Copy Raw Script") + idx = self.txInView.selectedIndexes()[0] + action = menu.exec_(QCursor.pos()) - if not self.havePriv or (self.wlt.useEncryption and self.wlt.isLocked): - self.chkList['PrivHexBE'].setEnabled(False) - self.chkList['PrivHexBE'].setChecked(False) - self.chkList['PrivB58' ].setEnabled(False) - self.chkList['PrivB58' ].setChecked(False) + if action == actCopyWltID: + s = str(self.txInView.model().index(idx.row(), TXINCOLS.WltID).data().toString()) + elif action == actCopySender: + s = str(self.txInView.model().index(idx.row(), TXINCOLS.Sender).data().toString()) + elif action == actCopyAmount: + s = str(self.txInView.model().index(idx.row(), TXINCOLS.Btc).data().toString()) + elif dev and action == actCopyOutPt: + s1 = str(self.txInView.model().index(idx.row(), TXINCOLS.OutPt).data().toString()) + s2 = str(self.txInView.model().index(idx.row(), TXINCOLS.OutIdx).data().toString()) + s = s1 + ':' + s2 + elif dev and action == actCopyScript: + s = str(self.txInView.model().index(idx.row(), TXINCOLS.Script).data().toString()) + else: + return - dlgLayout = QGridLayout() - dlgLayout.addWidget(frmDescr, 0,0, 1,1) - dlgLayout.addWidget(frmChks, 0,1, 1,1) - dlgLayout.addWidget(self.txtBox, 1,0, 1,2) - dlgLayout.addWidget(frmGoBack, 2,0, 1,2) - dlgLayout.setRowStretch(0, 0) - dlgLayout.setRowStretch(1, 1) - dlgLayout.setRowStretch(2, 0) + clipb = QApplication.clipboard() + clipb.clear() + clipb.setText(s.strip()) - self.setLayout(dlgLayout) - self.rewriteList() - self.setWindowTitle('All Wallet Keys') + ############################################################################# + def showContextMenuTxOut(self, pos): + menu = QMenu(self.txOutView) + std = (self.main.usermode == USERMODE.Standard) + adv = (self.main.usermode == USERMODE.Advanced) + dev = (self.main.usermode == USERMODE.Expert) - + if True: actCopySender = menu.addAction("Copy Recipient Address") + if True: actCopyWltID = menu.addAction("Copy Wallet ID") + if True: actCopyAmount = menu.addAction("Copy Amount") + if dev: actCopyScript = menu.addAction("Copy Raw Script") + idx = self.txOutView.selectedIndexes()[0] + action = menu.exec_(QCursor.pos()) - def rewriteList(self, *args): - """ - Write out all the wallet data - """ - whitespace = '' if self.chkOmitSpaces.isChecked() else ' ' + if action == actCopyWltID: + s = self.txOutView.model().index(idx.row(), TXOUTCOLS.WltID).data().toString() + elif action == actCopySender: + s = self.txOutView.model().index(idx.row(), TXOUTCOLS.Recip).data().toString() + elif action == actCopyAmount: + s = self.txOutView.model().index(idx.row(), TXOUTCOLS.Btc).data().toString() + elif dev and action == actCopyScript: + s = self.txOutView.model().index(idx.row(), TXOUTCOLS.Script).data().toString() + else: + return - def fmtBin(s, nB=4, sw=False): - h = binary_to_hex(s) - if sw: - h = hex_switchEndian(h) - return whitespace.join([h[i:i+nB] for i in range(0, len(h), nB)]) + clipb = QApplication.clipboard() + clipb.clear() + clipb.setText(str(s).strip()) - L = [] - L.append('Created: ' + unixTimeToFormatStr(RightNow(), self.main.getPreferredDateFormat())) - L.append('Wallet ID: ' + self.wlt.uniqueIDB58) - L.append('Wallet Name: ' + self.wlt.labelName) - L.append('') - if self.chkDispRootKey.isChecked(): - binPriv0 = self.rootKeyCopy.binPrivKey32_Plain.toBinStr()[:16] - binPriv1 = self.rootKeyCopy.binPrivKey32_Plain.toBinStr()[16:] - binChain0 = self.rootKeyCopy.chaincode.toBinStr()[:16] - binChain1 = self.rootKeyCopy.chaincode.toBinStr()[16:] - binPriv0Chk = computeChecksum(binPriv0, nBytes=2) - binPriv1Chk = computeChecksum(binPriv1, nBytes=2) - binChain0Chk = computeChecksum(binChain0, nBytes=2) - binChain1Chk = computeChecksum(binChain1, nBytes=2) - binPriv0 = binary_to_easyType16(binPriv0 + binPriv0Chk) - binPriv1 = binary_to_easyType16(binPriv1 + binPriv1Chk) - binChain0 = binary_to_easyType16(binChain0 + binChain0Chk) - binChain1 = binary_to_easyType16(binChain1 + binChain1Chk) - L.append('-'*80) - L.append('The following is the same information contained on your paper backup.') - L.append('All NON-imported addresses in your wallet are backed up by this data.') - L.append('') - L.append('Root Key: ' + ' '.join([binPriv0[i:i+4] for i in range(0,36,4)])) - L.append(' ' + ' '.join([binPriv1[i:i+4] for i in range(0,36,4)])) - if self.needChaincode: - L.append('Chain Code: ' + ' '.join([binChain0[i:i+4] for i in range(0,36,4)])) - L.append(' ' + ' '.join([binChain1[i:i+4] for i in range(0,36,4)])) - L.append('-'*80) - L.append('') - # Cleanup all that sensitive data laying around in RAM - binPriv0, binPriv1 = None,None - binChain0, binChain1 = None,None - binPriv0Chk, binPriv1Chk = None,None - binChain0Chk, binChain1Chk = None,None +class GfxViewPaper(QGraphicsView): + def __init__(self, parent=None, main=None): + super(GfxViewPaper, self).__init__(parent) + self.setRenderHint(QPainter.TextAntialiasing) - self.havePriv = False - topChain = self.wlt.highestUsedChainIndex - extraLbl = '' - #c = ',' if self.chkCSV.isChecked() else '' - for addr in self.addrCopies: +class GfxItemText(QGraphicsTextItem): + """ + So far, I'm pretty bad at setting the boundingRect properly. I have + hacked it to be usable for this specific situation, but it's not very + reusable... + """ + def __init__(self, text, position, scene, font=GETFONT('Courier', 8), lineWidth=None): + super(GfxItemText, self).__init__(text) + self.setFont(font) + self.setPos(position) + if lineWidth: + self.setTextWidth(lineWidth) - # Address pool - if self.chkWithAddrPool.isChecked(): - if addr.chainIndex > topChain: - extraLbl = ' (Unused/Address Pool)' - else: - if addr.chainIndex > topChain: - continue + self.setDefaultTextColor(self.PAGE_TEXT_COLOR) - # Imported Addresses - if self.chkImportedOnly.isChecked(): - if not addr.chainIndex==-2: - continue - else: - if addr.chainIndex==-2: - extraLbl = ' (Imported)' + def boundingRect(self): + w, h = relaxedSizeStr(self, self.toPlainText()) + nLine = 1 + if self.textWidth() > 0: + twid = self.textWidth() + nLine = max(1, int(float(w) / float(twid) + 0.5)) + return QRectF(0, 0, w, nLine * (1.5 * h)) - if self.chkList['AddrStr' ].isChecked(): - L.append(addr.getAddrStr() + extraLbl) - if self.chkList['PubKeyHash'].isChecked(): - L.append(' Hash160 : ' + fmtBin(addr.getAddr160())) - if self.chkList['PrivB58' ].isChecked(): - pB58 = encodePrivKeyBase58(addr.binPrivKey32_Plain.toBinStr()) - pB58Stretch = whitespace.join([pB58[i:i+6] for i in range(0, len(pB58), 6)]) - L.append(' PrivBase58: ' + pB58Stretch) - self.havePriv = True - if self.chkList['PrivCrypt' ].isChecked(): - L.append(' PrivCrypt : ' + fmtBin(addr.binPrivKey32_Encr.toBinStr())) - if self.chkList['PrivHexBE' ].isChecked(): - L.append(' PrivHexBE : ' + fmtBin(addr.binPrivKey32_Plain.toBinStr())) - self.havePriv = True - if self.chkList['PubKey' ].isChecked(): - L.append(' PublicX : ' + fmtBin(addr.binPublicKey65.toBinStr()[1:33 ])) - L.append(' PublicY : ' + fmtBin(addr.binPublicKey65.toBinStr()[ 33:])) - if self.chkList['ChainIndex'].isChecked(): - L.append(' ChainIndex: ' + str(addr.chainIndex)) - self.txtBox.setText('\n'.join(L)) - if self.havePriv: - self.lblDescr.setText(self.strDescrReg + self.strDescrWarn) +class GfxItemQRCode(QGraphicsItem): + """ + Converts binary data to base58, and encodes the Base58 characters in + the QR-code. It seems weird to use Base58 instead of binary, but the + QR-code has no problem with the size, instead, we want the data in the + QR-code to match exactly what is human-readable on the page, which is + in Base58. + + You must supply exactly one of "totalSize" or "modSize". TotalSize + guarantees that the QR code will fit insides box of a given size. + ModSize is how big each module/pixel of the QR code is, which means + that a bigger QR block results in a bigger physical size on paper. + """ + def __init__(self, rawDataToEncode, maxSize=None): + super(GfxItemQRCode, self).__init__() + self.maxSize = maxSize + self.updateQRData(rawDataToEncode) + + def boundingRect(self): + return self.Rect + + def updateQRData(self, toEncode, maxSize=None): + if maxSize == None: + maxSize = self.maxSize else: - self.lblDescr.setText(self.strDescrReg) + self.maxSize = maxSize - - - def saveToFile(self): - if self.havePriv: - if not self.main.getSettingOrSetDefault('DNAA_WarnPrintKeys', False): - result = MsgBoxWithDNAA(MSGBOX.Warning, title='Plaintext Private Keys', \ - msg='REMEMBER: The data you ' - 'are about to save contains private keys. Please make sure ' - 'that only trusted persons will have access to this file.' - '

    Are you sure you want to continue?', \ - dnaaMsg=None, wCancel=True) - if not result[0]: - return - self.main.writeSetting('DNAA_WarnPrintKeys', result[1]) - - wltID = self.wlt.uniqueIDB58 - fn = self.main.getFileSave(title='Save Key List', \ - ffilter=['Text Files (*.txt)'], \ - defaultFilename='keylist_%s_.txt'%wltID) - if len(fn)>0: - fileobj = open(fn,'w') - fileobj.write(str(self.txtBox.toPlainText())) - fileobj.close() - + self.qrmtrx, self.modCt = CreateQRMatrix(toEncode, 'H') + self.modSz = round(float(self.maxSize) / float(self.modCt) - 0.5) + totalSize = self.modCt * self.modSz + self.Rect = QRectF(0, 0, totalSize, totalSize) + def paint(self, painter, option, widget=None): + painter.setPen(Qt.NoPen) + painter.setBrush(QBrush(QColor(0, 0, 0))) - def copyToClipboard(self): - clipb = QApplication.clipboard() - clipb.clear() - clipb.setText(str(self.txtBox.toPlainText())) - self.lblCopied.setText('Copied!') - + for r in range(self.modCt): + for c in range(self.modCt): + if self.qrmtrx[r][c] > 0: + painter.drawRect(*[self.modSz * a for a in [r, c, 1, 1]]) - def cleanup(self): - self.rootKeyCopy.binPrivKey32_Plain.destroy() - for addr in self.addrCopies: - addr.binPrivKey32_Plain.destroy() - self.rootKeyCopy = None - self.addrCopies = None - def accept(self): - self.cleanup() - super(DlgShowKeyList, self).accept() +class SimplePrintableGraphicsScene(object): - def reject(self): - self.cleanup() - super(DlgShowKeyList, self).reject() - + def __init__(self, parent, main): + """ + We use the following coordinates: -################################################################################ -class DlgTxFeeOptions(ArmoryDialog): - def __init__(self, wlt, parent=None, main=None): - super(DlgTxFeeOptions, self).__init__(parent, main) + -----> +x + | + | + V +y - lblDescr = QLabel( \ - 'Transaction fees go to people who contribute processing power to ' - 'the Bitcoin network to process transactions and keep it secure.') - lblDescr2 = QLabel( \ - 'Nearly all transactions are guaranteed to be ' - 'processed if a fee of 0.0005 BTC is included (less than $0.01 USD). You ' - 'will be prompted for confirmation if a higher fee amount is required for ' - 'your transaction.') + """ + self.parent = parent + self.main = main + self.INCH = 72 + self.PAPER_A4_WIDTH = 8.5 * self.INCH + self.PAPER_A4_HEIGHT = 11.0 * self.INCH + self.MARGIN_PIXELS = 0.6 * self.INCH -################################################################################ -class DlgAddressProperties(ArmoryDialog): - def __init__(self, wlt, parent=None, main=None): - super(DlgAddressProperties, self).__init__(parent, main) + self.PAGE_BKGD_COLOR = QColor(255, 255, 255) + self.PAGE_TEXT_COLOR = QColor(0, 0, 0) - + self.fontFix = GETFONT('Courier', 9) + self.fontVar = GETFONT('Times', 10) + self.gfxScene = QGraphicsScene(self.parent) + self.gfxScene.setSceneRect(0, 0, self.PAPER_A4_WIDTH, self.PAPER_A4_HEIGHT) + self.gfxScene.setBackgroundBrush(self.PAGE_BKGD_COLOR) -################################################################################ -def extractTxInfo(pytx, rcvTime=None): + # For when it eventually makes it to the printer + # self.printer = QPrinter(QPrinter.HighResolution) + # self.printer.setPageSize(QPrinter.Letter) + # self.gfxPainter = QPainter(self.printer) + # self.gfxPainter.setRenderHint(QPainter.TextAntialiasing) + # self.gfxPainter.setPen(Qt.NoPen) + # self.gfxPainter.setBrush(QBrush(self.PAGE_TEXT_COLOR)) - - pytxdp = None - if isinstance(pytx, PyTxDistProposal): - pytxdp = pytx - pytx = pytxdp.pytxObj.copy() - - txHash = pytx.getHash() - txOutToList, sumTxOut, txinFromList, sumTxIn, txTime, txBlk, txIdx = [None]*7 + self.cursorPos = QPointF(self.MARGIN_PIXELS, self.MARGIN_PIXELS) + self.lastCursorMove = (0, 0) - txOutToList = pytx.makeRecipientsList() - sumTxOut = sum([t[1] for t in txOutToList]) - txcpp = Tx() - if TheBDM.getBDMState()=='BlockchainReady': - txcpp = TheBDM.getTxByHash(txHash) - if txcpp.isInitialized(): - hgt = txcpp.getBlockHeight() - if hgt < TheBDM.getTopBlockHeight(): - headref = TheBDM.getHeaderByHeight(hgt) - txTime = unixTimeToFormatStr(headref.getTimestamp()) - txBlk = headref.getBlockHeight() - txIdx = txcpp.getBlockTxIndex() - else: - if rcvTime==None: - txTime = 'Unknown' - elif rcvTime==-1: - txTime = '[[Not broadcast yet]]' - elif isinstance(rcvTime, basestring): - txTime = rcvTime - else: - txTime = unixTimeToFormatStr(rcvTime) - txBlk = UINT32_MAX - txIdx = -1 - - txinFromList = [] - if TheBDM.getBDMState()=='BlockchainReady' and txcpp.isInitialized(): - # Use BDM to get all the info about the TxOut being spent - # Recip, value, block-that-incl-tx, tx-that-incl-txout, txOut-index - haveAllInput=True - for i in range(txcpp.getNumTxIn()): - txinFromList.append([]) - cppTxin = txcpp.getTxInCopy(i) - prevTxHash = cppTxin.getOutPoint().getTxHash() - if TheBDM.getTxByHash(prevTxHash).isInitialized(): - prevTx = TheBDM.getPrevTx(cppTxin) - prevTxOut = prevTx.getTxOutCopy(cppTxin.getOutPoint().getTxOutIndex()) - txinFromList[-1].append(CheckHash160(TheBDM.getSenderScrAddr(cppTxin))) - txinFromList[-1].append(TheBDM.getSentValue(cppTxin)) - if prevTx.isInitialized(): - txinFromList[-1].append(prevTx.getBlockHeight()) - txinFromList[-1].append(prevTx.getThisHash()) - txinFromList[-1].append(prevTxOut.getIndex()) - else: - LOGERROR('How did we get a bad parent pointer? (extractTxInfo)') - prevTxOut.pprint() - txinFromList[-1].append('') - txinFromList[-1].append('') - txinFromList[-1].append('') - else: - haveAllInput=False - txin = PyTxIn().unserialize(cppTxin.serialize()) - txinFromList[-1].append(TxInScriptExtractAddr160IfAvail(txin)) - txinFromList[-1].append('') - txinFromList[-1].append('') - txinFromList[-1].append('') - txinFromList[-1].append('') + def getCursorXY(self): + return (self.cursorPos.x(), self.cursorPos.y()) - elif not pytxdp==None: - haveAllInput=True - for i,txin in enumerate(pytxdp.pytxObj.inputs): - txinFromList.append([]) - txinFromList[-1].append(TxOutScriptExtractAddr160(pytxdp.txOutScripts[i])) - txinFromList[-1].append(pytxdp.inputValues[i]) - txinFromList[-1].append('') - txinFromList[-1].append('') - txinFromList[-1].append('') - else: # BDM is not initialized - haveAllInput=False - for i,txin in enumerate(pytx.inputs): - txinFromList.append([]) - txinFromList[-1].append(TxInScriptExtractAddr160IfAvail(txin)) - txinFromList[-1].append('') - txinFromList[-1].append('') - txinFromList[-1].append('') - txinFromList[-1].append('') - - + def getScene(self): + return self.gfxScene - if haveAllInput: - sumTxIn = sum([x[1] for x in txinFromList]) - else: - sumTxIn = None + def pageRect(self): + marg = self.MARGIN_PIXELS + return QRectF(marg, marg, self.PAPER_A4_WIDTH - marg, self.PAPER_A4_HEIGHT - marg) - return [txHash, txOutToList, sumTxOut, txinFromList, sumTxIn, txTime, txBlk, txIdx] - + def insidePageRect(self, pt=None): + if pt == None: + pt = self.cursorPos + return self.pageRect.contains(pt) + def moveCursor(self, dx, dy, absolute=False): + xOld, yOld = self.getCursorXY() + if absolute: + self.cursorPos = QPointF(dx, dy) + self.lastCursorMove = (dx - xOld, dy - yOld) + else: + self.cursorPos = QPointF(xOld + dx, yOld + dy) + self.lastCursorMove = (dx, dy) -#def createTxInfoFrameStd(pytx, le, wlt=None): + def resetScene(self): + self.gfxScene.clear() + self.resetCursor() + def resetCursor(self): + self.cursorPos = QPointF(self.MARGIN_PIXELS, self.MARGIN_PIXELS) - -class DlgDispTxInfo(ArmoryDialog): - def __init__(self, pytx, wlt=None, parent=None, main=None, mode=None, \ - precomputeIdxGray=None, precomputeAmt=None, txtime=None): - """ - This got freakin' complicated, because I'm trying to handle - wallet/nowallet, BDM/noBDM and Std/Adv/Dev all at once. - We can override the user mode as an input argument, in case a std - user decides they want to see the tx in adv/dev mode - """ - super(DlgDispTxInfo, self).__init__(parent, main) - self.mode = mode + def newLine(self, extra_dy=0): + xOld, yOld = self.getCursorXY() + xNew = self.MARGIN_PIXELS + yNew = self.cursorPos.y() + self.lastItemSize[1] + extra_dy - 5 + self.moveCursor(xNew - xOld, yNew - yOld) - FIELDS = enum('Hash','OutList','SumOut','InList','SumIn', 'Time', 'Blk', 'Idx') - data = extractTxInfo(pytx, txtime) - - # If this is actually a TxDP in here... - pytxdp = None - if isinstance(pytx, PyTxDistProposal): - pytxdp = pytx - pytx = pytxdp.pytxObj.copy() + def drawHLine(self, width=None, penWidth=1): + if width == None: + width = 3 * self.INCH + currX, currY = self.getCursorXY() + lineItem = QGraphicsLineItem(currX, currY, currX + width, currY) + pen = QPen() + pen.setWidth(penWidth) + lineItem.setPen(pen) + self.gfxScene.addItem(lineItem) + rect = lineItem.boundingRect() + self.lastItemSize = (rect.width(), rect.height()) + self.moveCursor(rect.width(), 0) + return self.lastItemSize + def drawRect(self, w, h, edgeColor=QColor(0, 0, 0), fillColor=None, penWidth=1): + rectItem = QGraphicsRectItem(self.cursorPos.x(), self.cursorPos.y(), w, h) + if edgeColor == None: + rectItem.setPen(QPen(Qt.NoPen)) + else: + pen = QPen(edgeColor) + pen.setWidth(penWidth) + rectItem.setPen(pen) - self.pytx = pytx.copy() + if fillColor == None: + rectItem.setBrush(QBrush(Qt.NoBrush)) + else: + rectItem.setBrush(QBrush(fillColor)) - if self.mode==None: - self.mode = self.main.usermode + self.gfxScene.addItem(rectItem) + rect = rectItem.boundingRect() + self.lastItemSize = (rect.width(), rect.height()) + self.moveCursor(rect.width(), 0) + return self.lastItemSize - txHash = data[FIELDS.Hash] - haveWallet = (wlt!=None) - haveBDM = TheBDM.getBDMState()=='BlockchainReady' + def drawText(self, txt, font=None, wrapWidth=None, useHtml=True): + if font == None: + font = GETFONT('Var', 9) + txtItem = QGraphicsTextItem('') + if useHtml: + txtItem.setHtml(toUnicode(txt)) + else: + txtItem.setPlainText(toUnicode(txt)) + txtItem.setDefaultTextColor(self.PAGE_TEXT_COLOR) + txtItem.setPos(self.cursorPos) + txtItem.setFont(font) + if not wrapWidth == None: + txtItem.setTextWidth(wrapWidth) + self.gfxScene.addItem(txtItem) + rect = txtItem.boundingRect() + self.lastItemSize = (rect.width(), rect.height()) + self.moveCursor(rect.width(), 0) + return self.lastItemSize - # Should try to identify what is change and what's not - wltLE = None - IsNonStandard = False - fee = None - txAmt = data[FIELDS.SumOut] + def drawPixmapFile(self, pixFn, sizePx=None): + pix = QPixmap(pixFn) + if not sizePx == None: + pix = pix.scaled(sizePx, sizePx) + pixItem = QGraphicsPixmapItem(pix) + pixItem.setPos(self.cursorPos) + pixItem.setMatrix(QMatrix()) + self.gfxScene.addItem(pixItem) + rect = pixItem.boundingRect() + self.lastItemSize = (rect.width(), rect.height()) + self.moveCursor(rect.width(), 0) + return self.lastItemSize - # Collect our own outputs only, and ID non-std tx - rvPairSelf = [] - rvPairOther = [] - indicesSelf = [] - indicesOther = [] - indicesMakeGray = [] - idx = 0 - for scrType, amt, recip in data[FIELDS.OutList]: - if scrType in (TXOUT_SCRIPT_STANDARD, TXOUT_SCRIPT_COINBASE): - if haveWallet and wlt.hasAddr(recip): - rvPairSelf.append( [recip, amt] ) - indicesSelf.append( idx ) - else: - rvPairOther.append( [recip, amt] ) - indicesOther.append( idx ) - else: - # This isn't actually true: P2Pool outputs get flagged as non-std... - IsNonStandard = True - idx+=1 + def drawQR(self, qrdata, size=150): + objQR = GfxItemQRCode(qrdata, size) + objQR.setPos(self.cursorPos) + objQR.setMatrix(QMatrix()) + self.gfxScene.addItem(objQR) + rect = objQR.boundingRect() + self.lastItemSize = (rect.width(), rect.height()) + self.moveCursor(rect.width(), 0) + return self.lastItemSize - txdir = None - changeIndex = None - rvPairDisp = None - if haveBDM and haveWallet and data[FIELDS.SumOut] and data[FIELDS.SumIn]: - fee = data[FIELDS.SumOut] - data[FIELDS.SumIn] - ldgr = wlt.getTxLedger() - for le in ldgr: - if le.getTxHash()==txHash: - wltLE = le - txAmt = le.getValue() - # If we found the LE for this tx, then we can display much - # more useful information... like ignoring change outputs, - if le.isSentToSelf(): - txdir = 'Sent-to-Self' - rvPairDisp = [] - if len(self.pytx.outputs): - txAmt = fee - triplet = data[FIELDS.OutList][0] - rvPairDisp.append([triplet[2], triplet[1]]) - else: - txAmt, changeIndex = determineSentToSelfAmt(le, wlt) - for i,triplet in enumerate(data[FIELDS.OutList]): - if not i==changeIndex: - rvPairDisp.append([triplet[2], triplet[1]]) - else: - indicesMakeGray.append(i) - else: - if le.getValue() > 0: - txdir = 'Received' - rvPairDisp = rvPairSelf - indicesMakeGray.extend(indicesOther) - if le.getValue() < 0: - txdir = 'Sent' - rvPairDisp = rvPairOther - indicesMakeGray.extend(indicesSelf) - break + def drawColumn(self, strList, rowHeight=None, font=None, useHtml=True): + """ + This draws a bunch of left-justified strings in a column. It returns + a tight bounding box around all elements in the column, which can easily + be used to start the next column. The rowHeight is returned, and also + an available input, in case you are drawing text/font that has a different + height in each column, and want to make sure they stay aligned. + Just like the other methods, this leaves the cursor sitting at the + original y-value, but shifted to the right by the width of the column. + """ + origX, origY = self.getCursorXY() + maxColWidth = 0 + cumulativeY = 0 + for r in strList: + szX, szY = self.drawText(tr(r), font=font, useHtml=useHtml) + prevY = self.cursorPos.y() + if rowHeight == None: + self.newLine() + szY = self.cursorPos.y() - prevY + self.moveCursor(origX - self.MARGIN_PIXELS, 0) + else: + self.moveCursor(-szX, rowHeight) + maxColWidth = max(maxColWidth, szX) + cumulativeY += szY - # If this is a TxDP, the above calculation probably didn't do its job - # It is possible, but it's also possible that this Tx has nothing to - # do with our wallet, which is not the focus of the above loop/conditions - # So we choose to pass in the amount we already computed based on extra - # information available in the TxDP structure - if precomputeAmt: - txAmt = precomputeAmt + if rowHeight == None: + rowHeight = float(cumulativeY) / len(strList) + self.moveCursor(origX + maxColWidth, origY, absolute=True) - # This is incorrectly flagging P2Pool outputs as non-std! - #if IsNonStandard: - ## TODO: Need to do something with this non-std tx! - #print '***Non-std transaction!' - #QMessageBox.critical(self, 'Non-Standard Transaction', \ - #'This is a non-standard transaction, which cannot be ' - #'interpretted by this program. DO NOT ASSUME that you ' - #'own these bitcoins, even if you see your address in ' - #'any part of the transaction. Only an expert can tell ' - #'you if and how these coins can be redeemed! \n\n' - #'If you would like more information, please copy the ' - #'information on the next window into an email and send ' - #'it to alan.reiner@gmail.com.', QMessageBox.Ok) + return [QRectF(origX, origY, maxColWidth, cumulativeY), rowHeight] - layout = QGridLayout() - lblDescr = QLabel('Transaction Information:') +class DlgPrintBackup(ArmoryDialog): + """ + Open up a "Make Paper Backup" dialog, so the user can print out a hard + copy of whatever data they need to recover their wallet should they lose + it. - layout.addWidget(lblDescr, 0,0, 1,1) - - frm = QFrame() - frm.setFrameStyle(STYLE_RAISED) - frmLayout = QGridLayout() - lbls = [] + This method is kind of a mess, because it ended up having to support + printing of single-sheet, imported keys, single fragments, multiple + fragments, with-or-without SecurePrint. + """ + def __init__(self, parent, main, wlt, printType='SingleSheet', \ + fragMtrx=[], fragMtrxCrypt=[], fragData=[], + privKey=None, chaincode=None): + super(DlgPrintBackup, self).__init__(parent, main) + self.wlt = wlt + self.binMask = SecureBinaryData(0) + self.binPriv = wlt.addrMap['ROOT'].binPrivKey32_Plain.copy() + self.binChain = wlt.addrMap['ROOT'].chaincode.copy() - # Show the transaction ID, with the user's preferred endianness - # I hate BE, but block-explorer requires it so it's probably a better default - endianness = self.main.getSettingOrSetDefault('PrefEndian', BIGENDIAN) - estr = '' - if self.mode in (USERMODE.Advanced, USERMODE.Expert): - estr = ' (BE)' if endianness==BIGENDIAN else ' (LE)' - - lbls.append([]) - lbls[-1].append(self.main.createToolTipWidget('Unique identifier for this transaction')) - lbls[-1].append(QLabel('Transaction ID' + estr + ':')) - + # This badBackup stuff was implemented to avoid making backups if there is + # an inconsistency in the data. Yes, this is like a goto! + try: + if privKey: + if not chaincode: + raise KeyDataError + self.binPriv = privKey.copy() + self.binChain = chaincode.copy() - # Want to display the hash of the Tx if we have a valid one: - # A TxDP does not have a valid hash until it's completely signed, though - longTxt = '[[ Transaction ID cannot be determined without all signatures ]]' - w,h = relaxedSizeStr(QRichLabel(''), longTxt) + if self.binPriv.getSize() < 32: + raise KeyDataError - tempPyTx = self.pytx.copy() - if pytxdp: - finalTx = pytxdp.getBroadcastTxIfReady() - if finalTx: - tempPyTx = finalTx.copy() - else: - tempPyTx = None - lbls[-1].append(QRichLabel( '' - '[[ Transaction ID cannot be determined without all signatures ]]' - '')) + except: + LOGEXCEPT("Problem with private key and/or chaincode. Aborting.") + QMessageBox.critical(self, tr("Error Creating Backup"), tr(""" + There was an error with the backup creator. The operation is being + canceled to avoid making bad backups!"""), QMessageBox.Ok) + return - if tempPyTx: - if endianness==BIGENDIAN: - lbls[-1].append(QLabel( binary_to_hex(tempPyTx.getHash(), endOut=BIGENDIAN) )) - else: - lbls[-1].append(QLabel( binary_to_hex(tempPyTx.getHash(), endOut=LITTLEENDIAN) )) - lbls[-1][-1].setMinimumWidth(w) - - if self.mode in (USERMODE.Expert,): - # Add protocol version and locktime to the display - lbls.append([]) - lbls[-1].append(self.main.createToolTipWidget('Bitcoin Protocol Version Number')) - lbls[-1].append(QLabel('Tx Version:')) - lbls[-1].append(QLabel( str(self.pytx.version))) - - lbls.append([]) - lbls[-1].append(self.main.createToolTipWidget( - 'The time at which this transaction becomes valid.')) - lbls[-1].append(QLabel('Lock-Time:')) - if self.pytx.lockTime==0: - lbls[-1].append(QLabel('Immediate (0)')) - elif self.pytx.lockTime<500000000: - lbls[-1].append(QLabel('Block %d' % self.pytx.lockTime)) - else: - lbls[-1].append(QLabel(unixTimeToFormatStr(self.pytx.lockTime))) + self.binImport = [] + self.fragMtrx = fragMtrx + self.doPrintFrag = printType.lower().startswith('frag') + self.fragMtrx = fragMtrx + self.fragMtrxCrypt = fragMtrxCrypt + self.fragData = fragData + if self.doPrintFrag: + self.doMultiFrag = len(fragData['Range']) > 1 - - lbls.append([]) - lbls[-1].append(self.main.createToolTipWidget('Comment stored for this transaction in this wallet')) - lbls[-1].append(QLabel('User Comment:')) - if wlt.getComment(txHash): - lbls[-1].append(QRichLabel(wlt.getComment(txHash))) - else: - lbls[-1].append(QRichLabel('[None]')) - + # A self-evident check of whether we need to print the chaincode. + # If we derive the chaincode from the private key, and it matches + # what's already in the wallet, we obviously don't need to print it! + testChain = DeriveChaincodeFromRootKey(self.binPriv) + self.noNeedChaincode = (testChain == self.binChain) - if not data[FIELDS.Time]==None: - lbls.append([]) - if data[FIELDS.Blk]>=2**32-1: - lbls[-1].append(self.main.createToolTipWidget( - 'The time that you computer first saw this transaction')) - else: - lbls[-1].append(self.main.createToolTipWidget( - 'All transactions are eventually included in a "block." The ' - 'time shown here is the time that the block entered the "blockchain."')) - lbls[-1].append(QLabel('Transaction Time:')) - lbls[-1].append(QLabel(data[FIELDS.Time])) + # Save off imported addresses in case they need to be printed, too + for a160, addr in self.wlt.addrMap.iteritems(): + if addr.chainIndex == -2: + if addr.binPrivKey32_Plain.getSize() == 33 or addr.isCompressed(): + prv = addr.binPrivKey32_Plain.toBinStr()[:32] + self.binImport.append([a160, SecureBinaryData(prv), 1]) + prv = None + else: + self.binImport.append([a160, addr.binPrivKey32_Plain.copy(), 0]) - if not data[FIELDS.Blk]==None: - nConf = 0 - if data[FIELDS.Blk]>=2**32-1: - lbls.append([]) - lbls[-1].append(self.main.createToolTipWidget( - 'This transaction has not yet been included in a block. ' - 'It usually takes 5-20 minutes for a transaction to get ' - 'included in a block after the user hits the "Send" button.')) - lbls[-1].append(QLabel('Block Number:')) - lbls[-1].append(QRichLabel( 'Not in the blockchain yet' )) - else: - idxStr = '' - if not data[FIELDS.Idx]==None and self.mode==USERMODE.Expert: - idxStr = ' (Tx #%d)'%data[FIELDS.Idx] - lbls.append([]) - lbls[-1].append(self.main.createToolTipWidget( - 'Every transaction is eventually included in a "block" which ' - 'is where the transaction is permanently recorded. A new block ' - 'is produced approximately every 10 minutes.')) - lbls[-1].append(QLabel('Included in Block:')) - lbls[-1].append(QRichLabel( str(data[FIELDS.Blk]) + idxStr )) - if TheBDM.getBDMState()=='BlockchainReady': - nConf = TheBDM.getTopBlockHeight() - data[FIELDS.Blk] + 1 - lbls.append([]) - lbls[-1].append(self.main.createToolTipWidget( - 'The number of blocks that have been produced since ' - 'this transaction entered the blockchain. A transaciton ' - 'with 6 more confirmations is nearly impossible to reverse.')) - lbls[-1].append(QLabel('Confirmations:')) - lbls[-1].append(QRichLabel( str(nConf))) + # USE PRINTER MASK TO PREVENT NETWORK DEVICES FROM SEEING PRIVATE KEYS + # Hardcode salt & IV because they should *never* change. + # Rainbow tables aren't all that useful here because the user + # is not creating the password -- it's *essentially* randomized + # with 64-bits of real entropy. (though, it is deterministic + # based on the private key, so that printing the backup multiple + # times will produce the same password). + SECPRINT = HardcodedKeyMaskParams() + start = RightNow() + self.randpass = SECPRINT['FUNC_PWD'](self.binPriv + self.binChain) + self.binCrypt32 = SECPRINT['FUNC_KDF'](self.randpass) + LOGINFO('Deriving SecurePrint code took %0.2f seconds' % (RightNow() - start)) + MASK = lambda x: SECPRINT['FUNC_MASK'](x, ekey=self.binCrypt32) - if rvPairDisp==None and precomputeAmt==None: - # Couldn't determine recip/change outputs - lbls.append([]) - lbls[-1].append(self.main.createToolTipWidget( - 'Most transactions have at least a recipient output and a ' - 'returned-change output. You do not have enough enough information ' - 'to determine which is which, and so this fields shows the sum ' - 'of all outputs.')) - lbls[-1].append(QLabel('Sum of Outputs:')) - lbls[-1].append(QLabel( coin2str(txAmt, maxZeros=1).strip() + ' BTC' )) - else: - lbls.append([]) - lbls[-1].append(self.main.createToolTipWidget( - 'Bitcoins were either sent or received, or sent-to-self')) - lbls[-1].append(QLabel('Transaction Direction:')) - lbls[-1].append(QRichLabel( txdir )) + self.binPrivCrypt = MASK(self.binPriv) + self.binChainCrypt = MASK(self.binChain) - lbls.append([]) - lbls[-1].append(self.main.createToolTipWidget( - 'The value shown here is the net effect on your ' - 'wallet, including transaction fee.')) - lbls[-1].append(QLabel('Transaction Amount:')) - lbls[-1].append(QRichLabel( coin2str(txAmt, maxZeros=1).strip() + ' BTC')) - if txAmt<0: - lbls[-1][-1].setText(''+lbls[-1][-1].text()+' ') - elif txAmt>0: - lbls[-1][-1].setText(''+lbls[-1][-1].text()+' ') - - - if not data[FIELDS.SumIn]==None: - fee = data[FIELDS.SumIn]-data[FIELDS.SumOut] - lbls.append([]) - lbls[-1].append(self.main.createToolTipWidget( - 'Transaction fees go to users supplying the Bitcoin network with ' - 'computing power for processing transactions and maintaining security.')) - lbls[-1].append(QLabel('Tx Fee Paid:')) - lbls[-1].append(QLabel( coin2str(fee, maxZeros=0).strip() + ' BTC')) + self.binImportCrypt = [] + for i in range(len(self.binImport)): + self.binImportCrypt.append([ self.binImport[i][0], \ + MASK(self.binImport[i][1]), \ + self.binImport[i][2] ]) + # If there is data in the fragments matrix, also convert it + if len(self.fragMtrx) > 0: + self.fragMtrxCrypt = [] + for sbdX, sbdY in self.fragMtrx: + self.fragMtrxCrypt.append([sbdX.copy(), MASK(sbdY)]) + self.binCrypt32.destroy() + # We need to figure out how many imported keys fit on one page + tempTxtItem = QGraphicsTextItem('') + tempTxtItem.setPlainText(toUnicode('0123QAZjqlmYy')) + tempTxtItem.setFont(GETFONT('Fix', 7)) + self.importHgt = tempTxtItem.boundingRect().height() - 5 - lastRow = 0 - for row,lbl3 in enumerate(lbls): - lastRow = row - for i in range(3): - lbl3[i].setAlignment(Qt.AlignLeft | Qt.AlignVCenter) - lbl3[i].setTextInteractionFlags(Qt.TextSelectableByMouse | \ - Qt.TextSelectableByKeyboard) - frmLayout.addWidget(lbl3[0], row, 0, 1,1) - frmLayout.addWidget(lbl3[1], row, 1, 1,1) - frmLayout.addWidget(lbl3[2], row, 3, 1,2) - spacer = QSpacerItem(20,20) - frmLayout.addItem(spacer, 0, 2, len(lbls), 1) + # Create the scene and the view. + self.scene = SimplePrintableGraphicsScene(self, self.main) + self.view = QGraphicsView() + self.view.setRenderHint(QPainter.TextAntialiasing) + self.view.setScene(self.scene.getScene()) - # Show the list of recipients, if possible - numShow = 3 - rlbls = [] - if not rvPairDisp==None: - numRV = len(rvPairDisp) - for i,rv in enumerate(rvPairDisp): - rlbls.append([]) - if i==0: - rlbls[-1].append(self.main.createToolTipWidget( - 'All outputs of the transaction excluding change-' - 'back-to-sender outputs. If this list does not look ' - 'correct, it is possible that the change-output was ' - 'detected incorrectly -- please check the complete ' - 'input/output list below.')) - rlbls[-1].append(QLabel('Recipients:')) - else: - rlbls[-1].extend([QLabel(), QLabel()]) - rlbls[-1].append(QLabel(hash160_to_addrStr(rv[0]))) - if numRV>1: - rlbls[-1].append(QLabel(coin2str(rv[1], maxZeros=1) + ' BTC')) - else: - rlbls[-1].append(QLabel('')) - ffixBold = GETFONT('Fixed', 10) - ffixBold.setWeight(QFont.Bold) - rlbls[-1][-1].setFont(ffixBold) - - if numRV>numShow and i==numShow-2: - moreStr = '[%d more recipients]' % (numRV-numShow+1) - rlbls.append([]) - rlbls[-1].extend([QLabel(), QLabel(), QLabel(moreStr), QLabel()]) - break - - ### - for i,lbl4 in enumerate(rlbls): - for j in range(4): - lbl4[j].setTextInteractionFlags(Qt.TextSelectableByMouse | \ - Qt.TextSelectableByKeyboard) - row = lastRow + 1 + i - frmLayout.addWidget(lbl4[0], row, 0, 1,1) - frmLayout.addWidget(lbl4[1], row, 1, 1,1) - frmLayout.addWidget(lbl4[2], row, 3, 1,1) - frmLayout.addWidget(lbl4[3], row, 4, 1,1) - + self.chkImportPrint = QCheckBox(tr('Print imported keys')) + self.connect(self.chkImportPrint, SIGNAL(CLICKED), self.clickImportChk) + self.lblPageStr = QRichLabel(tr('Page:')) + self.comboPageNum = QComboBox() + self.lblPageMaxStr = QRichLabel('') + self.connect(self.comboPageNum, SIGNAL('activated(int)'), self.redrawBackup) - # TxIns/Senders - wWlt = relaxedSizeStr(GETFONT('Var'), 'A'*10)[0] - wAddr = relaxedSizeStr(GETFONT('Var'), 'A'*31)[0] - wAmt = relaxedSizeStr(GETFONT('Fixed'), 'A'*20)[0] - if pytxdp: - self.txInModel = TxInDispModel(pytxdp, data[FIELDS.InList], self.main) - else: - self.txInModel = TxInDispModel(pytx, data[FIELDS.InList], self.main) - self.txInView = QTableView() - self.txInView.setModel(self.txInModel) - self.txInView.setSelectionBehavior(QTableView.SelectRows) - self.txInView.setSelectionMode(QTableView.SingleSelection) - self.txInView.horizontalHeader().setStretchLastSection(True) - self.txInView.verticalHeader().setDefaultSectionSize(20) - self.txInView.verticalHeader().hide() - w,h = tightSizeNChar(self.txInView, 1) - self.txInView.setMinimumHeight(2*(1.4*h)) - self.txInView.setMaximumHeight(5*(1.4*h)) - self.txInView.hideColumn(TXINCOLS.OutPt) - self.txInView.hideColumn(TXINCOLS.OutIdx) - self.txInView.hideColumn(TXINCOLS.Script) - - if self.mode==USERMODE.Standard: - initialColResize(self.txInView, [wWlt, wAddr, wAmt, 0, 0, 0, 0, 0, 0]) - self.txInView.hideColumn(TXINCOLS.FromBlk) - self.txInView.hideColumn(TXINCOLS.ScrType) - self.txInView.hideColumn(TXINCOLS.Sequence) - #self.txInView.setSelectionMode(QTableView.NoSelection) - elif self.mode==USERMODE.Advanced: - initialColResize(self.txInView, [0.8*wWlt, 0.6*wAddr, wAmt, 0, 0, 0, 0.2, 0, 0]) - self.txInView.hideColumn(TXINCOLS.FromBlk) - self.txInView.hideColumn(TXINCOLS.Sequence) - #self.txInView.setSelectionMode(QTableView.NoSelection) - elif self.mode==USERMODE.Expert: - self.txInView.resizeColumnsToContents() - - self.txInView.setContextMenuPolicy(Qt.CustomContextMenu) - self.txInView.customContextMenuRequested.connect(self.showContextMenuTxIn) + # We enable printing of imported addresses but not frag'ing them.... way + # too much work for everyone (developer and user) to deal with 2x or 3x + # the amount of data to type + self.chkImportPrint.setVisible(len(self.binImport) > 0 and not self.doPrintFrag) + self.lblPageStr.setVisible(False) + self.comboPageNum.setVisible(False) + self.lblPageMaxStr.setVisible(False) - # List of TxOuts/Recipients - if not precomputeIdxGray==None: - indicesMakeGray = precomputeIdxGray[:] - self.txOutModel = TxOutDispModel(self.pytx, self.main, idxGray=indicesMakeGray) - self.txOutView = QTableView() - self.txOutView.setModel(self.txOutModel) - self.txOutView.setSelectionBehavior(QTableView.SelectRows) - self.txOutView.setSelectionMode(QTableView.SingleSelection) - self.txOutView.verticalHeader().setDefaultSectionSize(20) - self.txOutView.verticalHeader().hide() - self.txOutView.setMinimumHeight(2*(1.3*h)) - self.txOutView.setMaximumHeight(5*(1.3*h)) - initialColResize(self.txOutView, [wWlt, 0.8*wAddr, wAmt, 0.25, 0]) - self.txOutView.hideColumn(TXOUTCOLS.Script) - if self.mode==USERMODE.Standard: - self.txOutView.hideColumn(TXOUTCOLS.ScrType) - initialColResize(self.txOutView, [wWlt, wAddr, 0.25, 0, 0]) - self.txOutView.horizontalHeader().setStretchLastSection(True) - #self.txOutView.setSelectionMode(QTableView.NoSelection) - elif self.mode==USERMODE.Advanced: - initialColResize(self.txOutView, [0.8*wWlt, 0.6*wAddr, wAmt, 0.25, 0]) - #self.txOutView.setSelectionMode(QTableView.NoSelection) - elif self.mode==USERMODE.Expert: - initialColResize(self.txOutView, [wWlt, wAddr, wAmt, 0.25, 0]) - #self.txOutView.resizeColumnsToContents() + self.chkSecurePrint = QCheckBox(tr(""" + Use SecurePrint\xe2\x84\xa2 to prevent exposing keys to printer or other + network devices""")) - self.txOutView.setContextMenuPolicy(Qt.CustomContextMenu) - self.txOutView.customContextMenuRequested.connect(self.showContextMenuTxOut) + if(self.doPrintFrag): + self.chkSecurePrint.setChecked(self.fragData['Secure']) - self.lblTxioInfo = QRichLabel('') - self.lblTxioInfo.setMinimumWidth( tightSizeNChar(self.lblTxioInfo, 30)[0]) - self.connect(self.txInView, SIGNAL('clicked(QModelIndex)'), \ - lambda: self.dispTxioInfo('In')) - self.connect(self.txOutView, SIGNAL('clicked(QModelIndex)'), \ - lambda: self.dispTxioInfo('Out')) - - #scrFrm = QFrame() - #scrFrm.setFrameStyle(STYLE_SUNKEN) - #scrFrmLayout = Q - + self.ttipSecurePrint = self.main.createToolTipWidget(tr(""" + SecurePrint\xe2\x84\xa2 encrypts your backup with a code displayed on + the screen, so that no other devices on your network see the plain + private keys when you send it to the printer. If you turn on + SecurePrint\xe2\x84\xa2 you must write the code on the page after + it is done printing! Turn off this feature if you copy the + "Root Key" and "Chaincode" by hand.""")) + self.lblSecurePrint = QRichLabel(tr(""" + IMPORTANT: You must write the SecurePrint\xe2\x84\xa2 + encryption code on each printed backup page! Your SecurePrint\xe2\x84\xa2 code is + %s. Your backup will not work + if this code is lost! """) % \ + (htmlColor('TextWarn'), htmlColor('TextBlue'), self.randpass.toBinStr(), \ + htmlColor('TextWarn'))) + self.connect(self.chkSecurePrint, SIGNAL("clicked()"), self.redrawBackup) - self.scriptArea = QScrollArea() - self.scriptArea.setWidget(self.lblTxioInfo) - self.scriptFrm = makeLayoutFrame('Horiz', [self.scriptArea]) - #self.scriptFrm.setMaximumWidth(150) - self.scriptArea.setMaximumWidth(200) - self.frmIOList = QFrame() - self.frmIOList.setFrameStyle(STYLE_SUNKEN) - frmIOListLayout = QGridLayout() + btnPrint = QPushButton('&Print...') + btnPrint.setMinimumWidth(3 * tightSizeStr(btnPrint, 'Print...')[0]) + self.btnCancel = QPushButton('&Cancel') + self.connect(btnPrint, SIGNAL(CLICKED), self.print_) + self.connect(self.btnCancel, SIGNAL(CLICKED), self.accept) - lblInputs = QLabel('Transaction Inputs (Sending addresses):') - ttipText = ('All transactions require previous transaction outputs as ' - 'inputs. ') - if not haveBDM: - ttipText += ('Since the blockchain is not available, not all input ' - 'information is available. You need to view this ' - 'transaction on a system with an internet connection ' - '(and blockchain) if you want to see the complete information.') + if self.doPrintFrag: + M, N = self.fragData['M'], self.fragData['N'] + lblDescr = QRichLabel(tr(""" + Print Wallet Backup Fragments

    + When any %d of these fragments are combined, all previous + and future addresses generated by this wallet will be + restored, giving you complete access to your bitcoins. The + data can be copied by hand if a working printer is not + available. Please make sure that all data lines contain + 9 columns + of 4 characters each (excluding "ID" lines).""") % M) else: - ttipText+= ('Each input is like an $X bill. Usually there are more inputs ' - 'than necessary for the transaction, and there will be an extra ' - 'output returning change to the sender') - ttipInputs = self.main.createToolTipWidget(ttipText) + withChain = '' if self.noNeedChaincode else 'and "Chaincode"' + lblDescr = QRichLabel(tr(""" + Print a Forever-Backup

    + Printing this sheet protects all previous and future addresses + generated by this wallet! You can copy the "Root Key" %s + by hand if a working printer is not available. Please make sure that + all data lines contain 9 columns + of 4 characters each.""") % withChain) - lblOutputs = QLabel('Transaction Outputs (Receiving addresses):') - ttipOutputs = self.main.createToolTipWidget( - 'Shows all outputs, including other recipients ' - 'of the same transaction, and change-back-to-sender outputs ' - '(change outputs are displayed in light gray).') - + lblDescr.setContentsMargins(5, 5, 5, 5) + frmDescr = makeHorizFrame([lblDescr], STYLE_RAISED) + self.redrawBackup() + frmChkImport = makeHorizFrame([self.chkImportPrint, \ + STRETCH, \ + self.lblPageStr, \ + self.comboPageNum, \ + self.lblPageMaxStr]) - inStrip = makeLayoutFrame('Horiz', [lblInputs, ttipInputs, 'Stretch']) - outStrip = makeLayoutFrame('Horiz', [lblOutputs, ttipOutputs, 'Stretch']) - - frmIOListLayout.addWidget(inStrip, 0,0, 1,1) - frmIOListLayout.addWidget(self.txInView, 1,0, 1,1) - frmIOListLayout.addWidget(outStrip, 2,0, 1,1) - frmIOListLayout.addWidget(self.txOutView, 3,0, 1,1) - #frmIOListLayout.addWidget(self.lblTxioInfo, 0,1, 4,1) - self.frmIOList.setLayout(frmIOListLayout) + frmSecurePrint = makeHorizFrame([self.chkSecurePrint, + self.ttipSecurePrint, + STRETCH]) - - self.btnIOList = QPushButton('') - self.btnCopy = QPushButton('Copy Raw Tx') - self.lblCopied = QRichLabel('') - self.btnOk = QPushButton('OK') - self.btnIOList.setCheckable(True) - self.connect(self.btnIOList, SIGNAL('clicked()'), self.extraInfoClicked) - self.connect(self.btnOk, SIGNAL('clicked()'), self.accept) - self.connect(self.btnCopy, SIGNAL('clicked()'), self.copyRawTx) + frmButtons = makeHorizFrame([self.btnCancel, STRETCH, btnPrint]) - btnStrip = makeLayoutFrame('Horiz', [self.btnIOList, self.btnCopy, self.lblCopied, 'Stretch', self.btnOk]) - if not self.mode==USERMODE.Expert: - self.btnCopy.setVisible(False) - - - if self.mode==USERMODE.Standard: - self.btnIOList.setChecked(False) - else: - self.btnIOList.setChecked(True) - self.extraInfoClicked() - - - frm.setLayout(frmLayout) - layout.addWidget(frm, 2,0, 1,1) - layout.addWidget(self.scriptArea, 2,1, 1,1) - layout.addWidget(self.frmIOList, 3,0, 1,2) - layout.addWidget(btnStrip, 4,0, 1,2) - - #bbox = QDialogButtonBox(QDialogButtonBox.Ok) - #self.connect(bbox, SIGNAL('accepted()'), self.accept) - #layout.addWidget(bbox, 6,0, 1,1) + layout = QVBoxLayout() + layout.addWidget(frmDescr) + layout.addWidget(frmChkImport) + layout.addWidget(self.view) + layout.addWidget(frmSecurePrint) + layout.addWidget(self.lblSecurePrint) + layout.addWidget(frmButtons) + setLayoutStretch(layout, 0, 1, 0, 0, 0) self.setLayout(layout) - self.layout().setSizeConstraint(QLayout.SetFixedSize) - self.setWindowTitle('Transaction Info') - + self.setWindowIcon(QIcon(':/printer_icon.png')) + self.setWindowTitle('Print Wallet Backup') - def extraInfoClicked(self): - if self.btnIOList.isChecked(): - self.frmIOList.setVisible(True) - self.btnCopy.setVisible(True) - self.lblCopied.setVisible(True) - self.scriptArea.setVisible(self.mode==USERMODE.Expert) - self.btnIOList.setText('<<< Less Info') - else: - self.frmIOList.setVisible(False) - self.scriptArea.setVisible(False) - self.btnCopy.setVisible(False) - self.lblCopied.setVisible(False) - self.btnIOList.setText('Advanced >>>') - - def dispTxioInfo(self, InOrOut): - hexScript = None - headStr = None - if InOrOut=='In': - selection = self.txInView.selectedIndexes() - if len(selection)==0: - return - row = selection[0].row() - hexScript = str(self.txInView.model().index(row, TXINCOLS.Script).data().toString()) - headStr = 'TxIn Script:' - elif InOrOut=='Out': - selection = self.txOutView.selectedIndexes() - if len(selection)==0: - return - row = selection[0].row() - hexScript = str(self.txOutView.model().index(row, TXOUTCOLS.Script).data().toString()) - headStr = 'TxOut Script:' + # Apparently I can't programmatically scroll until after it's painted + def scrollTop(): + vbar = self.view.verticalScrollBar() + vbar.setValue(vbar.minimum()) + from twisted.internet import reactor + reactor.callLater(0.01, scrollTop) - if hexScript: - oplist = convertScriptToOpStrings(hex_to_binary(hexScript)) - opprint = [] - for op in oplist: - if len(op)==40 and not '[' in op: - opprint.append(op + ' (%s)' % \ - hash160_to_addrStr(hex_to_binary(op))) - elif len(op)==130 and not '[' in op: - opprint.append(op + ' (%s)' % \ - hash160_to_addrStr(hash160(hex_to_binary(op)))) - else: - opprint.append(op) - lblScript = QRichLabel('') - lblScript.setText('Script:

    ' + '
    '.join(opprint)) - lblScript.setWordWrap(False) - lblScript.setTextInteractionFlags(Qt.TextSelectableByMouse | \ - Qt.TextSelectableByKeyboard) + # if len(self.bin + # reactor.callLater(0.5, warnImportedKeys) - self.scriptArea.setWidget( makeLayoutFrame('Vert', [lblScript])) - self.scriptArea.setMaximumWidth(200) - - def copyRawTx(self): - clipb = QApplication.clipboard() - clipb.clear() - clipb.setText(binary_to_hex(self.pytx.serialize())) - self.lblCopied.setText('Copied to Clipboard!') + def redrawBackup(self): + cmbPage = 1 + if self.comboPageNum.count() > 0: + cmbPage = int(str(self.comboPageNum.currentText())) - - ############################################################################# - def showContextMenuTxIn(self, pos): - menu = QMenu(self.txInView) - std = (self.main.usermode==USERMODE.Standard) - adv = (self.main.usermode==USERMODE.Advanced) - dev = (self.main.usermode==USERMODE.Expert) - - if True: actCopySender = menu.addAction("Copy Sender Address") - if True: actCopyWltID = menu.addAction("Copy Wallet ID") - if True: actCopyAmount = menu.addAction("Copy Amount") - if dev: actCopyOutPt = menu.addAction("Copy Outpoint") - if dev: actCopyScript = menu.addAction("Copy Raw Script") - idx = self.txInView.selectedIndexes()[0] - action = menu.exec_(QCursor.pos()) + if self.doPrintFrag: + cmbPage -= 1 + if not self.doMultiFrag: + cmbPage = self.fragData['Range'][0] + elif self.comboPageNum.count() > 0: + cmbPage = int(str(self.comboPageNum.currentText())) - 1 - if action==actCopyWltID: - s = str(self.txInView.model().index(idx.row(), TXINCOLS.WltID).data().toString()) - elif action==actCopySender: - s = str(self.txInView.model().index(idx.row(), TXINCOLS.Sender).data().toString()) - elif action==actCopyAmount: - s = str(self.txInView.model().index(idx.row(), TXINCOLS.Btc).data().toString()) - elif dev and action==actCopyOutPt: - s1 = str(self.txInView.model().index(idx.row(), TXINCOLS.OutPt).data().toString()) - s2 = str(self.txInView.model().index(idx.row(), TXINCOLS.OutIdx).data().toString()) - s = s1 + ':' + s2 - elif dev and action==actCopyScript: - s = str(self.txInView.model().index(idx.row(), TXINCOLS.Script).data().toString()) + self.createPrintScene('Fragmented Backup', cmbPage) else: - return + pgSelect = cmbPage if self.chkImportPrint.isChecked() else 1 + if pgSelect == 1: + self.createPrintScene('SingleSheetFirstPage', '') + else: + pg = pgSelect - 2 + nKey = self.maxKeysPerPage + self.createPrintScene('SingleSheetImported', [pg * nKey, (pg + 1) * nKey]) - clipb = QApplication.clipboard() - clipb.clear() - clipb.setText(s.strip()) - - ############################################################################# - def showContextMenuTxOut(self, pos): - menu = QMenu(self.txOutView) - std = (self.main.usermode==USERMODE.Standard) - adv = (self.main.usermode==USERMODE.Advanced) - dev = (self.main.usermode==USERMODE.Expert) - - if True: actCopySender = menu.addAction("Copy Recipient Address") - if True: actCopyWltID = menu.addAction("Copy Wallet ID") - if True: actCopyAmount = menu.addAction("Copy Amount") - if dev: actCopyScript = menu.addAction("Copy Raw Script") - idx = self.txOutView.selectedIndexes()[0] - action = menu.exec_(QCursor.pos()) - - if action==actCopyWltID: - s = self.txOutView.model().index(idx.row(), TXOUTCOLS.WltID).data().toString() - elif action==actCopySender: - s = self.txOutView.model().index(idx.row(), TXOUTCOLS.Recip).data().toString() - elif action==actCopyAmount: - s = self.txOutView.model().index(idx.row(), TXOUTCOLS.Btc).data().toString() - elif dev and action==actCopyScript: - s = self.txOutView.model().index(idx.row(), TXOUTCOLS.Script).data().toString() - else: - return - clipb = QApplication.clipboard() - clipb.clear() - clipb.setText(str(s).strip()) + showPageCombo = self.chkImportPrint.isChecked() or \ + (self.doPrintFrag and self.doMultiFrag) + self.showPageSelect(showPageCombo) + self.view.update() + def clickImportChk(self): + if self.numImportPages > 1 and self.chkImportPrint.isChecked(): + ans = QMessageBox.warning(self, tr('Lots to Print!'), tr(""" + This wallet contains %d imported keys, which will require + %d pages to print. Not only will this use a lot of paper, + it will be a lot of work to manually type in these keys in the + event that you need to restore this backup. It is recommended + that you do not print your imported keys and instead make + a digital backup, which can be restored instantly if needed. +

    Do you want to print the imported keys, anyway?""") % \ + (len(self.binImport), self.numImportPages), \ + QMessageBox.Yes | QMessageBox.No) + if not ans == QMessageBox.Yes: + self.chkImportPrint.setChecked(False) + showPageCombo = self.chkImportPrint.isChecked() or \ + (self.doPrintFrag and self.doMultiFrag) + self.showPageSelect(showPageCombo) + self.comboPageNum.setCurrentIndex(0) + self.redrawBackup() -class GfxViewPaper(QGraphicsView): - def __init__(self, parent=None, main=None): - super(GfxViewPaper, self).__init__(parent) - self.setRenderHint(QPainter.TextAntialiasing) -class GfxItemText(QGraphicsTextItem): - """ - So far, I'm pretty bad at setting the boundingRect properly. I have - hacked it to be usable for this specific situation, but it's not very - reusable... - """ - def __init__(self, text, position, scene, font=GETFONT('Courier',8), lineWidth=None): - super(GfxItemText, self).__init__(text) - self.setFont(font) - self.setPos(position) - if lineWidth: - self.setTextWidth(lineWidth) + def showPageSelect(self, doShow=True): + MARGIN = self.scene.MARGIN_PIXELS + bottomOfPage = self.scene.pageRect().height() + MARGIN + totalHgt = bottomOfPage - self.bottomOfSceneHeader + self.maxKeysPerPage = int(totalHgt / (self.importHgt)) + self.numImportPages = (len(self.binImport) - 1) / self.maxKeysPerPage + 1 + if self.comboPageNum.count() == 0: + if self.doPrintFrag: + numFrag = len(self.fragData['Range']) + for i in range(numFrag): + self.comboPageNum.addItem(str(i + 1)) + self.lblPageMaxStr.setText(tr('of %d') % (numFrag,)) + else: + for i in range(self.numImportPages + 1): + self.comboPageNum.addItem(str(i + 1)) + self.lblPageMaxStr.setText(tr('of %d') % (self.numImportPages + 1,)) - self.setDefaultTextColor(self.PAGE_TEXT_COLOR) - def boundingRect(self): - w,h = relaxedSizeStr(self, self.toPlainText()) - nLine=1 - if self.textWidth()>0: - twid = self.textWidth() - nLine = max(1, int(float(w) / float(twid) + 0.5)) - return QRectF(0, 0, w, nLine*(1.5*h)) + self.lblPageStr.setVisible(doShow) + self.comboPageNum.setVisible(doShow) + self.lblPageMaxStr.setVisible(doShow) - -class GfxItemQRCode(QGraphicsItem): - """ - Converts binary data to base58, and encodes the Base58 characters in - the QR-code. It seems weird to use Base58 instead of binary, but the - QR-code has no problem with the size, instead, we want the data in the - QR-code to match exactly what is human-readable on the page, which is - in Base58. - You must supply exactly one of "totalSize" or "modSize". TotalSize - guarantees that the QR code will fit insides box of a given size. - ModSize is how big each module/pixel of the QR code is, which means - that a bigger QR block results in a bigger physical size on paper. - """ - def __init__(self, rawDataToEncode, maxSize=None): - super(GfxItemQRCode, self).__init__() - self.maxSize = maxSize - self.updateQRData(rawDataToEncode) - def boundingRect(self): - return self.Rect - def updateQRData(self, toEncode, maxSize=None): - if maxSize==None: - maxSize = self.maxSize - else: - self.maxSize = maxSize + def print_(self): + LOGINFO('Printing!') + self.printer = QPrinter(QPrinter.HighResolution) + self.printer.setPageSize(QPrinter.Letter) - self.qrmtrx, self.modCt = CreateQRMatrix(toEncode, 'H') - self.modSz = round(float(self.maxSize)/ float(self.modCt) - 0.5) - totalSize = self.modCt*self.modSz - self.Rect = QRectF(0,0, totalSize, totalSize) - - def paint(self, painter, option, widget=None): - painter.setPen(Qt.NoPen) - painter.setBrush(QBrush(QColor(0,0,0))) + if QPrintDialog(self.printer).exec_(): + painter = QPainter(self.printer) + painter.setRenderHint(QPainter.TextAntialiasing) - for r in range(self.modCt): - for c in range(self.modCt): - if self.qrmtrx[r][c] > 0: - painter.drawRect(*[self.modSz*a for a in [r,c,1,1]]) + if self.doPrintFrag: + for i in self.fragData['Range']: + self.createPrintScene('Fragment', i) + self.scene.getScene().render(painter) + if not i == len(self.fragData['Range']) - 1: + self.printer.newPage() + else: + self.createPrintScene('SingleSheetFirstPage', '') + self.scene.getScene().render(painter) -class SimplePrintableGraphicsScene(object): + if len(self.binImport) > 0 and self.chkImportPrint.isChecked(): + nKey = self.maxKeysPerPage + for i in range(self.numImportPages): + self.printer.newPage() + self.createPrintScene('SingleSheetImported', [i * nKey, (i + 1) * nKey]) + self.scene.getScene().render(painter) + painter.end() - def __init__(self, parent, main): - """ - We use the following coordinates: - - -----> +x - | - | - V +y + # The last scene printed is what's displayed now. Set the combo box + self.comboPageNum.setCurrentIndex(self.comboPageNum.count() - 1) - """ - self.parent = parent - self.main = main + if self.chkSecurePrint.isChecked(): + QMessageBox.warning(self, 'SecurePrint Code', tr(""" +
    You must write your SecurePrint\xe2\x84\xa2 + code on each sheet of paper you just printed! + Write it in the red box in upper-right corner + of each printed page.

    SecurePrint\xe2\x84\xa2 code: + %s

    + NOTE: the above code is case-sensitive!""") % \ + (htmlColor('TextBlue'), self.randpass.toBinStr()), \ + QMessageBox.Ok) + if self.chkSecurePrint.isChecked(): + self.btnCancel.setText('Done') + else: + self.accept() - self.INCH = 72 - self.PAPER_A4_WIDTH = 8.5*self.INCH - self.PAPER_A4_HEIGHT = 11.0*self.INCH - self.MARGIN_PIXELS = 0.6*self.INCH - self.PAGE_BKGD_COLOR = QColor(255,255,255) - self.PAGE_TEXT_COLOR = QColor( 0, 0, 0) + def cleanup(self): + self.binPriv.destroy() + self.binChain.destroy() + self.binPrivCrypt.destroy() + self.binChainCrypt.destroy() + self.randpass.destroy() + for a160, priv, compr in self.binImport: + priv.destroy() - self.fontFix = GETFONT('Courier', 9) - self.fontVar = GETFONT('Times', 10) - - self.gfxScene = QGraphicsScene(self.parent) - self.gfxScene.setSceneRect(0,0, self.PAPER_A4_WIDTH, self.PAPER_A4_HEIGHT) - self.gfxScene.setBackgroundBrush(self.PAGE_BKGD_COLOR) + for x, y in self.fragMtrxCrypt: + x.destroy() + y.destroy() - # For when it eventually makes it to the printer - #self.printer = QPrinter(QPrinter.HighResolution) - #self.printer.setPageSize(QPrinter.Letter) - #self.gfxPainter = QPainter(self.printer) - #self.gfxPainter.setRenderHint(QPainter.TextAntialiasing) - #self.gfxPainter.setPen(Qt.NoPen) - #self.gfxPainter.setBrush(QBrush(self.PAGE_TEXT_COLOR)) + def accept(self): + self.cleanup() + super(DlgPrintBackup, self).accept() - self.cursorPos = QPointF(self.MARGIN_PIXELS, self.MARGIN_PIXELS) - self.lastCursorMove = (0,0) + def reject(self): + self.cleanup() + super(DlgPrintBackup, self).reject() - - def getCursorXY(self): - return (self.cursorPos.x(), self.cursorPos.y()) - def getScene(self): - return self.gfxScene - - def pageRect(self): - marg = self.MARGIN_PIXELS - return QRectF(marg, marg, self.PAPER_A4_WIDTH-marg, self.PAPER_A4_HEIGHT-marg) + ############################################################################# + ############################################################################# + def createPrintScene(self, printType, printData): + self.scene.gfxScene.clear() + self.scene.resetCursor() - def insidePageRect(self, pt=None): - if pt==None: - pt = self.cursorPos + pr = self.scene.pageRect() + self.scene.drawRect(pr.width(), pr.height(), edgeColor=None, fillColor=QColor(255, 255, 255)) + self.scene.resetCursor() - return self.pageRect.contains(pt) - - def moveCursor(self, dx, dy, absolute=False): - xOld,yOld = self.getCursorXY() - if absolute: - self.cursorPos = QPointF(dx,dy) - self.lastCursorMove = (dx-xOld, dy-yOld) - else: - self.cursorPos = QPointF(xOld+dx, yOld+dy) - self.lastCursorMove = (dx, dy) + INCH = self.scene.INCH + MARGIN = self.scene.MARGIN_PIXELS - def resetScene(self): - self.gfxScene.clear() - self.resetCursor() + doMask = self.chkSecurePrint.isChecked() - def resetCursor(self): - self.cursorPos = QPointF(self.MARGIN_PIXELS, self.MARGIN_PIXELS) + if USE_TESTNET: + self.scene.drawPixmapFile(':/armory_logo_green_h56.png') + else: + self.scene.drawPixmapFile(':/armory_logo_h36.png') + self.scene.newLine() + self.scene.drawText('Paper Backup for Armory Wallet', GETFONT('Var', 11)) + self.scene.newLine() + self.scene.drawText('http://www.bitcoinarmory.com') - def newLine(self, extra_dy=0): - xOld,yOld = self.getCursorXY() - xNew = self.MARGIN_PIXELS - yNew = self.cursorPos.y() + self.lastItemSize[1] + extra_dy - 5 - self.moveCursor(xNew-xOld, yNew-yOld) + self.scene.newLine(extra_dy=20) + self.scene.drawHLine() + self.scene.newLine(extra_dy=20) - def drawHLine(self, width=None, penWidth=1): - if width==None: - width = 3*self.INCH - currX,currY = self.getCursorXY() - lineItem = QGraphicsLineItem(currX, currY, currX+width, currY) - pen = QPen() - pen.setWidth(penWidth) - lineItem.setPen( pen) - self.gfxScene.addItem(lineItem) - rect = lineItem.boundingRect() - self.lastItemSize = (rect.width(), rect.height()) - self.moveCursor(rect.width(), 0) - return self.lastItemSize + ssType = ' (SecurePrint\xe2\x84\xa2)' if doMask else ' (Unencrypted)' + if printType == 'SingleSheetFirstPage': + bType = tr('Single-Sheet ' + ssType) + elif printType == 'SingleSheetImported': + bType = tr('Imported Keys ' + ssType) + elif printType.lower().startswith('frag'): + bstr = tr('Fragmented Backup (%d-of-%d)') % (self.fragData['M'], self.fragData['N']) + bType = bstr + ' ' + tr(ssType) - def drawRect(self, w, h, edgeColor=QColor(0,0,0), fillColor=None, penWidth=1): - rectItem = QGraphicsRectItem(self.cursorPos.x(), self.cursorPos.y(), w, h) - if edgeColor==None: - rectItem.setPen(QPen(Qt.NoPen)) + if printType.startswith('SingleSheet'): + colRect, rowHgt = self.scene.drawColumn(['Wallet Version:', 'Wallet ID:', \ + 'Wallet Name:', 'Backup Type:']) + self.scene.moveCursor(15, 0) + suf = 'c' if self.noNeedChaincode else 'a' + colRect, rowHgt = self.scene.drawColumn(['1.35' + suf, self.wlt.uniqueIDB58, \ + self.wlt.labelName, bType]) + self.scene.moveCursor(15, colRect.y() + colRect.height(), absolute=True) else: - pen = QPen(edgeColor) - pen.setWidth(penWidth) - rectItem.setPen(pen) + colRect, rowHgt = self.scene.drawColumn(['Wallet Version:', 'Wallet ID:', \ + 'Wallet Name:', 'Backup Type:', \ + 'Fragment:']) + baseID = self.fragData['FragIDStr'] + fragNum = printData + 1 + fragID = tr('%s-#%d') % \ + (baseID, htmlColor('TextBlue'), fragNum) + self.scene.moveCursor(15, 0) + suf = 'c' if self.noNeedChaincode else 'a' + colRect, rowHgt = self.scene.drawColumn(['1.35' + suf, self.wlt.uniqueIDB58, \ + self.wlt.labelName, bType, fragID]) + self.scene.moveCursor(15, colRect.y() + colRect.height(), absolute=True) - if fillColor==None: - rectItem.setBrush(QBrush(Qt.NoBrush)) - else: - rectItem.setBrush(QBrush(fillColor)) - self.gfxScene.addItem(rectItem) - rect = rectItem.boundingRect() - self.lastItemSize = (rect.width(), rect.height()) - self.moveCursor(rect.width(), 0) - return self.lastItemSize - + # Display warning about unprotected key data + wrap = 0.9 * self.scene.pageRect().width() - def drawText(self, txt, font=None, wrapWidth=None, useHtml=True): - if font==None: - font = GETFONT('Var',9) - txtItem = QGraphicsTextItem('') - if useHtml: - txtItem.setHtml(toUnicode(txt)) + if self.doPrintFrag: + warnMsg = tr(""" + Any subset of %d fragments with this + ID (%s) are sufficient to recover all the + coins contained in this wallet. To optimize the physical security of + your wallet, please store the fragments in different locations.""") % \ + (htmlColor('TextBlue'), self.fragData['M'], \ + htmlColor('TextBlue'), self.fragData['FragIDStr']) else: - txtItem.setPlainText(toUnicode(txt)) - txtItem.setDefaultTextColor(self.PAGE_TEXT_COLOR) - txtItem.setPos(self.cursorPos) - txtItem.setFont(font) - if not wrapWidth==None: - txtItem.setTextWidth(wrapWidth) - self.gfxScene.addItem(txtItem) - rect = txtItem.boundingRect() - self.lastItemSize = (rect.width(), rect.height()) - self.moveCursor(rect.width(), 0) - return self.lastItemSize + container = 'this wallet' if printType == 'SingleSheetFirstPage' else 'these addresses' + warnMsg = tr(""" + WARNING: Anyone who has access to this + page has access to all the bitcoins in %s! Please keep this + page in a safe place.""" % container) - def drawPixmapFile(self, pixFn, sizePx=None): - pix = QPixmap(pixFn) - if not sizePx==None: - pix = pix.scaled(sizePx, sizePx) - pixItem = QGraphicsPixmapItem( pix ) - pixItem.setPos( self.cursorPos ) - pixItem.setMatrix( QMatrix() ) - self.gfxScene.addItem(pixItem) - rect = pixItem.boundingRect() - self.lastItemSize = (rect.width(), rect.height()) - self.moveCursor(rect.width(), 0) - return self.lastItemSize - - def drawQR(self, qrdata, size=150): - objQR = GfxItemQRCode(qrdata, size) - objQR.setPos(self.cursorPos) - objQR.setMatrix(QMatrix()) - self.gfxScene.addItem( objQR ) - rect = objQR.boundingRect() - self.lastItemSize = (rect.width(), rect.height()) - self.moveCursor(rect.width(), 0) - return self.lastItemSize + self.scene.newLine() + self.scene.drawText(warnMsg, GETFONT('Var', 9), wrapWidth=wrap) - - def drawColumn(self, strList, rowHeight=None, font=None, useHtml=True): - """ - This draws a bunch of left-justified strings in a column. It returns - a tight bounding box around all elements in the column, which can easily - be used to start the next column. The rowHeight is returned, and also - an available input, in case you are drawing text/font that has a different - height in each column, and want to make sure they stay aligned. + self.scene.newLine(extra_dy=20) + self.scene.drawHLine() + self.scene.newLine(extra_dy=20) - Just like the other methods, this leaves the cursor sitting at the - original y-value, but shifted to the right by the width of the column. - """ - origX, origY = self.getCursorXY() - maxColWidth = 0 - cumulativeY = 0 - for r in strList: - szX, szY = self.drawText(tr(r), font=font, useHtml=useHtml) - prevY = self.cursorPos.y() - if rowHeight==None: - self.newLine() - szY = self.cursorPos.y() - prevY - self.moveCursor(origX-self.MARGIN_PIXELS, 0) - else: - self.moveCursor(-szX, rowHeight) - maxColWidth = max(maxColWidth, szX) - cumulativeY += szY - - if rowHeight==None: - rowHeight = float(cumulativeY)/len(strList) + if self.doPrintFrag: + numLine = 'three' if self.noNeedChaincode else 'five' + else: + numLine = 'two' if self.noNeedChaincode else 'four' - self.moveCursor(origX + maxColWidth, origY, absolute=True) - - return [QRectF(origX, origY, maxColWidth, cumulativeY), rowHeight] + if printType == 'SingleSheetFirstPage': + descrMsg = tr(""" + The following %s lines backup all addresses + ever generated by this wallet (previous and future). + This can be used to recover your wallet if you forget your passphrase or + suffer hardware failure and lose your wallet files. """ % numLine) + elif printType == 'SingleSheetImported': + if self.chkSecurePrint.isChecked(): + descrMsg = tr(""" + The following is a list of all private keys imported into your + wallet before this backup was made. These keys are encrypted + with the SecurePrint\xe2\x84\xa2 code and can only be restored + by entering them into Armory. Print a copy of this backup without + the SecurePrint\xe2\x84\xa2 option if you want to be able to import + them into another application.""") + else: + descrMsg = tr(""" + The following is a list of all private keys imported into your + wallet before this backup was made. Each one must be copied + manually into the application where you wish to import them. """) + elif printType.lower().startswith('frag'): + fragNum = printData + 1 + descrMsg = tr(""" + The following is fragment #%d for this + wallet. """) % (htmlColor('TextBlue'), printData + 1) + self.scene.drawText(descrMsg, GETFONT('var', 8), wrapWidth=wrap) + self.scene.newLine(extra_dy=10) -class DlgPrintBackup(ArmoryDialog): - """ - Open up a "Make Paper Backup" dialog, so the user can print out a hard - copy of whatever data they need to recover their wallet should they lose - it. + ########################################################################### + # Draw the SecurePrint box if needed, frag pie, then return cursor + prevCursor = self.scene.getCursorXY() - This method is kind of a mess, because it ended up having to support - printing of single-sheet, imported keys, single fragments, multiple - fragments, with-or-without SecurePrint. - """ - def __init__(self, parent, main, wlt, printType='SingleSheet', \ - fragMtrx=[], fragMtrxCrypt=[], fragData=[], - privKey=None, chaincode=None): - super(DlgPrintBackup, self).__init__(parent, main) + self.lblSecurePrint.setVisible(doMask) + if doMask: + self.scene.resetCursor() + self.scene.moveCursor(4.0 * INCH, 0) + spWid, spHgt = 2.75 * INCH, 1.5 * INCH, + if doMask: + self.scene.drawRect(spWid, spHgt, edgeColor=QColor(180, 0, 0), penWidth=3) + self.scene.resetCursor() + self.scene.moveCursor(4.07 * INCH, 0.07 * INCH) - self.wlt = wlt - self.binMask = SecureBinaryData(0) - self.binPriv = wlt.addrMap['ROOT'].binPrivKey32_Plain.copy() - self.binChain = wlt.addrMap['ROOT'].chaincode.copy() - - # This badBackup stuff was implemented to avoid making backups if there is - # an inconsistency in the data. Yes, this is like a goto! - try: - if privKey: - if not chaincode: - raise KeyDataError - self.binPriv = privKey.copy() - self.binChain = chaincode.copy() - - if self.binPriv.getSize() < 32: - raise KeyDataError - - except: - LOGEXCEPT("Problem with private key and/or chaincode. Aborting.") - QMessageBox.critical(self, tr("Error Creating Backup"), tr(""" - There was an error with the backup creator. The operation is being - canceled to avoid making bad backups!"""), QMessageBox.Ok) - return - - - self.binImport = [] - self.fragMtrx = fragMtrx + self.scene.drawText(tr(""" + CRITICAL: This backup will not + work without the SecurePrint\xe2\x84\xa2 + code displayed on the screen during printing. + Copy it here in ink:"""), wrapWidth=spWid * 0.93, font=GETFONT('Var', 7)) - self.doPrintFrag = printType.lower().startswith('frag') - self.fragMtrx = fragMtrx - self.fragMtrxCrypt = fragMtrxCrypt - self.fragData = fragData - if self.doPrintFrag: - self.doMultiFrag = len(fragData['Range'])>1 + self.scene.newLine(extra_dy=8) + self.scene.moveCursor(4.07 * INCH, 0) + codeWid, codeHgt = self.scene.drawText('Code:') + self.scene.moveCursor(0, codeHgt - 3) + wid = spWid - codeWid + w, h = self.scene.drawHLine(width=wid * 0.9, penWidth=2) - # A self-evident check of whether we need to print the chaincode. - # If we derive the chaincode from the private key, and it matches - # what's already in the wallet, we obviously don't need to print it! - testChain = DeriveChaincodeFromRootKey(self.binPriv) - self.noNeedChaincode = (testChain == self.binChain) - # Save off imported addresses in case they need to be printed, too - for a160,addr in self.wlt.addrMap.iteritems(): - if addr.chainIndex==-2: - if addr.binPrivKey32_Plain.getSize()==33 or addr.isCompressed(): - prv = addr.binPrivKey32_Plain.toBinStr()[:32] - self.binImport.append( [a160, SecureBinaryData(prv), 1]) - prv = None - else: - self.binImport.append( [a160, addr.binPrivKey32_Plain.copy(), 0]) + # Done drawing other stuff, so return to the original drawing location + self.scene.moveCursor(*prevCursor, absolute=True) + ########################################################################### - # USE PRINTER MASK TO PREVENT NETWORK DEVICES FROM SEEING PRIVATE KEYS - # Hardcode salt & IV because they should *never* change. - # Rainbow tables aren't all that useful here because the user - # is not creating the password -- it's *essentially* randomized - # with 64-bits of real entropy. (though, it is deterministic - # based on the private key, so that printing the backup multiple - # times will produce the same password). - SECPRINT = HardcodedKeyMaskParams() - start = RightNow() - self.randpass = SECPRINT['FUNC_PWD'](self.binPriv + self.binChain) - self.binCrypt32 = SECPRINT['FUNC_KDF'](self.randpass) - LOGINFO('Deriving SecurePrint code took %0.2f seconds' % (RightNow() - start)) + ########################################################################### + # Finally, draw the backup information. - MASK = lambda x: SECPRINT['FUNC_MASK'](x, ekey=self.binCrypt32) + # If this page is only imported addresses, draw them then bail + self.bottomOfSceneHeader = self.scene.cursorPos.y() + if printType == 'SingleSheetImported': + self.scene.moveCursor(0, 0.1 * INCH) + importList = self.binImport + if self.chkSecurePrint.isChecked(): + importList = self.binImportCrypt - self.binPrivCrypt = MASK(self.binPriv) - self.binChainCrypt = MASK(self.binChain) + for a160, priv, isCompr in importList[printData[0]:printData[1]]: + comprByte = ('\x01' if isCompr == 1 else '') + prprv = encodePrivKeyBase58(priv.toBinStr() + comprByte) + toPrint = [prprv[i * 6:(i + 1) * 6] for i in range((len(prprv) + 5) / 6)] + addrHint = ' (%s...)' % hash160_to_addrStr(a160)[:12] + self.scene.drawText(' '.join(toPrint), GETFONT('Fix', 7)) + self.scene.moveCursor(0.02 * INCH, 0) + self.scene.drawText(addrHint, GETFONT('Var', 7)) + self.scene.newLine(extra_dy=-3) + prprv = None + return - self.binImportCrypt = [] - for i in range(len(self.binImport)): - self.binImportCrypt.append([ self.binImport[i][0], \ - MASK(self.binImport[i][1]), \ - self.binImport[i][2] ]) - # If there is data in the fragments matrix, also convert it - if len(self.fragMtrx)>0: - self.fragMtrxCrypt = [] - for sbdX,sbdY in self.fragMtrx: - self.fragMtrxCrypt.append( [sbdX.copy(), MASK(sbdY)] ) - - self.binCrypt32.destroy() + if self.doPrintFrag: + M = self.fragData['M'] + Lines = [] + Prefix = [] + fmtrx = self.fragMtrxCrypt if doMask else self.fragMtrx + try: + yBin = fmtrx[printData][1].toBinStr() + binID = self.wlt.uniqueIDBin + IDLine = ComputeFragIDLineHex(M, printData, binID, doMask, addSpaces=True) + if len(yBin) == 32: + Prefix.append('ID:'); Lines.append(IDLine) + Prefix.append('F1:'); Lines.append(makeSixteenBytesEasy(yBin[:16 ])) + Prefix.append('F2:'); Lines.append(makeSixteenBytesEasy(yBin[ 16:])) + elif len(yBin) == 64: + Prefix.append('ID:'); Lines.append(IDLine) + Prefix.append('F1:'); Lines.append(makeSixteenBytesEasy(yBin[:16 ])) + Prefix.append('F2:'); Lines.append(makeSixteenBytesEasy(yBin[ 16:32 ])) + Prefix.append('F3:'); Lines.append(makeSixteenBytesEasy(yBin[ 32:48 ])) + Prefix.append('F4:'); Lines.append(makeSixteenBytesEasy(yBin[ 48:])) + else: + LOGERROR('yBin is not 32 or 64 bytes! It is %s bytes', len(yBin)) + finally: + yBin = None - # We need to figure out how many imported keys fit on one page - tempTxtItem = QGraphicsTextItem('') - tempTxtItem.setPlainText(toUnicode('0123QAZjqlmYy')) - tempTxtItem.setFont(GETFONT('Fix',7)) - self.importHgt = tempTxtItem.boundingRect().height() - 5 - - - # Create the scene and the view. - self.scene = SimplePrintableGraphicsScene(self, self.main) - self.view = QGraphicsView() - self.view.setRenderHint(QPainter.TextAntialiasing) - self.view.setScene(self.scene.getScene()) + else: + # Single-sheet backup + if doMask: + code12 = self.binPrivCrypt.toBinStr() + code34 = self.binChainCrypt.toBinStr() + else: + code12 = self.binPriv.toBinStr() + code34 = self.binChain.toBinStr() - - self.chkImportPrint = QCheckBox(tr('Print imported keys')) - self.connect(self.chkImportPrint, SIGNAL('clicked()'), self.clickImportChk) - self.lblPageStr = QRichLabel(tr('Page:')) - self.comboPageNum = QComboBox() - self.lblPageMaxStr = QRichLabel('') - self.connect(self.comboPageNum, SIGNAL('activated(int)'), self.redrawBackup) + Lines = [] + Prefix = [] + Prefix.append('Root Key:'); Lines.append(makeSixteenBytesEasy(code12[:16])) + Prefix.append(''); Lines.append(makeSixteenBytesEasy(code12[16:])) + Prefix.append('Chaincode:'); Lines.append(makeSixteenBytesEasy(code34[:16])) + Prefix.append(''); Lines.append(makeSixteenBytesEasy(code34[16:])) - # We enable printing of imported addresses but not frag'ing them.... way - # too much work for everyone (developer and user) to deal with 2x or 3x - # the amount of data to type - self.chkImportPrint.setVisible( len(self.binImport)>0 and not self.doPrintFrag) - self.lblPageStr.setVisible(False) - self.comboPageNum.setVisible(False) - self.lblPageMaxStr.setVisible(False) + if self.noNeedChaincode: + Prefix = Prefix[:2] + Lines = Lines[:2] - self.chkSecurePrint = QCheckBox(tr( """ - Use SecurePrint\xe2\x84\xa2 to prevent exposing keys to printer or other - network devices""")) + # Draw the prefix + origX, origY = self.scene.getCursorXY() + self.scene.moveCursor(20, 0) + colRect, rowHgt = self.scene.drawColumn(['' + l + '' for l in Prefix]) - if(self.doPrintFrag): - self.chkSecurePrint.setChecked(self.fragData['Secure']) + nudgeDown = 2 # because the differing font size makes it look unaligned + self.scene.moveCursor(20, nudgeDown) + self.scene.drawColumn(Lines, + font=GETFONT('Fixed', 8, bold=True), \ + rowHeight=rowHgt, + useHtml=False) - self.ttipSecurePrint = self.main.createToolTipWidget( tr(""" - SecurePrint\xe2\x84\xa2 encrypts your backup with a code displayed on - the screen, so that no other devices on your network see the plain - private keys when you send it to the printer. If you turn on - SecurePrint\xe2\x84\xa2 you must write the code on the page after - it is done printing! Turn off this feature if you copy the - "Root Key" and "Chaincode" by hand.""")) - self.lblSecurePrint = QRichLabel(tr(""" - IMPORTANT: You must write the SecurePrint\xe2\x84\xa2 - encryption code on each printed backup page! Your SecurePrint\xe2\x84\xa2 code is - %s. Your backup will not work - if this code is lost! """) % \ - (htmlColor('TextWarn'), htmlColor('TextBlue'), self.randpass.toBinStr(), \ - htmlColor('TextWarn'))) - self.connect(self.chkSecurePrint, SIGNAL("clicked()"), self.redrawBackup) - + self.scene.moveCursor(MARGIN, colRect.y() - 2, absolute=True) + width = self.scene.pageRect().width() - 2 * MARGIN + self.scene.drawRect(width, colRect.height() + 7, edgeColor=QColor(0, 0, 0), fillColor=None) - btnPrint = QPushButton('&Print...') - btnPrint.setMinimumWidth( 3*tightSizeStr(btnPrint,'Print...')[0]) - self.btnCancel = QPushButton('&Cancel') - self.connect(btnPrint, SIGNAL('clicked()'), self.print_) - self.connect(self.btnCancel, SIGNAL('clicked()'), self.accept) + self.scene.newLine(extra_dy=30) + self.scene.drawText(tr(""" + The following QR code is for convenience only. It contains the + exact same data as the %s lines above. If you copy this backup + by hand, you can safely ignore this QR code. """ % numLine), wrapWidth=4 * INCH) - if self.doPrintFrag: - M,N = self.fragData['M'], self.fragData['N'] - lblDescr = QRichLabel(tr( """ - Print Wallet Backup Fragments

    - When any %d of these fragments are combined, all previous - and future addresses generated by this wallet will be - restored, giving you complete access to your bitcoins. The - data can be copied by hand if a working printer is not - available. Please make sure that all data lines contain - 9 columns - of 4 characters each (excluding "ID" lines).""") % M) - else: - withChain = '' if self.noNeedChaincode else 'and "Chaincode"' - lblDescr = QRichLabel(tr( """ - Print a Forever-Backup

    - Printing this sheet protects all previous and future addresses - generated by this wallet! You can copy the "Root Key" %s - by hand if a working printer is not available. Please make sure that - all data lines contain 9 columns - of 4 characters each.""") % withChain) + self.scene.moveCursor(20, 0) + x, y = self.scene.getCursorXY() + edgeRgt = self.scene.pageRect().width() - MARGIN + edgeBot = self.scene.pageRect().height() - MARGIN - lblDescr.setContentsMargins(5,5,5,5) - frmDescr = makeHorizFrame([lblDescr], STYLE_RAISED) + qrSize = max(1.5 * INCH, min(edgeRgt - x, edgeBot - y, 2.0 * INCH)) + self.scene.drawQR('\n'.join(Lines), qrSize) + self.scene.newLine(extra_dy=25) - self.redrawBackup() - frmChkImport = makeHorizFrame([self.chkImportPrint, \ - 'Stretch', \ - self.lblPageStr, \ - self.comboPageNum, \ - self.lblPageMaxStr]) + Lines = None - frmSecurePrint = makeHorizFrame([self.chkSecurePrint, - self.ttipSecurePrint, - 'Stretch']) + # Finally, draw some pie slices at the bottom + if self.doPrintFrag: + M, N = self.fragData['M'], self.fragData['N'] + bottomOfPage = self.scene.pageRect().height() + MARGIN + maxPieHeight = bottomOfPage - self.scene.getCursorXY()[1] - 8 + maxPieWidth = int((self.scene.pageRect().width() - 2 * MARGIN) / N) - 10 + pieSize = min(72., maxPieHeight, maxPieWidth) + for i in range(N): + startX, startY = self.scene.getCursorXY() + drawSize = self.scene.drawPixmapFile(':/frag%df.png' % M, sizePx=pieSize) + self.scene.moveCursor(10, 0) + if i == printData: + returnX, returnY = self.scene.getCursorXY() + self.scene.moveCursor(startX, startY, absolute=True) + self.scene.moveCursor(-5, -5) + self.scene.drawRect(drawSize[0] + 10, \ + drawSize[1] + 10, \ + edgeColor=Colors.TextBlue, \ + penWidth=3) + self.scene.newLine() + self.scene.moveCursor(startX - MARGIN, 0) + self.scene.drawText('#%d' % \ + (htmlColor('TextBlue'), fragNum), GETFONT('Var', 10)) + self.scene.moveCursor(returnX, returnY, absolute=True) - frmButtons = makeHorizFrame([self.btnCancel, 'Stretch', btnPrint]) - layout = QVBoxLayout() - layout.addWidget(frmDescr) - layout.addWidget(frmChkImport) - layout.addWidget(self.view) - layout.addWidget(frmSecurePrint) - layout.addWidget(self.lblSecurePrint) - layout.addWidget(frmButtons) - setLayoutStretch(layout, 0,1,0,0,0) - self.setLayout(layout) + vbar = self.view.verticalScrollBar() + vbar.setValue(vbar.minimum()) + self.view.update() - self.setWindowIcon(QIcon(':/printer_icon.png')) - self.setWindowTitle('Print Wallet Backup') - # Apparently I can't programmatically scroll until after it's painted - def scrollTop(): - vbar = self.view.verticalScrollBar() - vbar.setValue(vbar.minimum()) - from twisted.internet import reactor - reactor.callLater(0.01, scrollTop) +################################################################################ +def OpenPaperBackupWindow(backupType, parent, main, wlt, unlockTitle=None): - #if len(self.bin - #reactor.callLater(0.5, warnImportedKeys) + if wlt.useEncryption and wlt.isLocked: + if unlockTitle == None: + unlockTitle = tr("Unlock Paper Backup") + dlg = DlgUnlockWallet(wlt, parent, main, unlockTitle) + if not dlg.exec_(): + QMessageBox.warning(parent, tr('Unlock Failed'), tr(""" + The wallet could not be unlocked. Please try again with + the correct unlock passphrase."""), QMessageBox.Ok) + return False + result = True + verifyText = '' + if backupType == 'Single': + result = DlgPrintBackup(parent, main, wlt).exec_() + verifyText = tr(""" + If the backup was printed with SecurePrint\xe2\x84\xa2, please + make sure you wrote the SecurePrint\xe2\x84\xa2 code on the + printed sheet of paper. Note that the code is + case-sensitive!""") + elif backupType == 'Frag': + result = DlgFragBackup(parent, main, wlt).exec_() + verifyText = tr(""" + If the backup was created with SecurePrint\xe2\x84\xa2, please + make sure you wrote the SecurePrint\xe2\x84\xa2 code on each + fragment (or stored with each file fragment). The code is the + same for all fragments.""") - def redrawBackup(self): - cmbPage = 1 - if self.comboPageNum.count() > 0: - cmbPage = int(str(self.comboPageNum.currentText())) + doTest = MsgBoxCustom(MSGBOX.Warning, tr('Verify Your Backup!'), tr(""" + Verify your backup! +

    + If you just made a backup, make sure that it is correct! + The following steps are recommended to verify its integrity: +
    +
      +
    • Verify each line of the backup data contains 9 columns + of 4 letters each (excluding any "ID" lines).
    • +
    • %s
    • +
    • Use Armory's backup tester to test the backup before you + physically secure it.
    • +
    +
    + Armory has a backup tester that uses the exact same + process as restoring your wallet, but stops before it writes any + data to disk. Would you like to test your backup now? + """) % verifyText, yesStr="Test Backup", noStr="Cancel") - if self.doPrintFrag: - cmbPage -= 1 - if not self.doMultiFrag: - cmbPage = self.fragData['Range'][0] - elif self.comboPageNum.count() > 0: - cmbPage = int(str(self.comboPageNum.currentText()))-1 + if doTest: + if backupType == 'Single': + DlgRestoreSingle(parent, main, True, wlt.uniqueIDB58).exec_() + if backupType == 'Frag': + DlgRestoreFragged(parent, main, True, wlt.uniqueIDB58).exec_() - self.createPrintScene('Fragmented Backup\xe2\x84\xa2',cmbPage) - else: - pgSelect = cmbPage if self.chkImportPrint.isChecked() else 1 - if pgSelect==1: - self.createPrintScene('SingleSheetFirstPage','') - else: - pg = pgSelect-2 - nKey = self.maxKeysPerPage - self.createPrintScene('SingleSheetImported', [pg*nKey,(pg+1)*nKey]) + return result +################################################################################ +class DlgBadConnection(ArmoryDialog): + def __init__(self, haveInternet, haveSatoshi, parent=None, main=None): + super(DlgBadConnection, self).__init__(parent, main) - showPageCombo = self.chkImportPrint.isChecked() or \ - (self.doPrintFrag and self.doMultiFrag) - self.showPageSelect(showPageCombo) - self.view.update() - + layout = QGridLayout() + lblWarnImg = QLabel() + lblWarnImg.setPixmap(QPixmap(':/MsgBox_warning48.png')) + lblWarnImg.setAlignment(Qt.AlignHCenter | Qt.AlignTop) + lblDescr = QLabel() + if not haveInternet and not CLI_OPTIONS.offline: + lblDescr = QRichLabel(\ + 'Armory was not able to detect an internet connection, so Armory ' + 'will operate in "Offline" mode. In this mode, only wallet' + '-management and unsigned-transaction functionality will be available. ' + '

    ' + 'If this is an error, please check your internet connection and ' + 'restart Armory.

    Would you like to continue in "Offline" mode? ') + elif haveInternet and not haveSatoshi: + lblDescr = QRichLabel(\ + 'Armory was not able to detect the presence of Bitcoin-Qt or bitcoind ' + 'client software (available at http://www.bitcoin.org). Please make sure that ' + 'the one of those programs is...
    ' + '
    (1) ...open and connected to the network ' + '
    (2) ...on the same network as Armory (main-network or test-network)' + '
    (3) ...synchronized with the blockchain before ' + 'starting Armory

    Without the Bitcoin-Qt or bitcoind open, you will only ' + 'be able to run Armory in "Offline" mode, which will not have access ' + 'to new blockchain data, and you will not be able to send outgoing ' + 'transactions

    If you do not want to be in "Offline" mode, please ' + 'restart Armory after one of these programs is open and synchronized with ' + 'the network') + else: + # Nothing to do -- we shouldn't have even gotten here + # self.reject() + pass - def clickImportChk(self): - if self.numImportPages > 1 and self.chkImportPrint.isChecked(): - ans = QMessageBox.warning(self, tr('Lots to Print!'), tr(""" - This wallet contains %d imported keys, which will require - %d pages to print. Not only will this use a lot of paper, - it will be a lot of work to manually type in these keys in the - event that you need to restore this backup. It is recommended - that you do not print your imported keys and instead make - a digital backup, which can be restored instantly if needed. -

    Do you want to print the imported keys, anyway?""") % \ - (len(self.binImport), self.numImportPages), \ - QMessageBox.Yes | QMessageBox.No) - if not ans==QMessageBox.Yes: - self.chkImportPrint.setChecked(False) - - showPageCombo = self.chkImportPrint.isChecked() or \ - (self.doPrintFrag and self.doMultiFrag) - self.showPageSelect(showPageCombo) - self.comboPageNum.setCurrentIndex(0) - self.redrawBackup() + self.main.abortLoad = False + def abortLoad(): + self.main.abortLoad = True + self.reject() - def showPageSelect(self, doShow=True): - MARGIN = self.scene.MARGIN_PIXELS - bottomOfPage = self.scene.pageRect().height() + MARGIN - totalHgt = bottomOfPage - self.bottomOfSceneHeader - self.maxKeysPerPage = int(totalHgt / (self.importHgt)) - self.numImportPages = (len(self.binImport)-1) / self.maxKeysPerPage + 1 - if self.comboPageNum.count() == 0: - if self.doPrintFrag: - numFrag = len(self.fragData['Range']) - for i in range(numFrag): - self.comboPageNum.addItem(str(i+1)) - self.lblPageMaxStr.setText(tr('of %d') % (numFrag,)) - else: - for i in range(self.numImportPages+1): - self.comboPageNum.addItem(str(i+1)) - self.lblPageMaxStr.setText(tr('of %d') % (self.numImportPages+1,)) + lblDescr.setMinimumWidth(500) + self.btnAccept = QPushButton("Continue in Offline Mode") + self.btnCancel = QPushButton("Close Armory") + self.connect(self.btnAccept, SIGNAL(CLICKED), self.accept) + self.connect(self.btnCancel, SIGNAL(CLICKED), abortLoad) + buttonBox = QDialogButtonBox() + buttonBox.addButton(self.btnAccept, QDialogButtonBox.AcceptRole) + buttonBox.addButton(self.btnCancel, QDialogButtonBox.RejectRole) + layout.addWidget(lblWarnImg, 0, 1, 2, 1) + layout.addWidget(lblDescr, 0, 2, 1, 1) + layout.addWidget(buttonBox, 1, 2, 1, 1) - self.lblPageStr.setVisible(doShow) - self.comboPageNum.setVisible(doShow) - self.lblPageMaxStr.setVisible(doShow) + self.setLayout(layout) + self.setWindowTitle('Network not available') - - - def print_(self): - LOGINFO('Printing!') - self.printer = QPrinter(QPrinter.HighResolution) - self.printer.setPageSize(QPrinter.Letter) +################################################################################ +def readSigBlock(parent, fullPacket): + addrB58, messageStr, pubkey, sig = '', '', '', '' + lines = fullPacket.split('\n') + readingMessage, readingPub, readingSig = False, False, False + for i in range(len(lines)): + s = lines[i].strip() - if QPrintDialog(self.printer).exec_(): - painter = QPainter(self.printer) - painter.setRenderHint(QPainter.TextAntialiasing) + # ADDRESS + if s.startswith('Addr'): + addrB58 = s.split(':')[-1].strip() - if self.doPrintFrag: - for i in self.fragData['Range']: - self.createPrintScene('Fragment', i) - self.scene.getScene().render(painter) - if not i==len(self.fragData['Range'])-1: - self.printer.newPage() - + # MESSAGE STRING + if s.startswith('Message') or readingMessage: + readingMessage = True + if s.startswith('Pub') or s.startswith('Sig') or ('END-CHAL' in s): + readingMessage = False else: - self.createPrintScene('SingleSheetFirstPage', '') - self.scene.getScene().render(painter) - - if len(self.binImport)>0 and self.chkImportPrint.isChecked(): - nKey = self.maxKeysPerPage - for i in range(self.numImportPages): - self.printer.newPage() - self.createPrintScene('SingleSheetImported', [i*nKey,(i+1)*nKey]) - self.scene.getScene().render(painter) - - painter.end() + # Message string needs to be exact, grab what's between the + # double quotes, no newlines + iq1 = s.index('"') + 1 + iq2 = s.index('"', iq1) + messageStr += s[iq1:iq2] - # The last scene printed is what's displayed now. Set the combo box - self.comboPageNum.setCurrentIndex(self.comboPageNum.count()-1) + # PUBLIC KEY + if s.startswith('Pub') or readingPub: + readingPub = True + if s.startswith('Sig') or ('END-SIGNATURE-BLOCK' in s): + readingPub = False + else: + pubkey += s.split(':')[-1].strip().replace(' ', '') - if self.chkSecurePrint.isChecked(): - QMessageBox.warning(self, 'SecurePrint Code', tr(""" -
    You must write your SecurePrint\xe2\x84\xa2 - code on each sheet of paper you just printed! - Write it in the red box in upper-right corner - of each printed page.

    SecurePrint\xe2\x84\xa2 code: - %s

    - NOTE: the above code is case-sensitive!""") % \ - (htmlColor('TextBlue'),self.randpass.toBinStr()), \ - QMessageBox.Ok) - if self.chkSecurePrint.isChecked(): - self.btnCancel.setText('Done') + # SIGNATURE + if s.startswith('Sig') or readingSig: + readingSig = True + if 'END-SIGNATURE-BLOCK' in s: + readingSig = False else: - self.accept() - + sig += s.split(':')[-1].strip().replace(' ', '') - def cleanup(self): - self.binPriv.destroy() - self.binChain.destroy() - self.binPrivCrypt.destroy() - self.binChainCrypt.destroy() - self.randpass.destroy() - for a160,priv,compr in self.binImport: - priv.destroy() - for x,y in self.fragMtrxCrypt: - x.destroy() - y.destroy() + if len(pubkey) > 0: + try: + pubkey = hex_to_binary(pubkey) + if len(pubkey) not in (32, 33, 64, 65): raise + except: + QMessageBox.critical(parent, 'Bad Public Key', \ + 'Public key data was not recognized', QMessageBox.Ok) + pubkey = '' - def accept(self): - self.cleanup() - super(DlgPrintBackup, self).accept() + if len(sig) > 0: + try: + sig = hex_to_binary(sig) + except: + QMessageBox.critical(parent, 'Bad Signature', \ + 'Signature data is malformed!', QMessageBox.Ok) + sig = '' - def reject(self): - self.cleanup() - super(DlgPrintBackup, self).reject() + pubkeyhash = hash160(pubkey) + if not pubkeyhash == addrStr_to_hash160(addrB58)[1]: + QMessageBox.critical(parent, 'Address Mismatch', \ + '!!! The address included in the signature block does not ' + 'match the supplied public key! This should never happen, ' + 'and may in fact be an attempt to mislead you !!!', QMessageBox.Ok) + sig = '' - ############################################################################# - ############################################################################# - def createPrintScene(self, printType, printData): - self.scene.gfxScene.clear() - self.scene.resetCursor() - - pr = self.scene.pageRect() - self.scene.drawRect(pr.width(), pr.height(), edgeColor=None, fillColor=QColor(255,255,255)) - self.scene.resetCursor() - - - INCH = self.scene.INCH - MARGIN = self.scene.MARGIN_PIXELS - - doMask = self.chkSecurePrint.isChecked() - - if USE_TESTNET: - self.scene.drawPixmapFile(':/armory_logo_green_h56.png') - else: - self.scene.drawPixmapFile(':/armory_logo_h36.png') - self.scene.newLine() - - self.scene.drawText('Paper Backup for Armory Wallet', GETFONT('Var', 11)) - self.scene.newLine() - self.scene.drawText('http://www.bitcoinarmory.com') - - self.scene.newLine(extra_dy=20) - self.scene.drawHLine() - self.scene.newLine(extra_dy=20) - - - ssType = ' (SecurePrint\xe2\x84\xa2)' if doMask else ' (Unencrypted)' - if printType=='SingleSheetFirstPage': - bType = tr('Single-Sheet ' + ssType) - elif printType=='SingleSheetImported': - bType = tr('Imported Keys ' + ssType) - elif printType.lower().startswith('frag'): - bstr = tr('Fragmented Backup\xe2\x84\xa2 (%d-of-%d)') % (self.fragData['M'], self.fragData['N']) - bType = bstr + ' ' + tr(ssType) - - if printType.startswith('SingleSheet'): - colRect, rowHgt = self.scene.drawColumn(['Wallet Version:', 'Wallet ID:', \ - 'Wallet Name:', 'Backup Type:']) - self.scene.moveCursor(15, 0) - suf = 'c' if self.noNeedChaincode else 'a' - colRect, rowHgt = self.scene.drawColumn(['1.35'+suf, self.wlt.uniqueIDB58, \ - self.wlt.labelName, bType]) - self.scene.moveCursor(15, colRect.y() + colRect.height(), absolute=True) - else: - colRect, rowHgt = self.scene.drawColumn(['Wallet Version:', 'Wallet ID:', \ - 'Wallet Name:', 'Backup Type:', \ - 'Fragment:']) - baseID = self.fragData['FragIDStr'] - fragNum = printData+1 - fragID = tr('%s-#%d') % \ - (baseID, htmlColor('TextBlue'), fragNum) - self.scene.moveCursor(15, 0) - suf = 'c' if self.noNeedChaincode else 'a' - colRect, rowHgt = self.scene.drawColumn(['1.35'+suf, self.wlt.uniqueIDB58, \ - self.wlt.labelName, bType, fragID]) - self.scene.moveCursor(15, colRect.y() + colRect.height(), absolute=True) - - # Display warning about unprotected key data - wrap = 0.9*self.scene.pageRect().width() - - if self.doPrintFrag: - warnMsg = tr(""" - Any subset of %d fragments with this - ID (%s) are sufficient to recover all the - coins contained in this wallet. To optimize the physical security of - your wallet, please store the fragments in different locations.""") % \ - (htmlColor('TextBlue'), self.fragData['M'], \ - htmlColor('TextBlue'), self.fragData['FragIDStr']) - else: - container = 'this wallet' if printType=='SingleSheetFirstPage' else 'these addresses' - warnMsg = tr(""" - WARNING: Anyone who has access to this - page has access to all the bitcoins in %s! Please keep this - page in a safe place.""" % container) - - self.scene.newLine() - self.scene.drawText(warnMsg, GETFONT('Var', 9), wrapWidth=wrap) - - self.scene.newLine(extra_dy=20) - self.scene.drawHLine() - self.scene.newLine(extra_dy=20) - - if self.doPrintFrag: - numLine = 'three' if self.noNeedChaincode else 'five' - else: - numLine = 'two' if self.noNeedChaincode else 'four' - if printType=='SingleSheetFirstPage': - descrMsg = tr(""" - The following %s lines backup all addresses - ever generated by this wallet (previous and future). - This can be used to recover your wallet if you forget your passphrase or - suffer hardware failure and lose your wallet files. """ % numLine) - elif printType=='SingleSheetImported': - if self.chkSecurePrint.isChecked(): - descrMsg = tr(""" - The following is a list of all private keys imported into your - wallet before this backup was made. These keys are encrypted - with the SecurePrint\xe2\x84\xa2 code and can only be restored - by entering them into Armory. Print a copy of this backup without - the SecurePrint\xe2\x84\xa2 option if you want to be able to import - them into another application.""") - else: - descrMsg = tr(""" - The following is a list of all private keys imported into your - wallet before this backup was made. Each one must be copied - manually into the application where you wish to import them. """) - elif printType.lower().startswith('frag'): - fragNum = printData+1 - descrMsg = tr(""" - The following is fragment #%d for this - wallet. """) % (htmlColor('TextBlue'), printData+1) - - - self.scene.drawText(descrMsg, GETFONT('var', 8), wrapWidth=wrap) - self.scene.newLine(extra_dy=10) - - ########################################################################### - # Draw the SecurePrint box if needed, frag pie, then return cursor - prevCursor = self.scene.getCursorXY() + return addrB58, messageStr, pubkey, sig - self.lblSecurePrint.setVisible(doMask) - if doMask: - self.scene.resetCursor() - self.scene.moveCursor(4.0*INCH, 0) - spWid, spHgt = 2.75*INCH, 1.5*INCH, - if doMask: - self.scene.drawRect(spWid, spHgt, edgeColor=QColor(180,0,0), penWidth=3) - - self.scene.resetCursor() - self.scene.moveCursor(4.07*INCH, 0.07*INCH) - - self.scene.drawText(tr(""" - CRITICAL: This backup will not - work without the SecurePrint\xe2\x84\xa2 - code displayed on the screen during printing. - Copy it here in ink:"""), wrapWidth=spWid*0.93, font=GETFONT('Var', 7)) - - self.scene.newLine(extra_dy = 8) - self.scene.moveCursor(4.07*INCH, 0) - codeWid,codeHgt = self.scene.drawText('Code:') - self.scene.moveCursor(0,codeHgt-3) - wid = spWid - codeWid - w,h = self.scene.drawHLine(width=wid*0.9, penWidth=2) +################################################################################ +def makeSigBlock(addrB58, MessageStr, binPubkey='', binSig=''): + lineWid = 50 + s = '-----BEGIN-SIGNATURE-BLOCK'.ljust(lineWid + 13, '-') + '\n' - - # Done drawing other stuff, so return to the original drawing location - self.scene.moveCursor(*prevCursor, absolute=True) - ########################################################################### - - - ########################################################################### - # Finally, draw the backup information. + ### Address ### + s += 'Address: %s\n' % addrB58 - # If this page is only imported addresses, draw them then bail - self.bottomOfSceneHeader = self.scene.cursorPos.y() - if printType=='SingleSheetImported': - self.scene.moveCursor(0, 0.1*INCH) - importList = self.binImport - if self.chkSecurePrint.isChecked(): - importList = self.binImportCrypt - - for a160,priv,isCompr in importList[printData[0]:printData[1]]: - comprByte = ('\x01' if isCompr==1 else '') - prprv = encodePrivKeyBase58(priv.toBinStr() + comprByte) - toPrint = [prprv[i*6:(i+1)*6] for i in range((len(prprv)+5)/6)] - addrHint = ' (%s...)' % hash160_to_addrStr(a160)[:12] - self.scene.drawText(' '.join(toPrint), GETFONT('Fix',7)) - self.scene.moveCursor(0.02*INCH,0) - self.scene.drawText(addrHint, GETFONT('Var',7)) - self.scene.newLine(extra_dy=-3) - prprv = None - return - - - if self.doPrintFrag: - M = self.fragData['M'] - Lines = [] - Prefix = [] - fmtrx = self.fragMtrxCrypt if doMask else self.fragMtrx + ### Message ### + chPerLine = lineWid - 2 + nMessageLines = (len(MessageStr) - 1) / chPerLine + 1 + for i in range(nMessageLines): + cLine = 'Message: "%s"\n' if i == 0 else ' "%s"\n' + s += cLine % MessageStr[i * chPerLine:(i + 1) * chPerLine] - try: - yBin = fmtrx[printData][1].toBinStr() - binID = self.wlt.uniqueIDBin - IDLine = ComputeFragIDLineHex(M, printData, binID, doMask, addSpaces=True) - if len(yBin)==32: - Prefix.append('ID:'); Lines.append(IDLine) - Prefix.append('F1:'); Lines.append(makeSixteenBytesEasy(yBin[:16 ])) - Prefix.append('F2:'); Lines.append(makeSixteenBytesEasy(yBin[ 16:])) - elif len(yBin)==64: - Prefix.append('ID:'); Lines.append(IDLine) - Prefix.append('F1:'); Lines.append(makeSixteenBytesEasy(yBin[:16 ])) - Prefix.append('F2:'); Lines.append(makeSixteenBytesEasy(yBin[ 16:32 ])) - Prefix.append('F3:'); Lines.append(makeSixteenBytesEasy(yBin[ 32:48 ])) - Prefix.append('F4:'); Lines.append(makeSixteenBytesEasy(yBin[ 48:])) - else: - LOGERROR('yBin is not 32 or 64 bytes! It is %s bytes', len(yBin)) - finally: - yBin = None + ### Public Key ### + if len(binPubkey) > 0: + hexPub = binary_to_hex(binPubkey) + nPubLines = (len(hexPub) - 1) / lineWid + 1 + for i in range(nPubLines): + pLine = 'PublicKey: %s\n' if i == 0 else ' %s\n' + s += pLine % hexPub[i * lineWid:(i + 1) * lineWid] - else: - # Single-sheet backup - if doMask: - code12 = self.binPrivCrypt.toBinStr() - code34 = self.binChainCrypt.toBinStr() - else: - code12 = self.binPriv.toBinStr() - code34 = self.binChain.toBinStr() - - - Lines = [] - Prefix = [] - Prefix.append('Root Key:'); Lines.append(makeSixteenBytesEasy(code12[:16])) - Prefix.append(''); Lines.append(makeSixteenBytesEasy(code12[16:])) - Prefix.append('Chaincode:'); Lines.append(makeSixteenBytesEasy(code34[:16])) - Prefix.append(''); Lines.append(makeSixteenBytesEasy(code34[16:])) - - if self.noNeedChaincode: - Prefix = Prefix[:2] - Lines = Lines[:2] - - # Draw the prefix - origX,origY = self.scene.getCursorXY() - self.scene.moveCursor(20,0) - colRect, rowHgt = self.scene.drawColumn([''+l+'' for l in Prefix]) - - nudgeDown = 2 # because the differing font size makes it look unaligned - self.scene.moveCursor(20, nudgeDown) - self.scene.drawColumn(Lines, - font=GETFONT('Fixed', 8, bold=True), \ - rowHeight=rowHgt, - useHtml=False) - - self.scene.moveCursor(MARGIN, colRect.y()-2, absolute=True) - width = self.scene.pageRect().width() - 2*MARGIN - self.scene.drawRect( width, colRect.height()+7, edgeColor=QColor(0,0,0), fillColor=None) - - self.scene.newLine(extra_dy=30) - self.scene.drawText( tr(""" - The following QR code is for convenience only. It contains the - exact same data as the %s lines above. If you copy this backup - by hand, you can safely ignore this QR code. """ % numLine), wrapWidth=4*INCH) - - self.scene.moveCursor(20,0) - x,y = self.scene.getCursorXY() - edgeRgt = self.scene.pageRect().width() - MARGIN - edgeBot = self.scene.pageRect().height() - MARGIN - - qrSize = max(1.5*INCH, min(edgeRgt - x, edgeBot - y, 2.0*INCH)) - self.scene.drawQR('\n'.join(Lines), qrSize) - self.scene.newLine(extra_dy=25) - - Lines = None + ### Signature ### + if len(binSig) > 0: + hexSig = binary_to_hex(binSig) + nSigLines = (len(hexSig) - 1) / lineWid + 1 + for i in range(nSigLines): + sLine = 'Signature: %s\n' if i == 0 else ' %s\n' + s += sLine % hexSig[i * lineWid:(i + 1) * lineWid] - # Finally, draw some pie slices at the bottom - if self.doPrintFrag: - M,N = self.fragData['M'], self.fragData['N'] - bottomOfPage = self.scene.pageRect().height() + MARGIN - maxPieHeight = bottomOfPage - self.scene.getCursorXY()[1] - 8 - maxPieWidth = int((self.scene.pageRect().width()-2*MARGIN) / N) - 10 - pieSize = min(72., maxPieHeight, maxPieWidth) - for i in range(N): - startX, startY = self.scene.getCursorXY() - drawSize = self.scene.drawPixmapFile(':/frag%df.png' % M, sizePx=pieSize) - self.scene.moveCursor(10,0) - if i==printData: - returnX, returnY = self.scene.getCursorXY() - self.scene.moveCursor(startX, startY, absolute=True) - self.scene.moveCursor(-5, -5) - self.scene.drawRect(drawSize[0]+10, \ - drawSize[1]+10, \ - edgeColor=Colors.TextBlue, \ - penWidth=3) - self.scene.newLine() - self.scene.moveCursor(startX-MARGIN, 0) - self.scene.drawText('#%d' % \ - (htmlColor('TextBlue'), fragNum), GETFONT('Var',10)) - self.scene.moveCursor(returnX, returnY, absolute=True) - - - - vbar = self.view.verticalScrollBar() - vbar.setValue(vbar.minimum()) - self.view.update() + s += '-----END-SIGNATURE-BLOCK'.ljust(lineWid + 13, '-') + '\n' + return s -################################################################################ -def OpenPaperBackupWindow(backupType, parent, main, wlt, unlockTitle=None): - - if wlt.useEncryption and wlt.isLocked: - if unlockTitle==None: - unlockTitle = tr("Unlock Paper Backup") - dlg = DlgUnlockWallet(wlt, parent, main, unlockTitle) - if not dlg.exec_(): - QMessageBox.warning(parent, tr('Unlock Failed'), tr(""" - The wallet could not be unlocked. Please try again with - the correct unlock passphrase."""), QMessageBox.Ok) - return False +class DlgExecLongProcess(ArmoryDialog): + """ + Execute a processing that may require having the user to wait a while. + Should appear like a splash screen, and will automatically close when + the processing is done. As such, you should have very little text, just + in case it finishes immediately, the user won't have time to read it. + + DlgExecLongProcess(execFunc, 'Short Description', self, self.main).exec_() + """ + def __init__(self, funcExec, msg='', parent=None, main=None): + super(DlgExecLongProcess, self).__init__(parent, main) + + self.func = funcExec + + waitFont = GETFONT('Var', 14) + descrFont = GETFONT('Var', 12) + palette = QPalette() + palette.setColor(QPalette.Window, QColor(235, 235, 255)) + self.setPalette(palette); + self.setAutoFillBackground(True) + + if parent: + qr = parent.geometry() + x, y, w, h = qr.left(), qr.top(), qr.width(), qr.height() + dlgW = relaxedSizeStr(waitFont, msg)[0] + dlgW = min(dlgW, 400) + dlgH = 150 + self.setGeometry(int(x + w / 2 - dlgW / 2), int(y + h / 2 - dlgH / 2), dlgW, dlgH) + + lblWaitMsg = QRichLabel('Please Wait...') + lblWaitMsg.setFont(waitFont) + lblWaitMsg.setAlignment(Qt.AlignVCenter | Qt.AlignHCenter) + + lblDescrMsg = QRichLabel(msg) + lblDescrMsg.setFont(descrFont) + lblDescrMsg.setAlignment(Qt.AlignVCenter | Qt.AlignHCenter) + + self.setWindowFlags(Qt.SplashScreen) + + layout = QVBoxLayout() + layout.addWidget(lblWaitMsg) + layout.addWidget(lblDescrMsg) + self.setLayout(layout) + + + def exec_(self): + def execAndClose(): + self.func() + self.accept() + + from twisted.internet import reactor + reactor.callLater(0.1, execAndClose) + QDialog.exec_(self) + + - result = True - verifyText = '' - if backupType=='Single': - result = DlgPrintBackup(parent, main, wlt).exec_() - verifyText = tr(""" - If the backup was printed with SecurePrint\xe2\x84\xa2, please - make sure you wrote the SecurePrint\xe2\x84\xa2 code on the - printed sheet of paper. Note that the code is - case-sensitive!""") - elif backupType=='Frag': - result = DlgFragBackup(parent, main, wlt).exec_() - verifyText = tr(""" - If the backup was created with SecurePrint\xe2\x84\xa2, please - make sure you wrote the SecurePrint\xe2\x84\xa2 code on each - fragment (or stored with each file fragment). The code is the - same for all fragments.""") - doTest = MsgBoxCustom(MSGBOX.Warning, tr('Verify Your Backup!'), tr(""" - Verify your backup! -

    - If you just made a backup, make sure that it is correct! - The following steps are recommended to verify its integrity: -
    -
      -
    • Verify each line of the backup data contains 9 columns - of 4 letters each (excluding any "ID" lines).
    • -
    • %s
    • -
    • Use Armory's backup tester to test the backup before you - physically secure it.
    • -
    -
    - Armory has a backup tester that uses the exact same - process as restoring your wallet, but stops before it writes any - data to disk. Would you like to test your backup now? - """) % verifyText, yesStr="Test Backup", noStr="Cancel") - if doTest: - if backupType=='Single': - DlgRestoreSingle(parent, main, True, wlt.uniqueIDB58).exec_() - if backupType=='Frag': - DlgRestoreFragged(parent, main, True, wlt.uniqueIDB58).exec_() - return result - ################################################################################ -class DlgBadConnection(ArmoryDialog): - def __init__(self, haveInternet, haveSatoshi, parent=None, main=None): - super(DlgBadConnection, self).__init__(parent, main) +class DlgECDSACalc(ArmoryDialog): + def __init__(self, parent=None, main=None, tabStart=0): + super(DlgECDSACalc, self).__init__(parent, main) + dispFont = GETFONT('Var', 8) + w, h = tightSizeNChar(dispFont, 40) + + + + ########################################################################## + ########################################################################## + # TAB: secp256k1 + ########################################################################## + ########################################################################## + # STUB: I'll probably finish implementing this eventually.... + eccWidget = QWidget() + + tabEccLayout = QGridLayout() + self.txtScalarScalarA = QLineEdit() + self.txtScalarScalarB = QLineEdit() + self.txtScalarScalarC = QLineEdit() + self.txtScalarScalarC.setReadOnly(True) + + self.txtScalarPtA = QLineEdit() + self.txtScalarPtB_x = QLineEdit() + self.txtScalarPtB_y = QLineEdit() + self.txtScalarPtC_x = QLineEdit() + self.txtScalarPtC_y = QLineEdit() + self.txtScalarPtC_x.setReadOnly(True) + self.txtScalarPtC_y.setReadOnly(True) + + self.txtPtPtA_x = QLineEdit() + self.txtPtPtA_y = QLineEdit() + self.txtPtPtB_x = QLineEdit() + self.txtPtPtB_y = QLineEdit() + self.txtPtPtC_x = QLineEdit() + self.txtPtPtC_y = QLineEdit() + self.txtPtPtC_x.setReadOnly(True) + self.txtPtPtC_y.setReadOnly(True) + + eccTxtList = [ \ + self.txtScalarScalarA, self.txtScalarScalarB, \ + self.txtScalarScalarC, self.txtScalarPtA, self.txtScalarPtB_x, \ + self.txtScalarPtB_y, self.txtScalarPtC_x, self.txtScalarPtC_y, \ + self.txtPtPtA_x, self.txtPtPtA_y, self.txtPtPtB_x, \ + self.txtPtPtB_y, self.txtPtPtC_x, self.txtPtPtC_y] + + dispFont = GETFONT('Var', 8) + w, h = tightSizeNChar(dispFont, 60) + for txt in eccTxtList: + txt.setMinimumWidth(w) + txt.setFont(dispFont) + + + self.btnCalcSS = QPushButton('Multiply Scalars (mod n)') + self.btnCalcSP = QPushButton('Scalar Multiply EC Point') + self.btnCalcPP = QPushButton('Add EC Points') + self.btnClearSS = QPushButton('Clear') + self.btnClearSP = QPushButton('Clear') + self.btnClearPP = QPushButton('Clear') + + + imgPlus = QRichLabel('+') + imgTimes1 = QRichLabel('*') + imgTimes2 = QRichLabel('*') + imgDown = QRichLabel('') + + self.connect(self.btnCalcSS, SIGNAL(CLICKED), self.multss) + self.connect(self.btnCalcSP, SIGNAL(CLICKED), self.multsp) + self.connect(self.btnCalcPP, SIGNAL(CLICKED), self.addpp) + + + ########################################################################## + # Scalar-Scalar Multiply + sslblA = QRichLabel('a', hAlign=Qt.AlignHCenter) + sslblB = QRichLabel('b', hAlign=Qt.AlignHCenter) + sslblC = QRichLabel('a*b mod n', hAlign=Qt.AlignHCenter) + + + ssLayout = QGridLayout() + ssLayout.addWidget(sslblA, 0, 0, 1, 1) + ssLayout.addWidget(sslblB, 0, 2, 1, 1) + + ssLayout.addWidget(self.txtScalarScalarA, 1, 0, 1, 1) + ssLayout.addWidget(imgTimes1, 1, 1, 1, 1) + ssLayout.addWidget(self.txtScalarScalarB, 1, 2, 1, 1) + + ssLayout.addWidget(makeHorizFrame([STRETCH, self.btnCalcSS, STRETCH]), \ + 2, 0, 1, 3) + ssLayout.addWidget(makeHorizFrame([STRETCH, sslblC, self.txtScalarScalarC, STRETCH]), \ + 3, 0, 1, 3) + ssLayout.setVerticalSpacing(1) + frmSS = QFrame() + frmSS.setFrameStyle(STYLE_SUNKEN) + frmSS.setLayout(ssLayout) + + ########################################################################## + # Scalar-ECPoint Multiply + splblA = QRichLabel('a', hAlign=Qt.AlignHCenter) + splblB = QRichLabel('B', hAlign=Qt.AlignHCenter) + splblBx = QRichLabel('Bx', hAlign=Qt.AlignRight) + splblBy = QRichLabel('By', hAlign=Qt.AlignRight) + splblC = QRichLabel('C = a*B', hAlign=Qt.AlignHCenter) + splblCx = QRichLabel('(a*B)x', hAlign=Qt.AlignRight) + splblCy = QRichLabel('(a*B)y', hAlign=Qt.AlignRight) + spLayout = QGridLayout() + spLayout.addWidget(splblA, 0, 0, 1, 1) + spLayout.addWidget(splblB, 0, 2, 1, 1) + + spLayout.addWidget(self.txtScalarPtA, 1, 0, 1, 1) + spLayout.addWidget(imgTimes2, 1, 1, 1, 1) + spLayout.addWidget(self.txtScalarPtB_x, 1, 2, 1, 1) + spLayout.addWidget(self.txtScalarPtB_y, 2, 2, 1, 1) + + spLayout.addWidget(makeHorizFrame([STRETCH, self.btnCalcSP, STRETCH]), \ + 3, 0, 1, 3) + spLayout.addWidget(makeHorizFrame([STRETCH, splblCx, self.txtScalarPtC_x, STRETCH]), \ + 4, 0, 1, 3) + spLayout.addWidget(makeHorizFrame([STRETCH, splblCy, self.txtScalarPtC_y, STRETCH]), \ + 5, 0, 1, 3) + spLayout.setVerticalSpacing(1) + frmSP = QFrame() + frmSP.setFrameStyle(STYLE_SUNKEN) + frmSP.setLayout(spLayout) + + ########################################################################## + # ECPoint Addition + pplblA = QRichLabel('A', hAlign=Qt.AlignHCenter) + pplblB = QRichLabel('B', hAlign=Qt.AlignHCenter) + pplblAx = QRichLabel('Ax', hAlign=Qt.AlignHCenter) + pplblAy = QRichLabel('Ay', hAlign=Qt.AlignHCenter) + pplblBx = QRichLabel('Bx', hAlign=Qt.AlignHCenter) + pplblBy = QRichLabel('By', hAlign=Qt.AlignHCenter) + pplblC = QRichLabel('C = A+B', hAlign=Qt.AlignHCenter) + pplblCx = QRichLabel('(A+B)x', hAlign=Qt.AlignRight) + pplblCy = QRichLabel('(A+B)y', hAlign=Qt.AlignRight) + ppLayout = QGridLayout() + ppLayout.addWidget(pplblA, 0, 0, 1, 1) + ppLayout.addWidget(pplblB, 0, 2, 1, 1) + ppLayout.addWidget(self.txtPtPtA_x, 1, 0, 1, 1) + ppLayout.addWidget(self.txtPtPtA_y, 2, 0, 1, 1) + ppLayout.addWidget(imgPlus, 1, 1, 2, 1) + ppLayout.addWidget(self.txtPtPtB_x, 1, 2, 1, 1) + ppLayout.addWidget(self.txtPtPtB_y, 2, 2, 1, 1) + ppLayout.addWidget(makeHorizFrame([STRETCH, self.btnCalcPP, STRETCH]), \ + 3, 0, 1, 3) + ppLayout.addWidget(makeHorizFrame([STRETCH, pplblCx, self.txtPtPtC_x, STRETCH]), \ + 4, 0, 1, 3) + ppLayout.addWidget(makeHorizFrame([STRETCH, pplblCy, self.txtPtPtC_y, STRETCH]), \ + 5, 0, 1, 3) + ppLayout.setVerticalSpacing(1) + frmPP = QFrame() + frmPP.setFrameStyle(STYLE_SUNKEN) + frmPP.setLayout(ppLayout) + + gxstr = '79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798' + gystr = '483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8' + + lblDescr = QRichLabel(tr(""" + Use this form to perform Bitcoin elliptic curve calculations. All + operations are performed on the secp256k1 elliptic curve, which is + the one used for Bitcoin. + Supply all values as 32-byte, big-endian, hex-encoded integers. +

    + The following is the secp256k1 generator point coordinates (G):
    + Gx: %s
    + Gy: %s""") % (gxstr, gystr)) + + lblDescr.setTextInteractionFlags(Qt.TextSelectableByMouse | \ + Qt.TextSelectableByKeyboard) + + btnClear = QPushButton('Clear') + btnClear.setMaximumWidth(2 * relaxedSizeStr(btnClear, 'Clear')[0]) + self.connect(btnClear, SIGNAL(CLICKED), self.eccClear) + + btnBack = QPushButton('<<< Go Back') + self.connect(btnBack, SIGNAL(CLICKED), self.accept) + frmBack = makeHorizFrame([btnBack, STRETCH]) + + eccLayout = QVBoxLayout() + eccLayout.addWidget(makeHorizFrame([lblDescr, btnClear])) + eccLayout.addWidget(frmSS) + eccLayout.addWidget(frmSP) + eccLayout.addWidget(frmBack) + + eccWidget.setLayout(eccLayout) + + calcLayout = QHBoxLayout() + calcLayout.addWidget(eccWidget) + self.setLayout(calcLayout) + + self.setWindowTitle('ECDSA Calculator') + self.setWindowIcon(QIcon(self.main.iconfile)) + + + ############################################################################# + def getBinary(self, widget, name): + try: + hexVal = str(widget.text()) + binVal = hex_to_binary(hexVal) + except: + QMessageBox.critical(self, 'Bad Input', \ + 'Value "%s" is invalid. Make sure the value is specified in ' + 'hex, big-endian' % name , QMessageBox.Ok) + return '' + return binVal + + + ############################################################################# + def multss(self): + binA = self.getBinary(self.txtScalarScalarA, 'a') + binB = self.getBinary(self.txtScalarScalarB, 'b') + C = CryptoECDSA().ECMultiplyScalars(binA, binB) + self.txtScalarScalarC.setText(binary_to_hex(C)) + + for txt in [self.txtScalarScalarA, \ + self.txtScalarScalarB, \ + self.txtScalarScalarC]: + txt.setCursorPosition(0) + + ############################################################################# + def multsp(self): + binA = self.getBinary(self.txtScalarPtA, 'a') + binBx = self.getBinary(self.txtScalarPtB_x, 'Bx') + binBy = self.getBinary(self.txtScalarPtB_y, 'By') + + if not CryptoECDSA().ECVerifyPoint(binBx, binBy): + QMessageBox.critical(self, 'Invalid EC Point', \ + 'The point you specified (B) is not on the ' + 'elliptic curved used in Bitcoin (secp256k1).', QMessageBox.Ok) + return + + C = CryptoECDSA().ECMultiplyPoint(binA, binBx, binBy) + self.txtScalarPtC_x.setText(binary_to_hex(C[:32])) + self.txtScalarPtC_y.setText(binary_to_hex(C[32:])) + + for txt in [self.txtScalarPtA, \ + self.txtScalarPtB_x, self.txtScalarPtB_y, \ + self.txtScalarPtC_x, self.txtScalarPtC_y]: + txt.setCursorPosition(0) + + ############################################################################# + def addpp(self): + binAx = self.getBinary(self.txtPtPtA_x, 'Ax') + binAy = self.getBinary(self.txtPtPtA_y, 'Ay') + binBx = self.getBinary(self.txtPtPtB_x, 'Bx') + binBy = self.getBinary(self.txtPtPtB_y, 'By') + + if not CryptoECDSA().ECVerifyPoint(binAx, binAy): + QMessageBox.critical(self, 'Invalid EC Point', \ + 'The point you specified (A) is not on the ' + 'elliptic curved used in Bitcoin (secp256k1).', QMessageBox.Ok) + return + + if not CryptoECDSA().ECVerifyPoint(binBx, binBy): + QMessageBox.critical(self, 'Invalid EC Point', \ + 'The point you specified (B) is not on the ' + 'elliptic curved used in Bitcoin (secp256k1).', QMessageBox.Ok) + return + + C = CryptoECDSA().ECAddPoints(binAx, binAy, binBx, binBy) + self.txtPtPtC_x.setText(binary_to_hex(C[:32])) + self.txtPtPtC_y.setText(binary_to_hex(C[32:])) + + for txt in [self.txtPtPtA_x, self.txtPtPtA_y, \ + self.txtPtPtB_x, self.txtPtPtB_y, \ + self.txtPtPtC_x, self.txtPtPtC_y]: + txt.setCursorPosition(0) + + + ############################################################################# + def eccClear(self): + self.txtScalarScalarA.setText('') + self.txtScalarScalarB.setText('') + self.txtScalarScalarC.setText('') + + self.txtScalarPtA.setText('') + self.txtScalarPtB_x.setText('') + self.txtScalarPtB_y.setText('') + self.txtScalarPtC_x.setText('') + self.txtScalarPtC_y.setText('') + + self.txtPtPtA_x.setText('') + self.txtPtPtA_y.setText('') + self.txtPtPtB_x.setText('') + self.txtPtPtB_y.setText('') + self.txtPtPtC_x.setText('') + self.txtPtPtC_y.setText('') + + + + + +################################################################################ +class DlgAddressBook(ArmoryDialog): + """ + This dialog is provided a widget which has a "setText()" method. When the + user selects the address, this dialog will enter the text into the widget + and then close itself. + """ + def __init__(self, parent, main, putResultInWidget=None, \ + defaultWltID=None, \ + actionStr='Select', \ + selectExistingOnly=False, \ + selectMineOnly=False): + super(DlgAddressBook, self).__init__(parent, main) + + self.target = putResultInWidget + self.actStr = actionStr + + self.isBrowsingOnly = (self.target == None) + + if defaultWltID == None: + defaultWltID = self.main.walletIDList[0] + + self.wlt = self.main.walletMap[defaultWltID] + + lblDescr = QRichLabel('Choose an address from your transaction history, ' + 'or your own wallet. If you choose to send to one ' + 'of your own wallets, the next unused address in ' + 'that wallet will be used.') + + if self.isBrowsingOnly or selectExistingOnly: + lblDescr = QRichLabel('Browse all receiving addresses in ' + 'this wallet, and all addresses to which this ' + 'wallet has sent bitcoins.') + + lblToWlt = QRichLabel('Send to Wallet:') + lblToAddr = QRichLabel('Send to Address:') + if self.isBrowsingOnly: + lblToWlt.setVisible(False) + lblToAddr.setVisible(False) + + + rowHeight = tightSizeStr(self.font, 'XygjpHI')[1] + + self.wltDispModel = AllWalletsDispModel(self.main) + self.wltDispView = QTableView() + self.wltDispView.setModel(self.wltDispModel) + self.wltDispView.setSelectionBehavior(QTableView.SelectRows) + self.wltDispView.setSelectionMode(QTableView.SingleSelection) + self.wltDispView.horizontalHeader().setStretchLastSection(True) + self.wltDispView.verticalHeader().setDefaultSectionSize(20) + self.wltDispView.setMaximumHeight(rowHeight * 7.7) + initialColResize(self.wltDispView, [0.15, 0.30, 0.2, 0.20]) + self.connect(self.wltDispView.selectionModel(), \ + SIGNAL('currentChanged(const QModelIndex &, const QModelIndex &)'), \ + self.wltTableClicked) + + + + + # DISPLAY sent-to addresses + self.addrBookTxModel = None + self.addrBookTxView = QTableView() + self.addrBookTxView.setSortingEnabled(True) + self.setAddrBookTxModel(defaultWltID) + self.connect(self.addrBookTxView, SIGNAL('doubleClicked(QModelIndex)'), \ + self.dblClickAddressTx) + self.addrBookTxView.setContextMenuPolicy(Qt.CustomContextMenu) + self.addrBookTxView.customContextMenuRequested.connect(self.showContextMenuTx) + + # DISPLAY receiving addresses + self.addrBookRxModel = None + self.addrBookRxView = QTableView() + self.addrBookRxView.setSortingEnabled(True) + self.setAddrBookRxModel(defaultWltID) + self.connect(self.addrBookRxView, SIGNAL('doubleClicked(QModelIndex)'), \ + self.dblClickAddressRx) + + self.addrBookRxView.setContextMenuPolicy(Qt.CustomContextMenu) + self.addrBookRxView.customContextMenuRequested.connect(self.showContextMenuRx) + + + self.tabWidget = QTabWidget() + self.tabWidget.addTab(self.addrBookRxView, 'Receiving (Mine)') + if not selectMineOnly: + self.tabWidget.addTab(self.addrBookTxView, 'Sending (Other\'s)') + self.tabWidget.setCurrentIndex(0) + + + + ttipSendWlt = self.main.createToolTipWidget(\ + 'The next unused address in that wallet will be calculated and selected. ') + ttipSendAddr = self.main.createToolTipWidget(\ + 'Addresses that are in other wallets you own are not showns.') + + + self.lblSelectWlt = QRichLabel('', doWrap=False) + self.btnSelectWlt = QPushButton('No Wallet Selected') + self.btnSelectAddr = QPushButton('No Address Selected') + self.btnSelectWlt.setEnabled(False) + self.btnSelectAddr.setEnabled(False) + btnCancel = QPushButton('Cancel') + + if self.isBrowsingOnly: + self.btnSelectWlt.setVisible(False) + self.btnSelectAddr.setVisible(False) + self.lblSelectWlt.setVisible(False) + btnCancel = QPushButton('<<< Go Back') + ttipSendAddr.setVisible(False) + + if selectExistingOnly: + lblToWlt.setVisible(False) + self.lblSelectWlt.setVisible(False) + self.btnSelectWlt.setVisible(False) + ttipSendWlt.setVisible(False) + + self.connect(self.btnSelectWlt, SIGNAL(CLICKED), self.acceptWltSelection) + self.connect(self.btnSelectAddr, SIGNAL(CLICKED), self.acceptAddrSelection) + self.connect(btnCancel, SIGNAL(CLICKED), self.reject) + + + dlgLayout = QGridLayout() + dlgLayout.addWidget(lblDescr, 0, 0) + dlgLayout.addWidget(HLINE(), 1, 0) + dlgLayout.addWidget(lblToWlt, 2, 0) + dlgLayout.addWidget(self.wltDispView, 3, 0) + dlgLayout.addWidget(makeHorizFrame([self.lblSelectWlt, STRETCH, self.btnSelectWlt]), 4, 0) + dlgLayout.addWidget(HLINE(), 5, 0) + dlgLayout.addWidget(lblToAddr, 6, 0) + dlgLayout.addWidget(self.tabWidget, 7, 0) + dlgLayout.addWidget(makeHorizFrame([STRETCH, self.btnSelectAddr]), 8, 0) + dlgLayout.addWidget(HLINE(), 9, 0) + dlgLayout.addWidget(makeHorizFrame([btnCancel, STRETCH]), 10, 0) + dlgLayout.setRowStretch(3, 1) + dlgLayout.setRowStretch(7, 2) + + self.setLayout(dlgLayout) + self.sizeHint = lambda: QSize(760, 500) + + # Auto-select the default wallet, if there is one + rowNum = 0 + if defaultWltID and self.main.walletMap.has_key(defaultWltID): + rowNum = self.main.walletIndices[defaultWltID] + rowIndex = self.wltDispModel.index(rowNum, 0) + self.wltDispView.setCurrentIndex(rowIndex) + self.wltTableClicked(rowIndex) + + self.setWindowTitle('Address Book') + self.setWindowIcon(QIcon(self.main.iconfile)) + + self.setMinimumWidth(300) + + hexgeom = self.main.settings.get('AddrBookGeometry') + wltgeom = self.main.settings.get('AddrBookWltTbl') + rxgeom = self.main.settings.get('AddrBookRxTbl') + txgeom = self.main.settings.get('AddrBookTxTbl') + if len(hexgeom) > 0: + geom = QByteArray.fromHex(hexgeom) + self.restoreGeometry(geom) + if len(wltgeom) > 0: + restoreTableView(self.wltDispView, wltgeom) + if len(rxgeom) > 0: + restoreTableView(self.addrBookRxView, rxgeom) + if len(txgeom) > 0 and not selectMineOnly: + restoreTableView(self.addrBookTxView, txgeom) + + ############################################################################# + def saveGeometrySettings(self): + self.main.writeSetting('AddrBookGeometry', str(self.saveGeometry().toHex())) + self.main.writeSetting('AddrBookWltTbl', saveTableView(self.wltDispView)) + self.main.writeSetting('AddrBookRxTbl', saveTableView(self.addrBookRxView)) + self.main.writeSetting('AddrBookTxTbl', saveTableView(self.addrBookTxView)) + + ############################################################################# + def closeEvent(self, event): + self.saveGeometrySettings() + super(DlgAddressBook, self).closeEvent(event) + + ############################################################################# + def accept(self, *args): + self.saveGeometrySettings() + super(DlgAddressBook, self).accept(*args) + + ############################################################################# + def reject(self, *args): + self.saveGeometrySettings() + super(DlgAddressBook, self).reject(*args) + + ############################################################################# + def setAddrBookTxModel(self, wltID): + self.addrBookTxModel = SentToAddrBookModel(wltID, self.main) + + # + self.addrBookTxProxy = SentAddrSortProxy(self) + self.addrBookTxProxy.setSourceModel(self.addrBookTxModel) + # self.addrBookTxProxy.sort(ADDRBOOKCOLS.Address) + + self.addrBookTxView.setModel(self.addrBookTxProxy) + self.addrBookTxView.setSortingEnabled(True) + self.addrBookTxView.setSelectionBehavior(QTableView.SelectRows) + self.addrBookTxView.setSelectionMode(QTableView.SingleSelection) + self.addrBookTxView.horizontalHeader().setStretchLastSection(True) + self.addrBookTxView.verticalHeader().setDefaultSectionSize(20) + freqSize = 1.3 * tightSizeStr(self.addrBookTxView, 'Times Used')[0] + initialColResize(self.addrBookTxView, [0.3, 0.1, freqSize, 0.5]) + self.addrBookTxView.hideColumn(ADDRBOOKCOLS.WltID) + self.connect(self.addrBookTxView.selectionModel(), \ + SIGNAL('currentChanged(const QModelIndex &, const QModelIndex &)'), \ + self.addrTableTxClicked) + + + ############################################################################# + def setAddrBookRxModel(self, wltID): + wlt = self.main.walletMap[wltID] + self.addrBookRxModel = WalletAddrDispModel(wlt, self) + + self.addrBookRxProxy = WalletAddrSortProxy(self) + self.addrBookRxProxy.setSourceModel(self.addrBookRxModel) + # self.addrBookRxProxy.sort(ADDRESSCOLS.Address) + + self.addrBookRxView.setModel(self.addrBookRxProxy) + self.addrBookRxView.setSelectionBehavior(QTableView.SelectRows) + self.addrBookRxView.setSelectionMode(QTableView.SingleSelection) + self.addrBookRxView.horizontalHeader().setStretchLastSection(True) + self.addrBookRxView.verticalHeader().setDefaultSectionSize(20) + iWidth = tightSizeStr(self.addrBookRxView, 'Imp')[0] + initialColResize(self.addrBookRxView, [iWidth * 1.3, 0.3, 0.35, 64, 0.3]) + self.connect(self.addrBookRxView.selectionModel(), \ + SIGNAL('currentChanged(const QModelIndex &, const QModelIndex &)'), \ + self.addrTableRxClicked) + + + ############################################################################# + def wltTableClicked(self, currIndex, prevIndex=None): + if prevIndex == currIndex: + return + + self.btnSelectWlt.setEnabled(True) + row = currIndex.row() + self.selectedWltID = str(currIndex.model().index(row, WLTVIEWCOLS.ID).data().toString()) + + self.setAddrBookTxModel(self.selectedWltID) + self.setAddrBookRxModel(self.selectedWltID) + + + if not self.isBrowsingOnly: + wlt = self.main.walletMap[self.selectedWltID] + self.btnSelectWlt.setText('%s Wallet: %s' % (self.actStr, self.selectedWltID)) + nextAddr160 = wlt.peekNextUnusedAddr160() + self.lblSelectWlt.setText('Will create new address: %s...' % \ + hash160_to_addrStr(nextAddr160)[:10]) + + # If switched wallet selection, de-select address so it doesn't look + # like the currently-selected address is for this different wallet + self.btnSelectAddr.setEnabled(False) + self.btnSelectAddr.setText('No Address Selected') + self.selectedAddr = '' + self.selectedCmmt = '' + self.addrBookTxModel.reset() + + + ############################################################################# + def addrTableTxClicked(self, currIndex, prevIndex=None): + if prevIndex == currIndex: + return + + self.btnSelectAddr.setEnabled(True) + row = currIndex.row() + self.selectedAddr = str(currIndex.model().index(row, ADDRBOOKCOLS.Address).data().toString()) + self.selectedCmmt = str(currIndex.model().index(row, ADDRBOOKCOLS.Comment).data().toString()) + + if not self.isBrowsingOnly: + self.btnSelectAddr.setText('%s Address: %s...' % (self.actStr, self.selectedAddr[:10])) + + + ############################################################################# + def addrTableRxClicked(self, currIndex, prevIndex=None): + if prevIndex == currIndex: + return + + self.btnSelectAddr.setEnabled(True) + row = currIndex.row() + self.selectedAddr = str(currIndex.model().index(row, ADDRESSCOLS.Address).data().toString()) + self.selectedCmmt = str(currIndex.model().index(row, ADDRESSCOLS.Comment).data().toString()) + + if not self.isBrowsingOnly: + self.btnSelectAddr.setText('%s Address: %s...' % (self.actStr, self.selectedAddr[:10])) + + + ############################################################################# + def dblClickAddressRx(self, index): + if index.column() != ADDRESSCOLS.Comment: + self.acceptAddrSelection() + return + + wlt = self.main.walletMap[self.selectedWltID] + + dialog = DlgSetComment(self.selectedCmmt, 'Address', self, self.main) + if dialog.exec_(): + newComment = str(dialog.edtComment.text()) + addr160 = addrStr_to_hash160(self.selectedAddr)[1] + wlt.setComment(addr160, newComment) + + ############################################################################# + def dblClickAddressTx(self, index): + if index.column() != ADDRBOOKCOLS.Comment: + self.acceptAddrSelection() + return + + wlt = self.main.walletMap[self.selectedWltID] + + dialog = DlgSetComment(self.selectedCmmt, 'Address', self, self.main) + if dialog.exec_(): + newComment = str(dialog.edtComment.text()) + addr160 = addrStr_to_hash160(self.selectedAddr)[1] + wlt.setComment(addr160, newComment) + + ############################################################################# + def acceptWltSelection(self): + wltID = self.selectedWltID + addr160 = self.main.walletMap[wltID].getNextUnusedAddress().getAddr160() + self.target.setText(hash160_to_addrStr(addr160)) + self.target.setCursorPosition(0) + self.accept() + + + ############################################################################# + def acceptAddrSelection(self): + if self.target: + self.target.setText(self.selectedAddr) + self.target.setCursorPosition(0) + self.accept() + + ############################################################################# + def showContextMenuTx(self, pos): + menu = QMenu(self.addrBookTxView) + std = (self.main.usermode == USERMODE.Standard) + adv = (self.main.usermode == USERMODE.Advanced) + dev = (self.main.usermode == USERMODE.Expert) + + if True: actionCopyAddr = menu.addAction("Copy Address") + if dev: actionCopyHash160 = menu.addAction("Copy Hash160 (hex)") + if True: actionCopyComment = menu.addAction("Copy Comment") + idx = self.addrBookTxView.selectedIndexes()[0] + action = menu.exec_(QCursor.pos()) + + if action == actionCopyAddr: + s = self.addrBookTxView.model().index(idx.row(), ADDRBOOKCOLS.Address).data().toString() + elif dev and action == actionCopyHash160: + s = str(self.addrBookTxView.model().index(idx.row(), ADDRBOOKCOLS.Address).data().toString()) + atype, addr160 = addrStr_to_hash160(s) + if atype==P2SHBYTE: + LOGWARN('Copying Hash160 of P2SH address: %s' % s) + s = binary_to_hex(addr160) + elif action == actionCopyComment: + s = self.addrBookTxView.model().index(idx.row(), ADDRBOOKCOLS.Comment).data().toString() + else: + return + + clipb = QApplication.clipboard() + clipb.clear() + clipb.setText(str(s).strip()) + + + ############################################################################# + def showContextMenuRx(self, pos): + menu = QMenu(self.addrBookRxView) + std = (self.main.usermode == USERMODE.Standard) + adv = (self.main.usermode == USERMODE.Advanced) + dev = (self.main.usermode == USERMODE.Expert) + + if True: actionCopyAddr = menu.addAction("Copy Address") + if dev: actionCopyHash160 = menu.addAction("Copy Hash160 (hex)") + if True: actionCopyComment = menu.addAction("Copy Comment") + idx = self.addrBookRxView.selectedIndexes()[0] + action = menu.exec_(QCursor.pos()) + + if action == actionCopyAddr: + s = self.addrBookRxView.model().index(idx.row(), ADDRESSCOLS.Address).data().toString() + elif dev and action == actionCopyHash160: + s = str(self.addrBookRxView.model().index(idx.row(), ADDRESSCOLS.Address).data().toString()) + atype, addr160 = addrStr_to_hash160(s) + if atype==P2SHBYTE: + LOGWARN('Copying Hash160 of P2SH address: %s' % s) + s = binary_to_hex(addr160) + elif action == actionCopyComment: + s = self.addrBookRxView.model().index(idx.row(), ADDRESSCOLS.Comment).data().toString() + else: + return + + clipb = QApplication.clipboard() + clipb.clear() + clipb.setText(str(s).strip()) + + +################################################################################ +def createAddrBookButton(parent, targWidget, defaultWlt, actionStr="Select", selectExistingOnly=False, selectMineOnly=False): + btn = QPushButton('') + ico = QIcon(QPixmap(':/addr_book_icon.png')) + btn.setIcon(ico) + def execAddrBook(): + if len(parent.main.walletMap) == 0: + QMessageBox.warning(parent, 'No wallets!', 'You have no wallets so ' + 'there is no address book to display.', QMessageBox.Ok) + return + dlg = DlgAddressBook(parent, parent.main, targWidget, defaultWlt, actionStr, selectExistingOnly, selectMineOnly) + dlg.exec_() + + btn.setMaximumWidth(24) + btn.setMaximumHeight(24) + parent.connect(btn, SIGNAL(CLICKED), execAddrBook) + btn.setToolTip('Select from Address Book') + return btn + + +################################################################################ +class DlgHelpAbout(ArmoryDialog): + def __init__(self, putResultInWidget, defaultWltID=None, parent=None, main=None): + super(DlgHelpAbout, self).__init__(parent) + + imgLogo = QLabel() + imgLogo.setPixmap(QPixmap(':/armory_logo_h56.png')) + imgLogo.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) + + lblHead = QRichLabel('Armory Bitcoin Wallet : Version %s-beta' % \ + getVersionString(BTCARMORY_VERSION), doWrap=False) + lblWebpage = QRichLabel('https://www.bitcoinarmory.com') + lblWebpage.setOpenExternalLinks(True) + lblCopyright = QRichLabel(u'Copyright \xa9 2011-2013 Armory Technologies, Inc.') + lblLicense = QRichLabel(tr(u'Licensed under the ' + '' + 'Affero General Public License, Version 3 (AGPLv3)')) + lblLicense.setOpenExternalLinks(True) + + lblHead.setAlignment(Qt.AlignHCenter) + lblWebpage.setAlignment(Qt.AlignHCenter) + lblCopyright.setAlignment(Qt.AlignHCenter) + lblLicense.setAlignment(Qt.AlignHCenter) + + dlgLayout = QHBoxLayout() + dlgLayout.addWidget(makeVertFrame([imgLogo, lblHead, lblCopyright, lblWebpage, STRETCH, lblLicense])) + self.setLayout(dlgLayout) + + self.setMinimumWidth(450) + + self.setWindowTitle('About Armory') + + +################################################################################ +class DlgSettings(ArmoryDialog): + def __init__(self, parent=None, main=None): + super(DlgSettings, self).__init__(parent, main) + + + + ########################################################################## + # bitcoind-management settings + self.chkManageSatoshi = QCheckBox(tr(""" + Let Armory run Bitcoin-Qt/bitcoind in the background""")) + self.edtSatoshiExePath = QLineEdit() + self.edtSatoshiHomePath = QLineEdit() + self.edtSatoshiExePath.setMinimumWidth(tightSizeNChar(GETFONT('Fixed', 10), 40)[0]) + self.connect(self.chkManageSatoshi, SIGNAL(CLICKED), self.clickChkManage) + self.startChk = self.main.getSettingOrSetDefault('ManageSatoshi', not OS_MACOSX) + if self.startChk: + self.chkManageSatoshi.setChecked(True) + if OS_MACOSX: + self.chkManageSatoshi.setEnabled(False) + lblManageSatoshi = QRichLabel(\ + 'Bitcoin-Qt/bitcoind management is not available on Mac/OSX') + else: + if self.main.settings.hasSetting('SatoshiExe'): + satexe = self.main.settings.get('SatoshiExe') + + sathome = BTC_HOME_DIR + if self.main.settings.hasSetting('SatoshiDatadir'): + sathome = self.main.settings.get('SatoshiDatadir') + + lblManageSatoshi = QRichLabel(\ + 'Bitcoin Software Management' + '

    ' + 'By default, Armory will manage the Bitcoin engine/software in the ' + 'background. You can choose to manage it yourself, or tell Armory ' + 'about non-standard installation configuration.') + if self.main.settings.hasSetting('SatoshiExe'): + self.edtSatoshiExePath.setText(self.main.settings.get('SatoshiExe')) + self.edtSatoshiExePath.home(False) + if self.main.settings.hasSetting('SatoshiDatadir'): + self.edtSatoshiHomePath.setText(self.main.settings.get('SatoshiDatadir')) + self.edtSatoshiHomePath.home(False) + + lblDescrExe = QRichLabel('Bitcoin Install Dir:') + lblDescrHome = QRichLabel('Bitcoin Home Dir:') + lblDefaultExe = QRichLabel('Leave blank to have Armory search default ' + 'locations for your OS', size=2) + lblDefaultHome = QRichLabel('Leave blank to use default datadir ' + '(%s)' % BTC_HOME_DIR, size=2) + + self.btnSetExe = createDirectorySelectButton(self, self.edtSatoshiExePath) + self.btnSetHome = createDirectorySelectButton(self, self.edtSatoshiHomePath) + + layoutMgmt = QGridLayout() + layoutMgmt.addWidget(lblManageSatoshi, 0, 0, 1, 3) + layoutMgmt.addWidget(self.chkManageSatoshi, 1, 0, 1, 3) + + layoutMgmt.addWidget(lblDescrExe, 2, 0) + layoutMgmt.addWidget(self.edtSatoshiExePath, 2, 1) + layoutMgmt.addWidget(self.btnSetExe, 2, 2) + layoutMgmt.addWidget(lblDefaultExe, 3, 1, 1, 2) + + layoutMgmt.addWidget(lblDescrHome, 4, 0) + layoutMgmt.addWidget(self.edtSatoshiHomePath, 4, 1) + layoutMgmt.addWidget(self.btnSetHome, 4, 2) + layoutMgmt.addWidget(lblDefaultHome, 5, 1, 1, 2) + frmMgmt = QFrame() + frmMgmt.setLayout(layoutMgmt) + + self.clickChkManage() + # bitcoind-management settings + ########################################################################## + + # We check for internet connection on each startup. + self.chkSkipOnlineCheck = QCheckBox(tr(""" + Skip online check on startup (assume internet is available, do + not check)""")) + skipOnlineChk = self.main.getSettingOrSetDefault('SkipOnlineCheck', False) + self.chkSkipOnlineCheck.setChecked(skipOnlineChk) + + self.chkSkipVersionCheck = QCheckBox(tr(""" + Skip periodic version queries to Armory server""")) + skipVerChk = self.main.getSettingOrSetDefault('SkipVersionCheck', False) + self.chkSkipVersionCheck.setChecked(skipVerChk) + + + self.chkDisableTorrent = QCheckBox(tr(""" + Disable torrent download (force synchronization via Bitcoin P2P)""")) + disableTorrent = self.main.getSettingOrSetDefault('DisableTorrent', False) + self.chkDisableTorrent.setChecked(disableTorrent) + + + lblDefaultUriTitle = QRichLabel(tr(""" + Set Armory as default URL handler""")) + lblDefaultURI = QRichLabel(tr(""" + Set Armory to be the default when you click on "bitcoin:" + links in your browser or in emails. + You can test if your operating system is supported by clicking + on a "bitcoin:" link right after clicking this button.""")) + btnDefaultURI = QPushButton(tr('Set Armory as Default')) + frmBtnDefaultURI = makeHorizFrame([btnDefaultURI, 'Stretch']) + + self.chkAskURIAtStartup = QCheckBox(tr(""" + Check whether Armory is the default handler at startup""")) + askuriDNAA = self.main.getSettingOrSetDefault('DNAA_DefaultApp', False) + self.chkAskURIAtStartup.setChecked(not askuriDNAA) + + def clickRegURI(): + self.main.setupUriRegistration(justDoIt=True) + QMessageBox.information(self, tr('Registered'), tr(""" + Armory just attempted to register itself to handle "bitcoin:" + links, but this does not work on all operating systems. You can + test it by going to the + Bitcoin Armory + website and clicking the link at the bottom of the + homepage."""), QMessageBox.Ok) + + self.connect(btnDefaultURI, SIGNAL(CLICKED), clickRegURI) + + + ############################################################### + # Announcements and Alerts + lblAnnounce = QRichLabel(tr(""" + Armory Technologies, Inc. will periodically post announcements and + security alerts. ATI will also use this channel to notify you of + new Armory versions. All these notifications are signed by an + offline private key controlled exclusively by ATI.""")) + self.radioAnnounce1024 = QRadioButton(tr(""" + (Level 1) All announcements including testing/unstable versions""")) + self.radioAnnounce2048 = QRadioButton(tr(""" + (Level 2) Standard announcements and notifications""")) + self.radioAnnounce3072 = QRadioButton(tr(""" + (Level 3) Only important announcements and alerts""")) + self.radioAnnounce4096 = QRadioButton(tr(""" + (Level 4) Only critical security alerts""")) + + self.chkDisableUpgradeNotify = QCheckBox(tr(""" + Disable software upgrade notifications """)) + + self.chkDisableUpgradeNotify.setChecked( \ + self.main.getSettingOrSetDefault('DisableUpgradeNotify', False)) + + lblDisableAnnounce = QRichLabel(tr(""" + If you must completely disable all notifications + from the Armory team, you can run Armory with the + "--skip-announce-check" flag from the command-line, or add it to + the Armory shortcut target""") % htmlColor('DisableFG')) + + btnGroupAnnounce = QButtonGroup(self) + btnGroupAnnounce.addButton(self.radioAnnounce1024) + btnGroupAnnounce.addButton(self.radioAnnounce2048) + btnGroupAnnounce.addButton(self.radioAnnounce3072) + btnGroupAnnounce.addButton(self.radioAnnounce4096) + btnGroupAnnounce.setExclusive(True) + + minPriority = self.main.getSettingOrSetDefault('NotifyMinPriority', \ + DEFAULT_MIN_PRIORITY) + if minPriority >= 4096: + self.radioAnnounce4096.setChecked(True) + elif minPriority >= 3072: + self.radioAnnounce3072.setChecked(True) + elif minPriority >= 2048: + self.radioAnnounce2048.setChecked(True) + elif minPriority >= 0: + self.radioAnnounce1024.setChecked(True) + + btnResetNotify = QPushButton(tr('Reset Notifications')) + frmBtnResetNotify = makeHorizFrame([btnResetNotify, 'Stretch']) + + def resetNotifyLong(): + self.main.notifyIgnoreLong = set() + self.main.notifyIgnoreShort = set() + self.main.writeSetting('NotifyIgnore', '') + QMessageBox.information(self, tr('Settings Changed'), tr(""" + All notifications have been reset!"""), QMessageBox.Ok) + + self.connect(btnResetNotify, SIGNAL(CLICKED), resetNotifyLong) + + + txFee = self.main.getSettingOrSetDefault('Default_Fee', MIN_TX_FEE) + lblDefaultFee = QRichLabel(tr(""" + Default fee to include with transactions:
    """)) + lblDefaultDescr = QRichLabel(tr(""" + Fees go to users that contribute computing power to keep the + Bitcoin network secure. It also increases the priority of your + transactions so they confirm faster (%s BTC is standard).""") % \ + coin2strNZS(MIN_TX_FEE)) + + ttipDefaultFee = self.main.createToolTipWidget(tr(""" + NOTE: some transactions will require a certain fee + regardless of your settings -- in such cases + you will be prompted to include the correct + value or cancel the transaction""")) + + self.edtDefaultFee = QLineEdit() + self.edtDefaultFee.setText(coin2str(txFee, maxZeros=1).strip()) + lblDefaultFee.setMinimumWidth(400) + + self.connect(self.edtDefaultFee, SIGNAL('returnPressed()'), self.accept) + + + ############################################################### + # Minimize on Close + lblMinimizeDescr = QRichLabel(tr(""" + Minimize to System Tray +
    + You can have Armory automatically minimize itself to your system + tray on open or close. Armory will stay open but run in the + background, and you will still receive notifications. Access Armory + through the icon on your system tray. +

    + If select "Minimize on close", the 'x' on the top window bar will + minimize Armory instead of exiting the application. You can always use + "File"\xe2\x86\x92"Quit Armory" to actually close it.""")) + + moo = self.main.getSettingOrSetDefault('MinimizeOnOpen', False) + self.chkMinOnOpen = QCheckBox(tr('Minimize to system tray on open')) + if moo: + self.chkMinOnOpen.setChecked(True) + + moc = self.main.getSettingOrSetDefault('MinimizeOrClose', 'DontKnow') + self.chkMinOrClose = QCheckBox(tr('Minimize to system tray on close')) + + if moc == 'Minimize': + self.chkMinOrClose.setChecked(True) + + + ############################################################### + # System Tray Notifications -- Don't work right on OSX + lblNotify = QRichLabel('Enable notifcations from the system-tray:') + notifyBtcIn = self.main.getSettingOrSetDefault('NotifyBtcIn', not OS_MACOSX) + notifyBtcOut = self.main.getSettingOrSetDefault('NotifyBtcOut', not OS_MACOSX) + notifyDiscon = self.main.getSettingOrSetDefault('NotifyDiscon', not OS_MACOSX) + notifyReconn = self.main.getSettingOrSetDefault('NotifyReconn', not OS_MACOSX) + + self.chkBtcIn = QCheckBox('Bitcoins Received') + self.chkBtcOut = QCheckBox('Bitcoins Sent') + self.chkDiscon = QCheckBox('Bitcoin-Qt/bitcoind disconnected') + self.chkReconn = QCheckBox('Bitcoin-Qt/bitcoind reconnected') + self.chkBtcIn.setChecked(notifyBtcIn) + self.chkBtcOut.setChecked(notifyBtcOut) + self.chkDiscon.setChecked(notifyDiscon) + self.chkReconn.setChecked(notifyReconn) + + if OS_MACOSX: + lblNotify = QRichLabel('Sorry! Notifications are not available on Mac/OSX') + self.chkBtcIn.setChecked(False) + self.chkBtcOut.setChecked(False) + self.chkDiscon.setChecked(False) + self.chkReconn.setChecked(False) + self.chkBtcIn.setEnabled(False) + self.chkBtcOut.setEnabled(False) + self.chkDiscon.setEnabled(False) + self.chkReconn.setEnabled(False) + + + ############################################################### + # Date format preferences + exampleTimeTuple = (2012, 4, 29, 19, 45, 0, -1, -1, -1) + self.exampleUnixTime = time.mktime(exampleTimeTuple) + exampleStr = unixTimeToFormatStr(self.exampleUnixTime, '%c') + lblDateFmt = QRichLabel('Preferred Date Format:
    ') + lblDateDescr = QRichLabel(\ + 'You can specify how you would like dates ' + 'to be displayed using percent-codes to ' + 'represent components of the date. The ' + 'mouseover text of the "(?)" icon shows ' + 'the most commonly used codes/symbols. ' + 'The text next to it shows how ' + '"%s" would be shown with the ' + 'specified format.' % exampleStr) + lblDateFmt.setAlignment(Qt.AlignTop) + fmt = self.main.getPreferredDateFormat() + ttipStr = 'Use any of the following symbols:
    ' + fmtSymbols = [x[0] + ' = ' + x[1] for x in FORMAT_SYMBOLS] + ttipStr += '
    '.join(fmtSymbols) + + fmtSymbols = [x[0] + '~' + x[1] for x in FORMAT_SYMBOLS] + lblStk = QRichLabel('; '.join(fmtSymbols)) + + self.edtDateFormat = QLineEdit() + self.edtDateFormat.setText(fmt) + self.ttipFormatDescr = self.main.createToolTipWidget(ttipStr) + + self.lblDateExample = QRichLabel('', doWrap=False) + self.connect(self.edtDateFormat, SIGNAL('textEdited(QString)'), self.doExampleDate) + self.doExampleDate() + self.btnResetFormat = QPushButton("Reset to Default") + + def doReset(): + self.edtDateFormat.setText(DEFAULT_DATE_FORMAT) + self.doExampleDate() + self.connect(self.btnResetFormat, SIGNAL(CLICKED), doReset) + + # Make a little subframe just for the date format stuff... everything + # fits nicer if I do this... + frmTop = makeHorizFrame([self.lblDateExample, STRETCH, self.ttipFormatDescr]) + frmMid = makeHorizFrame([self.edtDateFormat]) + frmBot = makeHorizFrame([self.btnResetFormat, STRETCH]) + fStack = makeVertFrame([frmTop, frmMid, frmBot, STRETCH]) + lblStk = makeVertFrame([lblDateFmt, lblDateDescr, STRETCH]) + subFrm = makeHorizFrame([lblStk, STRETCH, fStack]) + + + ############################################################### + # SelectCoins preferences + # NOT ENABLED YET -- BUT WILL BE SOON + # lblSelectCoin = QRichLabel('Coin Selection Preferences:') + # lblSelectCoinDescr = QRichLabel( \ + # 'When Armory constructs a transaction, there are many different ' + # 'ways for it to select from coins that make up your balance. ' + # 'The "SelectCoins" algorithm can be set to prefer more-anonymous ' + # 'coin selections or to prefer avoiding mandatory transaction fees. ' + # 'No guarantees are made about the relative anonymity of the ' + # 'coin selection, only that Armory will prefer a transaction ' + # 'that requires a fee if it can increase anonymity.') + # self.cmbSelectCoins = QComboBox() + # self.cmbSelectCoins.clear() + # self.cmbSelectCoins.addItem( 'Prefer free transactions' ) + # self.cmbSelectCoins.addItem( 'Maximize anonymity' ) + # self.cmbSelectCoins.setCurrentIndex(0) + # i+=1 + # dlgLayout.addWidget(lblSelectCoin, i,0) + # dlgLayout.addWidget(self.cmbSelectCoins, i,1) + # i+=1 + # dlgLayout.addWidget(lblSelectCoinDescr, i,0, 1,2) + + + ############################################################### + # Save/Cancel Button + self.btnCancel = QPushButton("Cancel") + self.btnAccept = QPushButton("Save") + self.connect(self.btnCancel, SIGNAL(CLICKED), self.reject) + self.connect(self.btnAccept, SIGNAL(CLICKED), self.accept) + + + + self.cmbUsermode = QComboBox() + self.cmbUsermode.clear() + self.cmbUsermode.addItem('Standard') + self.cmbUsermode.addItem('Advanced') + self.cmbUsermode.addItem('Expert') + + self.usermodeInit = self.main.usermode + + if self.main.usermode == USERMODE.Standard: + self.cmbUsermode.setCurrentIndex(0) + elif self.main.usermode == USERMODE.Advanced: + self.cmbUsermode.setCurrentIndex(1) + elif self.main.usermode == USERMODE.Expert: + self.cmbUsermode.setCurrentIndex(2) + + lblUsermode = QRichLabel('Armory user mode:') + self.lblUsermodeDescr = QRichLabel('') + self.setUsermodeDescr() + + self.connect(self.cmbUsermode, SIGNAL('activated(int)'), self.setUsermodeDescr) + + + + + frmLayout = QGridLayout() + + i = 0 + frmLayout.addWidget(HLINE(), i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(frmMgmt, i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(self.chkSkipOnlineCheck, i, 0, 1, 3) + + #i += 1 + #frmLayout.addWidget(self.chkSkipVersionCheck, i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(self.chkDisableTorrent, i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(HLINE(), i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(lblDefaultUriTitle, i, 0) + + i += 1 + frmLayout.addWidget(lblDefaultURI, i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(frmBtnDefaultURI, i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(self.chkAskURIAtStartup, i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(HLINE(), i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(lblDefaultFee, i, 0) + frmLayout.addWidget(ttipDefaultFee, i, 1) + frmLayout.addWidget(self.edtDefaultFee, i, 2) + + i += 1 + frmLayout.addWidget(lblDefaultDescr, i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(HLINE(), i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(subFrm, i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(HLINE(), i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(lblMinimizeDescr, i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(self.chkMinOnOpen, i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(self.chkMinOrClose, i, 0, 1, 3) + + + i += 1 + frmLayout.addWidget(HLINE(), i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(lblNotify, i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(self.chkBtcIn, i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(self.chkBtcOut, i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(self.chkDiscon, i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(self.chkReconn, i, 0, 1, 3) + + + i += 1 + frmLayout.addWidget(HLINE(), i, 0, 1, 3) + + + i += 1 + frmLayout.addWidget(lblAnnounce, i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(self.radioAnnounce1024, i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(self.radioAnnounce2048, i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(self.radioAnnounce3072, i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(self.radioAnnounce4096, i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(lblDisableAnnounce, i, 0, 1, 4) + + i += 1 + frmLayout.addWidget(self.chkDisableUpgradeNotify , i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(frmBtnResetNotify , i, 0, 1, 3) + + + i += 1 + frmLayout.addWidget(HLINE(), i, 0, 1, 3) + + i += 1 + frmLayout.addWidget(lblUsermode, i, 0) + frmLayout.addWidget(QLabel(''), i, 1) + frmLayout.addWidget(self.cmbUsermode, i, 2) + + i += 1 + frmLayout.addWidget(self.lblUsermodeDescr, i, 0, 1, 3) + + + frmOptions = QFrame() + frmOptions.setLayout(frmLayout) + + self.scrollOptions = QScrollArea() + self.scrollOptions.setWidget(frmOptions) - layout = QGridLayout() - lblWarnImg = QLabel() - lblWarnImg.setPixmap(QPixmap(':/MsgBox_warning48.png')) - lblWarnImg.setAlignment(Qt.AlignHCenter | Qt.AlignTop) - lblDescr = QLabel() - if not haveInternet and not CLI_OPTIONS.offline: - lblDescr = QRichLabel( \ - 'Armory was not able to detect an internet connection, so Armory ' - 'will operate in "Offline" mode. In this mode, only wallet' - '-management and unsigned-transaction functionality will be available. ' - '

    ' - 'If this is an error, please check your internet connection and ' - 'restart Armory.

    Would you like to continue in "Offline" mode? ') - elif haveInternet and not haveSatoshi: - lblDescr = QRichLabel( \ - 'Armory was not able to detect the presence of Bitcoin-Qt or bitcoind ' - 'client software (available at http://www.bitcoin.org). Please make sure that ' - 'the one of those programs is...
    ' - '
    (1) ...open and connected to the network ' - '
    (2) ...on the same network as Armory (main-network or test-network)' - '
    (3) ...synchronized with the blockchain before ' - 'starting Armory

    Without the Bitcoin-Qt or bitcoind open, you will only ' - 'be able to run Armory in "Offline" mode, which will not have access ' - 'to new blockchain data, and you will not be able to send outgoing ' - 'transactions

    If you do not want to be in "Offline" mode, please ' - 'restart Armory after one of these programs is open and synchronized with ' - 'the network') - else: - # Nothing to do -- we shouldn't have even gotten here - #self.reject() - pass - - - self.main.abortLoad = False - def abortLoad(): - self.main.abortLoad = True - self.reject() - - lblDescr.setMinimumWidth(500) - self.btnAccept = QPushButton("Continue in Offline Mode") - self.btnCancel = QPushButton("Close Armory") - self.connect(self.btnAccept, SIGNAL('clicked()'), self.accept) - self.connect(self.btnCancel, SIGNAL('clicked()'), abortLoad) - buttonBox = QDialogButtonBox() - buttonBox.addButton(self.btnAccept, QDialogButtonBox.AcceptRole) - buttonBox.addButton(self.btnCancel, QDialogButtonBox.RejectRole) - layout.addWidget(lblWarnImg, 0,1, 2,1) - layout.addWidget(lblDescr, 0,2, 1,1) - layout.addWidget(buttonBox, 1,2, 1,1) + dlgLayout = QVBoxLayout() + dlgLayout.addWidget(self.scrollOptions) + dlgLayout.addWidget(makeHorizFrame([STRETCH, self.btnCancel, self.btnAccept])) - self.setLayout(layout) - self.setWindowTitle('Network not available') + self.setLayout(dlgLayout) + self.setMinimumWidth(650) + self.setWindowTitle('Armory Settings') -################################################################################ -def readSigBlock(parent, fullPacket): - addrB58, messageStr, pubkey, sig = '','','','' - lines = fullPacket.split('\n') - readingMessage, readingPub, readingSig = False, False, False - for i in range(len(lines)): - s = lines[i].strip() + # NOTE: This was getting complicated for a variety of reasons, so switched + # to manually constructing the options window. May come back to this + # at a later time. + # + # Let's create a scalable list of options. Each row of this list looks like: + # + # [OptionType, SettingsName, DefaultValue, BoldText, NormalText, Tooltip] + # + # SettingsName is the string used in self.main.getSettingOrSetDefault() + # OptionType can be one of: + # {'Checkbox', 'LineEdit', 'Combo|Opt1|Opt2|...', 'Separator', 'Header'} + # + # "Separator adds a horizontal-ruler to separate option groups, and "Header" + # is basically a textual separator with no actual option - # ADDRESS - if s.startswith('Addr'): - addrB58 = s.split(':')[-1].strip() + # self.Options = [] + # self.Options.append( ['LineEdit', 'Default_Fee', MIN_TX_FEE, \ + # 'Default fee to include with transactions.', \ + # 'Fees go to users that contribute computing power ' + # 'to keep the Bitcoin network secure (0.0005 BTC is ' + # 'standard).', \ + # 'NOTE: some transactions will require a fee ' + # 'regardless of your preferences -- in such cases ' + # 'you will be prompted to include the correct ' + # 'value or abort the transaction']) - # MESSAGE STRING - if s.startswith('Message') or readingMessage: - readingMessage = True - if s.startswith('Pub') or s.startswith('Sig') or ('END-CHAL' in s): - readingMessage = False - else: - # Message string needs to be exact, grab what's between the - # double quotes, no newlines - iq1 = s.index('"') + 1 - iq2 = s.index('"', iq1) - messageStr += s[iq1:iq2] - # PUBLIC KEY - if s.startswith('Pub') or readingPub: - readingPub = True - if s.startswith('Sig') or ('END-SIGNATURE-BLOCK' in s): - readingPub = False + ############################################################################# + def accept(self, *args): + + if self.chkManageSatoshi.isChecked(): + # Check valid path is supplied for bitcoin installation + pathExe = unicode(self.edtSatoshiExePath.text()).strip() + if len(pathExe) > 0: + if not os.path.exists(pathExe): + exeName = 'bitcoin-qt.exe' if OS_WINDOWS else 'bitcoin-qt' + QMessageBox.warning(self, 'Invalid Path', \ + 'The path you specified for the Bitcoin software installation ' + 'does not exist. Please select the directory that contains %s ' + 'or leave it blank to have Armory search the default location ' + 'for your operating system' % exeName, QMessageBox.Ok) + return + if os.path.isfile(pathExe): + pathExe = os.path.dirname(pathExe) + self.main.writeSetting('SatoshiExe', pathExe) else: - pubkey += s.split(':')[-1].strip().replace(' ','') + self.main.settings.delete('SatoshiExe') - # SIGNATURE - if s.startswith('Sig') or readingSig: - readingSig = True - if 'END-SIGNATURE-BLOCK' in s: - readingSig = False + # Check valid path is supplied for bitcoind home directory + pathHome = unicode(self.edtSatoshiHomePath.text()).strip() + if len(pathHome) > 0: + if not os.path.exists(pathHome): + exeName = 'bitcoin-qt.exe' if OS_WINDOWS else 'bitcoin-qt' + QMessageBox.warning(self, 'Invalid Path', \ + 'The path you specified for the Bitcoin software home directory ' + 'does not exist. Only specify this directory if you use a ' + 'non-standard "-datadir=" option when running Bitcoin-Qt or ' + 'bitcoind. If you leave this field blank, the following ' + 'path will be used:

    %s' % BTC_HOME_DIR, QMessageBox.Ok) + return + self.main.writeSetting('SatoshiDatadir', pathHome) else: - sig += s.split(':')[-1].strip().replace(' ','') - - - if len(pubkey)>0: - try: - pubkey = hex_to_binary(pubkey) - if len(pubkey) not in (32, 33, 64, 65): raise - except: - QMessageBox.critical(parent, 'Bad Public Key', \ - 'Public key data was not recognized', QMessageBox.Ok) - pubkey = '' + self.main.settings.delete('SatoshiDatadir') + + self.main.writeSetting('ManageSatoshi', self.chkManageSatoshi.isChecked()) + self.main.writeSetting('SkipOnlineCheck', self.chkSkipOnlineCheck.isChecked()) + self.main.writeSetting('DisableTorrent', self.chkDisableTorrent.isChecked()) + + # Reset the DNAA flag as needed + askuriDNAA = self.chkAskURIAtStartup.isChecked() + self.main.writeSetting('DNAA_DefaultApp', not askuriDNAA) + - if len(sig)>0: try: - sig = hex_to_binary(sig) + defaultFee = str2coin(str(self.edtDefaultFee.text()).replace(' ', '')) + self.main.writeSetting('Default_Fee', defaultFee) except: - QMessageBox.critical(parent, 'Bad Signature', \ - 'Signature data is malformed!', QMessageBox.Ok) - sig = '' + QMessageBox.warning(self, 'Invalid Amount', \ + 'The default fee specified could not be understood. Please ' + 'specify in BTC with no more than 8 decimal places.', \ + QMessageBox.Ok) + return - - pubkeyhash = hash160(pubkey) - if not pubkeyhash==addrStr_to_hash160(addrB58): - QMessageBox.critical(parent, 'Address Mismatch', \ - '!!! The address included in the signature block does not ' - 'match the supplied public key! This should never happen, ' - 'and may in fact be an attempt to mislead you !!!', QMessageBox.Ok) - sig = '' - - + if not self.main.setPreferredDateFormat(str(self.edtDateFormat.text())): + return - return addrB58, messageStr, pubkey, sig + if not self.usermodeInit == self.cmbUsermode.currentIndex(): + modestr = str(self.cmbUsermode.currentText()) + if modestr.lower() == 'standard': + self.main.setUserMode(USERMODE.Standard) + elif modestr.lower() == 'advanced': + self.main.setUserMode(USERMODE.Advanced) + elif modestr.lower() == 'expert': + self.main.setUserMode(USERMODE.Expert) + if self.chkMinOrClose.isChecked(): + self.main.writeSetting('MinimizeOrClose', 'Minimize') + else: + self.main.writeSetting('MinimizeOrClose', 'Close') -################################################################################ -def makeSigBlock(addrB58, MessageStr, binPubkey='', binSig=''): - lineWid = 50 - s = '-----BEGIN-SIGNATURE-BLOCK'.ljust(lineWid+13,'-') + '\n' + self.main.writeSetting('MinimizeOnOpen', self.chkMinOnOpen.isChecked()) - ### Address ### - s += 'Address: %s\n' % addrB58 + # self.main.writeSetting('LedgDisplayFee', self.chkInclFee.isChecked()) + self.main.writeSetting('NotifyBtcIn', self.chkBtcIn.isChecked()) + self.main.writeSetting('NotifyBtcOut', self.chkBtcOut.isChecked()) + self.main.writeSetting('NotifyDiscon', self.chkDiscon.isChecked()) + self.main.writeSetting('NotifyReconn', self.chkReconn.isChecked()) - ### Message ### - chPerLine = lineWid-2 - nMessageLines = (len(MessageStr)-1)/chPerLine + 1 - for i in range(nMessageLines): - cLine = 'Message: "%s"\n' if i==0 else ' "%s"\n' - s += cLine % MessageStr[i*chPerLine:(i+1)*chPerLine] - ### Public Key ### - if len(binPubkey)>0: - hexPub = binary_to_hex(binPubkey) - nPubLines = (len(hexPub)-1)/lineWid + 1 - for i in range(nPubLines): - pLine = 'PublicKey: %s\n' if i==0 else ' %s\n' - s += pLine % hexPub[i*lineWid:(i+1)*lineWid] - - ### Signature ### - if len(binSig)>0: - hexSig = binary_to_hex(binSig) - nSigLines = (len(hexSig)-1)/lineWid + 1 - for i in range(nSigLines): - sLine = 'Signature: %s\n' if i==0 else ' %s\n' - s += sLine % hexSig[i*lineWid:(i+1)*lineWid] - - s += '-----END-SIGNATURE-BLOCK'.ljust(lineWid+13,'-') + '\n' - return s + if self.radioAnnounce1024.isChecked(): + self.main.writeSetting('NotifyMinPriority', 0) + elif self.radioAnnounce2048.isChecked(): + self.main.writeSetting('NotifyMinPriority', 2048) + elif self.radioAnnounce3072.isChecked(): + self.main.writeSetting('NotifyMinPriority', 3072) + elif self.radioAnnounce4096.isChecked(): + self.main.writeSetting('NotifyMinPriority', 4096) + self.main.writeSetting('DisableUpgradeNotify', \ + self.chkDisableUpgradeNotify.isChecked()) -class DlgExecLongProcess(ArmoryDialog): - """ - Execute a processing that may require having the user to wait a while. - Should appear like a splash screen, and will automatically close when - the processing is done. As such, you should have very little text, just - in case it finishes immediately, the user won't have time to read it. + self.main.createCombinedLedger() + super(DlgSettings, self).accept(*args) - DlgExecLongProcess(execFunc, 'Short Description', self, self.main).exec_() - """ - def __init__(self, funcExec, msg='', parent=None, main=None): - super(DlgExecLongProcess, self).__init__(parent, main) - - self.func = funcExec - waitFont = GETFONT('Var', 14) - descrFont = GETFONT('Var', 12) - palette = QPalette() - palette.setColor( QPalette.Window, QColor(235,235,255)) - self.setPalette( palette ); - self.setAutoFillBackground(True) + ############################################################################# + def setUsermodeDescr(self): + strDescr = '' + modestr = str(self.cmbUsermode.currentText()) + if modestr.lower() == 'standard': + strDescr += \ + ('"Standard" is for users that only need the core set of features ' + 'to send and receive bitcoins. This includes maintaining multiple ' + 'wallets, wallet encryption, and the ability to make backups ' + 'of your wallets.') + elif modestr.lower() == 'advanced': + strDescr += \ + ('"Advanced" mode provides ' + 'extra Armory features such as private key ' + 'importing & sweeping, message signing, and the offline wallet ' + 'interface. But, with advanced features come advanced risks...') + elif modestr.lower() == 'expert': + strDescr += \ + ('"Expert" mode is similar to "Advanced" but includes ' + 'access to lower-level info about transactions, scripts, keys ' + 'and network protocol. Most extra functionality is geared ' + 'towards Bitcoin software developers.') + self.lblUsermodeDescr.setText(strDescr) - if parent: - qr = parent.geometry() - x,y,w,h = qr.left(),qr.top(),qr.width(),qr.height() - dlgW = relaxedSizeStr(waitFont, msg)[0] - dlgW = min(dlgW, 400) - dlgH = 150 - self.setGeometry(int(x+w/2-dlgW/2),int(y+h/2-dlgH/2), dlgW, dlgH) - lblWaitMsg = QRichLabel('Please Wait...') - lblWaitMsg.setFont(waitFont) - lblWaitMsg.setAlignment(Qt.AlignVCenter | Qt.AlignHCenter) + ############################################################################# + def doExampleDate(self, qstr=None): + fmtstr = str(self.edtDateFormat.text()) + try: + self.lblDateExample.setText('Sample: ' + unixTimeToFormatStr(self.exampleUnixTime, fmtstr)) + self.isValidFormat = True + except: + self.lblDateExample.setText('Sample: [[invalid date format]]') + self.isValidFormat = False - lblDescrMsg = QRichLabel(msg) - lblDescrMsg.setFont(descrFont) - lblDescrMsg.setAlignment(Qt.AlignVCenter | Qt.AlignHCenter) + ############################################################################# + def clickChkManage(self): + self.edtSatoshiExePath.setEnabled(self.chkManageSatoshi.isChecked()) + self.edtSatoshiHomePath.setEnabled(self.chkManageSatoshi.isChecked()) + self.btnSetExe.setEnabled(self.chkManageSatoshi.isChecked()) + self.btnSetHome.setEnabled(self.chkManageSatoshi.isChecked()) - self.setWindowFlags(Qt.SplashScreen) - - layout = QVBoxLayout() - layout.addWidget(lblWaitMsg) - layout.addWidget(lblDescrMsg) - self.setLayout( layout ) +################################################################################ +class DlgExportTxHistory(ArmoryDialog): + def __init__(self, parent=None, main=None): + super(DlgExportTxHistory, self).__init__(parent, main) - def exec_(self): - def execAndClose(): - self.func() - self.accept() - from twisted.internet import reactor - reactor.callLater(0.1, execAndClose) - QDialog.exec_(self) + + self.cmbWltSelect = QComboBox() + self.cmbWltSelect.clear() + self.cmbWltSelect.addItem('My Wallets') + self.cmbWltSelect.addItem('Offline Wallets') + self.cmbWltSelect.addItem('Other Wallets') + self.cmbWltSelect.addItem('All Wallets') + for wltID in self.main.walletIDList: + self.cmbWltSelect.addItem(self.main.walletMap[wltID].labelName) + self.cmbWltSelect.insertSeparator(4) + self.cmbWltSelect.insertSeparator(4) - + self.cmbSortSelect = QComboBox() + self.cmbSortSelect.clear() + self.cmbSortSelect.addItem('Date (newest first)') + self.cmbSortSelect.addItem('Date (oldest first)') + self.cmbSortSelect.addItem('Transaction ID (ascending)') + self.cmbSortSelect.addItem('Transaction ID (descending)') -################################################################################ -class DlgECDSACalc(ArmoryDialog): - def __init__(self, parent=None, main=None, tabStart=0): - super(DlgECDSACalc, self).__init__(parent, main) + self.cmbFileFormat = QComboBox() + self.cmbFileFormat.clear() + self.cmbFileFormat.addItem('Comma-Separated Values (*.csv)') - dispFont = GETFONT('Var', 8) - w,h = tightSizeNChar(dispFont, 40) - + fmt = self.main.getPreferredDateFormat() + ttipStr = 'Use any of the following symbols:
    ' + fmtSymbols = [x[0] + ' = ' + x[1] for x in FORMAT_SYMBOLS] + ttipStr += '
    '.join(fmtSymbols) + + self.edtDateFormat = QLineEdit() + self.edtDateFormat.setText(fmt) + self.ttipFormatDescr = self.main.createToolTipWidget(ttipStr) + + self.lblDateExample = QRichLabel('', doWrap=False) + self.connect(self.edtDateFormat, SIGNAL('textEdited(QString)'), self.doExampleDate) + self.doExampleDate() + self.btnResetFormat = QPushButton("Reset to Default") + + def doReset(): + self.edtDateFormat.setText(DEFAULT_DATE_FORMAT) + self.doExampleDate() + self.connect(self.btnResetFormat, SIGNAL(CLICKED), doReset) - ########################################################################## - ########################################################################## - # TAB: secp256k1 - ########################################################################## - ########################################################################## - # STUB: I'll probably finish implementing this eventually.... - eccWidget = QWidget() - tabEccLayout = QGridLayout() - self.txtScalarScalarA = QLineEdit() - self.txtScalarScalarB = QLineEdit() - self.txtScalarScalarC = QLineEdit() - self.txtScalarScalarC.setReadOnly(True) - self.txtScalarPtA = QLineEdit() - self.txtScalarPtB_x = QLineEdit() - self.txtScalarPtB_y = QLineEdit() - self.txtScalarPtC_x = QLineEdit() - self.txtScalarPtC_y = QLineEdit() - self.txtScalarPtC_x.setReadOnly(True) - self.txtScalarPtC_y.setReadOnly(True) - self.txtPtPtA_x = QLineEdit() - self.txtPtPtA_y = QLineEdit() - self.txtPtPtB_x = QLineEdit() - self.txtPtPtB_y = QLineEdit() - self.txtPtPtC_x = QLineEdit() - self.txtPtPtC_y = QLineEdit() - self.txtPtPtC_x.setReadOnly(True) - self.txtPtPtC_y.setReadOnly(True) - eccTxtList = [ \ - self.txtScalarScalarA, self.txtScalarScalarB, \ - self.txtScalarScalarC, self.txtScalarPtA, self.txtScalarPtB_x, \ - self.txtScalarPtB_y, self.txtScalarPtC_x, self.txtScalarPtC_y, \ - self.txtPtPtA_x, self.txtPtPtA_y, self.txtPtPtB_x, \ - self.txtPtPtB_y, self.txtPtPtC_x, self.txtPtPtC_y] - - dispFont = GETFONT('Var', 8) - w,h = tightSizeNChar(dispFont, 60) - for txt in eccTxtList: - txt.setMinimumWidth(w) - txt.setFont(dispFont) - - self.btnCalcSS = QPushButton('Multiply Scalars (mod n)') - self.btnCalcSP = QPushButton('Scalar Multiply EC Point') - self.btnCalcPP = QPushButton('Add EC Points') - self.btnClearSS = QPushButton('Clear') - self.btnClearSP = QPushButton('Clear') - self.btnClearPP = QPushButton('Clear') + # Add the usual buttons + self.btnCancel = QPushButton("Cancel") + self.btnAccept = QPushButton("Export") + self.connect(self.btnCancel, SIGNAL(CLICKED), self.reject) + self.connect(self.btnAccept, SIGNAL(CLICKED), self.accept) + btnBox = makeHorizFrame([STRETCH, self.btnCancel, self.btnAccept]) - imgPlus = QRichLabel('+') - imgTimes1= QRichLabel('*') - imgTimes2= QRichLabel('*') - imgDown = QRichLabel('') + dlgLayout = QGridLayout() - self.connect(self.btnCalcSS, SIGNAL('clicked()'), self.multss) - self.connect(self.btnCalcSP, SIGNAL('clicked()'), self.multsp) - self.connect(self.btnCalcPP, SIGNAL('clicked()'), self.addpp) + i = 0 + dlgLayout.addWidget(QRichLabel('Export Format:'), i, 0) + dlgLayout.addWidget(self.cmbFileFormat, i, 1) + i += 1 + dlgLayout.addWidget(HLINE(), i, 0, 1, 2) - ########################################################################## - # Scalar-Scalar Multiply - sslblA = QRichLabel('a', hAlign=Qt.AlignHCenter) - sslblB = QRichLabel('b', hAlign=Qt.AlignHCenter) - sslblC = QRichLabel('a*b mod n', hAlign=Qt.AlignHCenter) + i += 1 + dlgLayout.addWidget(QRichLabel('Wallet(s) to export:'), i, 0) + dlgLayout.addWidget(self.cmbWltSelect, i, 1) - - ssLayout = QGridLayout() - ssLayout.addWidget(sslblA, 0,0, 1,1) - ssLayout.addWidget(sslblB, 0,2, 1,1) + i += 1 + dlgLayout.addWidget(HLINE(), i, 0, 1, 2) - ssLayout.addWidget(self.txtScalarScalarA, 1,0, 1,1) - ssLayout.addWidget(imgTimes1, 1,1, 1,1) - ssLayout.addWidget(self.txtScalarScalarB, 1,2, 1,1) + i += 1 + dlgLayout.addWidget(QRichLabel('Sort Table:'), i, 0) + dlgLayout.addWidget(self.cmbSortSelect, i, 1) - ssLayout.addWidget(makeHorizFrame(['Stretch', self.btnCalcSS, 'Stretch']), \ - 2,0, 1,3) - ssLayout.addWidget(makeHorizFrame(['Stretch', sslblC, self.txtScalarScalarC, 'Stretch']), \ - 3,0, 1,3) - ssLayout.setVerticalSpacing(1) - frmSS = QFrame() - frmSS.setFrameStyle(STYLE_SUNKEN) - frmSS.setLayout(ssLayout) + i += 1 + dlgLayout.addWidget(HLINE(), i, 0, 1, 2) - ########################################################################## - # Scalar-ECPoint Multiply - splblA = QRichLabel('a', hAlign=Qt.AlignHCenter) - splblB = QRichLabel('B', hAlign=Qt.AlignHCenter) - splblBx= QRichLabel('Bx', hAlign=Qt.AlignRight) - splblBy= QRichLabel('By', hAlign=Qt.AlignRight) - splblC = QRichLabel('C = a*B', hAlign=Qt.AlignHCenter) - splblCx= QRichLabel('(a*B)x', hAlign=Qt.AlignRight) - splblCy= QRichLabel('(a*B)y', hAlign=Qt.AlignRight) - spLayout = QGridLayout() - spLayout.addWidget(splblA, 0,0, 1,1) - spLayout.addWidget(splblB, 0,2, 1,1) - - spLayout.addWidget(self.txtScalarPtA, 1,0, 1,1) - spLayout.addWidget(imgTimes2, 1,1, 1,1) - spLayout.addWidget(self.txtScalarPtB_x, 1,2, 1,1) - spLayout.addWidget(self.txtScalarPtB_y, 2,2, 1,1) - - spLayout.addWidget(makeHorizFrame(['Stretch', self.btnCalcSP, 'Stretch']), \ - 3,0, 1,3) - spLayout.addWidget(makeHorizFrame(['Stretch', splblCx, self.txtScalarPtC_x, 'Stretch']), \ - 4,0, 1,3) - spLayout.addWidget(makeHorizFrame(['Stretch', splblCy, self.txtScalarPtC_y, 'Stretch']), \ - 5,0, 1,3) - spLayout.setVerticalSpacing(1) - frmSP = QFrame() - frmSP.setFrameStyle(STYLE_SUNKEN) - frmSP.setLayout(spLayout) - - ########################################################################## - # ECPoint Addition - pplblA = QRichLabel('A', hAlign=Qt.AlignHCenter) - pplblB = QRichLabel('B', hAlign=Qt.AlignHCenter) - pplblAx = QRichLabel('Ax', hAlign=Qt.AlignHCenter) - pplblAy = QRichLabel('Ay', hAlign=Qt.AlignHCenter) - pplblBx = QRichLabel('Bx', hAlign=Qt.AlignHCenter) - pplblBy = QRichLabel('By', hAlign=Qt.AlignHCenter) - pplblC = QRichLabel('C = A+B', hAlign=Qt.AlignHCenter) - pplblCx= QRichLabel('(A+B)x', hAlign=Qt.AlignRight) - pplblCy= QRichLabel('(A+B)y', hAlign=Qt.AlignRight) - ppLayout = QGridLayout() - ppLayout.addWidget(pplblA, 0,0, 1,1) - ppLayout.addWidget(pplblB, 0,2, 1,1) - ppLayout.addWidget(self.txtPtPtA_x, 1,0, 1,1) - ppLayout.addWidget(self.txtPtPtA_y, 2,0, 1,1) - ppLayout.addWidget(imgPlus, 1,1, 2,1) - ppLayout.addWidget(self.txtPtPtB_x, 1,2, 1,1) - ppLayout.addWidget(self.txtPtPtB_y, 2,2, 1,1) - ppLayout.addWidget(makeHorizFrame(['Stretch', self.btnCalcPP, 'Stretch']), \ - 3,0, 1,3) - ppLayout.addWidget(makeHorizFrame(['Stretch', pplblCx, self.txtPtPtC_x, 'Stretch']), \ - 4,0, 1,3) - ppLayout.addWidget(makeHorizFrame(['Stretch', pplblCy, self.txtPtPtC_y, 'Stretch']), \ - 5,0, 1,3) - ppLayout.setVerticalSpacing(1) - frmPP = QFrame() - frmPP.setFrameStyle(STYLE_SUNKEN) - frmPP.setLayout(ppLayout) + i += 1 + dlgLayout.addWidget(QRichLabel('Date Format:'), i, 0) + fmtfrm = makeHorizFrame([self.lblDateExample, STRETCH, self.ttipFormatDescr]) + dlgLayout.addWidget(fmtfrm, i, 1) - gxstr = '79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798' - gystr = '483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8' - - lblDescr = QRichLabel( tr(""" - Use this form to perform Bitcoin elliptic curve calculations. All - operations are performed on the secp256k1 elliptic curve, which is - the one used for Bitcoin. - Supply all values as 32-byte, big-endian, hex-encoded integers. -

    - The following is the secp256k1 generator point coordinates (G):
    - Gx: %s
    - Gy: %s""") % (gxstr, gystr)) + i += 1 + dlgLayout.addWidget(self.btnResetFormat, i, 0) + dlgLayout.addWidget(self.edtDateFormat, i, 1) - lblDescr.setTextInteractionFlags(Qt.TextSelectableByMouse | \ - Qt.TextSelectableByKeyboard) + i += 1 + dlgLayout.addWidget(HLINE(), i, 0, 1, 2) - btnClear = QPushButton('Clear') - btnClear.setMaximumWidth(2*relaxedSizeStr(btnClear, 'Clear')[0]) - self.connect(btnClear, SIGNAL('clicked()'), self.eccClear) + i += 1 + dlgLayout.addWidget(HLINE(), i, 0, 1, 2) - btnBack = QPushButton('<<< Go Back') - self.connect(btnBack, SIGNAL('clicked()'), self.accept) - frmBack = makeHorizFrame([btnBack, 'Stretch']) + i += 1 + dlgLayout.addWidget(btnBox, i, 0, 1, 2) - eccLayout = QVBoxLayout() - eccLayout.addWidget(makeHorizFrame([lblDescr, btnClear])) - eccLayout.addWidget(frmSS) - eccLayout.addWidget(frmSP) - eccLayout.addWidget(frmBack) + self.setLayout(dlgLayout) - eccWidget.setLayout(eccLayout) - calcLayout = QHBoxLayout() - calcLayout.addWidget(eccWidget) - self.setLayout(calcLayout) - - self.setWindowTitle('ECDSA Calculator') - self.setWindowIcon(QIcon( self.main.iconfile)) ############################################################################# - def getBinary(self, widget, name): + def doExampleDate(self, qstr=None): + fmtstr = str(self.edtDateFormat.text()) try: - hexVal = str(widget.text()) - binVal = hex_to_binary(hexVal) + self.lblDateExample.setText('Example: ' + unixTimeToFormatStr(1030501970, fmtstr)) + self.isValidFormat = True except: - QMessageBox.critical(self, 'Bad Input', \ - 'Value "%s" is invalid. Make sure the value is specified in ' - 'hex, big-endian' % name , QMessageBox.Ok) - return '' - return binVal - + self.lblDateExample.setText('Example: [[invalid date format]]') + self.isValidFormat = False ############################################################################# - def multss(self): - binA = self.getBinary(self.txtScalarScalarA, 'a') - binB = self.getBinary(self.txtScalarScalarB, 'b') - C = CryptoECDSA().ECMultiplyScalars(binA, binB) - self.txtScalarScalarC.setText( binary_to_hex(C)) + def accept(self, *args): + if self.createFile_CSV(): + super(DlgExportTxHistory, self).accept(*args) + - for txt in [self.txtScalarScalarA, \ - self.txtScalarScalarB, \ - self.txtScalarScalarC]: - txt.setCursorPosition(0) - ############################################################################# - def multsp(self): - binA = self.getBinary(self.txtScalarPtA, 'a') - binBx = self.getBinary(self.txtScalarPtB_x, 'Bx') - binBy = self.getBinary(self.txtScalarPtB_y, 'By') + def createFile_CSV(self): + if not self.isValidFormat: + QMessageBox.warning(self, 'Invalid date format', \ + 'Cannot create CSV without a valid format for transaction ' + 'dates and times', QMessageBox.Ok) + return False - if not CryptoECDSA().ECVerifyPoint(binBx, binBy): - QMessageBox.critical(self, 'Invalid EC Point', \ - 'The point you specified (B) is not on the ' - 'elliptic curved used in Bitcoin (secp256k1).', QMessageBox.Ok) - return + # This was pretty much copied from the createCombinedLedger method... + # I rarely do this, but modularizing this piece is a non-trivial + wltIDList = [] + typelist = [[wid, determineWalletType(self.main.walletMap[wid], self.main)[0]] \ + for wid in self.main.walletIDList] + currIdx = self.cmbWltSelect.currentIndex() + if currIdx >= 4: + wltIDList = [self.main.walletIDList[currIdx - 6]] + else: + listOffline = [t[0] for t in filter(lambda x: x[1] == WLTTYPES.Offline, typelist)] + listWatching = [t[0] for t in filter(lambda x: x[1] == WLTTYPES.WatchOnly, typelist)] + listCrypt = [t[0] for t in filter(lambda x: x[1] == WLTTYPES.Crypt, typelist)] + listPlain = [t[0] for t in filter(lambda x: x[1] == WLTTYPES.Plain, typelist)] - C = CryptoECDSA().ECMultiplyPoint(binA, binBx, binBy) - self.txtScalarPtC_x.setText(binary_to_hex(C[:32])) - self.txtScalarPtC_y.setText(binary_to_hex(C[32:])) - - for txt in [self.txtScalarPtA, \ - self.txtScalarPtB_x, self.txtScalarPtB_y, \ - self.txtScalarPtC_x, self.txtScalarPtC_y]: - txt.setCursorPosition(0) + if currIdx == 0: + wltIDList = listOffline + listCrypt + listPlain + elif currIdx == 1: + wltIDList = listOffline + elif currIdx == 2: + wltIDList = listWatching + elif currIdx == 3: + wltIDList = self.main.walletIDList + else: + pass - ############################################################################# - def addpp(self): - binAx = self.getBinary(self.txtPtPtA_x, 'Ax') - binAy = self.getBinary(self.txtPtPtA_y, 'Ay') - binBx = self.getBinary(self.txtPtPtB_x, 'Bx') - binBy = self.getBinary(self.txtPtPtB_y, 'By') + totalFunds, spendFunds, unconfFunds, combinedLedger = 0, 0, 0, [] + for wltID in wltIDList: + wlt = self.main.walletMap[wltID] + id_le_pairs = [[wltID, le] for le in wlt.getTxLedger('Full')] + combinedLedger.extend(id_le_pairs) + totalFunds += wlt.getBalance('Total') + spendFunds += wlt.getBalance('Spendable') + unconfFunds += wlt.getBalance('Unconfirmed') + # END createCombinedLedger copy - if not CryptoECDSA().ECVerifyPoint(binAx, binAy): - QMessageBox.critical(self, 'Invalid EC Point', \ - 'The point you specified (A) is not on the ' - 'elliptic curved used in Bitcoin (secp256k1).', QMessageBox.Ok) - return + ledgerTable = self.main.convertLedgerToTable(combinedLedger) - if not CryptoECDSA().ECVerifyPoint(binBx, binBy): - QMessageBox.critical(self, 'Invalid EC Point', \ - 'The point you specified (B) is not on the ' - 'elliptic curved used in Bitcoin (secp256k1).', QMessageBox.Ok) + sortTxt = str(self.cmbSortSelect.currentText()) + if 'newest' in sortTxt: + ledgerTable.sort(key=lambda x: x[LEDGERCOLS.UnixTime], reverse=True) + elif 'oldest' in sortTxt: + ledgerTable.sort(key=lambda x: x[LEDGERCOLS.UnixTime]) + elif 'ascend' in sortTxt: + ledgerTable.sort(key=lambda x: hex_switchEndian(x[LEDGERCOLS.TxHash])) + elif 'descend' in sortTxt: + ledgerTable.sort(key=lambda x: hex_switchEndian(x[LEDGERCOLS.TxHash]), reverse=True) + else: + LOGERROR('***ERROR: bad sort string!?') return - C = CryptoECDSA().ECAddPoints(binAx, binAy, binBx, binBy) - self.txtPtPtC_x.setText(binary_to_hex(C[:32])) - self.txtPtPtC_y.setText(binary_to_hex(C[32:])) - for txt in [self.txtPtPtA_x, self.txtPtPtA_y, \ - self.txtPtPtB_x, self.txtPtPtB_y, \ - self.txtPtPtC_x, self.txtPtPtC_y]: - txt.setCursorPosition(0) + wltSelectStr = str(self.cmbWltSelect.currentText()).replace(' ', '_') + timestampStr = unixTimeToFormatStr(RightNow(), '%Y%m%d_%H%M') + filenamePrefix = 'ArmoryTxHistory_%s_%s' % (wltSelectStr, timestampStr) + fmtstr = str(self.cmbFileFormat.currentText()) + if 'csv' in fmtstr: + defaultName = filenamePrefix + '.csv' + fullpath = self.main.getFileSave('Save CSV File', \ + ['Comma-Separated Values (*.csv)'], \ + defaultName) + if len(fullpath) == 0: + return - ############################################################################# - def eccClear(self): - self.txtScalarScalarA.setText('') - self.txtScalarScalarB.setText('') - self.txtScalarScalarC.setText('') + f = open(fullpath, 'w') - self.txtScalarPtA.setText('') - self.txtScalarPtB_x.setText('') - self.txtScalarPtB_y.setText('') - self.txtScalarPtC_x.setText('') - self.txtScalarPtC_y.setText('') + f.write('Export Date:, %s\n' % unixTimeToFormatStr(RightNow())) + f.write('Total Funds:, %s\n' % coin2str(totalFunds, maxZeros=0).strip()) + f.write('Spendable Funds:, %s\n' % coin2str(spendFunds, maxZeros=0).strip()) + f.write('Unconfirmed Funds:, %s\n' % coin2str(unconfFunds, maxZeros=0).strip()) + f.write('\n') - self.txtPtPtA_x.setText('') - self.txtPtPtA_y.setText('') - self.txtPtPtB_x.setText('') - self.txtPtPtB_y.setText('') - self.txtPtPtC_x.setText('') - self.txtPtPtC_y.setText('') + f.write('Included Wallets:\n') + for wltID in wltIDList: + wlt = self.main.walletMap[wltID] + f.write('%s,%s\n' % (wltID, wlt.labelName.replace(',', ';'))) + f.write('\n') + + f.write('Date,Transaction ID,#Conf,Wallet ID, Wallet Name,Total Credit,Total Debit,Fee (wallet paid),Comment\n') + COL = LEDGERCOLS + for row in ledgerTable: + vals = [] + + fmtstr = str(self.edtDateFormat.text()) + unixTime = row[COL.UnixTime] + vals.append(unixTimeToFormatStr(unixTime, fmtstr)) + vals.append(hex_switchEndian(row[COL.TxHash])) + vals.append(row[COL.NumConf]) + vals.append(row[COL.WltID]) + vals.append(self.main.walletMap[row[COL.WltID]].labelName.replace(',', ';')) + + wltEffect = row[COL.Amount] + txFee = getFeeForTx(hex_to_binary(row[COL.TxHash])) + if float(wltEffect) > 0: + vals.append(wltEffect.strip()) + vals.append(' ') + vals.append(' ') + else: + vals.append(' ') + vals.append(wltEffect.strip()) + vals.append(coin2str(-txFee).strip()) + vals.append(row[COL.Comment]) + f.write('%s,%s,%d,%s,%s,%s,%s,%s,"%s"\n' % tuple(vals)) + f.close() + return True ################################################################################ -class DlgAddressBook(ArmoryDialog): - """ - This dialog is provided a widget which has a "setText()" method. When the - user selects the address, this dialog will enter the text into the widget - and then close itself. - """ - def __init__(self, parent, main, putResultInWidget=None, \ - defaultWltID=None, \ - actionStr='Select', \ - selectExistingOnly=False,\ - selectMineOnly=False): - super(DlgAddressBook, self).__init__(parent, main) +class DlgRequestPayment(ArmoryDialog): + def __init__(self, parent, main, recvAddr, amt=None, msg=''): + super(DlgRequestPayment, self).__init__(parent, main) - self.target = putResultInWidget - self.actStr = actionStr - self.isBrowsingOnly = (self.target==None) + if isLikelyDataType(recvAddr, DATATYPE.Binary) and len(recvAddr) == 20: + self.recvAddr = hash160_to_addrStr(recvAddr) + elif isLikelyDataType(recvAddr, DATATYPE.Base58): + self.recvAddr = recvAddr + else: + raise BadAddressError('Unrecognized address input') - if defaultWltID==None: - defaultWltID = self.main.walletIDList[0] - self.wlt = self.main.walletMap[defaultWltID] + # Amount + self.edtAmount = QLineEdit() + self.edtAmount.setFont(GETFONT('Fixed')) + self.edtAmount.setMaximumWidth(relaxedSizeNChar(GETFONT('Fixed'), 13)[0]) + if amt: + self.edtAmount.setText(coin2str(amt, maxZeros=0)) - lblDescr = QRichLabel('Choose an address from your transaction history, ' - 'or your own wallet. If you choose to send to one ' - 'of your own wallets, the next unused address in ' - 'that wallet will be used.') - if self.isBrowsingOnly or selectExistingOnly: - lblDescr = QRichLabel('Browse all receiving addresses in ' - 'this wallet, and all addresses to which this ' - 'wallet has sent bitcoins.') + # Message: + self.edtMessage = QLineEdit() + self.edtMessage.setMaxLength(128) + if msg: + self.edtMessage.setText(msg) + else: + self.edtMessage.setText('Joe\'s Fish Shop - Order #123 - 3 kg tuna - (888)555-1212') + self.edtMessage.setCursorPosition(0) - lblToWlt = QRichLabel('Send to Wallet:') - lblToAddr = QRichLabel('Send to Address:') - if self.isBrowsingOnly: - lblToWlt.setVisible(False) - lblToAddr.setVisible(False) - rowHeight = tightSizeStr(self.font, 'XygjpHI')[1] + # Address: + self.edtAddress = QLineEdit() + self.edtAddress.setText(self.recvAddr) - self.wltDispModel = AllWalletsDispModel(self.main) - self.wltDispView = QTableView() - self.wltDispView.setModel(self.wltDispModel) - self.wltDispView.setSelectionBehavior(QTableView.SelectRows) - self.wltDispView.setSelectionMode(QTableView.SingleSelection) - self.wltDispView.horizontalHeader().setStretchLastSection(True) - self.wltDispView.verticalHeader().setDefaultSectionSize(20) - self.wltDispView.setMaximumHeight(rowHeight*7.7) - initialColResize(self.wltDispView, [0.15, 0.30, 0.2, 0.20]) - self.connect(self.wltDispView.selectionModel(), \ - SIGNAL('currentChanged(const QModelIndex &, const QModelIndex &)'), \ - self.wltTableClicked) - + # Link Text: + self.edtLinkText = QLineEdit() + defaultText = binary_to_hex('Click here to pay for your order!') + linkText = hex_to_binary(self.main.getSettingOrSetDefault('DefaultLinkText', defaultText)) + self.edtLinkText.setText(linkText) + self.edtLinkText.setCursorPosition(0) - - - # DISPLAY sent-to addresses - self.addrBookTxModel = None - self.addrBookTxView = QTableView() - self.addrBookTxView.setSortingEnabled(True) - self.setAddrBookTxModel(defaultWltID) - self.connect(self.addrBookTxView, SIGNAL('doubleClicked(QModelIndex)'), \ - self.dblClickAddressTx) + qpal = QPalette() + qpal.setColor(QPalette.Text, Colors.TextBlue) + self.edtLinkText.setPalette(qpal) + edtFont = self.edtLinkText.font() + edtFont.setUnderline(True) + self.edtLinkText.setFont(edtFont) - self.addrBookTxView.setContextMenuPolicy(Qt.CustomContextMenu) - self.addrBookTxView.customContextMenuRequested.connect(self.showContextMenuTx) - # DISPLAY receiving addresses - self.addrBookRxModel = None - self.addrBookRxView = QTableView() - self.addrBookRxView.setSortingEnabled(True) - self.setAddrBookRxModel(defaultWltID) - self.connect(self.addrBookRxView, SIGNAL('doubleClicked(QModelIndex)'), \ - self.dblClickAddressRx) - self.addrBookRxView.setContextMenuPolicy(Qt.CustomContextMenu) - self.addrBookRxView.customContextMenuRequested.connect(self.showContextMenuRx) + self.connect(self.edtMessage, SIGNAL('textChanged(QString)'), self.setLabels) + self.connect(self.edtAddress, SIGNAL('textChanged(QString)'), self.setLabels) + self.connect(self.edtAmount, SIGNAL('textChanged(QString)'), self.setLabels) + self.connect(self.edtLinkText, SIGNAL('textChanged(QString)'), self.setLabels) + self.connect(self.edtMessage, SIGNAL('editingFinished()'), self.updateQRCode) + self.connect(self.edtAddress, SIGNAL('editingFinished()'), self.updateQRCode) + self.connect(self.edtAmount, SIGNAL('editingFinished()'), self.updateQRCode) + self.connect(self.edtLinkText, SIGNAL('editingFinished()'), self.updateQRCode) - self.tabWidget = QTabWidget() - self.tabWidget.addTab(self.addrBookRxView, 'Receiving (Mine)') - if not selectMineOnly: - self.tabWidget.addTab(self.addrBookTxView, 'Sending (Other\'s)') - self.tabWidget.setCurrentIndex(0) - + # This is the "output" + self.lblLink = QRichLabel('') + self.lblLink.setOpenExternalLinks(True) + self.lblLink.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard) + self.lblLink.setMinimumHeight(3 * tightSizeNChar(self, 1)[1]) + self.lblLink.setAlignment(Qt.AlignVCenter | Qt.AlignLeft) + self.lblLink.setContentsMargins(10, 5, 10, 5) + self.lblLink.setStyleSheet('QLabel { background-color : %s }' % htmlColor('SlightBkgdDark')) + frmOut = makeHorizFrame([self.lblLink], QFrame.Box | QFrame.Raised) + frmOut.setLineWidth(1) + frmOut.setMidLineWidth(5) - ttipSendWlt = self.main.createToolTipWidget( \ - 'The next unused address in that wallet will be calculated and selected. ') - ttipSendAddr = self.main.createToolTipWidget( \ - 'Addresses that are in other wallets you own are not showns.') + self.lblWarn = QRichLabel('') + self.lblWarn.setAlignment(Qt.AlignVCenter | Qt.AlignHCenter) - self.lblSelectWlt = QRichLabel('', doWrap=False) - self.btnSelectWlt = QPushButton('No Wallet Selected') - self.btnSelectAddr = QPushButton('No Address Selected') - self.btnSelectWlt.setEnabled(False) - self.btnSelectAddr.setEnabled(False) - btnCancel = QPushButton('Cancel') + self.btnOtherOpt = QPushButton('Other Options >>>') + self.btnCopyRich = QPushButton('Copy to Clipboard') + self.btnCopyHtml = QPushButton('Copy Raw HTML') + self.btnCopyRaw = QPushButton('Copy Raw URL') + self.btnCopyAll = QPushButton('Copy All Text') - if self.isBrowsingOnly: - self.btnSelectWlt.setVisible(False) - self.btnSelectAddr.setVisible(False) - self.lblSelectWlt.setVisible(False) - btnCancel = QPushButton('<<< Go Back') - ttipSendAddr.setVisible(False) - - if selectExistingOnly: - lblToWlt.setVisible(False) - self.lblSelectWlt.setVisible(False) - self.btnSelectWlt.setVisible(False) - ttipSendWlt.setVisible(False) + # I never actally got this button working right... + self.btnCopyRich.setVisible(True) + self.btnOtherOpt.setCheckable(True) + self.btnCopyAll.setVisible(False) + self.btnCopyHtml.setVisible(False) + self.btnCopyRaw.setVisible(False) + frmCopyBtnStrip = makeHorizFrame([ \ + self.btnCopyRich, \ + self.btnOtherOpt, \ + self.btnCopyHtml, \ + self.btnCopyRaw, \ + STRETCH, \ + self.lblWarn]) + # self.btnCopyAll, \ - self.connect(self.btnSelectWlt, SIGNAL('clicked()'), self.acceptWltSelection) - self.connect(self.btnSelectAddr, SIGNAL('clicked()'), self.acceptAddrSelection) - self.connect(btnCancel, SIGNAL('clicked()'), self.reject) + self.connect(self.btnCopyRich, SIGNAL(CLICKED), self.clickCopyRich) + self.connect(self.btnOtherOpt, SIGNAL('toggled(bool)'), self.clickOtherOpt) + self.connect(self.btnCopyRaw, SIGNAL(CLICKED), self.clickCopyRaw) + self.connect(self.btnCopyHtml, SIGNAL(CLICKED), self.clickCopyHtml) + self.connect(self.btnCopyAll, SIGNAL(CLICKED), self.clickCopyAll) + lblDescr = QRichLabel(\ + 'Create a clickable link that you can copy into email or webpage to ' + 'request a payment. If the user is running a Bitcoin program ' + 'that supports "bitcoin:" links, that program will open with ' + 'all this information pre-filled after they click the link.') - dlgLayout = QGridLayout() - dlgLayout.addWidget(lblDescr, 0,0) - dlgLayout.addWidget(HLINE(), 1,0) - dlgLayout.addWidget(lblToWlt, 2,0) - dlgLayout.addWidget(self.wltDispView, 3,0) - dlgLayout.addWidget(makeHorizFrame([self.lblSelectWlt, 'Stretch', self.btnSelectWlt]), 4,0) - dlgLayout.addWidget(HLINE(), 5,0) - dlgLayout.addWidget(lblToAddr, 6,0) - dlgLayout.addWidget(self.tabWidget, 7,0) - dlgLayout.addWidget(makeHorizFrame(['Stretch', self.btnSelectAddr]), 8,0) - dlgLayout.addWidget(HLINE(), 9,0) - dlgLayout.addWidget(makeHorizFrame([btnCancel, 'Stretch']), 10,0) - dlgLayout.setRowStretch(3, 1) - dlgLayout.setRowStretch(7, 2) + lblDescr.setContentsMargins(5, 5, 5, 5) + frmDescr = makeHorizFrame([lblDescr], STYLE_SUNKEN) - self.setLayout(dlgLayout) - self.sizeHint = lambda: QSize(760, 500) - # Auto-select the default wallet, if there is one - rowNum = 0 - if defaultWltID and self.main.walletMap.has_key(defaultWltID): - rowNum = self.main.walletIndices[defaultWltID] - rowIndex = self.wltDispModel.index(rowNum,0) - self.wltDispView.setCurrentIndex(rowIndex) - self.wltTableClicked(rowIndex) - - self.setWindowTitle('Address Book') - self.setWindowIcon(QIcon(self.main.iconfile)) + ttipPreview = self.main.createToolTipWidget(\ + 'The following Bitcoin desktop applications try to ' + 'register themselves with your computer to handle "bitcoin:" ' + 'links: Armory, Multibit, Electrum') + ttipLinkText = self.main.createToolTipWidget(\ + 'This is the text to be shown as the clickable link. It should ' + 'usually begin with "Click here..." to reaffirm to the user it is ' + 'is clickable.') + ttipAmount = self.main.createToolTipWidget(\ + 'All amounts are specifed in BTC') + ttipAddress = self.main.createToolTipWidget(\ + 'The person clicking the link will be sending bitcoins to this address') + ttipMessage = self.main.createToolTipWidget(\ + 'This text will be pre-filled as the label/comment field ' + 'after the user clicks on the link. They ' + 'can modify it to meet their own needs, but you can ' + 'provide useful information such as contact details and ' + 'purchase info as a convenience to them.') - self.setMinimumWidth(300) - hexgeom = self.main.settings.get('AddrBookGeometry') - wltgeom = self.main.settings.get('AddrBookWltTbl') - rxgeom = self.main.settings.get('AddrBookRxTbl') - txgeom = self.main.settings.get('AddrBookTxTbl') - if len(hexgeom)>0: - geom = QByteArray.fromHex(hexgeom) - self.restoreGeometry(geom) - if len(wltgeom)>0: - restoreTableView(self.wltDispView, wltgeom) - if len(rxgeom)>0: - restoreTableView(self.addrBookRxView, rxgeom) - if len(txgeom)>0 and not selectMineOnly: - restoreTableView(self.addrBookTxView, txgeom) + btnClose = QPushButton('Close') + self.connect(btnClose, SIGNAL(CLICKED), self.accept) - ############################################################################# - def saveGeometrySettings(self): - self.main.writeSetting('AddrBookGeometry', str(self.saveGeometry().toHex())) - self.main.writeSetting('AddrBookWltTbl', saveTableView(self.wltDispView)) - self.main.writeSetting('AddrBookRxTbl', saveTableView(self.addrBookRxView)) - self.main.writeSetting('AddrBookTxTbl', saveTableView(self.addrBookTxView)) - ############################################################################# - def closeEvent(self, event): - self.saveGeometrySettings() - super(DlgAddressBook, self).closeEvent(event) + frmEntry = QFrame() + frmEntry.setFrameStyle(STYLE_SUNKEN) + layoutEntry = QGridLayout() + i = 0 + layoutEntry.addWidget(QRichLabel('Link Text:'), i, 0) + layoutEntry.addWidget(self.edtLinkText, i, 1) + layoutEntry.addWidget(ttipLinkText, i, 2) + + i += 1 + layoutEntry.addWidget(QRichLabel('Address (yours):'), i, 0) + layoutEntry.addWidget(self.edtAddress, i, 1) + layoutEntry.addWidget(ttipAddress, i, 2) + + i += 1 + layoutEntry.addWidget(QRichLabel('Request (BTC):'), i, 0) + layoutEntry.addWidget(self.edtAmount, i, 1) + + i += 1 + layoutEntry.addWidget(QRichLabel('Label:'), i, 0) + layoutEntry.addWidget(self.edtMessage, i, 1) + layoutEntry.addWidget(ttipMessage, i, 2) + frmEntry.setLayout(layoutEntry) - ############################################################################# - def accept(self, *args): - self.saveGeometrySettings() - super(DlgAddressBook, self).accept(*args) - ############################################################################# - def reject(self, *args): - self.saveGeometrySettings() - super(DlgAddressBook, self).reject(*args) - - ############################################################################# - def setAddrBookTxModel(self, wltID): - self.addrBookTxModel = SentToAddrBookModel(wltID, self.main) + lblOut = QRichLabel('Copy and paste the following text into email or other document:') + frmOutput = makeVertFrame([lblOut, frmOut, frmCopyBtnStrip], STYLE_SUNKEN) + frmOutput.layout().setStretch(0, 0) + frmOutput.layout().setStretch(1, 1) + frmOutput.layout().setStretch(2, 0) + frmClose = makeHorizFrame([STRETCH, btnClose]) - # - self.addrBookTxProxy = SentAddrSortProxy(self) - self.addrBookTxProxy.setSourceModel(self.addrBookTxModel) - #self.addrBookTxProxy.sort(ADDRBOOKCOLS.Address) - self.addrBookTxView.setModel(self.addrBookTxProxy) - self.addrBookTxView.setSortingEnabled(True) - self.addrBookTxView.setSelectionBehavior(QTableView.SelectRows) - self.addrBookTxView.setSelectionMode(QTableView.SingleSelection) - self.addrBookTxView.horizontalHeader().setStretchLastSection(True) - self.addrBookTxView.verticalHeader().setDefaultSectionSize(20) - freqSize = 1.3 * tightSizeStr(self.addrBookTxView, 'Times Used')[0] - initialColResize(self.addrBookTxView, [0.3, 0.1, freqSize, 0.5]) - self.addrBookTxView.hideColumn(ADDRBOOKCOLS.WltID) - self.connect(self.addrBookTxView.selectionModel(), \ - SIGNAL('currentChanged(const QModelIndex &, const QModelIndex &)'), \ - self.addrTableTxClicked) + self.qrURI = QRCodeWidget('', parent=self) + lblQRDescr = QRichLabel('This QR code contains address and the ' + 'other payment information shown to the left.') + lblQRDescr.setAlignment(Qt.AlignTop | Qt.AlignHCenter) + frmQR = makeVertFrame([self.qrURI, STRETCH, lblQRDescr, STRETCH], STYLE_SUNKEN) + frmQR.layout().setStretch(0, 0) + frmQR.layout().setStretch(1, 0) + frmQR.layout().setStretch(2, 1) - ############################################################################# - def setAddrBookRxModel(self, wltID): - wlt = self.main.walletMap[wltID] - self.addrBookRxModel = WalletAddrDispModel(wlt, self) + self.maxQRSize = int(1.25 * QRCodeWidget('a' * 200).getSize()) + frmQR.setMinimumWidth(self.maxQRSize) + self.qrURI.setMinimumHeight(self.maxQRSize) - self.addrBookRxProxy = WalletAddrSortProxy(self) - self.addrBookRxProxy.setSourceModel(self.addrBookRxModel) - #self.addrBookRxProxy.sort(ADDRESSCOLS.Address) - self.addrBookRxView.setModel(self.addrBookRxProxy) - self.addrBookRxView.setSelectionBehavior(QTableView.SelectRows) - self.addrBookRxView.setSelectionMode(QTableView.SingleSelection) - self.addrBookRxView.horizontalHeader().setStretchLastSection(True) - self.addrBookRxView.verticalHeader().setDefaultSectionSize(20) - iWidth = tightSizeStr(self.addrBookRxView, 'Imp')[0] - initialColResize(self.addrBookRxView, [iWidth*1.3, 0.3, 0.35, 64, 0.3]) - self.connect(self.addrBookRxView.selectionModel(), \ - SIGNAL('currentChanged(const QModelIndex &, const QModelIndex &)'), \ - self.addrTableRxClicked) + dlgLayout = QGridLayout() + dlgLayout.addWidget(frmDescr, 0, 0, 1, 2) + dlgLayout.addWidget(frmEntry, 1, 0, 1, 1) + dlgLayout.addWidget(frmOutput, 2, 0, 1, 1) + dlgLayout.addWidget(HLINE(), 3, 0, 1, 2) + dlgLayout.addWidget(frmClose, 4, 0, 1, 2) - ############################################################################# - def wltTableClicked(self, currIndex, prevIndex=None): - if prevIndex==currIndex: - return + dlgLayout.addWidget(frmQR, 1, 1, 2, 1) - self.btnSelectWlt.setEnabled(True) - row = currIndex.row() - self.selectedWltID = str(currIndex.model().index(row, WLTVIEWCOLS.ID).data().toString()) + dlgLayout.setRowStretch(0, 0) + dlgLayout.setRowStretch(1, 0) + dlgLayout.setRowStretch(2, 1) + dlgLayout.setRowStretch(3, 0) + dlgLayout.setRowStretch(4, 0) + + + self.setLabels() + self.prevURI = '' + self.closed = False # kind of a hack to end the update loop + self.updateQRCode() + self.setLayout(dlgLayout) + self.setWindowTitle('Create Payment Request Link') - self.setAddrBookTxModel(self.selectedWltID) - self.setAddrBookRxModel(self.selectedWltID) + from twisted.internet import reactor + reactor.callLater(1, self.periodicUpdate) + hexgeom = str(self.main.settings.get('PayReqestGeometry')) + if len(hexgeom) > 0: + geom = QByteArray.fromHex(hexgeom) + self.restoreGeometry(geom) + self.setMinimumSize(750, 500) - if not self.isBrowsingOnly: - wlt = self.main.walletMap[self.selectedWltID] - self.btnSelectWlt.setText('%s Wallet: %s' % (self.actStr, self.selectedWltID)) - nextAddr160 = wlt.peekNextUnusedAddr160() - self.lblSelectWlt.setText('Will create new address: %s...' % \ - hash160_to_addrStr(nextAddr160)[:10]) - # If switched wallet selection, de-select address so it doesn't look - # like the currently-selected address is for this different wallet - self.btnSelectAddr.setEnabled(False) - self.btnSelectAddr.setText('No Address Selected') - self.selectedAddr = '' - self.selectedCmmt = '' - self.addrBookTxModel.reset() + def saveLinkText(self): + linktext = str(self.edtLinkText.text()).strip() + if len(linktext) > 0: + hexText = binary_to_hex(linktext) + self.main.writeSetting('DefaultLinkText', hexText) ############################################################################# - def addrTableTxClicked(self, currIndex, prevIndex=None): - if prevIndex==currIndex: - return + def saveGeometrySettings(self): + self.main.writeSetting('PayReqestGeometry', str(self.saveGeometry().toHex())) - self.btnSelectAddr.setEnabled(True) - row = currIndex.row() - self.selectedAddr = str(currIndex.model().index(row, ADDRBOOKCOLS.Address).data().toString()) - self.selectedCmmt = str(currIndex.model().index(row, ADDRBOOKCOLS.Comment).data().toString()) + ############################################################################# + def closeEvent(self, event): + self.saveGeometrySettings() + self.saveLinkText() + super(DlgRequestPayment, self).closeEvent(event) - if not self.isBrowsingOnly: - self.btnSelectAddr.setText('%s Address: %s...' % (self.actStr, self.selectedAddr[:10])) + ############################################################################# + def accept(self, *args): + self.saveGeometrySettings() + self.saveLinkText() + super(DlgRequestPayment, self).accept(*args) + + ############################################################################# + def reject(self, *args): + self.saveGeometrySettings() + super(DlgRequestPayment, self).reject(*args) ############################################################################# - def addrTableRxClicked(self, currIndex, prevIndex=None): - if prevIndex==currIndex: - return + def setLabels(self): - self.btnSelectAddr.setEnabled(True) - row = currIndex.row() - self.selectedAddr = str(currIndex.model().index(row, ADDRESSCOLS.Address).data().toString()) - self.selectedCmmt = str(currIndex.model().index(row, ADDRESSCOLS.Comment).data().toString()) + lastTry = '' + try: + # The + lastTry = 'Amount' + amtStr = str(self.edtAmount.text()).strip() + if len(amtStr) == 0: + amt = None + else: + amt = str2coin(amtStr) - if not self.isBrowsingOnly: - self.btnSelectAddr.setText('%s Address: %s...' % (self.actStr, self.selectedAddr[:10])) + lastTry = 'Message' + msgStr = str(self.edtMessage.text()).strip() + if len(msgStr) == 0: + msgStr = None + lastTry = 'Address' + addr = str(self.edtAddress.text()).strip() + if not checkAddrStrValid(addr): + raise - ############################################################################# - def dblClickAddressRx(self, index): - if index.column()!=ADDRESSCOLS.Comment: - self.acceptAddrSelection() + errorIn = 'Inputs' + # must have address, maybe have amount and/or message + self.rawURI = createBitcoinURI(addr, amt, msgStr) + except: + self.lblWarn.setText('Invalid %s' % lastTry) + self.btnCopyRaw.setEnabled(False) + self.btnCopyHtml.setEnabled(False) + self.btnCopyAll.setEnabled(False) + # self.lblLink.setText('
    '.join(str(self.lblLink.text()).split('
    ')[1:])) + self.lblLink.setEnabled(False) + self.lblLink.setTextInteractionFlags(Qt.NoTextInteraction) return - wlt = self.main.walletMap[self.selectedWltID] - - dialog = DlgSetComment(self.selectedCmmt, 'Address', self, self.main) - if dialog.exec_(): - newComment = str(dialog.edtComment.text()) - addr160 = addrStr_to_hash160(self.selectedAddr) - wlt.setComment(addr160, newComment) + self.lblLink.setTextInteractionFlags(Qt.TextSelectableByMouse | \ + Qt.TextSelectableByKeyboard) - ############################################################################# - def dblClickAddressTx(self, index): - if index.column()!=ADDRBOOKCOLS.Comment: - self.acceptAddrSelection() - return + self.rawHtml = '%s' % (self.rawURI, str(self.edtLinkText.text())) + self.lblWarn.setText('') + self.dispText = self.rawHtml[:] + self.dispText += '
    ' + self.dispText += 'If clicking on the line above does not work, use this payment info:' + self.dispText += '
    ' + self.dispText += 'Pay to:\t%s
    ' % addr + if amtStr: + self.dispText += 'Amount:\t%s BTC
    ' % coin2str(amt, maxZeros=0).strip() + if msgStr: + self.dispText += 'Message:\t%s
    ' % msgStr + self.lblLink.setText(self.dispText) - wlt = self.main.walletMap[self.selectedWltID] + self.lblLink.setEnabled(True) + self.btnCopyRaw.setEnabled(True) + self.btnCopyHtml.setEnabled(True) + self.btnCopyAll.setEnabled(True) - dialog = DlgSetComment(self.selectedCmmt, 'Address', self, self.main) - if dialog.exec_(): - newComment = str(dialog.edtComment.text()) - addr160 = addrStr_to_hash160(self.selectedAddr) - wlt.setComment(addr160, newComment) + # Plain text to copy to clipboard as "text/plain" + self.plainText = str(self.edtLinkText.text()) + '\n' + self.plainText += 'If clicking on the line above does not work, use this payment info:\n' + self.plainText += 'Pay to: %s' % addr + if amtStr: + self.plainText += '\nAmount: %s BTC' % coin2str(amt, maxZeros=0).strip() + if msgStr: + self.plainText += '\nMessage: %s' % msgStr + self.plainText += '\n' - ############################################################################# - def acceptWltSelection(self): - wltID = self.selectedWltID - addr160 = self.main.walletMap[wltID].getNextUnusedAddress().getAddr160() - self.target.setText(hash160_to_addrStr(addr160)) - self.target.setCursorPosition(0) - self.accept() - + # The rich-text to copy to the clipboard, as "text/html" + self.clipText = (\ + ' ' + '' + '' + '

    ' + '' + '' + '%s
    ' + 'If clicking on the line above does not work, use this payment info:' + '
    Pay to: %s') % \ + (self.rawURI, str(self.edtLinkText.text()), addr) + if amt: + self.clipText += ('
    Amount' + ': %s' % coin2str(amt, maxZeros=0)) + if msgStr: + self.clipText += ('
    Message' + ': %s' % msgStr) + self.clipText += '

    ' - ############################################################################# - def acceptAddrSelection(self): - if self.target: - self.target.setText(self.selectedAddr) - self.target.setCursorPosition(0) - self.accept() + def periodicUpdate(self, nsec=1): + if not self.closed: + from twisted.internet import reactor + self.updateQRCode() + reactor.callLater(nsec, self.periodicUpdate) - ############################################################################# - def showContextMenuTx(self, pos): - menu = QMenu(self.addrBookTxView) - std = (self.main.usermode==USERMODE.Standard) - adv = (self.main.usermode==USERMODE.Advanced) - dev = (self.main.usermode==USERMODE.Expert) - - if True: actionCopyAddr = menu.addAction("Copy Address") - if dev: actionCopyHash160 = menu.addAction("Copy Hash160 (hex)") - if True: actionCopyComment = menu.addAction("Copy Comment") - idx = self.addrBookTxView.selectedIndexes()[0] - action = menu.exec_(QCursor.pos()) - - if action==actionCopyAddr: - s = self.addrBookTxView.model().index(idx.row(), ADDRBOOKCOLS.Address).data().toString() - elif dev and action==actionCopyHash160: - s = str(self.addrBookTxView.model().index(idx.row(), ADDRBOOKCOLS.Address).data().toString()) - s = binary_to_hex(addrStr_to_hash160(s)) - elif action==actionCopyComment: - s = self.addrBookTxView.model().index(idx.row(), ADDRBOOKCOLS.Comment).data().toString() - else: - return - clipb = QApplication.clipboard() - clipb.clear() - clipb.setText(str(s).strip()) + def accept(self, *args): + # Kind of a hacky way to get the loop to end, but it seems to work + self.closed = True + super(DlgRequestPayment, self).accept(*args) + def reject(self, *args): + # Kind of a hacky way to get the loop to end, but it seems to work + self.closed = True + super(DlgRequestPayment, self).reject(*args) - ############################################################################# - def showContextMenuRx(self, pos): - menu = QMenu(self.addrBookRxView) - std = (self.main.usermode==USERMODE.Standard) - adv = (self.main.usermode==USERMODE.Advanced) - dev = (self.main.usermode==USERMODE.Expert) - - if True: actionCopyAddr = menu.addAction("Copy Address") - if dev: actionCopyHash160 = menu.addAction("Copy Hash160 (hex)") - if True: actionCopyComment = menu.addAction("Copy Comment") - idx = self.addrBookRxView.selectedIndexes()[0] - action = menu.exec_(QCursor.pos()) - - if action==actionCopyAddr: - s = self.addrBookRxView.model().index(idx.row(), ADDRESSCOLS.Address).data().toString() - elif dev and action==actionCopyHash160: - s = str(self.addrBookRxView.model().index(idx.row(), ADDRESSCOLS.Address).data().toString()) - s = binary_to_hex(addrStr_to_hash160(s)) - elif action==actionCopyComment: - s = self.addrBookRxView.model().index(idx.row(), ADDRESSCOLS.Comment).data().toString() - else: - return + def updateQRCode(self, e=None): + if not self.prevURI == self.rawURI: + self.qrURI.setAsciiData(self.rawURI) + self.qrURI.setPreferredSize(self.maxQRSize - 10, 'max') + self.repaint() + self.prevURI = self.rawURI + def clickCopyRich(self): clipb = QApplication.clipboard() clipb.clear() - clipb.setText(str(s).strip()) - - -################################################################################ -def createAddrBookButton(parent, targWidget, defaultWlt, actionStr="Select", selectExistingOnly=False, selectMineOnly=False): - btn = QPushButton('') - ico = QIcon(QPixmap(':/addr_book_icon.png')) - btn.setIcon(ico) - def execAddrBook(): - if len(parent.main.walletMap) == 0: - QMessageBox.warning(parent, 'No wallets!', 'You have no wallets so ' - 'there is no address book to display.', QMessageBox.Ok) - return - dlg = DlgAddressBook(parent, parent.main, targWidget, defaultWlt, actionStr, selectExistingOnly, selectMineOnly) - dlg.exec_() - - btn.setMaximumWidth(24) - btn.setMaximumHeight(24) - parent.connect(btn, SIGNAL('clicked()'), execAddrBook) - btn.setToolTip('Select from Address Book') - return btn + qmd = QMimeData() + if OS_WINDOWS: + qmd.setText(self.plainText) + qmd.setHtml(self.clipText) + else: + prefix = '' + qmd.setText(self.plainText) + qmd.setHtml(prefix + self.dispText) + clipb.setMimeData(qmd) + self.lblWarn.setText('Copied!') -################################################################################ -class DlgHelpAbout(ArmoryDialog): - def __init__(self, putResultInWidget, defaultWltID=None, parent=None, main=None): - super(DlgHelpAbout, self).__init__(parent) - imgLogo = QLabel() - imgLogo.setPixmap(QPixmap(':/armory_logo_h56.png')) - imgLogo.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) + def clickOtherOpt(self, boolState): + self.btnCopyHtml.setVisible(boolState) + self.btnCopyRaw.setVisible(boolState) - lblHead = QRichLabel('Armory Bitcoin Wallet : Version %s-beta' % \ - getVersionString(BTCARMORY_VERSION), doWrap=False) - lblWebpage = QRichLabel('https://www.bitcoinarmory.com') - lblWebpage.setOpenExternalLinks(True) - lblCopyright = QRichLabel(u'Copyright \xa9 2011-2013 Armory Technologies, Inc.') - lblLicense = QRichLabel(tr(u'Licensed under the ' - '' - 'Affero General Public License, Version 3 (AGPLv3)')) - lblLicense.setOpenExternalLinks(True) + if boolState: + self.btnOtherOpt.setText('Hide Buttons <<<') + else: + self.btnOtherOpt.setText('Other Options >>>') - lblHead.setAlignment(Qt.AlignHCenter) - lblWebpage.setAlignment(Qt.AlignHCenter) - lblCopyright.setAlignment(Qt.AlignHCenter) - lblLicense.setAlignment(Qt.AlignHCenter) + def clickCopyRaw(self): + clipb = QApplication.clipboard() + clipb.clear() + clipb.setText(self.rawURI) + self.lblWarn.setText('Copied!') - dlgLayout = QHBoxLayout() - dlgLayout.addWidget(makeVertFrame([imgLogo, lblHead, lblCopyright, lblWebpage, 'Stretch', lblLicense] )) - self.setLayout(dlgLayout) + def clickCopyHtml(self): + clipb = QApplication.clipboard() + clipb.clear() + clipb.setText(self.rawHtml) + self.lblWarn.setText('Copied!') - self.setMinimumWidth(450) + def clickCopyAll(self): + clipb = QApplication.clipboard() + clipb.clear() + qmd = QMimeData() + qmd.setHtml(self.dispText) + clipb.setMimeData(qmd) + self.lblWarn.setText('Copied!') - self.setWindowTitle('About Armory') ################################################################################ -class DlgSettings(ArmoryDialog): - def __init__(self, parent=None, main=None): - super(DlgSettings, self).__init__(parent, main) +class DlgNotificationWithDNAA(ArmoryDialog): + """ + This dialog will be used for automatic popups when notifications come in, + as well as displaying specific notifications if viewed and selected in + the Announcements tab. + """ + def __init__(self, parent, main, nid, notifyMap, showBtnDNAA=True): + super(DlgNotificationWithDNAA, self).__init__(parent, main) + + self.notifyID = nid + isUpgrade = ('upgrade' in notifyMap['ALERTTYPE'].lower()) + isTesting = (notifyMap['ALERTTYPE'].lower()=='upgrade-testing') + + #if notifyMap is None: + #notifyMap = self.main.almostFullNotificationList[nid] + + priority = int(notifyMap['PRIORITY']) + shortDescr = notifyMap['SHORTDESCR'] + longDescr = notifyMap['LONGDESCR'] + startTime = long(notifyMap['STARTTIME']) + + minver = notifyMap['MINVERSION'] + maxver = notifyMap['MAXVERSION'] + minExclude = minver.startswith('>') + maxExclude = maxver.startswith('<') + minver = minver[1:] if minExclude else minver + maxver = maxver[1:] if maxExclude else maxver + + + LTE = '\xe2\x89\xa4' + GTE = '\xe2\x89\xa5' + + if isUpgrade: + currVerStr = getVersionString(BTCARMORY_VERSION) + versionString = tr("""You are using version %s
    """) % currVerStr + elif minver=='*': + versionString = tr('Affects Armory versions: ') + if maxver=='*': + versionString = 'Affects all Armory versions' + elif maxExclude: + versionString += tr('before %s
    ' % maxver) + else: + versionString += tr('%s%s
    ' % (LTE, maxver)) + elif minExclude: + versionString = tr('Affects Armory versions ') + if maxver=='*': + versionString += tr('after %s
    ' % minver) + elif maxExclude: + versionString += tr('between %s and %s
    ' % (minver, maxver)) + else: + versionString += tr('after %s, %s%s
    ' % (minver, LTE, maxver)) + else: + versionString = tr('Affects Armory versions ') + if maxver=='*': + versionString += tr('%s%s
    ' % (GTE,minver)) + elif maxExclude: + versionString += tr('%s%s and before %s
    ' % (GTE, minver, maxver)) + else: + versionString += tr('%s%s and %s%s
    ' % (GTE,minver,LTE,maxver)) + startTimeStr = '' + if startTime > 0: + if isUpgrade: + for verStr,dateStr,updList in self.main.changelog: + if verStr==notifyMap['MAXVERSION'][1:]: + startTimeStr = tr('Released: %s
    ' % dateStr) + break + else: + startTimeStr = unixTimeToFormatStr(startTime, 'Date: %B %d, %Y
    ') - ########################################################################## - # bitcoind-management settings - self.chkManageSatoshi = QCheckBox('Let Armory run Bitcoin-Qt/bitcoind in the background') - self.edtSatoshiExePath = QLineEdit() - self.edtSatoshiHomePath = QLineEdit() - self.edtSatoshiExePath.setMinimumWidth(tightSizeNChar(GETFONT('Fixed',10), 40)[0]) - self.connect(self.chkManageSatoshi, SIGNAL('clicked()'), self.clickChkManage) - self.startChk = self.main.getSettingOrSetDefault('ManageSatoshi', not OS_MACOSX) - if self.startChk: - self.chkManageSatoshi.setChecked(True) - if OS_MACOSX: - self.chkManageSatoshi.setEnabled(False) - lblManageSatoshi = QRichLabel( \ - 'Bitcoin-Qt/bitcoind management is not available on Mac/OSX') - else: - if self.main.settings.hasSetting('SatoshiExe'): - satexe = self.main.settings.get('SatoshiExe') - - sathome = BTC_HOME_DIR - if self.main.settings.hasSetting('SatoshiDatadir'): - sathome = self.main.settings.get('SatoshiDatadir') - - lblManageSatoshi = QRichLabel( \ - 'Bitcoin Software Management' - '

    ' - 'By default, Armory will manage the Bitcoin engine/software in the ' - 'background. You can choose to manage it yourself, or tell Armory ' - 'about non-standard installation configuration.') - if self.main.settings.hasSetting('SatoshiExe'): - self.edtSatoshiExePath.setText(self.main.settings.get('SatoshiExe')) - self.edtSatoshiExePath.home(False) - if self.main.settings.hasSetting('SatoshiDatadir'): - self.edtSatoshiHomePath.setText(self.main.settings.get('SatoshiDatadir')) - self.edtSatoshiHomePath.home(False) + if isUpgrade: + iconFile = ':/MsgBox_info48.png' + headerSz = 4 + + if isTesting: + titleStr = tr('New Armory Test Build') + headerStr = tr("""New Testing Version Available!""") + else: + titleStr = tr('Upgrade Armory') + headerStr = tr("""Armory is out-of-date!""") + + elif 0 <= priority < 2048: + iconFile = ':/MsgBox_info48.png' + titleStr = tr('Information') + headerSz = 4 + headerStr = tr("""General Notification""") + elif 2048 <= priority < 4096: + iconFile = ':/MsgBox_warning48.png' + titleStr = '' + headerSz = 4 + headerStr = tr(""" + Important Information from Armory Technologies, Inc.""") + elif 4096 <= priority < 5120: + iconFile = ':/MsgBox_critical64.png' + titleStr = tr('Alert') + headerSz = 4 + headerStr = tr(""" + Security Alert from Armory Technologies, Inc.""") + elif 5120 <= priority: + iconFile = ':/MsgBox_critical64.png' + titleStr = tr('Alert') + headerSz = 4 + headerStr = tr(""" + Critical Security Alert from Armory Technologies, Inc.""") + + lblHeader = QRichLabel(tr(""" + %s
    """) % \ + (headerSz, htmlColor('TextWarn'), headerStr), \ + doWrap=False, hAlign=Qt.AlignHCenter) + + lblTopInfo = QRichLabel(tr(""" + %(shortDescr)s
    + %(startTimeStr)s +
    + %(versionString)s + """) % locals()) + + lastWord = '' + if not isUpgrade: + lastWord = tr(""" + If new versions of Armory are available, you can get them + using our secure + downloader """) + + lblBottomInfo = QRichLabel(tr(""" + You can access all alerts and announcements from the + "Announcements" tab on the main Armory window.""") + lastWord) + + + def doUDD(href=None): + self.accept() + self.main.openDLArmory() - lblDescrExe = QRichLabel('Bitcoin Install Dir:') - lblDescrHome = QRichLabel('Bitcoin Home Dir:') - lblDefaultExe = QRichLabel('Leave blank to have Armory search default ' - 'locations for your OS', size=2) - lblDefaultHome = QRichLabel('Leave blank to use default datadir ' - '(%s)' % BTC_HOME_DIR, size=2) + lblBottomInfo.setOpenExternalLinks(False) + self.connect(lblBottomInfo, SIGNAL('linkActivated(const QString &)'), doUDD) - self.btnSetExe = createDirectorySelectButton(self, self.edtSatoshiExePath) - self.btnSetHome = createDirectorySelectButton(self, self.edtSatoshiHomePath) + # Setup the long descr + def openLink(url): + print 'opening ', url + import webbrowser + webbrowser.open(str(url)) - layoutMgmt = QGridLayout() - layoutMgmt.addWidget(lblManageSatoshi, 0,0, 1,3) - layoutMgmt.addWidget(self.chkManageSatoshi, 1,0, 1,3) + self.txtLongDescr = QTextBrowser() + self.txtLongDescr.setHtml(longDescr) + self.txtLongDescr.setOpenExternalLinks(True) - layoutMgmt.addWidget(lblDescrExe, 2,0) - layoutMgmt.addWidget(self.edtSatoshiExePath, 2,1) - layoutMgmt.addWidget(self.btnSetExe, 2,2) - layoutMgmt.addWidget(lblDefaultExe, 3,1, 1,2) - - layoutMgmt.addWidget(lblDescrHome, 4,0) - layoutMgmt.addWidget(self.edtSatoshiHomePath,4,1) - layoutMgmt.addWidget(self.btnSetHome, 4,2) - layoutMgmt.addWidget(lblDefaultHome, 5,1, 1,2) - frmMgmt = QFrame() - frmMgmt.setLayout(layoutMgmt) - self.clickChkManage() - # bitcoind-management settings - ########################################################################## - self.chkSkipOnlineCheck = QCheckBox('Skip online check on startup (assume ' - 'internet is available, do not check)') - settingSkipCheck = self.main.getSettingOrSetDefault('SkipOnlineCheck', False) - self.chkSkipOnlineCheck.setChecked(settingSkipCheck) + notifyIcon = QLabel() + pixfile = QPixmap(iconFile) + notifyIcon.setPixmap(pixfile) + notifyIcon.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) - lblDefaultUriTitle = QRichLabel('Set Armory as default URL handler') - lblDefaultURI = QRichLabel( - 'Set Armory to be the default when you click on "bitcoin:" ' - 'links in your browser or in emails. ' - 'You can test if your operating system is supported by clicking ' - 'on a "bitcoin:" link right after clicking this button.', doWrap=True) - btnFrmDefaultURI = QPushButton('Set Armory as Default') - def clickRegURI(): - self.main.setupUriRegistration(justDoIt=True) - QMessageBox.information(self, 'Registered', \ - 'Armory just attempted to register itself to handle "bitcoin:" ' - 'links, but this does not work on all operating systems. You can ' - 'test it by going to the ' - 'Bitcoin Armory website and ' - 'clicking the link at the bottom of the homepage.', QMessageBox.Ok) - - self.connect(btnFrmDefaultURI, SIGNAL('clicked()'), clickRegURI) + btnDismiss = QPushButton(tr('Close')) + btnIgnoreLong = QPushButton(tr('Do not popup again')) + btnDownload = QPushButton(tr('Secure Download')) - txFee = self.main.getSettingOrSetDefault('Default_Fee', MIN_TX_FEE) - lblDefaultFee = QRichLabel('Default fee to include with transactions:
    ') - lblDefaultDescr = QRichLabel( \ - 'Fees go to users that contribute computing power ' - 'to keep the Bitcoin network secure and increases ' - 'the priority of your transactions on the network ' - '(%s BTC is standard).' % \ - coin2str(MIN_TX_FEE, maxZeros=0).strip()) - ttipDefaultFee = self.main.createToolTipWidget( \ - 'NOTE: some transactions will require a certain fee ' - 'regardless of your settings -- in such cases ' - 'you will be prompted to include the correct ' - 'value or cancel the transaction') - self.edtDefaultFee = QLineEdit() - self.edtDefaultFee.setText( coin2str(txFee, maxZeros=1).strip()) - lblDefaultFee.setMinimumWidth(400) + btnIgnoreLong.setVisible(showBtnDNAA) - self.connect(self.edtDefaultFee, SIGNAL('returnPressed()'), self.accept) + def openUpgrader(): + self.accept() + self.main.openDLArmory() + self.connect(btnDismiss, SIGNAL(CLICKED), self.acceptShortIgnore) + self.connect(btnIgnoreLong, SIGNAL(CLICKED), self.acceptLongIgnore) + self.connect(btnDownload, SIGNAL(CLICKED), openUpgrader) - ############################################################### - # Minimize on Close - moc = self.main.getSettingOrSetDefault('MinimizeOrClose', 'DontKnow') - lblMinOrClose = QRichLabel('Minimize to system tray on close:') - lblMoCDescr = QRichLabel( 'When you click the "x" on the top window bar, ' - 'Armory will stay open but run in the background. ' - 'You will still receive notifications, and ' - 'can access it through the system tray.') - ttipMinOrClose = self.main.createToolTipWidget( \ - 'If this is checked, you can still close Armory through the right-click menu ' - 'on the system tray icon, or by "File"->"Quit Armory" on the main window') - self.chkMinOrClose = QCheckBox('') - if moc=='Minimize': - self.chkMinOrClose.setChecked(True) + if not isUpgrade: + btnDownload.setVisible(False) + # You cannot permanently ignore a critical security alert! + if priority >= 5120: + btnIgnoreLong.setVisible(False) - #doInclFee = self.main.getSettingOrSetDefault('LedgDisplayFee', True) - #lblLedgerFee = QRichLabel('Include fee in transaction value on the ' - #'primary ledger.
    Unselect if you want to ' - #'see only the value received by the recipient.') - #ttipLedgerFee = self.main.createToolTipWidget( \ - #'If you send someone 1.0 ' - #'BTC with a 0.001 fee, the ledger will display ' - #'"1.001" in the "Amount" column if this option ' - #'is checked.') - #self.chkInclFee = QCheckBox('') - #self.chkInclFee.setChecked(doInclFee) + layout = QVBoxLayout() + frmTop = makeHorizFrame([notifyIcon, 'Space(20)', lblTopInfo]) + frmTop.layout().setStretch(0, 0) + frmTop.layout().setStretch(1, 0) + frmTop.layout().setStretch(2, 1) + frmButton = makeHorizFrame(['Stretch', \ + btnDismiss, \ + btnIgnoreLong, \ + btnDownload]) + layout.addWidget(lblHeader) + layout.addWidget(HLINE()) + layout.addWidget(frmTop) + layout.addWidget(self.txtLongDescr) + layout.addItem(QSpacerItem(20, 20)) + layout.addWidget(lblBottomInfo) + layout.addWidget(frmButton) + layout.setStretch(0, 0) + layout.setStretch(1, 0) + layout.setStretch(2, 0) + layout.setStretch(3, 1) + layout.setStretch(4, 0) + layout.setStretch(5, 0) + layout.setStretch(6, 0) + self.setLayout(layout) - ############################################################### - # Notifications -- Don't work right on OSX - lblNotify = QRichLabel('Enable notifcations from the system-tray:') - notifyBtcIn = self.main.getSettingOrSetDefault('NotifyBtcIn', not OS_MACOSX) - notifyBtcOut = self.main.getSettingOrSetDefault('NotifyBtcOut', not OS_MACOSX) - notifyDiscon = self.main.getSettingOrSetDefault('NotifyDiscon', not OS_MACOSX) - notifyReconn = self.main.getSettingOrSetDefault('NotifyReconn', not OS_MACOSX) + self.setMinimumWidth(500) - self.chkBtcIn = QCheckBox('Bitcoins Received') - self.chkBtcOut = QCheckBox('Bitcoins Sent') - self.chkDiscon = QCheckBox('Bitcoin-Qt/bitcoind disconnected') - self.chkReconn = QCheckBox('Bitcoin-Qt/bitcoind reconnected') - self.chkBtcIn.setChecked(notifyBtcIn) - self.chkBtcOut.setChecked(notifyBtcOut) - self.chkDiscon.setChecked(notifyDiscon) - self.chkReconn.setChecked(notifyReconn) + # TODO: Dear god this is terrible, but for my life I cannot figure + # out how to move the vbar, because you can't do it until + # the dialog is drawn which doesn't happen til after __init__ + from twisted.internet import reactor + reactor.callLater(0.05, self.resizeEvent) - if OS_MACOSX: - lblNotify = QRichLabel('Sorry! Notifications are not available on Mac/OSX') - self.chkBtcIn.setChecked(False) - self.chkBtcOut.setChecked(False) - self.chkDiscon.setChecked(False) - self.chkReconn.setChecked(False) - self.chkBtcIn.setEnabled(False) - self.chkBtcOut.setEnabled(False) - self.chkDiscon.setEnabled(False) - self.chkReconn.setEnabled(False) - + self.setWindowTitle(titleStr) + self.setWindowIcon(QIcon(iconFile)) - ############################################################### - # Date format preferences - exampleTimeTuple = (2012, 4, 29, 19,45, 0, -1,-1,-1) - self.exampleUnixTime = time.mktime(exampleTimeTuple) - exampleStr = unixTimeToFormatStr(self.exampleUnixTime, '%c') - lblDateFmt = QRichLabel('Preferred Date Format:
    ') - lblDateDescr = QRichLabel( \ - 'You can specify how you would like dates ' - 'to be displayed using percent-codes to ' - 'represent components of the date. The ' - 'mouseover text of the "(?)" icon shows ' - 'the most commonly used codes/symbols. ' - 'The text next to it shows how ' - '"%s" would be shown with the ' - 'specified format.' % exampleStr ) - lblDateFmt.setAlignment(Qt.AlignTop) - fmt = self.main.getPreferredDateFormat() - ttipStr = 'Use any of the following symbols:
    ' - fmtSymbols = [x[0] + ' = ' + x[1] for x in FORMAT_SYMBOLS] - ttipStr += '
    '.join(fmtSymbols) + def resizeEvent(self, ev=None): + super(DlgNotificationWithDNAA, self).resizeEvent(ev) + vbar = self.txtLongDescr.verticalScrollBar() + vbar.setValue(vbar.minimum()) - fmtSymbols = [x[0] + '~' + x[1] for x in FORMAT_SYMBOLS] - lblStk = QRichLabel('; '.join(fmtSymbols)) - self.edtDateFormat = QLineEdit() - self.edtDateFormat.setText(fmt) - self.ttipFormatDescr = self.main.createToolTipWidget( ttipStr ) + def acceptLongIgnore(self): + self.main.notifyIgnoreLong.add(self.notifyID) + self.main.notifyIgnoreShort.add(self.notifyID) + self.main.writeSetting('NotifyIgnore',''.join(self.main.notifyIgnoreLong)) + self.accept() - self.lblDateExample = QRichLabel( '', doWrap=False) - self.connect(self.edtDateFormat, SIGNAL('textEdited(QString)'), self.doExampleDate) - self.doExampleDate() - self.btnResetFormat = QPushButton("Reset to Default") + def acceptShortIgnore(self): + self.main.notifyIgnoreShort.add(self.notifyID) + self.accept() - def doReset(): - self.edtDateFormat.setText(DEFAULT_DATE_FORMAT) - self.doExampleDate() - self.connect(self.btnResetFormat, SIGNAL('clicked()'), doReset) - # Make a little subframe just for the date format stuff... everything - # fits nicer if I do this... - frmTop = makeHorizFrame([self.lblDateExample, 'Stretch', self.ttipFormatDescr]) - frmMid = makeHorizFrame([self.edtDateFormat]) - frmBot = makeHorizFrame([self.btnResetFormat, 'Stretch']) - fStack = makeVertFrame( [frmTop, frmMid, frmBot, 'Stretch']) - lblStk = makeVertFrame( [lblDateFmt, lblDateDescr, 'Stretch']) - subFrm = makeHorizFrame([lblStk, 'Stretch', fStack]) +################################################################################ +class DlgUriCopyAndPaste(ArmoryDialog): + def __init__(self, parent, main): + super(DlgUriCopyAndPaste, self).__init__(parent, main) + self.uriDict = {} + lblDescr = QRichLabel('Copy and paste a raw bitcoin URL string here. ' + 'A valid string starts with "bitcoin:" followed ' + 'by a bitcoin address.' + '

    ' + 'You should use this feature if there is a "bitcoin:" ' + 'link in a webpage or email that does not load Armory ' + 'when you click on it. Instead, right-click on the ' + 'link and select "Copy Link Location" then paste it ' + 'into the box below. ') - ############################################################### - # SelectCoins preferences - # NOT ENABLED YET -- BUT WILL BE SOON - #lblSelectCoin = QRichLabel('Coin Selection Preferences:') - #lblSelectCoinDescr = QRichLabel( \ - #'When Armory constructs a transaction, there are many different ' - #'ways for it to select from coins that make up your balance. ' - #'The "SelectCoins" algorithm can be set to prefer more-anonymous ' - #'coin selections or to prefer avoiding mandatory transaction fees. ' - #'No guarantees are made about the relative anonymity of the ' - #'coin selection, only that Armory will prefer a transaction ' - #'that requires a fee if it can increase anonymity.') - #self.cmbSelectCoins = QComboBox() - #self.cmbSelectCoins.clear() - #self.cmbSelectCoins.addItem( 'Prefer free transactions' ) - #self.cmbSelectCoins.addItem( 'Maximize anonymity' ) - #self.cmbSelectCoins.setCurrentIndex(0) - #i+=1 - #dlgLayout.addWidget(lblSelectCoin, i,0) - #dlgLayout.addWidget(self.cmbSelectCoins, i,1) - #i+=1 - #dlgLayout.addWidget(lblSelectCoinDescr, i,0, 1,2) + lblShowExample = QLabel() + lblShowExample.setPixmap(QPixmap(':/armory_rightclickcopy.png')) + self.txtUriString = QLineEdit() + self.txtUriString.setFont(GETFONT('Fixed', 8)) - ############################################################### - # Save/Cancel Button - self.btnCancel = QPushButton("Cancel") - self.btnAccept = QPushButton("Save") - self.connect(self.btnCancel, SIGNAL('clicked()'), self.reject) - self.connect(self.btnAccept, SIGNAL('clicked()'), self.accept) + self.btnOkay = QPushButton('Done') + self.btnCancel = QPushButton('Cancel') + buttonBox = QDialogButtonBox() + buttonBox.addButton(self.btnOkay, QDialogButtonBox.AcceptRole) + buttonBox.addButton(self.btnCancel, QDialogButtonBox.RejectRole) + self.connect(self.btnOkay, SIGNAL(CLICKED), self.clickedOkay) + self.connect(self.btnCancel, SIGNAL(CLICKED), self.reject) + frmImg = makeHorizFrame([STRETCH, lblShowExample, STRETCH]) - self.cmbUsermode = QComboBox() - self.cmbUsermode.clear() - self.cmbUsermode.addItem( 'Standard' ) - self.cmbUsermode.addItem( 'Advanced' ) - self.cmbUsermode.addItem( 'Expert' ) - self.usermodeInit = self.main.usermode + layout = QVBoxLayout() + layout.addWidget(lblDescr) + layout.addWidget(HLINE()) + layout.addWidget(frmImg) + layout.addWidget(HLINE()) + layout.addWidget(self.txtUriString) + layout.addWidget(buttonBox) + self.setLayout(layout) - if self.main.usermode==USERMODE.Standard: - self.cmbUsermode.setCurrentIndex(0) - elif self.main.usermode==USERMODE.Advanced: - self.cmbUsermode.setCurrentIndex(1) - elif self.main.usermode==USERMODE.Expert: - self.cmbUsermode.setCurrentIndex(2) - lblUsermode = QRichLabel('Armory user mode:') - self.lblUsermodeDescr = QRichLabel('') - self.setUsermodeDescr() + def clickedOkay(self): + uriStr = str(self.txtUriString.text()) + self.uriDict = self.main.parseUriLink(uriStr, 'enter') + self.accept() - self.connect(self.cmbUsermode, SIGNAL('activated(int)'), self.setUsermodeDescr) - frmLayout = QGridLayout() +class DlgCoinControl(ArmoryDialog): + def __init__(self, parent, main, wlt, currSelect=None): + super(DlgCoinControl, self).__init__(parent, main) - i=0 - frmLayout.addWidget( HLINE(), i,0, 1,3) - - i+=1 - frmLayout.addWidget(frmMgmt, i,0, 1,3) + self.wlt = wlt - i+=1 - frmLayout.addWidget(self.chkSkipOnlineCheck,i,0, 1,3) + lblDescr = QRichLabel(\ + 'By default, transactions are created using any available coins from ' + 'all addresses in this wallet. You can control the source addresses ' + 'used for this transaction by selecting them below, and unchecking ' + 'all other addresses.') - i+=1 - frmLayout.addWidget( HLINE(), i,0, 1,3) + self.chkSelectAll = QCheckBox('Select All') + self.chkSelectAll.setChecked(True) + self.connect(self.chkSelectAll, SIGNAL(CLICKED), self.clickAll) - i+=1 - frmLayout.addWidget( lblDefaultUriTitle, i,0 ) + addrToInclude = [] + totalBal = 0 + for addr160 in wlt.addrMap.iterkeys(): + bal = wlt.getAddrBalance(addr160) + if bal > 0: + addrToInclude.append([addr160, bal]) + totalBal += bal - i+=1 - frmLayout.addWidget( lblDefaultURI, i,0, 1,2 ) - frmLayout.addWidget( btnFrmDefaultURI, i,2 ) + frmTableLayout = QGridLayout() + self.dispTable = [] + frmTableLayout.addWidget(QRichLabel('Address'), 0, 0) + frmTableLayout.addWidget(VLINE(), 0, 1) + frmTableLayout.addWidget(QRichLabel('Balance'), 0, 2) + frmTableLayout.addWidget(VLINE(), 0, 3) + frmTableLayout.addWidget(QRichLabel('Comment'), 0, 4) + frmTableLayout.addWidget(HLINE(), 1, 0, 1, 5) + for i in range(len(addrToInclude)): + a160, bal = addrToInclude[i] + fullcmt = self.wlt.getCommentForAddress(a160) + shortcmt = fullcmt + if shortcmt == CHANGE_ADDR_DESCR_STRING: + shortcmt = 'Change Address' + fullcmt = '(This address was created only to receive change from another transaction)' + elif len(shortcmt) > 20: + shortcmt = fullcmt[:20] + '...' + self.dispTable.append([None, None, None]) + self.dispTable[-1][0] = QCheckBox(hash160_to_addrStr(a160)) + self.dispTable[-1][1] = QMoneyLabel(bal) + self.dispTable[-1][2] = QRichLabel(shortcmt, doWrap=False) + self.dispTable[-1][0].setChecked(currSelect == None or (a160 in currSelect)) + if len(shortcmt) > 0: + self.dispTable[-1][0].setToolTip('' + fullcmt) + self.dispTable[-1][1].setToolTip('' + fullcmt) + self.dispTable[-1][2].setToolTip('' + fullcmt) + self.connect(self.dispTable[-1][0], SIGNAL(CLICKED), self.clickOne) + frmTableLayout.addWidget(self.dispTable[-1][0], i + 2, 0) + frmTableLayout.addWidget(VLINE(), i + 2, 1) + frmTableLayout.addWidget(self.dispTable[-1][1], i + 2, 2) + frmTableLayout.addWidget(VLINE(), i + 2, 3) + frmTableLayout.addWidget(self.dispTable[-1][2], i + 2, 4) - i+=1 - frmLayout.addWidget( HLINE(), i,0, 1,3) + frmTable = QFrame() + frmTable.setLayout(frmTableLayout) + self.scrollAddrList = QScrollArea() + self.scrollAddrList.setWidget(frmTable) - i+=1 - frmLayout.addWidget( lblDefaultFee, i,0 ) - frmLayout.addWidget( ttipDefaultFee, i,1 ) - frmLayout.addWidget( self.edtDefaultFee, i,2 ) + self.sizeHint = lambda: QSize(frmTable.width() + 40, 400) - i+=1 - frmLayout.addWidget( lblDefaultDescr, i,0, 1,3) + lblDescrSum = QRichLabel('Balance of selected addresses:', doWrap=False) + self.lblSum = QMoneyLabel(totalBal, wBold=True) + frmSum = makeHorizFrame([STRETCH, lblDescrSum, self.lblSum, STRETCH]) - i+=1 - frmLayout.addWidget( HLINE(), i,0, 1,3) + self.btnAccept = QPushButton("Accept") + self.btnCancel = QPushButton("Cancel") + self.connect(self.btnAccept, SIGNAL(CLICKED), self.acceptSelection) + self.connect(self.btnCancel, SIGNAL(CLICKED), self.reject) + buttonBox = QDialogButtonBox() + buttonBox.addButton(self.btnAccept, QDialogButtonBox.AcceptRole) + buttonBox.addButton(self.btnCancel, QDialogButtonBox.RejectRole) - i+=1 - frmLayout.addWidget(subFrm, i,0, 1,3) + layout = QGridLayout() + layout.addWidget(lblDescr, 0, 0) + layout.addWidget(self.chkSelectAll, 1, 0) + layout.addWidget(self.scrollAddrList, 2, 0) + layout.addWidget(frmSum, 3, 0) + layout.addWidget(buttonBox, 4, 0) + layout.setRowStretch(0, 0) + layout.setRowStretch(1, 0) + layout.setRowStretch(2, 1) + layout.setRowStretch(3, 0) + layout.setRowStretch(4, 0) + self.setLayout(layout) - i+=1 - frmLayout.addWidget( HLINE(), i,0, 1,3) + self.recalcBalance() - i+=1 - frmLayout.addWidget( lblMinOrClose, i,0 ) - frmLayout.addWidget( ttipMinOrClose, i,1 ) - frmLayout.addWidget( self.chkMinOrClose, i,2 ) + self.setWindowTitle('Coin Control (Expert)') - i+=1 - frmLayout.addWidget( lblMoCDescr, i,0, 1,3) + def clickAll(self): + for dispList in self.dispTable: + dispList[0].setChecked(self.chkSelectAll.isChecked()) + self.recalcBalance() - i+=1 - frmLayout.addWidget( HLINE(), i,0, 1,3) + def clickOne(self): + self.recalcBalance() - i+=1 - frmLayout.addWidget( lblNotify, i,0, 1,3) - i+=1 - frmLayout.addWidget( self.chkBtcIn, i,0, 1,3) + def recalcBalance(self): + totalBal = 0 + for dispList in self.dispTable: + if dispList[0].isChecked(): + atype, a160 = addrStr_to_hash160(str(dispList[0].text()), False) - i+=1 - frmLayout.addWidget( self.chkBtcOut, i,0, 1,3) - - i+=1 - frmLayout.addWidget( self.chkDiscon, i,0, 1,3) + totalBal += self.wlt.getAddrBalance(a160) + else: + self.chkSelectAll.setChecked(False) - i+=1 - frmLayout.addWidget( self.chkReconn, i,0, 1,3) + self.lblSum.setValueText(totalBal) - i+=1 - frmLayout.addWidget( HLINE(), i,0, 1,3) - i+=1 - frmLayout.addWidget( lblUsermode, i,0 ) - frmLayout.addWidget( QLabel(''), i,1 ) - frmLayout.addWidget( self.cmbUsermode, i,2 ) - i+=1 - frmLayout.addWidget( self.lblUsermodeDescr, i,0, 1,3) + def acceptSelection(self): + self.coinControlList = [] + for dispList in self.dispTable: + if dispList[0].isChecked(): + atype, a160 = addrStr_to_hash160(str(dispList[0].text()), False) + bal = self.wlt.getAddrBalance(a160) + self.coinControlList.append([a160, bal]) - frmOptions = QFrame() - frmOptions.setLayout(frmLayout) + if len(self.coinControlList) == 0: + QMessageBox.warning(self, 'Nothing Selected', \ + 'You must select at least one address to fund your ' + 'transaction.', QMessageBox.Ok) + return - self.scrollOptions = QScrollArea() - self.scrollOptions.setWidget(frmOptions) + self.accept() - dlgLayout = QVBoxLayout() - dlgLayout.addWidget(self.scrollOptions) - dlgLayout.addWidget(makeHorizFrame(['Stretch', self.btnCancel, self.btnAccept])) - self.setLayout(dlgLayout) - - self.setMinimumWidth(650) - self.setWindowTitle('Armory Settings') +################################################################################ +class DlgQRCodeDisplay(ArmoryDialog): + def __init__(self, parent, main, dataToQR, descrUp='', descrDown=''): + super(DlgQRCodeDisplay, self).__init__(parent, main) - # NOTE: This was getting complicated for a variety of reasons, so switched - # to manually constructing the options window. May come back to this - # at a later time. - # - # Let's create a scalable list of options. Each row of this list looks like: - # - # [OptionType, SettingsName, DefaultValue, BoldText, NormalText, Tooltip] - # - # SettingsName is the string used in self.main.getSettingOrSetDefault() - # OptionType can be one of: - # {'Checkbox', 'LineEdit', 'Combo|Opt1|Opt2|...', 'Separator', 'Header'} - # - # "Separator adds a horizontal-ruler to separate option groups, and "Header" - # is basically a textual separator with no actual option + btnDone = QPushButton('Close') + self.connect(btnDone, SIGNAL(CLICKED), self.accept) + frmBtn = makeHorizFrame([STRETCH, btnDone, STRETCH]) - #self.Options = [] - #self.Options.append( ['LineEdit', 'Default_Fee', MIN_TX_FEE, \ - #'Default fee to include with transactions.', \ - #'Fees go to users that contribute computing power ' - #'to keep the Bitcoin network secure (0.0005 BTC is ' - #'standard).', \ - #'NOTE: some transactions will require a fee ' - #'regardless of your preferences -- in such cases ' - #'you will be prompted to include the correct ' - #'value or abort the transaction']) + qrDisp = QRCodeWidget(dataToQR, parent=self) + frmQR = makeHorizFrame([STRETCH, qrDisp, STRETCH]) - - ############################################################################# - def accept(self, *args): + lblUp = QRichLabel(descrUp) + lblUp.setAlignment(Qt.AlignVCenter | Qt.AlignHCenter) + lblDn = QRichLabel(descrDown) + lblDn.setAlignment(Qt.AlignVCenter | Qt.AlignHCenter) - if self.chkManageSatoshi.isChecked(): - # Check valid path is supplied for bitcoin installation - pathExe = unicode(self.edtSatoshiExePath.text()).strip() - if len(pathExe)>0: - if not os.path.exists(pathExe): - exeName = 'bitcoin-qt.exe' if OS_WINDOWS else 'bitcoin-qt' - QMessageBox.warning(self, 'Invalid Path', \ - 'The path you specified for the Bitcoin software installation ' - 'does not exist. Please select the directory that contains %s ' - 'or leave it blank to have Armory search the default location ' - 'for your operating system' % exeName, QMessageBox.Ok) - return - if os.path.isfile(pathExe): - pathExe = os.path.dirname(pathExe) - self.main.writeSetting('SatoshiExe', pathExe) - else: - self.main.settings.delete('SatoshiExe') - - # Check valid path is supplied for bitcoind home directory - pathHome = unicode(self.edtSatoshiHomePath.text()).strip() - if len(pathHome)>0: - if not os.path.exists(pathHome): - exeName = 'bitcoin-qt.exe' if OS_WINDOWS else 'bitcoin-qt' - QMessageBox.warning(self, 'Invalid Path', \ - 'The path you specified for the Bitcoin software home directory ' - 'does not exist. Only specify this directory if you use a ' - 'non-standard "-datadir=" option when running Bitcoin-Qt or ' - 'bitcoind. If you leave this field blank, the following ' - 'path will be used:

    %s' % BTC_HOME_DIR, QMessageBox.Ok) - return - self.main.writeSetting('SatoshiDatadir', pathHome) - else: - self.main.settings.delete('SatoshiDatadir') - - self.main.writeSetting('ManageSatoshi', self.chkManageSatoshi.isChecked()) - - - self.main.writeSetting('SkipOnlineCheck', self.chkSkipOnlineCheck.isChecked()) - - try: - defaultFee = str2coin( str(self.edtDefaultFee.text()).replace(' ','') ) - self.main.writeSetting('Default_Fee', defaultFee) - except: - QMessageBox.warning(self, 'Invalid Amount', \ - 'The default fee specified could not be understood. Please ' - 'specify in BTC with no more than 8 decimal places.', \ - QMessageBox.Ok) - return + layout = QVBoxLayout() + layout.addWidget(lblUp) + layout.addWidget(frmQR) + layout.addWidget(lblDn) + layout.addWidget(HLINE()) + layout.addWidget(frmBtn) - if not self.main.setPreferredDateFormat(str(self.edtDateFormat.text())): - return + self.setLayout(layout) - if not self.usermodeInit == self.cmbUsermode.currentIndex(): - modestr = str(self.cmbUsermode.currentText()) - if modestr.lower() == 'standard': - self.main.setUserMode(USERMODE.Standard) - elif modestr.lower() == 'advanced': - self.main.setUserMode(USERMODE.Advanced) - elif modestr.lower() == 'expert': - self.main.setUserMode(USERMODE.Expert) + w1, h1 = relaxedSizeStr(lblUp, descrUp) + w2, h2 = relaxedSizeStr(lblDn, descrDown) + self.setMinimumWidth(1.2 * max(w1, w2)) - if self.chkMinOrClose.isChecked(): - self.main.writeSetting('MinimizeOrClose', 'Minimize') - else: - self.main.writeSetting('MinimizeOrClose', 'Close') - #self.main.writeSetting('LedgDisplayFee', self.chkInclFee.isChecked()) - self.main.writeSetting('NotifyBtcIn', self.chkBtcIn.isChecked()) - self.main.writeSetting('NotifyBtcOut', self.chkBtcOut.isChecked()) - self.main.writeSetting('NotifyDiscon', self.chkDiscon.isChecked()) - self.main.writeSetting('NotifyReconn', self.chkReconn.isChecked()) - self.main.createCombinedLedger() - super(DlgSettings, self).accept(*args) - - ############################################################################# - def setUsermodeDescr(self): - strDescr = '' - modestr = str(self.cmbUsermode.currentText()) - if modestr.lower() == 'standard': - strDescr += \ - ('"Standard" is for users that only need the core set of features ' - 'to send and receive bitcoins. This includes maintaining multiple ' - 'wallets, wallet encryption, and the ability to make backups ' - 'of your wallets.') - elif modestr.lower() == 'advanced': - strDescr += \ - ('"Advanced" mode provides ' - 'extra Armory features such as private key ' - 'importing & sweeping, message signing, and the offline wallet ' - 'interface. But, with advanced features come advanced risks...') - elif modestr.lower() == 'expert': - strDescr += \ - ('"Expert" mode is similar to "Advanced" but includes ' - 'access to lower-level info about transactions, scripts, keys ' - 'and network protocol. Most extra functionality is geared ' - 'towards Bitcoin software developers.') - self.lblUsermodeDescr.setText(strDescr) - ############################################################################# - def doExampleDate(self, qstr=None): - fmtstr = str(self.edtDateFormat.text()) - try: - self.lblDateExample.setText('Sample: ' + unixTimeToFormatStr(self.exampleUnixTime, fmtstr)) - self.isValidFormat = True - except: - self.lblDateExample.setText('Sample: [[invalid date format]]') - self.isValidFormat = False - ############################################################################# - def clickChkManage(self): - self.edtSatoshiExePath.setEnabled(self.chkManageSatoshi.isChecked()) - self.edtSatoshiHomePath.setEnabled(self.chkManageSatoshi.isChecked()) - self.btnSetExe.setEnabled(self.chkManageSatoshi.isChecked()) - self.btnSetHome.setEnabled(self.chkManageSatoshi.isChecked()) ################################################################################ -class DlgExportTxHistory(ArmoryDialog): - def __init__(self, parent=None, main=None): - super(DlgExportTxHistory, self).__init__(parent, main) +# STUB STUB STUB STUB STUB +class ArmoryPref(object): + """ + Create a class that will handle arbitrary preferences for Armory. This + means that I can just create maps/lists of preferences, and auto-include + them in the preferences dialog, and know how to set/get them. This will + be subclassed for each unique/custom preference type that is needed. + """ + def __init__(self, prefName, dispStr, setType, defaultVal, validRange, descr, ttip, usermodes=None): + self.preference = prefName + self.displayStr = dispStr + self.preferType = setType + self.defaultVal = defaultVal + self.validRange = validRange + self.description = descr + self.ttip = ttip + # Some options may only be displayed for certain usermodes + self.users = usermodes + if usermodes == None: + self.users = set([USERMODE.Standard, USERMODE.Advanced, USERMODE.Expert]) + if self.preferType == 'str': + self.entryObj = QLineEdit() + elif self.preferType == 'num': + self.entryObj = QLineEdit() + elif self.preferType == 'file': + self.entryObj = QLineEdit() + elif self.preferType == 'bool': + self.entryObj = QCheckBox() + elif self.preferType == 'combo': + self.entryObj = QComboBox() - self.cmbWltSelect = QComboBox() - self.cmbWltSelect.clear() - self.cmbWltSelect.addItem( 'My Wallets' ) - self.cmbWltSelect.addItem( 'Offline Wallets' ) - self.cmbWltSelect.addItem( 'Other Wallets' ) - self.cmbWltSelect.addItem( 'All Wallets' ) - for wltID in self.main.walletIDList: - self.cmbWltSelect.addItem( self.main.walletMap[wltID].labelName ) - self.cmbWltSelect.insertSeparator(4) - self.cmbWltSelect.insertSeparator(4) + def setEntryVal(self): + pass + def readEntryVal(self): + pass - self.cmbSortSelect = QComboBox() - self.cmbSortSelect.clear() - self.cmbSortSelect.addItem('Date (newest first)') - self.cmbSortSelect.addItem('Date (oldest first)') - self.cmbSortSelect.addItem('Transaction ID (ascending)') - self.cmbSortSelect.addItem('Transaction ID (descending)') + def setWidthChars(self, nChar): + self.entryObj.setMinimumWidth(relaxedSizeNChar(self.entryObj, nChar)[0]) - self.cmbFileFormat = QComboBox() - self.cmbFileFormat.clear() - self.cmbFileFormat.addItem('Comma-Separated Values (*.csv)') + def render(self): + """ + Return a map of qt objects to insert into the frame + """ + toDraw = [] + row = 0 + if len(self.description) > 0: + toDraw.append([QRichLabel(self.description), row, 0, 1, 4]) + row += 1 - fmt = self.main.getPreferredDateFormat() - ttipStr = 'Use any of the following symbols:
    ' - fmtSymbols = [x[0] + ' = ' + x[1] for x in FORMAT_SYMBOLS] - ttipStr += '
    '.join(fmtSymbols) - self.edtDateFormat = QLineEdit() - self.edtDateFormat.setText(fmt) - self.ttipFormatDescr = self.main.createToolTipWidget( ttipStr ) - - self.lblDateExample = QRichLabel( '', doWrap=False) - self.connect(self.edtDateFormat, SIGNAL('textEdited(QString)'), self.doExampleDate) - self.doExampleDate() - self.btnResetFormat = QPushButton("Reset to Default") - def doReset(): - self.edtDateFormat.setText(DEFAULT_DATE_FORMAT) - self.doExampleDate() - self.connect(self.btnResetFormat, SIGNAL('clicked()'), doReset) +################################################################################ +class DlgInstallLinux(ArmoryDialog): + def __init__(self, parent, main): + super(DlgInstallLinux, self).__init__(parent, main) + import platform + self.distro, self.dver, self.dname = platform.linux_distribution() - + lblOptions = QRichLabel(\ + 'If you have manually installed Bitcoin-Qt or bitcoind on this system ' + 'before, it is recommended you use the method here you previously used. ' + 'If you get errors using this option, try using the manual instructions ' + 'below.') + self.radioUbuntuPPA = QRadioButton('Install from bitcoin.org PPA (Ubuntu only)') + self.radioDlBinaries = QRadioButton('Download and unpack binaries (All Linux)') + btngrp = QButtonGroup(self) + btngrp.addButton(self.radioDlBinaries) + btngrp.addButton(self.radioUbuntuPPA) + btngrp.setExclusive(True) + self.connect(self.radioDlBinaries, SIGNAL(CLICKED), self.clickInstallOpt) + self.connect(self.radioUbuntuPPA, SIGNAL(CLICKED), self.clickInstallOpt) - # Add the usual buttons - self.btnCancel = QPushButton("Cancel") - self.btnAccept = QPushButton("Export") - self.connect(self.btnCancel, SIGNAL('clicked()'), self.reject) - self.connect(self.btnAccept, SIGNAL('clicked()'), self.accept) - btnBox = makeHorizFrame(['Stretch', self.btnCancel, self.btnAccept]) + ########################################################################## + # Install via PPA + lblAutoPPATitle = QRichLabel('Install PPA for me (Ubuntu only):') + lblAutoPPA = QRichLabel(\ + 'Have Armory install the PPA for you. The does not work on all ' + 'systems, so try the manual instructions below, if it fails. ' + 'Using the PPA will install the Bitcoin software using your ' + 'system\'s package manager, and you will be notified of updates along with ' + 'other software on your system.') + self.btnAutoPPA = QPushButton('Install Bitcoin PPA') + self.connect(self.btnAutoPPA, SIGNAL(CLICKED), self.doPPA) + self.btnAutoPPA.setToolTip(\ + 'Click to install the Bitcoin PPA for Ubuntu') + frmDoItForMeBtn = makeHorizFrame([STRETCH, \ + self.btnAutoPPA, \ + STRETCH]) - dlgLayout = QGridLayout() - - i=0 - dlgLayout.addWidget(QRichLabel('Export Format:'), i,0) - dlgLayout.addWidget(self.cmbFileFormat, i,1) + lblInstallPPATitle = QRichLabel('Manual PPA Installation:', doWrap=False) + lblInstallPPA = QRichLabel(\ + 'Open a terminal window and copy the following three commands ' + 'one-by-one, pressing [ENTER] after each one. You can open a terminal by hitting ' + 'Alt-F2 and typing "terminal" (without quotes), or in ' + 'the "Applications" menu under "Accessories".') + + lblInstallPPACmds = QRichLabel(\ + 'sudo add-apt-repository ppa:bitcoin/bitcoin' + '
    ' + 'sudo apt-get update' + '
    ' + 'sudo apt-get install bitcoin-qt bitcoind') + lblInstallPPACmds.setFont(GETFONT('Courier', 10)) + lblInstallPPACmds.setTextInteractionFlags(Qt.TextSelectableByMouse | \ + Qt.TextSelectableByKeyboard) - i+=1 - dlgLayout.addWidget(HLINE(), i,0, 1,2) - i+=1 - dlgLayout.addWidget(QRichLabel('Wallet(s) to export:'),i,0) - dlgLayout.addWidget(self.cmbWltSelect, i,1) + frmCmds = makeHorizFrame([lblInstallPPACmds], STYLE_SUNKEN) + self.frmPPA = makeVertFrame([ \ + lblAutoPPATitle, \ + lblAutoPPA, \ + frmDoItForMeBtn, \ + HLINE(), \ + lblInstallPPATitle, \ + lblInstallPPA, \ + frmCmds], STYLE_SUNKEN) + # Install via PPA + ########################################################################## - i+=1 - dlgLayout.addWidget(HLINE(), i,0, 1,2) - - i+=1 - dlgLayout.addWidget(QRichLabel('Sort Table:'), i,0) - dlgLayout.addWidget(self.cmbSortSelect, i,1) + ########################################################################## + # Install via Manual Download + lblManualExperiment = QRichLabel(\ + 'Download and set it up for me! (All Linux):' + '

    ' + 'Armory will download and verify the binaries from www.bitcoin.org. ' + 'Your Armory settings will automatically be adjusted to point to that ' + 'as the installation directory.') + btnManualExperiment = QPushButton('Install for me!') + self.connect(btnManualExperiment, SIGNAL(CLICKED), self.tryManualInstall) + self.chkCustomDLPath = QCheckBox('Select custom download location') - i+=1 - dlgLayout.addWidget(HLINE(), i,0, 1,2) + lblInstallManualDescr = QRichLabel(\ + 'Manual download and install of the Bitcoin software:
    ' + '
      ' + '
    1. Go to ' + 'http://www.bitcoin.org/en/download
    2. ' + '
    3. Click on the link that says "Download for Linux (tgz, 32/64-bit)"
    4. ' + '
    5. Open a file browser and navigate to the download directory
    6. ' + '
    7. Right-click on the downloaded file, and select "Extract Here"
    8. ' + '
    ' + '
    ' + 'Once the downloaded archive is unpacked, then click the button below ' + 'to open the Armory settings and change the "Bitcoin Installation Path" ' + 'to point to the new directory. Then restart Armory') + lblInstallManualDescr.setOpenExternalLinks(True) - i+=1 - dlgLayout.addWidget(QRichLabel('Date Format:'), i,0) - fmtfrm = makeHorizFrame([self.lblDateExample, 'Stretch', self.ttipFormatDescr]) - dlgLayout.addWidget(fmtfrm, i,1) - i+=1 - dlgLayout.addWidget(self.btnResetFormat, i,0) - dlgLayout.addWidget(self.edtDateFormat, i,1) + btnInstallSettings = QPushButton('Change Settings') + self.connect(btnInstallSettings, SIGNAL(CLICKED), self.main.openSettings) + frmChngSettings = makeHorizFrame([ + STRETCH, \ + btnInstallSettings, \ + STRETCH], \ + STYLE_SUNKEN) - i+=1 - dlgLayout.addWidget(HLINE(), i,0, 1,2) + btnAndChk = makeHorizFrame([btnManualExperiment, self.chkCustomDLPath]) + frmManualExper = makeHorizFrame([STRETCH, btnAndChk, STRETCH]) + self.frmManual = makeVertFrame([ \ + lblManualExperiment, \ + frmManualExper, \ + HLINE(), \ + lblInstallManualDescr, \ + frmChngSettings, \ + STRETCH]) - i+=1 - dlgLayout.addWidget(HLINE(), i,0, 1,2) - i+=1 - dlgLayout.addWidget(btnBox, i,0, 1,2) + # Install via Manual Download + ########################################################################## - self.setLayout(dlgLayout) + self.stkInstruct = QStackedWidget() + self.stkInstruct.addWidget(self.frmPPA) + self.stkInstruct.addWidget(self.frmManual) + btnOkay = QPushButton("OK") + self.connect(btnOkay, SIGNAL(CLICKED), self.accept) + layout = QVBoxLayout() + layout.addWidget(lblOptions) + layout.addWidget(self.radioUbuntuPPA) + layout.addWidget(self.radioDlBinaries) + layout.addWidget(HLINE()) + layout.addWidget(self.stkInstruct) + layout.addWidget(makeHorizFrame([STRETCH, btnOkay])) + self.setLayout(layout) + self.setMinimumWidth(600) + self.radioUbuntuPPA.setChecked(True) + self.clickInstallOpt() + self.setWindowTitle('Install Bitcoin in Linux') - ############################################################################# - def doExampleDate(self, qstr=None): - fmtstr = str(self.edtDateFormat.text()) - try: - self.lblDateExample.setText('Example: ' + unixTimeToFormatStr(1030501970, fmtstr)) - self.isValidFormat = True - except: - self.lblDateExample.setText('Example: [[invalid date format]]') - self.isValidFormat = False + from twisted.internet import reactor + reactor.callLater(0.2, self.main.checkForLatestVersion) ############################################################################# - def accept(self, *args): - if self.createFile_CSV(): - super(DlgExportTxHistory, self).accept(*args) - + def tryManualInstall(self): + dlDict = self.main.downloadDict.copy() + if not 'SATOSHI' in dlDict or not 'Linux' in dlDict['SATOSHI']: + QMessageBox.warning(self, 'Not available', \ + 'Armory does not actually have the information needed to execute ' + 'this process securely. Please visit the bitcoin.org and download ' + 'the Linux version of the Bitcoin software, then modify your ' + 'settings to point to where it was unpacked. ', QMessageBox.Ok) + return - ############################################################################# - def createFile_CSV(self): - if not self.isValidFormat: - QMessageBox.warning(self, 'Invalid date format', \ - 'Cannot create CSV without a valid format for transaction ' - 'dates and times', QMessageBox.Ok) - return False - - # This was pretty much copied from the createCombinedLedger method... - # I rarely do this, but modularizing this piece is a non-trivial - wltIDList = [] - typelist = [[wid, determineWalletType(self.main.walletMap[wid], self.main)[0]] \ - for wid in self.main.walletIDList] - currIdx = self.cmbWltSelect.currentIndex() - if currIdx>=4: - wltIDList = [self.main.walletIDList[currIdx-6]] + if not self.chkCustomDLPath.isChecked(): + installPath = os.path.join(ARMORY_HOME_DIR, 'downloaded') + if not os.path.exists(installPath): + os.makedirs(installPath) else: - listOffline = [t[0] for t in filter(lambda x: x[1]==WLTTYPES.Offline, typelist)] - listWatching = [t[0] for t in filter(lambda x: x[1]==WLTTYPES.WatchOnly, typelist)] - listCrypt = [t[0] for t in filter(lambda x: x[1]==WLTTYPES.Crypt, typelist)] - listPlain = [t[0] for t in filter(lambda x: x[1]==WLTTYPES.Plain, typelist)] - - if currIdx==0: - wltIDList = listOffline + listCrypt + listPlain - elif currIdx==1: - wltIDList = listOffline - elif currIdx==2: - wltIDList = listWatching - elif currIdx==3: - wltIDList = self.main.walletIDList + title = 'Download Bitcoin software to...' + initPath = self.main.settings.get('LastDirectory') + if not OS_MACOSX: + installPath = unicode(QFileDialog.getExistingDirectory(self, title, initPath)) else: - pass - - totalFunds,spendFunds,unconfFunds,combinedLedger = 0,0,0,[] - for wltID in wltIDList: - wlt = self.main.walletMap[wltID] - id_le_pairs = [[wltID, le] for le in wlt.getTxLedger('Full')] - combinedLedger.extend(id_le_pairs) - totalFunds += wlt.getBalance('Total') - spendFunds += wlt.getBalance('Spendable') - unconfFunds += wlt.getBalance('Unconfirmed') - # END createCombinedLedger copy + installPath = unicode(QFileDialog.getExistingDirectory(self, title, initPath, \ + options=QFileDialog.DontUseNativeDialog)) - ledgerTable = self.main.convertLedgerToTable(combinedLedger) + if not os.path.exists(installPath): + if len(installPath.strip()) > 0: + QMessageBox.warning(self, 'Invalid Directory', \ + 'The directory you chose does not exist. How did you do that?', \ + QMessageBox.Ok) + return - sortTxt = str(self.cmbSortSelect.currentText()) - if 'newest' in sortTxt: - ledgerTable.sort(key=lambda x: x[LEDGERCOLS.UnixTime], reverse=True) - elif 'oldest' in sortTxt: - ledgerTable.sort(key=lambda x: x[LEDGERCOLS.UnixTime]) - elif 'ascend' in sortTxt: - ledgerTable.sort(key=lambda x: hex_switchEndian(x[LEDGERCOLS.TxHash])) - elif 'descend' in sortTxt: - ledgerTable.sort(key=lambda x: hex_switchEndian(x[LEDGERCOLS.TxHash]), reverse=True) - else: - LOGERROR('***ERROR: bad sort string!?') + print dlDict['SATOSHI']['Linux'] + theLink = dlDict['SATOSHI']['Linux'][0] + theHash = dlDict['SATOSHI']['Linux'][1] + dlg = DlgDownloadFile(self, self.main, theLink, theHash) + dlg.exec_() + fileData = dlg.dlFileData + if len(fileData) == 0 or dlg.dlVerifyFailed: + QMessageBox.critical(self, 'Download Failed', \ + 'The download failed. Please visit www.bitcoin.org ' + 'to download and install Bitcoin-Qt manually.', QMessageBox.Ok) + import webbrowser + webbrowser.open('http://www.bitcoin.org/en/download') return + fullPath = os.path.join(installPath, dlg.dlFileName) + LOGINFO('Installer path: %s', fullPath) + instFile = open(fullPath, 'wb') + instFile.write(fileData) + instFile.close() - wltSelectStr = str(self.cmbWltSelect.currentText()).replace(' ','_') - timestampStr = unixTimeToFormatStr(RightNow(), '%Y%m%d_%H%M') - filenamePrefix = 'ArmoryTxHistory_%s_%s' % (wltSelectStr, timestampStr) - fmtstr = str(self.cmbFileFormat.currentText()) - if 'csv' in fmtstr: - defaultName = filenamePrefix + '.csv' - fullpath = self.main.getFileSave( 'Save CSV File', \ - ['Comma-Separated Values (*.csv)'], \ - defaultName) + newDir = fullPath[:-7] + if os.path.exists(newDir): + shutil.rmtree(newDir) + os.makedirs(newDir) + launchProcess(['tar', '-zxf', fullPath, '-C', installPath]) + self.main.writeSetting('SatoshiExe', newDir) - if len(fullpath)==0: - return + QMessageBox.information(self, 'Succeeded', \ + 'The download succeeded!', QMessageBox.Ok) + from twisted.internet import reactor + reactor.callLater(0.5, self.main.executeModeSwitch) + self.accept() - f = open(fullpath, 'w') - f.write('Export Date:, %s\n' % unixTimeToFormatStr(RightNow())) - f.write('Total Funds:, %s\n' % coin2str(totalFunds, maxZeros=0).strip()) - f.write('Spendable Funds:, %s\n' % coin2str(spendFunds, maxZeros=0).strip()) - f.write('Unconfirmed Funds:, %s\n' % coin2str(unconfFunds, maxZeros=0).strip()) - f.write('\n') - f.write('Included Wallets:\n') - for wltID in wltIDList: - wlt = self.main.walletMap[wltID] - f.write('%s,%s\n' % (wltID, wlt.labelName.replace(',',';'))) - f.write('\n') - f.write('Date,Transaction ID,#Conf,Wallet ID, Wallet Name,Total Credit,Total Debit,Fee (wallet paid),Comment\n') - COL = LEDGERCOLS - for row in ledgerTable: - vals = [] - fmtstr = str(self.edtDateFormat.text()) - unixTime = row[COL.UnixTime] - vals.append( unixTimeToFormatStr(unixTime, fmtstr) ) - vals.append( hex_switchEndian(row[COL.TxHash]) ) - vals.append( row[COL.NumConf] ) - vals.append( row[COL.WltID] ) - vals.append( self.main.walletMap[row[COL.WltID]].labelName.replace(',',';')) + ############################################################################# + def clickInstallOpt(self): + if self.radioUbuntuPPA.isChecked(): + self.stkInstruct.setCurrentIndex(0) + elif self.radioDlBinaries.isChecked(): + self.stkInstruct.setCurrentIndex(1) + else: + LOGERROR('How is neither instruction option checked!?') - wltEffect = row[COL.Amount] - txFee = getFeeForTx(hex_to_binary(row[COL.TxHash])) - if float(wltEffect) > 0: - vals.append( wltEffect.strip() ) - vals.append( ' ' ) - vals.append( ' ' ) - else: - vals.append( ' ' ) - vals.append( wltEffect.strip() ) - vals.append( coin2str(-txFee).strip() ) + ############################################################################# + def loadGpgKeyring(self): + pubDirLocal = os.path.join(ARMORY_HOME_DIR, 'tempKeyring') + # if os.path.exists(pubDirLocal): - vals.append( row[COL.Comment] ) + pubDirInst = os.path.join(GetExecDir(), 'PublicKeys') - f.write('%s,%s,%d,%s,%s,%s,%s,%s,"%s"\n' % tuple(vals)) + gpgCmdList = ['gpg'] + cmdImportKey = ('gpg ' + '--keyring ~/.armory/testkeyring.gpg ' + '--no-default-keyring ' + '--import %s/AndresenCodeSign.asc') + cmdVerifyFile = ('gpg ' + '--keyring ~/.armory/testkeyring.gpg ' + '--verify bitcoin.0.8.1.tar.gz') - f.close() - return True + ############################################################################# + def doPPA(self): + out, err = execAndWait('gksudo install_bitcoinqt', timeout=20) + tryInstallLinux(self.main) + self.main.settings.delete('SatoshiExe') + self.accept() -################################################################################ -class DlgRequestPayment(ArmoryDialog): - def __init__(self, parent, main, recvAddr, amt=None, msg=''): - super(DlgRequestPayment, self).__init__(parent, main) +################################################################################ +def tryInstallLinux(main): + def doit(): + print '\n' + print '***** Executing auto-install in linux...' + out, err = execAndWait('gksudo "apt-get remove -y bitcoin-qt bitcoind"', \ + timeout=20) + out, err = execAndWait(('gksudo apt-add-repository ppa:bitcoin/bitcoin; ' + 'gksudo apt-get update; ' + 'gksudo "apt-get install -y bitcoin-qt bitcoind"'), \ + timeout=120) + try: + TheSDM.setupSDM() + from twisted.internet import reactor + reactor.callLater(0.1, main.executeModeSwitch) + QMessageBox.information(main, 'Success!', \ + 'The installation appears to have succeeded!') + except: + LOGINFO('***** Printing output\n' + out) + LOGINFO('***** End print output\n') + LOGINFO('***** Printing errors\n' + err) + LOGINFO('***** End print errors\n') + QMessageBox.warning(main, 'Unknown Error', \ + 'An error was reported while trying to install the Bitcoin ' + 'software. The following information is given:

    %s' % err, \ + QMessageBox.Ok) + raise - if isLikelyDataType(recvAddr, DATATYPE.Binary) and len(recvAddr)==20: - self.recvAddr = hash160_to_addrStr(recvAddr) - elif isLikelyDataType(recvAddr, DATATYPE.Base58): - self.recvAddr = recvAddr - else: - raise BadAddressError, 'Unrecognized address input' + DlgExecLongProcess(doit, 'Installing Bitcoin Software...', main, main).exec_() - # Amount - self.edtAmount = QLineEdit() - self.edtAmount.setFont(GETFONT('Fixed')) - self.edtAmount.setMaximumWidth(relaxedSizeNChar(GETFONT('Fixed'), 13)[0]) - if amt: - self.edtAmount.setText( coin2str(amt, maxZeros=0) ) +################################################################################ +class DlgInstallWindows(ArmoryDialog): + def __init__(self, parent, main, dataToQR, descrUp='', descrDown=''): + super(DlgInstallWindows, self).__init__(parent, main) - # Message: - self.edtMessage = QLineEdit() - self.edtMessage.setMaxLength(128) - if msg: - self.edtMessage.setText(msg) - else: - self.edtMessage.setText('Joe\'s Fish Shop - Order #123 - 3 kg tuna - (888)555-1212' ) - self.edtMessage.setCursorPosition(0) +################################################################################ +class DlgDownloadFile(ArmoryDialog): + def __init__(self, parent, main, dlfile, expectHash=None, msg=''): + super(DlgDownloadFile, self).__init__(parent, main) - # Address: - self.edtAddress = QLineEdit() - self.edtAddress.setText(self.recvAddr) + self.dlFullPath = dlfile + self.dlFileName = os.path.basename(self.dlFullPath) + self.dlSiteName = '/'.join(self.dlFullPath.split('/')[:3]) + self.dlFileSize = 0 + self.dlFileData = '' + self.dlDownBytes = 0 + self.dlExpectHash = expectHash + self.dlStartTime = RightNow() + self.dlVerifyFailed = False - # Link Text: - self.edtLinkText = QLineEdit() - defaultText = binary_to_hex('Click here to pay for your order!') - linkText = hex_to_binary(self.main.getSettingOrSetDefault('DefaultLinkText',defaultText)) - self.edtLinkText.setText(linkText) - self.edtLinkText.setCursorPosition(0) - qpal = QPalette() - qpal.setColor(QPalette.Text, Colors.TextBlue) - self.edtLinkText.setPalette(qpal) - edtFont = self.edtLinkText.font() - edtFont.setUnderline(True) - self.edtLinkText.setFont(edtFont) - + self.StopDownloadFlag = False + self.lblDownloaded = QRichLabel('') + self.barWorking = QProgressBar() + self.barWorking.setRange(0, 100) + self.barWorking.setValue(0) + self.barWorking.setFormat('') - self.connect(self.edtMessage, SIGNAL('textChanged(QString)'), self.setLabels) - self.connect(self.edtAddress, SIGNAL('textChanged(QString)'), self.setLabels) - self.connect(self.edtAmount, SIGNAL('textChanged(QString)'), self.setLabels) - self.connect(self.edtLinkText, SIGNAL('textChanged(QString)'), self.setLabels) - self.connect(self.edtMessage, SIGNAL('editingFinished()'), self.updateQRCode) - self.connect(self.edtAddress, SIGNAL('editingFinished()'), self.updateQRCode) - self.connect(self.edtAmount, SIGNAL('editingFinished()'), self.updateQRCode) - self.connect(self.edtLinkText, SIGNAL('editingFinished()'), self.updateQRCode) + lblDescr = QRichLabel(\ + 'Please wait while file is downloading' + '' % htmlColor('TextBlue'), hAlign=Qt.AlignHCenter) + frmDescr = makeHorizFrame([lblDescr], STYLE_RAISED) - # This is the "output" - self.lblLink = QRichLabel('') - self.lblLink.setOpenExternalLinks(True) - self.lblLink.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard) - self.lblLink.setMinimumHeight( 3*tightSizeNChar(self, 1)[1] ) - self.lblLink.setAlignment(Qt.AlignVCenter | Qt.AlignLeft) - self.lblLink.setContentsMargins(10,5,10,5) - self.lblLink.setStyleSheet('QLabel { background-color : %s }' % htmlColor('SlightBkgdDark')) - frmOut = makeHorizFrame([self.lblLink], QFrame.Box | QFrame.Raised) - frmOut.setLineWidth(1) - frmOut.setMidLineWidth(5) + frmInfo = QFrame() + layoutFileInfo = QGridLayout() + layoutFileInfo.addWidget(QRichLabel('File name:', bold=True), 0, 0) + layoutFileInfo.addWidget(QRichLabel(self.dlFileName), 0, 2) + layoutFileInfo.addWidget(QRichLabel('From site:', bold=True), 1, 0) + layoutFileInfo.addWidget(QRichLabel(self.dlSiteName), 1, 2) + layoutFileInfo.addWidget(QRichLabel('Progress:', bold=True), 2, 0) + layoutFileInfo.addWidget(self.lblDownloaded, 2, 2) + layoutFileInfo.addItem(QSpacerItem(30, 1, QSizePolicy.Fixed, QSizePolicy.Expanding), 0, 1, 3, 1) + layoutFileInfo.setColumnStretch(0, 0) + layoutFileInfo.setColumnStretch(1, 0) + layoutFileInfo.setColumnStretch(2, 1) + frmInfo.setLayout(layoutFileInfo) - self.lblWarn = QRichLabel('') - self.lblWarn.setAlignment(Qt.AlignVCenter | Qt.AlignHCenter) - self.btnOtherOpt = QPushButton('Other Options >>>') - self.btnCopyRich = QPushButton('Copy to Clipboard') - self.btnCopyHtml = QPushButton('Copy Raw HTML') - self.btnCopyRaw = QPushButton('Copy Raw URL') - self.btnCopyAll = QPushButton('Copy All Text') + self.STEPS = enum('Query', 'Download', 'Verify', 'Count') + self.dispSteps = ['Getting file information', \ + 'Downloading', \ + 'Verifying signatures'] + self.lblSteps = [] + for i in range(self.STEPS.Count): + self.lblSteps.append([QRichLabel('', doWrap=False), QRichLabel('')]) - # I never actally got this button working right... - self.btnCopyRich.setVisible(True) - self.btnOtherOpt.setCheckable(True) - self.btnCopyAll.setVisible(False) - self.btnCopyHtml.setVisible(False) - self.btnCopyRaw.setVisible(False) - frmCopyBtnStrip = makeHorizFrame([ \ - self.btnCopyRich, \ - self.btnOtherOpt, \ - self.btnCopyHtml, \ - self.btnCopyRaw, \ - 'Stretch', \ - self.lblWarn]) - #self.btnCopyAll, \ + layoutSteps = QGridLayout() + for i in range(self.STEPS.Count): + layoutSteps.addWidget(self.lblSteps[i][0], i, 0) + layoutSteps.addWidget(self.lblSteps[i][1], i, 1) + frmSteps = QFrame() + frmSteps.setLayout(layoutSteps) + frmSteps.setFrameStyle(STYLE_SUNKEN) + self.dlInstallStatus = self.STEPS.Query + self.updateProgressLabels() - self.connect(self.btnCopyRich, SIGNAL('clicked()'), self.clickCopyRich) - self.connect(self.btnOtherOpt, SIGNAL('toggled(bool)'), self.clickOtherOpt) - self.connect(self.btnCopyRaw, SIGNAL('clicked()'), self.clickCopyRaw ) - self.connect(self.btnCopyHtml, SIGNAL('clicked()'), self.clickCopyHtml) - self.connect(self.btnCopyAll, SIGNAL('clicked()'), self.clickCopyAll) - lblDescr = QRichLabel( \ - 'Create a clickable link that you can copy into email or webpage to ' - 'request a payment. If the user is running a Bitcoin program ' - 'that supports "bitcoin:" links, that program will open with ' - 'all this information pre-filled after they click the link.') + lblExtraMsg = QRichLabel(msg) - lblDescr.setContentsMargins(5, 5, 5, 5) - frmDescr = makeHorizFrame([lblDescr], STYLE_SUNKEN) + btnCancel = QPushButton("Cancel") + self.connect(btnCancel, SIGNAL(CLICKED), self.reject) + frmCancel = makeHorizFrame([STRETCH, btnCancel, STRETCH]) - ttipPreview = self.main.createToolTipWidget( \ - 'The following Bitcoin desktop applications try to ' - 'register themselves with your computer to handle "bitcoin:" ' - 'links: Armory, Multibit, Electrum') - ttipLinkText = self.main.createToolTipWidget( \ - 'This is the text to be shown as the clickable link. It should ' - 'usually begin with "Click here..." to reaffirm to the user it is ' - 'is clickable.') - ttipAmount = self.main.createToolTipWidget( \ - 'All amounts are specifed in BTC') - ttipAddress = self.main.createToolTipWidget( \ - 'The person clicking the link will be sending bitcoins to this address') - ttipMessage = self.main.createToolTipWidget( \ - 'This text will be pre-filled as the label/comment field ' - 'after the user clicks on the link. They ' - 'can modify it to meet their own needs, but you can ' - 'provide useful information such as contact details and ' - 'purchase info as a convenience to them.') + frm = makeVertFrame([frmDescr, \ + frmInfo, \ + self.barWorking, \ + frmSteps, \ + lblExtraMsg, \ + frmCancel]) + layout = QVBoxLayout() + layout.addWidget(frm) + self.setLayout(layout) + self.setMinimumWidth(400) - btnClose = QPushButton('Close') - self.connect(btnClose, SIGNAL('clicked()'), self.accept) + def startBackgroundDownload(dlg): + thr = PyBackgroundThread(dlg.startDL) + thr.start() + print 'Starting download in 1s...' + from twisted.internet import reactor + reactor.callLater(1, startBackgroundDownload, self) + self.main.extraHeartbeatSpecial.append(self.checkDownloadProgress) + self.setWindowTitle('Downloading File...') - frmEntry = QFrame() - frmEntry.setFrameStyle(STYLE_SUNKEN) - layoutEntry = QGridLayout() - i=0 - layoutEntry.addWidget(QRichLabel('Link Text:'), i,0) - layoutEntry.addWidget(self.edtLinkText, i,1) - layoutEntry.addWidget(ttipLinkText, i,2) - - i+=1 - layoutEntry.addWidget(QRichLabel('Address (yours):'), i,0) - layoutEntry.addWidget(self.edtAddress, i,1) - layoutEntry.addWidget(ttipAddress, i,2) - - i+=1 - layoutEntry.addWidget(QRichLabel('Request (BTC):'), i,0) - layoutEntry.addWidget(self.edtAmount, i,1) - - i+=1 - layoutEntry.addWidget(QRichLabel('Label:'), i,0) - layoutEntry.addWidget(self.edtMessage, i,1) - layoutEntry.addWidget(ttipMessage, i,2) - frmEntry.setLayout(layoutEntry) - - lblOut = QRichLabel('Copy and paste the following text into email or other document:') - frmOutput = makeVertFrame([lblOut, frmOut, frmCopyBtnStrip], STYLE_SUNKEN) - frmOutput.layout().setStretch(0, 0) - frmOutput.layout().setStretch(1, 1) - frmOutput.layout().setStretch(2, 0) - frmClose = makeHorizFrame(['Stretch', btnClose]) + def reject(self): + self.StopDownloadFlag = True + self.dlFileData = '' + super(DlgDownloadFile, self).reject() + def accept(self): + self.StopDownloadFlag = True + super(DlgDownloadFile, self).accept() - self.qrURI = QRCodeWidget('', parent=self) - lblQRDescr = QRichLabel('This QR code contains address and the ' - 'other payment information shown to the left.') + def startDL(self): + self.dlInstallStatus = self.STEPS.Query + keepTrying = True + nTries = 0 + self.httpObj = None + while keepTrying: + nTries += 1 + try: + import urllib2 + self.httpObj = urllib2.urlopen(self.dlFullPath, timeout=10) + break + except urllib2.HTTPError: + LOGERROR('urllib2 failed to urlopen the download link') + LOGERROR('Link: %s', self.dlFullPath) + break + except socket.timeout: + LOGERROR('timed out once') + if nTries > 2: + keepTrying = False + except: + print sys.exc_info() + break - lblQRDescr.setAlignment(Qt.AlignTop | Qt.AlignHCenter) - frmQR = makeVertFrame([self.qrURI, 'Stretch', lblQRDescr,'Stretch'], STYLE_SUNKEN) - frmQR.layout().setStretch(0, 0) - frmQR.layout().setStretch(1, 0) - frmQR.layout().setStretch(2, 1) + if self.httpObj == None: + self.StopDownloadFlag = True + return - self.maxQRSize = int(1.25*QRCodeWidget('a'*200).getSize()) - frmQR.setMinimumWidth(self.maxQRSize) - self.qrURI.setMinimumHeight(self.maxQRSize) + self.dlFileSize = 0 + for line in self.httpObj.info().headers: + if line.startswith('Content-Length'): + try: + self.dlFileSize = int(line.split()[-1]) + except: + raise - dlgLayout = QGridLayout() + LOGINFO('Starting download') + self.dlInstallStatus = self.STEPS.Download + bufSize = 32768 + bufferData = 1 + while bufferData: + if self.StopDownloadFlag: + return + bufferData = self.httpObj.read(bufSize) + self.dlFileData += bufferData + self.dlDownBytes += bufSize - dlgLayout.addWidget(frmDescr, 0,0, 1,2) - dlgLayout.addWidget(frmEntry, 1,0, 1,1) - dlgLayout.addWidget(frmOutput, 2,0, 1,1) - dlgLayout.addWidget(HLINE(), 3,0, 1,2) - dlgLayout.addWidget(frmClose, 4,0, 1,2) + self.dlInstallStatus = self.STEPS.Verify + hexHash = binary_to_hex(sha256(self.dlFileData)) + LOGINFO('Hash of downloaded file: ') + LOGINFO(hexHash) + if self.dlExpectHash: + if not self.dlExpectHash == hexHash: + LOGERROR('Downloaded file does not authenticate!') + LOGERROR('Aborting download') + self.dlFileData = '' + self.dlVerifyFailed = True + else: + LOGINFO('Downloaded file is cryptographically verified!') + self.dlVerifyFailed = False - dlgLayout.addWidget(frmQR, 1,1, 2,1) + self.dlInstallStatus = self.STEPS.Count # one past end - dlgLayout.setRowStretch(0, 0) - dlgLayout.setRowStretch(1, 0) - dlgLayout.setRowStretch(2, 1) - dlgLayout.setRowStretch(3, 0) - dlgLayout.setRowStretch(4, 0) - - self.setLabels() - self.prevURI = '' - self.closed = False # kind of a hack to end the update loop - self.updateQRCode() - self.setLayout(dlgLayout) - self.setWindowTitle('Create Payment Request Link') - from twisted.internet import reactor - reactor.callLater(1, self.periodicUpdate) + def checkDownloadProgress(self): + if self.StopDownloadFlag: + from twisted.internet import reactor + reactor.callLater(1, self.reject) + return -1 - hexgeom = str(self.main.settings.get('PayReqestGeometry')) - if len(hexgeom)>0: - geom = QByteArray.fromHex(hexgeom) - self.restoreGeometry(geom) - self.setMinimumSize(750,500) + if self.dlFileSize == 0: + return 0.1 + self.updateProgressLabels() - def saveLinkText(self): - linktext = str(self.edtLinkText.text()).strip() - if len(linktext)>0: - hexText = binary_to_hex(linktext) - self.main.writeSetting('DefaultLinkText',hexText) - + try: + if self.dlInstallStatus >= self.STEPS.Download: + self.barWorking.setVisible(True) - ############################################################################# - def saveGeometrySettings(self): - self.main.writeSetting('PayReqestGeometry', str(self.saveGeometry().toHex())) + trueRatio = float(self.dlDownBytes) / float(self.dlFileSize) + dispRatio = min(trueRatio, 1) + if self.dlFileSize > 0: + self.barWorking.setValue(100 * dispRatio) + self.barWorking.setFormat('%p%') - ############################################################################# - def closeEvent(self, event): - self.saveGeometrySettings() - self.saveLinkText() - super(DlgRequestPayment, self).closeEvent(event) + dlSizeHuman = bytesToHumanSize(self.dlDownBytes) + totalSizeHuman = bytesToHumanSize(self.dlFileSize) + self.lblDownloaded.setText('%s of %s' % (dlSizeHuman, totalSizeHuman)) - ############################################################################# - def accept(self, *args): - self.saveGeometrySettings() - self.saveLinkText() - super(DlgRequestPayment, self).accept(*args) - ############################################################################# - def reject(self, *args): - self.saveGeometrySettings() - super(DlgRequestPayment, self).reject(*args) + if self.dlInstallStatus > self.STEPS.Verify: + from twisted.internet import reactor + reactor.callLater(2, self.accept) + return -1 + else: + return 0.1 + except: + LOGEXCEPT("Failed to check download progress") + return -1 - ############################################################################# - def setLabels(self): - - lastTry = '' - try: - # The - lastTry = 'Amount' - amtStr = str(self.edtAmount.text()).strip() - if len(amtStr)==0: - amt = None + def updateProgressLabels(self): + # Highlight the correct labels and show checkmarks + for i in range(self.STEPS.Count): + if i == self.dlInstallStatus: + self.lblSteps[i][0].setText(self.dispSteps[i], bold=True, color='Foreground') + self.lblSteps[i][1].setText('...', bold=True) + elif i < self.dlInstallStatus: + self.lblSteps[i][0].setText(self.dispSteps[i], color='Foreground') + self.lblSteps[i][1].setPixmap(QPixmap(':/checkmark32.png').scaled(20, 20)) else: - amt = str2coin(amtStr) - - lastTry = 'Message' - msgStr = str(self.edtMessage.text()).strip() - if len(msgStr)==0: - msgStr = None - - lastTry = 'Address' - addr = str(self.edtAddress.text()).strip() - if not checkAddrStrValid(addr): - raise + self.lblSteps[i][0].setText(self.dispSteps[i], \ + bold=False, color='DisableFG') - errorIn = 'Inputs' - # must have address, maybe have amount and/or message - self.rawURI = createBitcoinURI(addr, amt, msgStr) - except: - self.lblWarn.setText('Invalid %s' % lastTry) - self.btnCopyRaw.setEnabled(False) - self.btnCopyHtml.setEnabled(False) - self.btnCopyAll.setEnabled(False) - #self.lblLink.setText('
    '.join(str(self.lblLink.text()).split('
    ')[1:])) - self.lblLink.setEnabled(False) - self.lblLink.setTextInteractionFlags(Qt.NoTextInteraction) - return - - self.lblLink.setTextInteractionFlags(Qt.TextSelectableByMouse | \ - Qt.TextSelectableByKeyboard) + if self.dlInstallStatus >= self.STEPS.Verify: + self.barWorking.setValue(100) + if self.dlVerifyFailed: + self.lblSteps[self.STEPS.Verify][1].setPixmap(QPixmap(':/MsgBox_error32.png').scaled(20, 20)) - self.rawHtml = '%s' % (self.rawURI, str(self.edtLinkText.text())) - self.lblWarn.setText('') - self.dispText = self.rawHtml[:] - self.dispText += '
    ' - self.dispText += 'If clicking on the line above does not work, use this payment info:' - self.dispText += '
    ' - self.dispText += 'Pay to:\t%s
    ' % addr - if amtStr: - self.dispText += 'Amount:\t%s BTC
    ' % coin2str(amt,maxZeros=0).strip() - if msgStr: - self.dispText += 'Message:\t%s
    ' % msgStr - self.lblLink.setText(self.dispText) - self.lblLink.setEnabled(True) - self.btnCopyRaw.setEnabled(True) - self.btnCopyHtml.setEnabled(True) - self.btnCopyAll.setEnabled(True) - # Plain text to copy to clipboard as "text/plain" - self.plainText = str(self.edtLinkText.text()) + '\n' - self.plainText += 'If clicking on the line above does not work, use this payment info:\n' - self.plainText += 'Pay to: %s' % addr - if amtStr: - self.plainText += '\nAmount: %s BTC' % coin2str(amt,maxZeros=0).strip() - if msgStr: - self.plainText += '\nMessage: %s' % msgStr - self.plainText += '\n' - # The rich-text to copy to the clipboard, as "text/html" - self.clipText = ( \ - ' ' - '' - '' - '

    ' - '' - '' - '%s
    ' - 'If clicking on the line above does not work, use this payment info:' - '
    Pay to: %s') % \ - (self.rawURI, str(self.edtLinkText.text()), addr) - if amt: - self.clipText += ('
    Amount' - ': %s' % coin2str(amt,maxZeros=0)) - if msgStr: - self.clipText += ('
    Message' - ': %s' % msgStr) - self.clipText += '

    ' - def periodicUpdate(self, nsec=1): - if not self.closed: - from twisted.internet import reactor - self.updateQRCode() - reactor.callLater(nsec, self.periodicUpdate) +################################################################################ +class QRadioButtonBackupCtr(QRadioButton): + def __init__(self, parent, txt, index): + super(QRadioButtonBackupCtr, self).__init__(txt) + self.parent = parent + self.index = index - def accept(self, *args): - # Kind of a hacky way to get the loop to end, but it seems to work - self.closed = True - super(DlgRequestPayment, self).accept(*args) + def enterEvent(self, ev): + pass + # self.parent.setDispFrame(self.index) + # self.setStyleSheet('QRadioButton { background-color : %s }' % \ + # htmlColor('SlightBkgdDark')) - def reject(self, *args): - # Kind of a hacky way to get the loop to end, but it seems to work - self.closed = True - super(DlgRequestPayment, self).reject(*args) + def leaveEvent(self, ev): + pass + # self.parent.setDispFrame(-1) + # self.setStyleSheet('QRadioButton { background-color : %s }' % \ + # htmlColor('Background')) - def updateQRCode(self,e=None): - if not self.prevURI==self.rawURI: - self.qrURI.setAsciiData(self.rawURI) - self.qrURI.setPreferredSize(self.maxQRSize-10, 'max') - self.repaint() - self.prevURI = self.rawURI - def clickCopyRich(self): - clipb = QApplication.clipboard() - clipb.clear() - qmd = QMimeData() - if OS_WINDOWS: - qmd.setText(self.plainText) - qmd.setHtml(self.clipText) - else: - prefix = '' - qmd.setText(self.plainText) - qmd.setHtml(prefix + self.dispText) - clipb.setMimeData(qmd) - self.lblWarn.setText('Copied!') - +################################################################################ +class DlgBackupCenter(ArmoryDialog): + ############################################################################# + def __init__(self, parent, main, wlt): + super(DlgBackupCenter, self).__init__(parent, main) - def clickOtherOpt(self, boolState): - self.btnCopyHtml.setVisible(boolState) - self.btnCopyRaw.setVisible(boolState) + self.wlt = wlt + wltID = wlt.uniqueIDB58 + wltName = wlt.labelName - if boolState: - self.btnOtherOpt.setText('Hide Buttons <<<') - else: - self.btnOtherOpt.setText('Other Options >>>') + self.walletBackupFrame = WalletBackupFrame(parent, main) + self.walletBackupFrame.setWallet(wlt) + self.btnDone = QPushButton('Done') + self.connect(self.btnDone, SIGNAL(CLICKED), self.reject) + frmBottomBtns = makeHorizFrame([STRETCH, self.btnDone]) - def clickCopyRaw(self): - clipb = QApplication.clipboard() - clipb.clear() - clipb.setText(self.rawURI) - self.lblWarn.setText('Copied!') + layoutDialog = QVBoxLayout() - def clickCopyHtml(self): - clipb = QApplication.clipboard() - clipb.clear() - clipb.setText(self.rawHtml) - self.lblWarn.setText('Copied!') + layoutDialog.addWidget(self.walletBackupFrame) - def clickCopyAll(self): - clipb = QApplication.clipboard() - clipb.clear() - qmd = QMimeData() - qmd.setHtml(self.dispText) - clipb.setMimeData(qmd) - self.lblWarn.setText('Copied!') + layoutDialog.addWidget(frmBottomBtns) + self.setLayout(layoutDialog) + self.setWindowTitle("Backup Center") + self.setMinimumSize(640, 350) +################################################################################ +class DlgSimpleBackup(ArmoryDialog): + def __init__(self, parent, main, wlt): + super(DlgSimpleBackup, self).__init__(parent, main) + self.wlt = wlt -################################################################################ -class DlgVersionNotify(ArmoryDialog): - def __init__(self, parent, main, changelog, wasRequested=False): - super(DlgVersionNotify, self).__init__(parent, main) - - self.myVersion = getVersionInt(BTCARMORY_VERSION) - self.latestVer = getVersionInt(readVersionString(changelog[0][0])) - self.myVersionStr = getVersionString(BTCARMORY_VERSION) - self.latestVerStr = getVersionString(readVersionString(changelog[0][0])) - lblDescr = QRichLabel('') - - if self.myVersion >= self.latestVer: - lblDescr = QRichLabel( \ - 'Your Armory installation is up-to-date!' - '' - '

    ' - 'Installed Version: %s' - '

    ' - 'When they become available, you can find and download new ' - 'versions of Armory from:

    ' - '' - 'https://bitcoinarmory.com/download ' % self.myVersionStr) - - else: - lblDescr = QRichLabel( \ - 'There is a new version of Armory available!' - '' - '

    ' - 'Current Version: %s
    ' - 'Lastest Version: %s' - '

    ' - 'Please visit the ' - 'Armory ' - 'download page (https://bitcoinarmory.com/download) ' - 'to get the most recent version. ' - 'All your wallets and settings will remain untouched when you ' - 'reinstall Armory.' % (self.myVersionStr, self.latestVerStr)) + lblDescrTitle = QRichLabel(tr(""" + Protect Your Bitcoins -- Make a Wallet Backup!""")) - lblDescr.setOpenExternalLinks(True) + lblDescr = QRichLabel(tr(""" + A failed hard-drive or forgotten passphrase will lead to + permanent loss of bitcoins! Luckily, Armory wallets only + need to be backed up one time, and protect you in both + of these events. If you've ever forgotten a password or had + a hardware failure, make a backup! """)) - lblChnglog = QRichLabel('') - if wasRequested: - lblChnglog = QRichLabel("Changelog:") - else: - lblChnglog = QRichLabel('New features added between version %s and %s:' % \ - (self.myVersionStr,self.latestVerStr)) + # ## Paper + lblPaper = QRichLabel(tr(""" + Use a printer or pen-and-paper to write down your wallet "seed." """)) + btnPaper = QPushButton(tr('Make Paper Backup')) + # ## Digital + lblDigital = QRichLabel(tr(""" + Create an unencrypted copy of your wallet file, including imported + addresses.""")) + btnDigital = QPushButton(tr('Make Digital Backup')) - - txtChangeLog = QTextEdit() - txtChangeLog.setReadOnly(True) - - w,h = tightSizeNChar(self, 100) - w = min(w,600) - h = min(h,400) - txtChangeLog.sizeHint = lambda: QSize(w,15*h) - - for versionTbl in changelog: - versionStr = versionTbl[0] - featureList = versionTbl[1] - txtChangeLog.insertHtml('Version %s

    ' % versionStr) - for feat in featureList: - txtChangeLog.insertHtml('%s
    ' % feat[0]) - for dtxt in feat[1]: - txtChangeLog.insertHtml('%s
    ' % dtxt) - txtChangeLog.insertHtml('

    ') - - curs = txtChangeLog.textCursor() - curs.movePosition(QTextCursor.Start) - txtChangeLog.setTextCursor(curs) - - btnDNAA = QPushButton('&Do not notify me of new versions') - btnDelay = QPushButton('&No more reminders until next version') - btnOkay = QPushButton('&OK') - self.connect(btnDNAA, SIGNAL('clicked()'), self.clickDNAA) - self.connect(btnDelay, SIGNAL('clicked()'), self.clickDelay) - self.connect(btnOkay, SIGNAL('clicked()'), self.clickOkay) - - frmDescr = makeVertFrame([lblDescr], STYLE_SUNKEN) - frmLog = makeVertFrame([lblChnglog, 'Space(5)', txtChangeLog], STYLE_SUNKEN) - frmBtn = makeHorizFrame([btnDNAA, btnDelay, 'Stretch', btnOkay], STYLE_SUNKEN) - dlgLayout = QVBoxLayout() - dlgLayout.addWidget(frmDescr) - dlgLayout.addWidget(frmLog) - dlgLayout.addWidget(frmBtn) - dlgLayout.setContentsMargins(20,20,20,20) - - self.setLayout(dlgLayout) - self.setWindowTitle('New Armory Version') - self.setWindowIcon(QIcon(self.main.iconfile)) + # ## Other + lblOther = QRichLabel(tr(""" """)) + btnOther = QPushButton(tr('See Other Backup Options')) + def backupDigital(): + if self.main.digitalBackupWarning(): + self.main.makeWalletCopy(self, self.wlt, 'Decrypt', 'decrypt') + self.accept() - def clickDNAA(self): - self.main.writeSetting('CheckVersion', 'Never') - self.accept() + def backupPaper(): + OpenPaperBackupWindow('Single', self, self.main, self.wlt) + self.accept() - def clickDelay(self): - self.main.settings.set('CheckVersion', 'v'+self.latestVerStr) - self.accept() + def backupOther(): + self.accept() + DlgBackupCenter(self, self.main, self.wlt).exec_() - def clickOkay(self): - self.main.writeSetting('CheckVersion', 'Always') - self.accept() - + self.connect(btnPaper, SIGNAL(CLICKED), backupPaper) + self.connect(btnDigital, SIGNAL(CLICKED), backupDigital) + self.connect(btnOther, SIGNAL(CLICKED), backupOther) + layout = QGridLayout() + layout.addWidget(lblPaper, 0, 0) + layout.addWidget(btnPaper, 0, 2) + layout.addWidget(HLINE(), 1, 0, 1, 3) -################################################################################ -class DlgUriCopyAndPaste(ArmoryDialog): - def __init__(self, parent, main): - super(DlgUriCopyAndPaste, self).__init__(parent, main) + layout.addWidget(lblDigital, 2, 0) + layout.addWidget(btnDigital, 2, 2) - self.uriDict = {} - lblDescr = QRichLabel('Copy and paste a raw bitcoin URL string here. ' - 'A valid string starts with "bitcoin:" followed ' - 'by a bitcoin address.' - '

    ' - 'You should use this feature if there is a "bitcoin:" ' - 'link in a webpage or email that does not load Armory ' - 'when you click on it. Instead, right-click on the ' - 'link and select "Copy Link Location" then paste it ' - 'into the box below. ' ) + layout.addWidget(HLINE(), 3, 0, 1, 3) - lblShowExample = QLabel() - lblShowExample.setPixmap(QPixmap(':/armory_rightclickcopy.png')) + layout.addWidget(makeHorizFrame([STRETCH, btnOther, STRETCH]), 4, 0, 1, 3) - self.txtUriString = QLineEdit() - self.txtUriString.setFont( GETFONT('Fixed', 8)) + # layout.addWidget( VLINE(), 0,1, 5,1) - self.btnOkay = QPushButton('Done') - self.btnCancel = QPushButton('Cancel') - buttonBox = QDialogButtonBox() - buttonBox.addButton(self.btnOkay, QDialogButtonBox.AcceptRole) - buttonBox.addButton(self.btnCancel, QDialogButtonBox.RejectRole) + layout.setContentsMargins(10, 5, 10, 5) + setLayoutStretchRows(layout, 1, 0, 1, 0, 0) + setLayoutStretchCols(layout, 1, 0, 0) - self.connect(self.btnOkay, SIGNAL('clicked()'), self.clickedOkay) - self.connect(self.btnCancel, SIGNAL('clicked()'), self.reject) + frmGrid = QFrame() + frmGrid.setFrameStyle(STYLE_PLAIN) + frmGrid.setLayout(layout) - frmImg = makeHorizFrame(['Stretch',lblShowExample,'Stretch']) + btnClose = QPushButton('Done') + self.connect(btnClose, SIGNAL(CLICKED), self.accept) + frmClose = makeHorizFrame([STRETCH, btnClose]) + frmAll = makeVertFrame([lblDescrTitle, lblDescr, frmGrid, frmClose]) + layoutAll = QVBoxLayout() + layoutAll.addWidget(frmAll) + self.setLayout(layoutAll) + self.sizeHint = lambda: QSize(400, 250) - layout = QVBoxLayout() - layout.addWidget(lblDescr) - layout.addWidget(HLINE()) - layout.addWidget(frmImg) - layout.addWidget(HLINE()) - layout.addWidget(self.txtUriString) - layout.addWidget(buttonBox) - self.setLayout(layout) + self.setWindowTitle(tr('Backup Options')) - def clickedOkay(self): - uriStr = str(self.txtUriString.text()) - self.uriDict = self.main.parseUriLink(uriStr, 'enter') - self.accept() - +################################################################################ +class DlgFragBackup(ArmoryDialog): + + ############################################################################# + def __init__(self, parent, main, wlt): + super(DlgFragBackup, self).__init__(parent, main) + self.wlt = wlt + self.randpass = None + self.binCrypt32 = None + lblDescrTitle = QRichLabel(tr(""" + Create M-of-N Fragmented Backup of "%s" (%s)""") % \ + (wlt.labelName, wlt.uniqueIDB58), doWrap=False) + lblDescrTitle.setContentsMargins(5, 5, 5, 5) -class DlgCoinControl(ArmoryDialog): - def __init__(self, parent, main, wlt, currSelect=None): - super(DlgCoinControl, self).__init__(parent, main) + self.lblAboveFrags = QRichLabel('') + self.lblAboveFrags.setContentsMargins(10, 0, 10, 0) - self.wlt = wlt + frmDescr = makeVertFrame([lblDescrTitle, self.lblAboveFrags], \ + STYLE_RAISED) - lblDescr = QRichLabel( \ - 'By default, transactions are created using any available coins from ' - 'all addresses in this wallet. You can control the source addresses ' - 'used for this transaction by selecting them below, and unchecking ' - 'all other addresses.') - self.chkSelectAll = QCheckBox('Select All') - self.chkSelectAll.setChecked(True) - self.connect(self.chkSelectAll, SIGNAL('clicked()'), self.clickAll) + self.maxM = 5 if not self.main.usermode == USERMODE.Expert else 8 + self.maxN = 6 if not self.main.usermode == USERMODE.Expert else 12 + self.currMinN = 2 + self.maxmaxN = 12 - addrToInclude = [] - totalBal = 0 - for addr160 in wlt.addrMap.iterkeys(): - bal = wlt.getAddrBalance(addr160) - if bal > 0: - addrToInclude.append([addr160,bal]) - totalBal += bal - - frmTableLayout = QGridLayout() - self.dispTable = [] - frmTableLayout.addWidget(QRichLabel('Address'), 0,0) - frmTableLayout.addWidget(VLINE(), 0,1) - frmTableLayout.addWidget(QRichLabel('Balance'), 0,2) - frmTableLayout.addWidget(VLINE(), 0,3) - frmTableLayout.addWidget(QRichLabel('Comment'), 0,4) - frmTableLayout.addWidget(HLINE(), 1,0, 1,5) - for i in range(len(addrToInclude)): - a160,bal = addrToInclude[i] - fullcmt = self.wlt.getCommentForAddress(a160) - shortcmt = fullcmt - if shortcmt==CHANGE_ADDR_DESCR_STRING: - shortcmt = 'Change Address' - fullcmt = '(This address was created only to receive change from another transaction)' - elif len(shortcmt)>20: - shortcmt = fullcmt[:20]+'...' - self.dispTable.append([None, None, None]) - self.dispTable[-1][0] = QCheckBox(hash160_to_addrStr(a160)) - self.dispTable[-1][1] = QMoneyLabel(bal) - self.dispTable[-1][2] = QRichLabel(shortcmt, doWrap=False) - self.dispTable[-1][0].setChecked(currSelect==None or (a160 in currSelect)) - if len(shortcmt)>0: - self.dispTable[-1][0].setToolTip(''+fullcmt) - self.dispTable[-1][1].setToolTip(''+fullcmt) - self.dispTable[-1][2].setToolTip(''+fullcmt) - self.connect(self.dispTable[-1][0], SIGNAL('clicked()'), self.clickOne) - frmTableLayout.addWidget(self.dispTable[-1][0], i+2,0) - frmTableLayout.addWidget(VLINE(), i+2,1) - frmTableLayout.addWidget(self.dispTable[-1][1], i+2,2) - frmTableLayout.addWidget(VLINE(), i+2,3) - frmTableLayout.addWidget(self.dispTable[-1][2], i+2,4) - - frmTable = QFrame() - frmTable.setLayout(frmTableLayout) - self.scrollAddrList = QScrollArea() - self.scrollAddrList.setWidget(frmTable) + self.comboM = QComboBox() + self.comboN = QComboBox() - self.sizeHint = lambda: QSize(frmTable.width()+40, 400) + for M in range(2, self.maxM + 1): + self.comboM.addItem(str(M)) - lblDescrSum = QRichLabel('Balance of selected addresses:', doWrap=False) - self.lblSum = QMoneyLabel(totalBal, wBold=True) - frmSum = makeHorizFrame(['Stretch', lblDescrSum, self.lblSum, 'Stretch']) + for N in range(self.currMinN, self.maxN + 1): + self.comboN.addItem(str(N)) - self.btnAccept = QPushButton("Accept") - self.btnCancel = QPushButton("Cancel") - self.connect(self.btnAccept, SIGNAL('clicked()'), self.acceptSelection) - self.connect(self.btnCancel, SIGNAL('clicked()'), self.reject) - buttonBox = QDialogButtonBox() - buttonBox.addButton(self.btnAccept, QDialogButtonBox.AcceptRole) - buttonBox.addButton(self.btnCancel, QDialogButtonBox.RejectRole) + self.comboM.setCurrentIndex(1) + self.comboN.setCurrentIndex(2) - layout = QGridLayout() - layout.addWidget(lblDescr, 0,0) - layout.addWidget(self.chkSelectAll, 1,0) - layout.addWidget(self.scrollAddrList, 2,0) - layout.addWidget(frmSum, 3,0) - layout.addWidget(buttonBox, 4,0) - layout.setRowStretch(0, 0) - layout.setRowStretch(1, 0) - layout.setRowStretch(2, 1) - layout.setRowStretch(3, 0) - layout.setRowStretch(4, 0) - self.setLayout(layout) + def updateM(): + self.updateComboN() + self.createFragDisplay() - self.recalcBalance() - - self.setWindowTitle('Coin Control (Expert)') + updateN = self.createFragDisplay - def clickAll(self): - for dispList in self.dispTable: - dispList[0].setChecked( self.chkSelectAll.isChecked() ) - self.recalcBalance() + self.connect(self.comboM, SIGNAL('activated(int)'), updateM) + self.connect(self.comboN, SIGNAL('activated(int)'), updateN) + self.comboM.setMinimumWidth(30) + self.comboN.setMinimumWidth(30) - def clickOne(self): - self.recalcBalance() + btnAccept = QPushButton(tr('Close')) + self.connect(btnAccept, SIGNAL(CLICKED), self.accept) + frmBottomBtn = makeHorizFrame([STRETCH, btnAccept]) + # We will hold all fragments here, in SBD objects. Destroy all of them + # before the dialog exits + self.secureRoot = self.wlt.addrMap['ROOT'].binPrivKey32_Plain.copy() + self.secureChain = self.wlt.addrMap['ROOT'].chaincode.copy() + self.secureMtrx = [] - def recalcBalance(self): - totalBal = 0 - for dispList in self.dispTable: - if dispList[0].isChecked(): - a160 = addrStr_to_hash160(str(dispList[0].text())) - totalBal += self.wlt.getAddrBalance(a160) - else: - self.chkSelectAll.setChecked(False) + testChain = DeriveChaincodeFromRootKey(self.secureRoot) + if testChain == self.secureChain: + self.noNeedChaincode = True + self.securePrint = self.secureRoot + else: + self.securePrint = self.secureRoot + self.secureChain - self.lblSum.setValueText(totalBal) - - + self.chkSecurePrint = QCheckBox(tr(""" + Use SecurePrint\xe2\x84\xa2 to prevent exposing keys to other devices""")) - def acceptSelection(self): - self.coinControlList = [] - for dispList in self.dispTable: - if dispList[0].isChecked(): - a160 = addrStr_to_hash160(str(dispList[0].text())) - bal = self.wlt.getAddrBalance(a160) - self.coinControlList.append([a160, bal]) + self.scrollArea = QScrollArea() + self.createFragDisplay() + self.scrollArea.setWidgetResizable(True) - if len(self.coinControlList)==0: - QMessageBox.warning(self, 'Nothing Selected', \ - 'You must select at least one address to fund your ' - 'transaction.', QMessageBox.Ok) - return + self.ttipSecurePrint = self.main.createToolTipWidget(tr(""" + SecurePrint\xe2\x84\xa2 encrypts your backup with a code displayed on + the screen, so that no other devices or processes has access to the + unencrypted private keys (either network devices when printing, or + other applications if you save a fragment to disk or USB device). + You must keep the SecurePrint\xe2\x84\xa2 code with the backup!""")) + self.lblSecurePrint = QRichLabel(tr(""" + IMPORTANT: You must keep the + SecurePrint\xe2\x84\xa2 encryption code with your backup! + Your SecurePrint\xe2\x84\xa2 code is + %s. + All fragments for a given wallet use the + same code.""") % \ + (htmlColor('TextWarn'), htmlColor('TextBlue'), self.randpass.toBinStr(), \ + htmlColor('TextWarn'))) + self.connect(self.chkSecurePrint, SIGNAL(CLICKED), self.clickChkSP) + self.chkSecurePrint.setChecked(False) + self.lblSecurePrint.setVisible(False) + frmChkSP = makeHorizFrame([self.chkSecurePrint, self.ttipSecurePrint, STRETCH]) - self.accept() + dlgLayout = QVBoxLayout() + dlgLayout.addWidget(frmDescr) + dlgLayout.addWidget(self.scrollArea) + dlgLayout.addWidget(self.chkSecurePrint) + dlgLayout.addWidget(self.lblSecurePrint) + dlgLayout.addWidget(frmBottomBtn) + setLayoutStretch(dlgLayout, 0, 1, 0, 0, 0) + self.setLayout(dlgLayout) + self.setMinimumWidth(650) + self.setMinimumHeight(450) + self.setWindowTitle('Create Backup Fragments') -# STUB -class dlgRawTx(ArmoryDialog): - def __init__(self, parent, main): - super(DlgVersionNotify, self).__init__(parent, main) - - lblRaw = QRichLabel('You may paste raw transaction data into the box below, ' - 'and then click "Broadcast" to send it to the Bitcoin ' - 'network. If the transaction has been broadcast before, ' - 'attempting to send it again is unlikely to do anything.') + ############################################################################# + def clickChkSP(self): + self.lblSecurePrint.setVisible(self.chkSecurePrint.isChecked()) + self.createFragDisplay() - self.txtRawTx = QTextEdit() - self.txtRawTx.setFont( GETFONT('Fixed',8) ) - #self.txtRawTx.sizeHint = lambda: QSize(w,h) - self.txtRawTx.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) - self.connect(self.txtRawTx, SIGNAL('textChanged()'), self.processTx) - - self.btnBroadcast = QPushButton("Broadcast") - self.connect(self.btnBroadcast, SIGNAL('clicked()'), self.broadcastTx) + ############################################################################# + def updateComboN(self): + M = int(str(self.comboM.currentText())) + oldN = int(str(self.comboN.currentText())) + self.currMinN = M + self.comboN.clear() - + for i, N in enumerate(range(self.currMinN, self.maxN + 1)): + self.comboN.addItem(str(N)) - - + if M > oldN: + self.comboN.setCurrentIndex(0) + else: + for i, N in enumerate(range(self.currMinN, self.maxN + 1)): + if N == oldN: + self.comboN.setCurrentIndex(i) -################################################################################ -class DlgQRCodeDisplay(ArmoryDialog): - def __init__(self, parent, main, dataToQR, descrUp='', descrDown=''): - super(DlgQRCodeDisplay, self).__init__(parent, main) - btnDone = QPushButton('Close') - self.connect(btnDone, SIGNAL('clicked()'), self.accept) - frmBtn = makeHorizFrame(['Stretch', btnDone, 'Stretch']) - qrDisp = QRCodeWidget(dataToQR, parent=self) - frmQR = makeHorizFrame(['Stretch', qrDisp, 'Stretch']) + ############################################################################# + def createFragDisplay(self): + self.recomputeFragData() + M = int(str(self.comboM.currentText())) + N = int(str(self.comboN.currentText())) - lblUp = QRichLabel(descrUp) - lblUp.setAlignment(Qt.AlignVCenter | Qt.AlignHCenter) - lblDn = QRichLabel(descrDown) - lblDn.setAlignment(Qt.AlignVCenter | Qt.AlignHCenter) + lblAboveM = QRichLabel(tr('Required Fragments '), hAlign=Qt.AlignHCenter, doWrap=False) + lblAboveN = QRichLabel(tr('Total Fragments '), hAlign=Qt.AlignHCenter) + frmComboM = makeHorizFrame([STRETCH, QLabel('M:'), self.comboM, STRETCH]) + frmComboN = makeHorizFrame([STRETCH, QLabel('N:'), self.comboN, STRETCH]) - layout = QVBoxLayout() - layout.addWidget(lblUp) - layout.addWidget(frmQR) - layout.addWidget(lblDn) - layout.addWidget(HLINE()) - layout.addWidget(frmBtn) + btnPrintAll = QPushButton('Print All Fragments') + self.connect(btnPrintAll, SIGNAL(CLICKED), self.clickPrintAll) + leftFrame = makeVertFrame([STRETCH, \ + lblAboveM, \ + frmComboM, \ + lblAboveN, \ + frmComboN, \ + STRETCH, \ + HLINE(), \ + btnPrintAll, \ + STRETCH], STYLE_STYLED) - self.setLayout(layout) + layout = QHBoxLayout() + layout.addWidget(leftFrame) - w1,h1 = relaxedSizeStr(lblUp, descrUp) - w2,h2 = relaxedSizeStr(lblDn, descrDown) - self.setMinimumWidth( 1.2*max(w1,w2) ) + for f in range(N): + layout.addWidget(self.createFragFrm(f)) + frmScroll = QFrame() + frmScroll.setFrameStyle(STYLE_SUNKEN) + frmScroll.setStyleSheet('QFrame { background-color : %s }' % \ + htmlColor('SlightBkgdDark')) + frmScroll.setLayout(layout) + self.scrollArea.setWidget(frmScroll) + BLUE = htmlColor('TextBlue') + self.lblAboveFrags.setText(tr(""" + Any %d of these + %d + fragments are sufficient to restore your wallet, and each fragment + has the ID, %s. All fragments with the + same fragment ID are compatible with each other! + Click + here to read more about our backup system.
    """) % \ + (BLUE, M, BLUE, N, BLUE, self.fragPrefixStr)) + ############################################################################# + def createFragFrm(self, idx): + doMask = self.chkSecurePrint.isChecked() + M = int(str(self.comboM.currentText())) + N = int(str(self.comboN.currentText())) + lblFragID = QRichLabel('Fragment ID:
    %s-%d
    ' % \ + (self.fragPrefixStr, idx + 1)) + # lblWltID = QRichLabel('(%s)' % self.wlt.uniqueIDB58) + lblFragPix = QImageLabel(self.fragPixmapFn, size=(72, 72)) + if doMask: + ys = self.secureMtrxCrypt[idx][1].toBinStr()[:42] + else: + ys = self.secureMtrx[idx][1].toBinStr()[:42] + easyYs1 = makeSixteenBytesEasy(ys[:16 ]) + easyYs2 = makeSixteenBytesEasy(ys[ 16:32]) -################################################################################ -# STUB STUB STUB STUB STUB -class ArmoryPref(object): - """ - Create a class that will handle arbitrary preferences for Armory. This - means that I can just create maps/lists of preferences, and auto-include - them in the preferences dialog, and know how to set/get them. This will - be subclassed for each unique/custom preference type that is needed. - """ - def __init__(self, prefName, dispStr, setType, defaultVal, validRange, descr, ttip, usermodes=None): - self.preference = prefName - self.displayStr = dispStr - self.preferType = setType - self.defaultVal = defaultVal - self.validRange = validRange - self.description = descr - self.ttip = ttip + binID = self.wlt.uniqueIDBin + ID = ComputeFragIDLineHex(M, idx, binID, doMask, addSpaces=True) - # Some options may only be displayed for certain usermodes - self.users = usermodes - if usermodes==None: - self.users = set([USERMODE.Standard, USERMODE.Advanced, USERMODE.Expert]) + fragPreview = 'ID: %s...
    ' % ID[:12] + fragPreview += 'F1: %s...
    ' % easyYs1[:12] + fragPreview += 'F2: %s... ' % easyYs2[:12] + lblPreview = QRichLabel(fragPreview) + lblPreview.setFont(GETFONT('Fixed', 9)) - if self.preferType == 'str': - self.entryObj = QLineEdit() - elif self.preferType == 'num': - self.entryObj = QLineEdit() - elif self.preferType == 'file': - self.entryObj = QLineEdit() - elif self.preferType == 'bool': - self.entryObj = QCheckBox() - elif self.preferType == 'combo': - self.entryObj = QComboBox() + lblFragIdx = QRichLabel('#%d' % (idx + 1), size=4, color='TextBlue', \ + hAlign=Qt.AlignHCenter) + frmTopLeft = makeVertFrame([lblFragID, lblFragIdx, STRETCH]) + frmTopRight = makeVertFrame([lblFragPix, STRETCH]) - def setEntryVal(self): - pass + frmPaper = makeVertFrame([lblPreview]) + frmPaper.setStyleSheet('QFrame { background-color : #ffffff }') - def readEntryVal(self): - pass + fnPrint = lambda: self.clickPrintFrag(idx) + fnSave = lambda: self.clickSaveFrag(idx) + btnPrintFrag = QPushButton('View/Print') + btnSaveFrag = QPushButton('Save to File') + self.connect(btnPrintFrag, SIGNAL(CLICKED), fnPrint) + self.connect(btnSaveFrag, SIGNAL(CLICKED), fnSave) + frmButtons = makeHorizFrame([btnPrintFrag, btnSaveFrag]) - def setWidthChars(self, nChar): - self.entryObj.setMinimumWidth( relaxedSizeNChar(self.entryObj, nChar)[0] ) - def render(self): - """ - Return a map of qt objects to insert into the frame - """ - toDraw = [] - row = 0 - if len(self.description) > 0: - toDraw.append( [QRichLabel(self.description), row, 0, 1, 4] ) - row += 1 - - - + layout = QGridLayout() + layout.addWidget(frmTopLeft, 0, 0, 1, 1) + layout.addWidget(frmTopRight, 0, 1, 1, 1) + layout.addWidget(frmPaper, 1, 0, 1, 2) + layout.addWidget(frmButtons, 2, 0, 1, 2) + layout.setSizeConstraint(QLayout.SetFixedSize) -################################################################################ -class DlgInstallLinux(ArmoryDialog): - def __init__(self, parent, main): - super(DlgInstallLinux, self).__init__(parent, main) + outFrame = QFrame() + outFrame.setFrameStyle(STYLE_STYLED) + outFrame.setLayout(layout) + return outFrame - import platform - self.distro, self.dver, self.dname = platform.linux_distribution() + ############################################################################# + def clickPrintAll(self): + self.clickPrintFrag(range(int(str(self.comboN.currentText())))) - lblOptions = QRichLabel( \ - 'If you have manually installed Bitcoin-Qt or bitcoind on this system ' - 'before, it is recommended you use the method here you previously used. ' - 'If you get errors using this option, try using the manual instructions ' - 'below.') - self.radioUbuntuPPA = QRadioButton('Install from bitcoin.org PPA (Ubuntu only)') - self.radioDlBinaries = QRadioButton('Download and unpack binaries (All Linux)') - btngrp = QButtonGroup(self) - btngrp.addButton(self.radioDlBinaries) - btngrp.addButton(self.radioUbuntuPPA) - btngrp.setExclusive(True) - self.connect(self.radioDlBinaries, SIGNAL('clicked()'), self.clickInstallOpt) - self.connect(self.radioUbuntuPPA, SIGNAL('clicked()'), self.clickInstallOpt) + ############################################################################# + def clickPrintFrag(self, zindex): + if not isinstance(zindex, (list, tuple)): + zindex = [zindex] + fragData = {} + fragData['M'] = int(str(self.comboM.currentText())) + fragData['N'] = int(str(self.comboN.currentText())) + fragData['FragIDStr'] = self.fragPrefixStr + fragData['FragPixmap'] = self.fragPixmapFn + fragData['Range'] = zindex + fragData['Secure'] = self.chkSecurePrint.isChecked() + dlg = DlgPrintBackup(self, self.main, self.wlt, 'Fragments', \ + self.secureMtrx, self.secureMtrxCrypt, fragData, \ + self.secureRoot, self.secureChain) + dlg.exec_() + ############################################################################# + def clickSaveFrag(self, zindex): + saveMtrx = self.secureMtrx; + doMask = False + if self.chkSecurePrint.isChecked(): + response = QMessageBox.question(self, 'Secure Backup?', tr(""" + You have selected to use SecurePrint\xe2\x84\xa2 for the printed + backups, which can also be applied to fragments saved to file. + Doing so will require you store the SecurePrint\xe2\x84\xa2 + code with the backup, but it will prevent unencrypted key data from + touching any disks.

    Do you want to encrypt the fragment + file with the same SecurePrint\xe2\x84\xa2 code?"""), \ + QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) - ########################################################################## - # Install via PPA - lblAutoPPATitle = QRichLabel('Install PPA for me (Ubuntu only):') - lblAutoPPA = QRichLabel( \ - 'Have Armory install the PPA for you. The does not work on all ' - 'systems, so try the manual instructions below, if it fails. ' - 'Using the PPA will install the Bitcoin software using your ' - 'system\'s package manager, and you will be notified of updates along with ' - 'other software on your system.') - self.btnAutoPPA = QPushButton('Install Bitcoin PPA') - self.connect(self.btnAutoPPA, SIGNAL('clicked()'), self.doPPA) - self.btnAutoPPA.setToolTip( \ - 'Click to install the Bitcoin PPA for Ubuntu') + if response == QMessageBox.Yes: + saveMtrx = self.secureMtrxCrypt; + doMask = True + elif response == QMessageBox.No: + pass + else: + return - frmDoItForMeBtn = makeHorizFrame(['Stretch', \ - self.btnAutoPPA, \ - 'Stretch']) - lblInstallPPATitle = QRichLabel( 'Manual PPA Installation:', doWrap=False) - lblInstallPPA = QRichLabel( \ - 'Open a terminal window and copy the following three commands ' - 'one-by-one, pressing [ENTER] after each one. You can open a terminal by hitting ' - 'Alt-F2 and typing "terminal" (without quotes), or in ' - 'the "Applications" menu under "Accessories".' ) - - lblInstallPPACmds = QRichLabel( \ - 'sudo add-apt-repository ppa:bitcoin/bitcoin' - '
    ' - 'sudo apt-get update' - '
    ' - 'sudo apt-get install bitcoin-qt bitcoind') - lblInstallPPACmds.setFont(GETFONT('Courier',10)) - lblInstallPPACmds.setTextInteractionFlags(Qt.TextSelectableByMouse | \ - Qt.TextSelectableByKeyboard) + wid = self.wlt.uniqueIDB58 + pref = self.fragPrefixStr + fnum = zindex + 1 + M = self.M + sec = 'secure.' if doMask else '' + defaultFn = 'wallet_%s_%s_num%d_need%d.%sfrag' % (wid, pref, fnum, M, sec) + print 'FragFN:', defaultFn + savepath = self.main.getFileSave(tr('Save Fragment'), \ + [tr('Wallet Fragments (*.frag)')], \ + defaultFn) - - frmCmds = makeHorizFrame([lblInstallPPACmds], STYLE_SUNKEN) - self.frmPPA = makeVertFrame([ \ - lblAutoPPATitle, \ - lblAutoPPA, \ - frmDoItForMeBtn, \ - HLINE(), \ - lblInstallPPATitle, \ - lblInstallPPA, \ - frmCmds], STYLE_SUNKEN) - # Install via PPA - ########################################################################## + if len(toUnicode(savepath)) == 0: + return - ########################################################################## - # Install via Manual Download - lblManualExperiment = QRichLabel( \ - 'Download and set it up for me! (All Linux):' - '

    ' - 'Armory will download and verify the binaries from www.bitcoin.org. ' - 'Your Armory settings will automatically be adjusted to point to that ' - 'as the installation directory.') - btnManualExperiment = QPushButton('Install for me!') - self.connect(btnManualExperiment, SIGNAL('clicked()'), self.tryManualInstall) - self.chkCustomDLPath = QCheckBox('Select custom download location') + fout = open(savepath, 'w') + fout.write('Wallet ID: %s\n' % wid) + fout.write('Create Date: %s\n' % unixTimeToFormatStr(RightNow())) + fout.write('Fragment ID: %s-#%d\n' % (pref, fnum)) + fout.write('Frag Needed: %d\n' % M) + fout.write('\n\n') - lblInstallManualDescr = QRichLabel( \ - 'Manual download and install of the Bitcoin software:
    ' - '
      ' - '
    1. Go to ' - 'http://www.bitcoin.org/en/download
    2. ' - '
    3. Click on the link that says "Download for Linux (tgz, 32/64-bit)"
    4. ' - '
    5. Open a file browser and navigate to the download directory
    6. ' - '
    7. Right-click on the downloaded file, and select "Extract Here"
    8. ' - '
    ' - '
    ' - 'Once the downloaded archive is unpacked, then click the button below ' - 'to open the Armory settings and change the "Bitcoin Installation Path" ' - 'to point to the new directory. Then restart Armory') - lblInstallManualDescr.setOpenExternalLinks(True) + try: + yBin = saveMtrx[zindex][1].toBinStr() + binID = self.wlt.uniqueIDBin + IDLine = ComputeFragIDLineHex(M, zindex, binID, doMask, addSpaces=True) + if len(yBin) == 32: + fout.write('ID: ' + IDLine + '\n') + fout.write('F1: ' + makeSixteenBytesEasy(yBin[:16 ]) + '\n') + fout.write('F2: ' + makeSixteenBytesEasy(yBin[ 16:]) + '\n') + elif len(yBin) == 64: + fout.write('ID: ' + IDLine + '\n') + fout.write('F1: ' + makeSixteenBytesEasy(yBin[:16 ]) + '\n') + fout.write('F2: ' + makeSixteenBytesEasy(yBin[ 16:32 ]) + '\n') + fout.write('F3: ' + makeSixteenBytesEasy(yBin[ 32:48 ]) + '\n') + fout.write('F4: ' + makeSixteenBytesEasy(yBin[ 48:]) + '\n') + else: + LOGERROR('yBin is not 32 or 64 bytes! It is %s bytes', len(yBin)) + finally: + yBin = None + fout.close() - btnInstallSettings = QPushButton('Change Settings') - self.connect(btnInstallSettings, SIGNAL('clicked()'), self.main.openSettings) - frmChngSettings = makeHorizFrame([ - 'Stretch', \ - btnInstallSettings, \ - 'Stretch'], \ - STYLE_SUNKEN) + qmsg = tr(""" + The fragment was successfully saved to the following location: +

    %s

    """) % savepath - btnAndChk = makeHorizFrame([btnManualExperiment, self.chkCustomDLPath]) - frmManualExper = makeHorizFrame(['Stretch',btnAndChk,'Stretch']) - self.frmManual = makeVertFrame([ \ - lblManualExperiment, \ - frmManualExper, \ - HLINE(), \ - lblInstallManualDescr, \ - frmChngSettings, \ - 'Stretch']) - - - # Install via Manual Download - ########################################################################## + if doMask: + qmsg += tr(""" + Important: + The fragment was encrypted with the + SecurePrint\xe2\x84\xa2 encryption code. You must keep this + code with the backup in order to use it! The code is + case-sensitive! +

    %s""") % \ + (htmlColor('TextWarn'), htmlColor('TextBlue'), self.randpass.toBinStr()) - self.stkInstruct = QStackedWidget() - self.stkInstruct.addWidget(self.frmPPA) - self.stkInstruct.addWidget(self.frmManual) + QMessageBox.information(self, 'Success', qmsg, QMessageBox.Ok) - btnOkay = QPushButton("OK") - self.connect(btnOkay, SIGNAL('clicked()'), self.accept) - - layout = QVBoxLayout() - layout.addWidget(lblOptions) - layout.addWidget(self.radioUbuntuPPA) - layout.addWidget(self.radioDlBinaries) - layout.addWidget(HLINE()) - layout.addWidget(self.stkInstruct) - layout.addWidget(makeHorizFrame(['Stretch',btnOkay])) - self.setLayout(layout) - self.setMinimumWidth(600) - - self.radioUbuntuPPA.setChecked(True) - self.clickInstallOpt() - self.setWindowTitle('Install Bitcoin in Linux') - from twisted.internet import reactor - reactor.callLater(0.2, self.main.checkForLatestVersion) ############################################################################# - def tryManualInstall(self): - dlDict = self.main.downloadDict.copy() - if not 'SATOSHI' in dlDict or not 'Linux' in dlDict['SATOSHI']: - QMessageBox.warning(self, 'Not available', \ - 'Armory does not actually have the information needed to execute ' - 'this process securely. Please visit the bitcoin.org and download ' - 'the Linux version of the Bitcoin software, then modify your ' - 'settings to point to where it was unpacked. ', QMessageBox.Ok) + def destroyFrags(self): + if len(self.secureMtrx) == 0: return - - if not self.chkCustomDLPath.isChecked(): - installPath = os.path.join(ARMORY_HOME_DIR, 'downloaded') - if not os.path.exists(installPath): - os.makedirs(installPath) + + if isinstance(self.secureMtrx[0], (list, tuple)): + for sbdList in self.secureMtrx: + for sbd in sbdList: + sbd.destroy() + for sbdList in self.secureMtrxCrypt: + for sbd in sbdList: + sbd.destroy() else: - title = 'Download Bitcoin software to...' - initPath = self.main.settings.get('LastDirectory') - if not OS_MACOSX: - installPath = unicode(QFileDialog.getExistingDirectory(self, title, initPath)) - else: - installPath = unicode(QFileDialog.getExistingDirectory(self, title, initPath, \ - options=QFileDialog.DontUseNativeDialog)) + for sbd in self.secureMtrx: + sbd.destroy() + for sbd in self.secureMtrxCrypt: + sbd.destroy() - if not os.path.exists(installPath): - if len(installPath.strip()) > 0: - QMessageBox.warning(self, 'Invalid Directory', \ - 'The directory you chose does not exist. How did you do that?', \ - QMessageBox.Ok) - return + self.secureMtrx = [] + self.secureMtrxCrypt = [] - print dlDict['SATOSHI']['Linux'] - theLink = dlDict['SATOSHI']['Linux'][0] - theHash = dlDict['SATOSHI']['Linux'][1] - dlg = DlgDownloadFile(self, self.main, theLink, theHash) - dlg.exec_() - fileData = dlg.dlFileData - if len(fileData)==0 or dlg.dlVerifyFailed: - QMessageBox.critical(self, 'Download Failed', \ - 'The download failed. Please visit www.bitcoin.org ' - 'to download and install Bitcoin-Qt manually.', QMessageBox.Ok) - import webbrowser - webbrowser.open('http://www.bitcoin.org/en/download') - return - - fullPath = os.path.join(installPath, dlg.dlFileName) - LOGINFO('Installer path: %s', fullPath) - instFile = open(fullPath, 'wb') - instFile.write(fileData) - instFile.close() - newDir = fullPath[:-7] - if os.path.exists(newDir): - shutil.rmtree(newDir) - os.makedirs(newDir) - launchProcess(['tar', '-zxf', fullPath, '-C', installPath]) - self.main.writeSetting('SatoshiExe', newDir) + ############################################################################# + def destroyEverything(self): + self.secureRoot.destroy() + self.secureChain.destroy() + self.securePrint.destroy() + self.destroyFrags() - QMessageBox.information(self, 'Succeeded', \ - 'The download succeeded!', QMessageBox.Ok) - from twisted.internet import reactor - reactor.callLater(0.5, self.main.pressModeSwitchButton) - self.accept() - - + ############################################################################# + def recomputeFragData(self): + """ + Only M is needed, since N doesn't change + """ + + M = int(str(self.comboM.currentText())) + N = int(str(self.comboN.currentText())) + # Make sure only local variables contain non-SBD data + self.destroyFrags() + insecureData = SplitSecret(self.securePrint, M, self.maxmaxN) + for x, y in insecureData: + self.secureMtrx.append([SecureBinaryData(x), SecureBinaryData(y)]) + insecureData, x, y = None, None, None + + ##### + # Now we compute the SecurePrint(TM) versions of the fragments + SECPRINT = HardcodedKeyMaskParams() + MASK = lambda x: SECPRINT['FUNC_MASK'](x, ekey=self.binCrypt32) + if not self.randpass or not self.binCrypt32: + self.randpass = SECPRINT['FUNC_PWD'](self.secureRoot + self.secureChain) + self.binCrypt32 = SECPRINT['FUNC_KDF'](self.randpass) + self.secureMtrxCrypt = [] + for sbdX, sbdY in self.secureMtrx: + self.secureMtrxCrypt.append([sbdX.copy(), MASK(sbdY)]) + ##### - + self.M, self.N = M, N + self.fragPrefixStr = ComputeFragIDBase58(self.M, self.wlt.uniqueIDBin) + self.fragPixmapFn = ':/frag%df.png' % M - ############################################################################# - def clickInstallOpt(self): - if self.radioUbuntuPPA.isChecked(): - self.stkInstruct.setCurrentIndex(0) - elif self.radioDlBinaries.isChecked(): - self.stkInstruct.setCurrentIndex(1) - else: - LOGERROR('How is neither instruction option checked!?') ############################################################################# - def loadGpgKeyring(self): - pubDirLocal = os.path.join(ARMORY_HOME_DIR, 'tempKeyring') - #if os.path.exists(pubDirLocal): - - pubDirInst = os.path.join(GetExecDir(), 'PublicKeys') + def accept(self): + self.destroyEverything() + super(DlgFragBackup, self).accept() - gpgCmdList = ['gpg'] - cmdImportKey = ('gpg ' - '--keyring ~/.armory/testkeyring.gpg ' - '--no-default-keyring ' - '--import %s/AndresenCodeSign.asc') - cmdVerifyFile= ('gpg ' - '--keyring ~/.armory/testkeyring.gpg ' - '--verify bitcoin.0.8.1.tar.gz') + ############################################################################# + def reject(self): + self.destroyEverything() + super(DlgFragBackup, self).reject() - ############################################################################# - def doPPA(self): - out,err = execAndWait('gksudo install_bitcoinqt', timeout=20) - tryInstallLinux(self.main) - self.main.settings.delete('SatoshiExe') - self.accept() ################################################################################ -def tryInstallLinux(main): - def doit(): - print '\n' - print '***** Executing auto-install in linux...' - out,err = execAndWait('gksudo "apt-get remove -y bitcoin-qt bitcoind"', \ - timeout=20) - out,err = execAndWait(('gksudo apt-add-repository ppa:bitcoin/bitcoin; ' - 'gksudo apt-get update; ' - 'gksudo "apt-get install -y bitcoin-qt bitcoind"'), \ - timeout=120) - try: - TheSDM.setupSDM() - from twisted.internet import reactor - reactor.callLater(0.1, main.pressModeSwitchButton) - QMessageBox.information(main, 'Success!', \ - 'The installation appears to have succeeded!') - except: - LOGINFO('***** Printing output\n' + out) - LOGINFO('***** End print output\n') - LOGINFO('***** Printing errors\n' + err) - LOGINFO('***** End print errors\n') - QMessageBox.warning(main, 'Unknown Error', \ - 'An error was reported while trying to install the Bitcoin ' - 'software. The following information is given:

    %s' % err, \ - QMessageBox.Ok) - raise - - DlgExecLongProcess(doit, 'Installing Bitcoin Software...', main, main).exec_() +class DlgUniversalRestoreSelect(ArmoryDialog): + ############################################################################# + def __init__(self, parent, main): + super(DlgUniversalRestoreSelect, self).__init__(parent, main) -################################################################################ -class DlgInstallWindows(ArmoryDialog): - def __init__(self, parent, main, dataToQR, descrUp='', descrDown=''): - super(DlgInstallWindows, self).__init__(parent, main) + lblDescrTitle = QRichLabel(tr(""" + Restore Wallet from Backup""")) + lblDescr = QRichLabel(tr("""You can restore any kind of backup ever created by Armory using + one of the options below. If you have a list of private keys + you should open the target wallet and select "Import/Sweep + Private Keys." """)) -################################################################################ -class DlgDownloadFile(ArmoryDialog): - def __init__(self, parent, main, dlfile, expectHash=None, msg=''): - super(DlgDownloadFile, self).__init__(parent, main) + lblRestore = QRichLabel(tr("""I am restoring a...""")) + self.rdoSingle = QRadioButton(tr('Single-Sheet Backup (printed)')) + self.rdoFragged = QRadioButton(tr('Fragmented Backup (incl. mix of paper and files)')) + self.rdoDigital = QRadioButton(tr('Import digital backup or watching-only wallet')) + self.chkTest = QCheckBox(tr('This is a test recovery to make sure my backup works')) + btngrp = QButtonGroup(self) + btngrp.addButton(self.rdoSingle) + btngrp.addButton(self.rdoFragged) + btngrp.addButton(self.rdoDigital) + btngrp.setExclusive(True) + self.rdoSingle.setChecked(True) + self.connect(self.rdoSingle, SIGNAL(CLICKED), self.clickedRadio) + self.connect(self.rdoFragged, SIGNAL(CLICKED), self.clickedRadio) + self.connect(self.rdoDigital, SIGNAL(CLICKED), self.clickedRadio) - self.dlFullPath = dlfile - self.dlFileName = os.path.basename(self.dlFullPath) - self.dlSiteName = '/'.join(self.dlFullPath.split('/')[:3]) - self.dlFileSize = 0 - self.dlFileData = '' - self.dlDownBytes = 0 - self.dlExpectHash = expectHash - self.dlStartTime = RightNow() - self.dlVerifyFailed = False - + self.btnOkay = QPushButton('Continue') + self.btnCancel = QPushButton('Cancel') + buttonBox = QDialogButtonBox() + buttonBox.addButton(self.btnOkay, QDialogButtonBox.AcceptRole) + buttonBox.addButton(self.btnCancel, QDialogButtonBox.RejectRole) + self.connect(self.btnOkay, SIGNAL(CLICKED), self.clickedOkay) + self.connect(self.btnCancel, SIGNAL(CLICKED), self.reject) - - self.StopDownloadFlag = False - self.lblDownloaded = QRichLabel('') - self.barWorking = QProgressBar() - self.barWorking.setRange(0,100) - self.barWorking.setValue(0) - self.barWorking.setFormat('') - - - lblDescr = QRichLabel( \ - 'Please wait while file is downloading' - '' % htmlColor('TextBlue'), hAlign=Qt.AlignHCenter) - frmDescr = makeHorizFrame([lblDescr], STYLE_RAISED) - + layout = QVBoxLayout() + layout.addWidget(lblDescrTitle) + layout.addWidget(lblDescr) + layout.addWidget(HLINE()) + layout.addWidget(self.rdoSingle) + layout.addWidget(self.rdoFragged) + layout.addWidget(self.rdoDigital) + layout.addWidget(HLINE()) + layout.addWidget(self.chkTest) + layout.addWidget(buttonBox) + self.setLayout(layout) + self.setMinimumWidth(450) - frmInfo = QFrame() - layoutFileInfo = QGridLayout() - layoutFileInfo.addWidget(QRichLabel('File name:', bold=True), 0,0) - layoutFileInfo.addWidget(QRichLabel(self.dlFileName), 0,2) - layoutFileInfo.addWidget(QRichLabel('From site:', bold=True), 1,0) - layoutFileInfo.addWidget(QRichLabel(self.dlSiteName), 1,2) - layoutFileInfo.addWidget(QRichLabel('Progress:', bold=True), 2,0) - layoutFileInfo.addWidget(self.lblDownloaded, 2,2) - layoutFileInfo.addItem(QSpacerItem(30, 1, QSizePolicy.Fixed, QSizePolicy.Expanding), 0, 1, 3, 1) - layoutFileInfo.setColumnStretch(0,0) - layoutFileInfo.setColumnStretch(1,0) - layoutFileInfo.setColumnStretch(2,1) - frmInfo.setLayout(layoutFileInfo) + def clickedRadio(self): + if self.rdoDigital.isChecked(): + self.chkTest.setChecked(False) + self.chkTest.setEnabled(False) + else: + self.chkTest.setEnabled(True) - - self.STEPS = enum('Query','Download','Verify','Count') - self.dispSteps = ['Getting file information', \ - 'Downloading', \ - 'Verifying signatures'] - self.lblSteps = [] - for i in range(self.STEPS.Count): - self.lblSteps.append([QRichLabel('',doWrap=False), QRichLabel('')]) + def clickedOkay(self): + # ## Test backup option - layoutSteps = QGridLayout() - for i in range(self.STEPS.Count): - layoutSteps.addWidget(self.lblSteps[i][0], i,0) - layoutSteps.addWidget(self.lblSteps[i][1], i,1) - frmSteps = QFrame() - frmSteps.setLayout(layoutSteps) - frmSteps.setFrameStyle(STYLE_SUNKEN) - self.dlInstallStatus = self.STEPS.Query - self.updateProgressLabels() - + doTest = self.chkTest.isChecked() - lblExtraMsg = QRichLabel( msg ) - + if self.rdoSingle.isChecked(): + self.accept() + dlg = DlgRestoreSingle(self.parent, self.main, doTest) + if dlg.exec_(): + self.main.addWalletToAppAndAskAboutRescan(dlg.newWallet) + LOGINFO('Wallet Restore Complete!') + # self.main.startRescanBlockchain() + # TheBDM.rescanBlockchain('AsNeeded', wait=False) - btnCancel = QPushButton("Cancel") - self.connect(btnCancel, SIGNAL('clicked()'), self.reject) - frmCancel = makeHorizFrame(['Stretch',btnCancel,'Stretch']) + elif self.rdoFragged.isChecked(): + self.accept() + dlg = DlgRestoreFragged(self.parent, self.main, doTest) + if dlg.exec_(): + self.main.addWalletToAppAndAskAboutRescan(dlg.newWallet) + LOGINFO('Wallet Restore Complete!') + # TheBDM.main.startRescanBlockchain() + elif self.rdoDigital.isChecked(): + self.main.execGetImportWltName() + self.accept() - frm = makeVertFrame([frmDescr, \ - frmInfo, \ - self.barWorking, \ - frmSteps, \ - lblExtraMsg, \ - frmCancel]) - layout = QVBoxLayout() - layout.addWidget(frm) - self.setLayout(layout) - self.setMinimumWidth(400) - - - def startBackgroundDownload(dlg): - thr = PyBackgroundThread(dlg.startDL) - thr.start() - print 'Starting download in 1s...' - from twisted.internet import reactor - reactor.callLater(1, startBackgroundDownload, self) - self.main.extraHeartbeatSpecial.append(self.checkDownloadProgress) - self.setWindowTitle('Downloading File...') - - - def reject(self): - self.StopDownloadFlag = True - self.dlFileData = '' - super(DlgDownloadFile, self).reject() - - def accept(self): - self.StopDownloadFlag = True - super(DlgDownloadFile, self).accept() - - def startDL(self): - self.dlInstallStatus = self.STEPS.Query - keepTrying = True - nTries=0 - self.httpObj = None - while keepTrying: - nTries+=1 - try: - import urllib2 - self.httpObj = urllib2.urlopen(self.dlFullPath, timeout=10) - break - except urllib2.HTTPError: - LOGERROR('urllib2 failed to urlopen the download link') - LOGERROR('Link: %s', self.dlFullPath) - break - except socket.timeout: - LOGERROR('timed out once') - if nTries > 2: - keepTrying=False - except: - print sys.exc_info() - break +################################################################################ +# Create a special QLineEdit with a masked input +# Forces the cursor to start at position 0 whenever there is no input +class MaskedInputLineEdit(QLineEdit): - if self.httpObj==None: - self.StopDownloadFlag = True - return + def __init__(self, inputMask): + super(MaskedInputLineEdit, self).__init__() + self.setInputMask(inputMask) + fixFont = GETFONT('Fix', 9) + self.setFont(fixFont) + self.setMinimumWidth(tightSizeStr(fixFont, inputMask)[0] + 10) + self.connect(self, SIGNAL('cursorPositionChanged(int,int)'), self.controlCursor) - self.dlFileSize = 0 - for line in self.httpObj.info().headers: - if line.startswith('Content-Length'): - try: - self.dlFileSize = int(line.split()[-1]) - except: - raise - + def controlCursor(self, oldpos, newpos): + if newpos != 0 and len(str(self.text()).strip()) == 0: + self.setCursorPosition(0) - LOGINFO('Starting download') - self.dlInstallStatus = self.STEPS.Download - bufSize = 32768 - bufferData = 1 - while bufferData: - if self.StopDownloadFlag: - return - bufferData = self.httpObj.read(bufSize) - self.dlFileData += bufferData - self.dlDownBytes += bufSize - self.dlInstallStatus = self.STEPS.Verify - hexHash = binary_to_hex(sha256(self.dlFileData)) - LOGINFO('Hash of downloaded file: ') - LOGINFO(hexHash) - if self.dlExpectHash: - if not self.dlExpectHash==hexHash: - LOGERROR('Downloaded file does not authenticate!') - LOGERROR('Aborting download') - self.dlFileData = '' - self.dlVerifyFailed = True - else: - LOGINFO('Downloaded file is cryptographically verified!') - self.dlVerifyFailed = False +################################################################################ +class DlgRestoreSingle(ArmoryDialog): + ############################################################################# + def __init__(self, parent, main, thisIsATest=False, expectWltID=None): + super(DlgRestoreSingle, self).__init__(parent, main) + + self.thisIsATest = thisIsATest + self.testWltID = expectWltID + headerStr = '' + if thisIsATest: + lblDescr = QRichLabel(tr(""" + Test a Paper Backup +

    + Use this window to test a single-sheet paper backup. If your + backup includes imported keys, those will not be covered by this test. """)) + else: + lblDescr = QRichLabel(tr(""" + Restore a Wallet from Paper Backup +

    + Use this window to restore a single-sheet paper backup. + If your backup includes extra pages with + imported keys, please restore the base wallet first, then + double-click the restored wallet and select "Import Private + Keys" from the right-hand menu. """)) + + + lblType = QRichLabel(tr("""Backup Type:"""), doWrap=False) + + self.version135Button = QRadioButton(tr('Version 1.35 (4 lines)'), self) + self.version135aButton = QRadioButton(tr('Version 1.35a (4 lines Unencrypted)'), self) + self.version135aSPButton = QRadioButton(tr('Version 1.35a (4 lines + SecurePrint\xe2\x84\xa2)'), self) + self.version135cButton = QRadioButton(tr('Version 1.35c (2 lines Unencrypted)'), self) + self.version135cSPButton = QRadioButton(tr('Version 1.35c (2 lines + SecurePrint\xe2\x84\xa2)'), self) + self.backupTypeButtonGroup = QButtonGroup(self) + self.backupTypeButtonGroup.addButton(self.version135Button) + self.backupTypeButtonGroup.addButton(self.version135aButton) + self.backupTypeButtonGroup.addButton(self.version135aSPButton) + self.backupTypeButtonGroup.addButton(self.version135cButton) + self.backupTypeButtonGroup.addButton(self.version135cSPButton) + self.version135cButton.setChecked(True) + self.connect(self.backupTypeButtonGroup, SIGNAL('buttonClicked(int)'), self.changeType) + + layoutRadio = QVBoxLayout() + layoutRadio.addWidget(self.version135Button) + layoutRadio.addWidget(self.version135aButton) + layoutRadio.addWidget(self.version135aSPButton) + layoutRadio.addWidget(self.version135cButton) + layoutRadio.addWidget(self.version135cSPButton) + layoutRadio.setSpacing(0) + + radioButtonFrame = QFrame() + radioButtonFrame.setLayout(layoutRadio) + + frmBackupType = makeVertFrame([lblType, radioButtonFrame]) - self.dlInstallStatus = self.STEPS.Count # one past end - - + self.lblSP = QRichLabel(tr('SecurePrint\xe2\x84\xa2 Code:'), doWrap=False) + self.editSecurePrint = QLineEdit() + self.prfxList = [QLabel(tr('Root Key:')), QLabel(''), QLabel(tr('Chaincode:')), QLabel('')] - def checkDownloadProgress(self): - if self.StopDownloadFlag: - from twisted.internet import reactor - reactor.callLater(1, self.reject) - return -1 + inpMask = '= self.STEPS.Download: - self.barWorking.setVisible(True) + frmAllInputs = QFrame() + frmAllInputs.setFrameStyle(STYLE_RAISED) + layoutAllInp = QGridLayout() + layoutAllInp.addWidget(self.frmSP, 0, 0, 1, 2) + for i in range(4): + layoutAllInp.addWidget(self.prfxList[i], i + 1, 0) + layoutAllInp.addWidget(self.edtList[i], i + 1, 1) + frmAllInputs.setLayout(layoutAllInp) - trueRatio = float(self.dlDownBytes)/float(self.dlFileSize) - dispRatio = min(trueRatio, 1) - if self.dlFileSize>0: - self.barWorking.setValue(100*dispRatio) - self.barWorking.setFormat('%p%') + doItText = tr('Test Backup' if thisIsATest else 'Restore Wallet') - dlSizeHuman = bytesToHumanSize(self.dlDownBytes) - totalSizeHuman = bytesToHumanSize(self.dlFileSize) - self.lblDownloaded.setText('%s of %s' % (dlSizeHuman, totalSizeHuman)) + self.btnAccept = QPushButton(doItText) + self.btnCancel = QPushButton("Cancel") + self.connect(self.btnAccept, SIGNAL(CLICKED), self.verifyUserInput) + self.connect(self.btnCancel, SIGNAL(CLICKED), self.reject) + buttonBox = QDialogButtonBox() + buttonBox.addButton(self.btnAccept, QDialogButtonBox.AcceptRole) + buttonBox.addButton(self.btnCancel, QDialogButtonBox.RejectRole) - - if self.dlInstallStatus > self.STEPS.Verify: - from twisted.internet import reactor - reactor.callLater(2, self.accept) - return -1 - else: - return 0.1 - except: - LOGEXCEPT("Failed to check download progress") - return -1 - + self.chkEncrypt = QCheckBox('Encrypt Wallet') + self.chkEncrypt.setChecked(True) + bottomFrm = makeHorizFrame([self.chkEncrypt, buttonBox]) - def updateProgressLabels(self): - # Highlight the correct labels and show checkmarks - for i in range(self.STEPS.Count): - if i == self.dlInstallStatus: - self.lblSteps[i][0].setText(self.dispSteps[i], bold=True, color='Foreground') - self.lblSteps[i][1].setText('...', bold=True) - elif i < self.dlInstallStatus: - self.lblSteps[i][0].setText(self.dispSteps[i], color='Foreground') - self.lblSteps[i][1].setPixmap(QPixmap(':/checkmark32.png').scaled(20,20)) - else: - self.lblSteps[i][0].setText(self.dispSteps[i], \ - bold=False, color='DisableFG') + walletRestoreTabs = QTabWidget() + backupTypeFrame = makeVertFrame([frmBackupType, frmAllInputs]) + walletRestoreTabs.addTab(backupTypeFrame, "Backup") + self.advancedOptionsTab = AdvancedOptionsFrame(parent, main) + walletRestoreTabs.addTab(self.advancedOptionsTab, "Advanced Options") - if self.dlInstallStatus >= self.STEPS.Verify: - self.barWorking.setValue(100) - if self.dlVerifyFailed: - self.lblSteps[self.STEPS.Verify][1].setPixmap(QPixmap(':/MsgBox_error32.png').scaled(20,20)) - + layout = QVBoxLayout() + layout.addWidget(lblDescr) + layout.addWidget(HLINE()) + layout.addWidget(walletRestoreTabs) + layout.addWidget(bottomFrm) + self.setLayout(layout) - - - -################################################################################ -class QRadioButtonBackupCtr(QRadioButton): - def __init__(self, parent, txt, index): - super(QRadioButtonBackupCtr, self).__init__(txt) - self.parent = parent - self.index = index + self.chkEncrypt.setChecked(not thisIsATest) + self.chkEncrypt.setVisible(not thisIsATest) + self.advancedOptionsTab.setEnabled(not thisIsATest) + if thisIsATest: + self.setWindowTitle('Test Single-Sheet Backup') + else: + self.setWindowTitle('Restore Single-Sheet Backup') + self.connect(self.chkEncrypt, SIGNAL(CLICKED), self.onEncryptCheckboxChange) - def enterEvent(self, ev): - pass - #self.parent.setDispFrame(self.index) - #self.setStyleSheet('QRadioButton { background-color : %s }' % \ - #htmlColor('SlightBkgdDark')) + self.setMinimumWidth(500) + self.layout().setSizeConstraint(QLayout.SetFixedSize) + self.changeType(self.backupTypeButtonGroup.checkedId()) - def leaveEvent(self, ev): - pass - #self.parent.setDispFrame(-1) - #self.setStyleSheet('QRadioButton { background-color : %s }' % \ - #htmlColor('Background')) + ############################################################################# + # Hide advanced options whenver the restored wallet is unencrypted + def onEncryptCheckboxChange(self): + self.advancedOptionsTab.setEnabled(self.chkEncrypt.isChecked()) + ############################################################################# + def changeType(self, sel): + if sel == self.backupTypeButtonGroup.id(self.version135Button): + visList = [0, 1, 1, 1, 1] + elif sel == self.backupTypeButtonGroup.id(self.version135aButton): + visList = [0, 1, 1, 1, 1] + elif sel == self.backupTypeButtonGroup.id(self.version135aSPButton): + visList = [1, 1, 1, 1, 1] + elif sel == self.backupTypeButtonGroup.id(self.version135cButton): + visList = [0, 1, 1, 0, 0] + elif sel == self.backupTypeButtonGroup.id(self.version135cSPButton): + visList = [1, 1, 1, 0, 0] + else: + LOGERROR('What the heck backup type is selected? %d', sel) + return -################################################################################ -class DlgBackupCenter(ArmoryDialog): - """ - Some static enums, and a QRadioButton with mouse-enter/mouse-leave events - """ - FEATURES = enum('ProtGen','ProtImport','LostPass','Durable', \ - 'Visual','Physical','Count') - OPTIONS = enum('Paper1','PaperN','DigPlain','DigCrypt','Export', 'Count') + self.doMask = (visList[0] == 1) + self.frmSP.setVisible(self.doMask) + for i in range(4): + self.prfxList[i].setVisible(visList[i + 1] == 1) + self.edtList[ i].setVisible(visList[i + 1] == 1) + self.isLongForm = (visList[-1] == 1) ############################################################################# - def __init__(self, parent, main, wlt): - super(DlgBackupCenter, self).__init__(parent, main) + def verifyUserInput(self): + inputLines = [] + nError = 0 + rawBin = None + nLine = 4 if self.isLongForm else 2 + for i in range(nLine): + hasError = False + try: + rawEntry = str(self.edtList[i].text()) + rawBin, err = readSixteenEasyBytes(rawEntry.replace(' ', '')) + if err == 'Error_2+': + hasError = True + elif err == 'Fixed_1': + nError += 1 + except: + hasError = True - self.wlt = wlt - wltID = wlt.uniqueIDB58 - wltName = wlt.labelName + if hasError: + lineNumber = i+1 + reply = QMessageBox.critical(self, tr('Invalid Data'), tr(""" + There is an error in the data you entered that could not be + fixed automatically. Please double-check that you entered the + text exactly as it appears on the wallet-backup page.

    + The error occured on line #%d.""") % lineNumber, \ + QMessageBox.Ok) + LOGERROR('Error in wallet restore field') + self.prfxList[i].setText('' + str(self.prfxList[i].text()) + '') + return - self.hasImportedAddr = self.wlt.hasAnyImported() + inputLines.append(rawBin) - lblTitle = QRichLabel( tr(""" - Backup Options for Wallet "%s" (%s)""" % (wltName, wltID))) - - lblTitleDescr = QRichLabel( tr(""" - Armory wallets only need to be backed up one time, ever. - The backup is good no matter how many addresses you use. """)) - lblTitleDescr.setOpenExternalLinks(True) - - - self.optPaperBackupTop = QRadioButtonBackupCtr(self, \ - tr('Printable Paper Backup'), self.OPTIONS.Paper1) - self.optPaperBackupOne = QRadioButtonBackupCtr(self, \ - tr('Single-Sheet (Recommended)'), self.OPTIONS.Paper1) - self.optPaperBackupFrag = QRadioButtonBackupCtr(self, \ - tr('Fragmented Backup\xe2\x84\xa2 (M-of-N)'), self.OPTIONS.PaperN) - - self.optDigitalBackupTop = QRadioButtonBackupCtr(self, \ - tr('Digital Backup'), self.OPTIONS.DigPlain) - self.optDigitalBackupPlain = QRadioButtonBackupCtr(self, \ - tr('Unencrypted'), self.OPTIONS.DigPlain) - self.optDigitalBackupCrypt = QRadioButtonBackupCtr(self, \ - tr('Encrypted'), self.OPTIONS.DigCrypt) + if self.chkEncrypt.isChecked() and self.advancedOptionsTab.getKdfSec() == -1: + QMessageBox.critical(self, 'Invalid Target Compute Time', \ + 'You entered Target Compute Time incorrectly.\n\nEnter: (ms, s)', QMessageBox.Ok) + return + if self.chkEncrypt.isChecked() and self.advancedOptionsTab.getKdfBytes() == -1: + QMessageBox.critical(self, 'Invalid Max Memory Usage', \ + 'You entered Max Memory Usage incorrectly.\n\nnter: (kb, mb)', QMessageBox.Ok) + return + if nError > 0: + pluralStr = 'error' if nError == 1 else 'errors' + QMessageBox.question(self, tr('Errors Corrected'), tr(""" + Detected %d @{error|errors}@ in the data you entered. + Armory attempted to fix the @{error|errors}@ but it is not + always right. Be sure to verify the "Wallet Unique ID" + closely on the next window.""", nError, nError), \ + QMessageBox.Ok) - self.optIndivKeyListTop = QRadioButtonBackupCtr(self, \ - tr('Export Key Lists'), self.OPTIONS.Export) + privKey = SecureBinaryData(''.join(inputLines[:2])) + if self.isLongForm: + chain = SecureBinaryData(''.join(inputLines[2:])) - - self.optPaperBackupTop.setFont(GETFONT('Var', bold=True)) - self.optDigitalBackupTop.setFont(GETFONT('Var', bold=True)) - self.optIndivKeyListTop.setFont(GETFONT('Var', bold=True)) - # I need to be able to unset the sub-options when they become disabled - self.optPaperBackupNONE = QRadioButton('') - self.optDigitalBackupNONE = QRadioButton('') - - btngrpTop = QButtonGroup(self) - btngrpTop.addButton(self.optPaperBackupTop) - btngrpTop.addButton(self.optDigitalBackupTop) - btngrpTop.addButton(self.optIndivKeyListTop) - btngrpTop.setExclusive(True) - - btngrpPaper = QButtonGroup(self) - btngrpPaper.addButton(self.optPaperBackupNONE) - btngrpPaper.addButton(self.optPaperBackupOne) - btngrpPaper.addButton(self.optPaperBackupFrag) - btngrpPaper.setExclusive(True) - - btngrpDig = QButtonGroup(self) - btngrpDig.addButton(self.optDigitalBackupNONE) - btngrpDig.addButton(self.optDigitalBackupPlain) - btngrpDig.addButton(self.optDigitalBackupCrypt) - btngrpDig.setExclusive(True) - - self.connect(self.optPaperBackupTop, SIGNAL('clicked()'), self.optionClicked) - self.connect(self.optPaperBackupOne, SIGNAL('clicked()'), self.optionClicked) - self.connect(self.optPaperBackupFrag, SIGNAL('clicked()'), self.optionClicked) - self.connect(self.optDigitalBackupTop, SIGNAL('clicked()'), self.optionClicked) - self.connect(self.optDigitalBackupPlain, SIGNAL('clicked()'), self.optionClicked) - self.connect(self.optDigitalBackupCrypt, SIGNAL('clicked()'), self.optionClicked) - self.connect(self.optIndivKeyListTop, SIGNAL('clicked()'), self.optionClicked) - - - spacer = lambda: QSpacerItem(20,1, QSizePolicy.Fixed, QSizePolicy.Expanding) - layoutOpts = QGridLayout() - layoutOpts.addWidget( self.optPaperBackupTop, 0,0, 1,2) - layoutOpts.addItem( spacer(), 1,0) - layoutOpts.addItem( spacer(), 2,0) - layoutOpts.addWidget( self.optDigitalBackupTop, 3,0, 1,2) - layoutOpts.addItem( spacer(), 4,0) - layoutOpts.addItem( spacer(), 5,0) - layoutOpts.addWidget( self.optIndivKeyListTop, 6,0, 1,2) - - layoutOpts.addWidget( self.optPaperBackupOne, 1,1) - layoutOpts.addWidget( self.optPaperBackupFrag, 2,1) - layoutOpts.addWidget( self.optDigitalBackupPlain, 4,1) - layoutOpts.addWidget( self.optDigitalBackupCrypt, 5,1) - layoutOpts.setColumnStretch(0,0) - layoutOpts.setColumnStretch(1,1) - - frmOpts = QFrame() - frmOpts.setLayout(layoutOpts) - frmOpts.setFrameStyle(STYLE_SUNKEN) - - - self.featuresTips = [None]*self.FEATURES.Count - self.featuresLbls = [None]*self.FEATURES.Count - self.featuresImgs = [None]*self.FEATURES.Count - - F = self.FEATURES - self.featuresTips[F.ProtGen] = self.main.createToolTipWidget( tr( """ - Every time you click "Receive Bitcoins," a new address is generated. - All of these addresses are generated from a single seed value, which - is included in all backups. Therefore, all addresses that you have - generated so far and will ever generate with this wallet, are - protected by this backup! """)) - if not self.hasImportedAddr: - self.featuresTips[F.ProtImport] = self.main.createToolTipWidget(tr( """ - This wallet does not currently have any imported - addresses, so you can safely ignore this feature!. - When imported addresses are present, backups only protects those - imported before the backup was made! You must replace that - backup if you import more addresses! """)) - else: - self.featuresTips[F.ProtImport] = self.main.createToolTipWidget(tr( """ - When imported addresses are present, backups only protects those - imported before the backup was made! You must replace that - backup if you import more addresses! - Your wallet does contain imported addresses.""")) - self.featuresTips[F.LostPass] = self.main.createToolTipWidget( tr( """ - Lost/forgotten passphrases are, by far, the most common - reason for users losing bitcoins. It is critical you have - at least one backup that works if you forget your wallet - passphrase. """)) - self.featuresTips[F.Durable] = self.main.createToolTipWidget( tr( """ - USB drives and CD/DVD disks are not intended for long-term storage. - They will probably last many years, but not guaranteed - even for 3-5 years. On the other hand, printed text on paper will - last many decades, and useful even when thoroughly faded. """)) - self.featuresTips[F.Visual] = self.main.createToolTipWidget( tr( """ - The ability to look at a backup and determine if - it is still usable. If a digital backup is stored in a safe - deposit box, you have no way to verify its integrity unless - you take a secure computer/device with you. A simple glance at - a paper backup is enough to verify that it is still intact. """)) - self.featuresTips[F.Physical] = self.main.createToolTipWidget( tr( """ - If multiple pieces/fragments are required to restore this wallet. - For instance, encrypted backups require the backup - and the passphrase. This feature is only needed for those - concerned about physical security, not just online security.""")) - + if self.doMask: + # Prepare the key mask parameters + SECPRINT = HardcodedKeyMaskParams() + pwd = str(self.editSecurePrint.text()).strip() + if len(pwd) < 9: + QMessageBox.critical(self, 'Invalid Code', tr(""" + You didn't enter a full SecurePrint\xe2\x84\xa2 code. This + code is needed to decrypt your backup. If this backup is + actually unencrypted and there is no code, then choose the + appropriate backup type from the drop-down box"""), QMessageBox.Ok) + return + if not SECPRINT['FUNC_CHKPWD'](pwd): + QMessageBox.critical(self, 'Bad Encryption Code', tr(""" + The SecurePrint\xe2\x84\xa2 code you entered has an error + in it. Note that the code is case-sensitive. Please verify + you entered it correctly and try again."""), QMessageBox.Ok) + return - MkFeatLabel = lambda x: QRichLabel( tr(x), doWrap=False ) - self.featuresLbls[F.ProtGen] = MkFeatLabel('Protects All Future Addresses') - self.featuresLbls[F.ProtImport] = MkFeatLabel('Protects Imported Addresses') - self.featuresLbls[F.LostPass] = MkFeatLabel('Forgotten Passphrase') - self.featuresLbls[F.Durable] = MkFeatLabel('Long-term Durability') - self.featuresLbls[F.Visual] = MkFeatLabel('Visual Integrity') - self.featuresLbls[F.Physical] = MkFeatLabel('Multi-Point Protection') - - if not self.hasImportedAddr: - self.featuresLbls[F.ProtImport].setEnabled(False) - - self.lblSelFeat = QRichLabel('', doWrap=False, hAlign=Qt.AlignHCenter) - - layoutFeat = QGridLayout() - layoutFeat.addWidget(self.lblSelFeat, 0,0, 1,3) - layoutFeat.addWidget(HLINE(), 1,0, 1,3) - for i in range(self.FEATURES.Count): - self.featuresImgs[i] = QLabel('') - layoutFeat.addWidget( self.featuresTips[i], i+2, 0) - layoutFeat.addWidget( self.featuresLbls[i], i+2, 1) - layoutFeat.addWidget( self.featuresImgs[i], i+2, 2) - layoutFeat.setColumnStretch(0,0) - layoutFeat.setColumnStretch(1,1) - layoutFeat.setColumnStretch(2,0) - - frmFeat = QFrame() - frmFeat.setLayout(layoutFeat) - frmFeat.setFrameStyle(STYLE_SUNKEN) - - - self.lblDescrSelected = QRichLabel('') - frmFeatDescr = makeVertFrame([self.lblDescrSelected]) - w,h = tightSizeNChar(self, 10) - self.lblDescrSelected.setMinimumHeight(h*8) + maskKey = SECPRINT['FUNC_KDF'](pwd) + privKey = SECPRINT['FUNC_UNMASK'](privKey, ekey=maskKey) + if self.isLongForm: + chain = SECPRINT['FUNC_UNMASK'](chain, ekey=maskKey) - self.btnDone = QPushButton('Done') - self.btnDoIt = QPushButton('Create Backup') - self.connect(self.btnDone, SIGNAL('clicked()'), self.reject) - self.connect(self.btnDoIt, SIGNAL('clicked()'), self.clickedDoIt) - frmBottomBtns = makeHorizFrame([self.btnDone, 'Stretch', self.btnDoIt]) + if not self.isLongForm: + chain = DeriveChaincodeFromRootKey(privKey) - ########################################################################## - layoutDialog = QGridLayout() - layoutDialog.addWidget(lblTitle, 0,0, 1,2) - layoutDialog.addWidget(lblTitleDescr, 1,0, 1,2) - layoutDialog.addWidget(frmOpts, 2,0) - layoutDialog.addWidget(frmFeat, 2,1) - layoutDialog.addWidget(frmFeatDescr, 3,0, 1,2) - layoutDialog.addWidget(frmBottomBtns, 4,0, 1,2) - layoutDialog.setRowStretch(0, 0) - layoutDialog.setRowStretch(1, 0) - layoutDialog.setRowStretch(2, 0) - layoutDialog.setRowStretch(3, 1) - layoutDialog.setRowStretch(4, 0) - self.setLayout(layoutDialog) - self.setWindowTitle("Backup Center") - self.setMinimumSize(640,350) + # If we got here, the data is valid, let's create the wallet and accept the dlg + # Now we should have a fully-plaintext rootkey and chaincode + root = PyBtcAddress().createFromPlainKeyData(privKey) + root.chaincode = chain + + first = root.extendAddressChain() + newWltID = binary_to_base58((ADDRBYTE + first.getAddr160()[:5])[::-1]) - self.optPaperBackupTop.setChecked(True) - self.optPaperBackupOne.setChecked(True) - self.setDispFrame(-1) - self.optionClicked() + # Stop here if this was just a test + if self.thisIsATest: + verifyRecoveryTestID(self, newWltID, self.testWltID) + return + dlgOwnWlt = None + if self.main.walletMap.has_key(newWltID): + dlgOwnWlt = DlgReplaceWallet(newWltID, self.parent, self.main) - - ############################################################################# - def setDispFrame(self, index): - if index < 0: - self.setDispFrame(self.getIndexChecked()) + if (dlgOwnWlt.exec_()): + if dlgOwnWlt.output == 0: + return + else: + self.reject() + return else: - # Highlight imported-addr feature if their wallet contains them - pcolor = 'TextWarn' if self.hasImportedAddr else 'DisableFG' - self.featuresLbls[self.FEATURES.ProtImport].setText(tr(\ - 'Protects Imported Addresses'), color=pcolor) - - txtPaper = tr( """ - Paper backups protect every address ever generated by your - wallet. It is unencrypted, which means it needs to be stored - in a secure place, but it will help you recover your wallet - if you forget your encryption passphrase! -

    - You don't need a printer to make a paper backup! - The data can be copied by hand with pen and paper. - Paper backups are preferred to digital backups, because you - know the paper backup will work no matter how many years (or - decades) it sits in storage. """) - txtDigital = tr( """ - Digital backups can be saved to an external hard-drive or - USB removable media. It is recommended you make a few - copies to protect against "bit rot" (degradation).

    """) - txtDigPlain = tr( """ - IMPORTANT: Do not save an unencrypted digital - backup to your primary hard drive! - Please save it directly to the backup device. - Deleting the file does not guarantee the data is actually - gone! """) - txtDigCrypt = tr( """ - IMPORTANT: It is critical that you have at least - one unencrypted backup! Without it, your bitcoins will - be lost forever if you forget your passphrase! This is - by far the most common reason users lose coins! Having - at least one paper backup is recommended.""") - txtIndivKeys = tr( """ - View and export invidivual addresses strings, - public keys and/or private keys contained in your wallet. - This is useful for exporting your private keys to be imported into - another wallet app or service. -

    - You can view/backup imported keys, as well as unused keys in your - keypool (pregenerated addresses protected by your backup that - have not yet been used). """) - + reply = QMessageBox.question(self, 'Verify Wallet ID', \ + 'The data you entered corresponds to a wallet with a wallet ID: \n\n \t' + + newWltID + '\n\nDoes this ID match the "Wallet Unique ID" ' + 'printed on your paper backup? If not, click "No" and reenter ' + 'key and chain-code data again.', \ + QMessageBox.Yes | QMessageBox.No) + if reply == QMessageBox.No: + return - chk = lambda: QPixmap(':/checkmark32.png').scaled(20,20) - _X_ = lambda: QPixmap(':/red_X.png').scaled(16,16) - if index==self.OPTIONS.Paper1: - self.lblSelFeat.setText(tr('Single-Sheet Paper Backup'), bold=True) - self.featuresImgs[self.FEATURES.ProtGen ].setPixmap(chk()) - self.featuresImgs[self.FEATURES.ProtImport].setPixmap(chk()) - self.featuresImgs[self.FEATURES.LostPass ].setPixmap(chk()) - self.featuresImgs[self.FEATURES.Durable ].setPixmap(chk()) - self.featuresImgs[self.FEATURES.Visual ].setPixmap(chk()) - self.featuresImgs[self.FEATURES.Physical ].setPixmap(_X_()) - self.lblDescrSelected.setText(txtPaper) - elif index==self.OPTIONS.PaperN: - self.lblSelFeat.setText(tr('Fragmented Paper\xe2\x84\xa2 Backup'), bold=True) - self.featuresImgs[self.FEATURES.ProtGen ].setPixmap(chk()) - self.featuresImgs[self.FEATURES.ProtImport].setPixmap(_X_()) - self.featuresImgs[self.FEATURES.LostPass ].setPixmap(chk()) - self.featuresImgs[self.FEATURES.Durable ].setPixmap(chk()) - self.featuresImgs[self.FEATURES.Visual ].setPixmap(chk()) - self.featuresImgs[self.FEATURES.Physical ].setPixmap(chk()) - self.lblDescrSelected.setText(txtPaper) - elif index==self.OPTIONS.DigPlain: - self.lblSelFeat.setText(tr('Unencrypted Digital Backup'), bold=True) - self.featuresImgs[self.FEATURES.ProtGen ].setPixmap(chk()) - self.featuresImgs[self.FEATURES.ProtImport].setPixmap(chk()) - self.featuresImgs[self.FEATURES.LostPass ].setPixmap(chk()) - self.featuresImgs[self.FEATURES.Durable ].setPixmap(_X_()) - self.featuresImgs[self.FEATURES.Visual ].setPixmap(_X_()) - self.featuresImgs[self.FEATURES.Physical ].setPixmap(_X_()) - self.lblDescrSelected.setText(txtDigital + txtDigPlain) - elif index==self.OPTIONS.DigCrypt: - self.lblSelFeat.setText(tr('Encrypted Digital Backup'), bold=True) - self.featuresImgs[self.FEATURES.ProtGen ].setPixmap(chk()) - self.featuresImgs[self.FEATURES.ProtImport].setPixmap(chk()) - self.featuresImgs[self.FEATURES.LostPass ].setPixmap(_X_()) - self.featuresImgs[self.FEATURES.Durable ].setPixmap(_X_()) - self.featuresImgs[self.FEATURES.Visual ].setPixmap(_X_()) - self.featuresImgs[self.FEATURES.Physical ].setPixmap(chk()) - self.lblDescrSelected.setText(txtDigital + txtDigCrypt) - elif index==self.OPTIONS.Export: - self.lblSelFeat.setText(tr('Export Key Lists'), bold=True) - self.featuresImgs[self.FEATURES.ProtGen ].setPixmap(chk()) - self.featuresImgs[self.FEATURES.ProtImport].setPixmap(chk()) - self.featuresImgs[self.FEATURES.LostPass ].setPixmap(chk()) - self.featuresImgs[self.FEATURES.Durable ].setPixmap(_X_()) - self.featuresImgs[self.FEATURES.Visual ].setPixmap(_X_()) - self.featuresImgs[self.FEATURES.Physical ].setPixmap(_X_()) - self.lblDescrSelected.setText(txtIndivKeys) + passwd = [] + if self.chkEncrypt.isChecked(): + dlgPasswd = DlgChangePassphrase(self, self.main) + if dlgPasswd.exec_(): + passwd = SecureBinaryData(str(dlgPasswd.edtPasswd1.text())) else: - LOGERROR('What index was sent to setDispFrame? %d', index) - + QMessageBox.critical(self, 'Cannot Encrypt', \ + 'You requested your restored wallet be encrypted, but no ' + 'valid passphrase was supplied. Aborting wallet recovery.', \ + QMessageBox.Ok) + return - ############################################################################# - def getIndexChecked(self): - if self.optPaperBackupOne.isChecked(): - return self.OPTIONS.Paper1 - elif self.optPaperBackupFrag.isChecked(): - return self.OPTIONS.PaperN - elif self.optPaperBackupTop.isChecked(): - return self.OPTIONS.Paper1 - elif self.optDigitalBackupPlain.isChecked(): - return self.OPTIONS.DigPlain - elif self.optDigitalBackupCrypt.isChecked(): - return self.OPTIONS.DigCrypt - elif self.optDigitalBackupTop.isChecked(): - return self.OPTIONS.DigPlain - elif self.optIndivKeyListTop.isChecked(): - return self.OPTIONS.Export + shortl = '' + longl = '' + nPool = 1000 + + if dlgOwnWlt is not None: + if dlgOwnWlt.Meta is not None: + shortl = ' - %s' % (dlgOwnWlt.Meta['shortLabel']) + longl = dlgOwnWlt.Meta['longLabel'] + nPool = max(nPool, dlgOwnWlt.Meta['naddress']) + + self.newWallet = PyBtcWallet() + + if passwd: + self.newWallet.createNewWallet( \ + plainRootKey=privKey, \ + chaincode=chain, \ + shortLabel='Restored - %s%s' % \ + (newWltID, shortl), \ + longLabel=longl, \ + withEncrypt=True, \ + securePassphrase=passwd, \ + kdfTargSec = \ + self.advancedOptionsTab.getKdfSec(), \ + kdfMaxMem = \ + self.advancedOptionsTab.getKdfBytes(), + isActuallyNew=False, \ + doRegisterWithBDM=False) else: - return 0 + self.newWallet.createNewWallet( \ + plainRootKey=privKey, \ + chaincode=chain, \ + shortLabel='Restored - %s%s' % \ + (newWltID, shortl), \ + longLabel=longl, \ + withEncrypt=False, \ + isActuallyNew=False, \ + doRegisterWithBDM=False) - ############################################################################# - def optionClicked(self): - if self.optPaperBackupTop.isChecked(): - self.optPaperBackupOne.setEnabled(True) - self.optPaperBackupFrag.setEnabled(True) - self.optDigitalBackupPlain.setEnabled(False) - self.optDigitalBackupCrypt.setEnabled(False) - self.optDigitalBackupPlain.setChecked(False) - self.optDigitalBackupCrypt.setChecked(False) - self.optDigitalBackupNONE.setChecked(True) - self.btnDoIt.setText(tr('Create Paper Backup')) - elif self.optDigitalBackupTop.isChecked(): - self.optDigitalBackupPlain.setEnabled(True) - self.optDigitalBackupCrypt.setEnabled(True) - self.optPaperBackupOne.setEnabled(False) - self.optPaperBackupFrag.setEnabled(False) - self.optPaperBackupOne.setChecked(False) - self.optPaperBackupFrag.setChecked(False) - self.optPaperBackupNONE.setChecked(True) - self.btnDoIt.setText(tr('Create Digital Backup')) - elif self.optIndivKeyListTop.isChecked(): - self.optPaperBackupOne.setEnabled(False) - self.optPaperBackupFrag.setEnabled(False) - self.optPaperBackupOne.setChecked(False) - self.optPaperBackupFrag.setChecked(False) - self.optDigitalBackupPlain.setEnabled(False) - self.optDigitalBackupCrypt.setEnabled(False) - self.optDigitalBackupPlain.setChecked(False) - self.optDigitalBackupCrypt.setChecked(False) - self.optDigitalBackupNONE.setChecked(True) - self.optPaperBackupNONE.setChecked(True) - self.btnDoIt.setText(tr('Export Key Lists')) - self.setDispFrame(-1) - + fillAddrPoolProgress = DlgProgress(self, self.main, HBar=1, + Title="Computing New Addresses") + fillAddrPoolProgress.exec_(self.newWallet.fillAddressPool, nPool) + + if dlgOwnWlt is not None: + if dlgOwnWlt.Meta is not None: + from armoryengine.PyBtcWallet import WLT_UPDATE_ADD + for n_cmt in range(0, dlgOwnWlt.Meta['ncomments']): + entrylist = [] + entrylist = list(dlgOwnWlt.Meta[n_cmt]) + self.newWallet.walletFileSafeUpdate([[WLT_UPDATE_ADD, + entrylist[2], + entrylist[1], + entrylist[0]]]) + + self.newWallet = PyBtcWallet().readWalletFile(dlgOwnWlt.wltPath) + self.accept() - def clickedDoIt(self): - if self.optPaperBackupOne.isChecked(): - self.accept() - OpenPaperBackupWindow('Single', self.parent, self.main, self.wlt) - elif self.optPaperBackupFrag.isChecked(): - self.accept() - OpenPaperBackupWindow('Frag', self.parent, self.main, self.wlt) - elif self.optDigitalBackupPlain.isChecked(): - if self.main.digitalBackupWarning(): - self.main.makeWalletCopy(self, self.wlt, 'Decrypt', 'decrypt') - elif self.optDigitalBackupCrypt.isChecked(): - self.main.makeWalletCopy(self, self.wlt, 'Encrypt', 'encrypt') - elif self.optIndivKeyListTop.isChecked(): - if self.wlt.useEncryption and self.wlt.isLocked: - dlg = DlgUnlockWallet(self.wlt, self, self.main, 'Unlock Private Keys') - if not dlg.exec_(): - if self.main.usermode==USERMODE.Expert: - QMessageBox.warning(self, tr('Unlock Failed'), tr(""" - Wallet was not be unlocked. The public keys and addresses - will still be shown, but private keys will not be available - unless you reopen the dialog with the correct passphrase."""), \ - QMessageBox.Ok) - else: - QMessageBox.warning(self, tr('Unlock Failed'), tr(""" - 'Wallet could not be unlocked to display individual keys."""), \ - QMessageBox.Ok) - if self.main.usermode==USERMODE.Standard: - return - DlgShowKeyList(self.wlt, self.parent, self.main).exec_() - self.accept() + +################################################################################ +class DlgRestoreFragged(ArmoryDialog): + def __init__(self, parent, main, thisIsATest=False, expectWltID=None): + super(DlgRestoreFragged, self).__init__(parent, main) + + self.thisIsATest = thisIsATest + self.testWltID = expectWltID + headerStr = '' + if thisIsATest: + headerStr = tr("""Testing a + Fragmented Backup""") else: - return 0 - + headerStr = tr('Restore Wallet from Fragments') + + descr = tr(""" + %s

    + Use this form to enter all the fragments to be restored. Fragments + can be stored on a mix of paper printouts, and saved files. + If any of the fragments require a SecurePrint\xe2\x84\xa2 code, + you will only have to enter it once, since that code is the same for + all fragments of any given wallet. """) % headerStr + + if self.thisIsATest: + descr += tr("""

    + For testing purposes, you may enter more fragments than needed + and Armory will test all subsets of the entered fragments to verify + that each one still recovers the wallet successfully.""") + + lblDescr = QRichLabel(descr) + + frmDescr = makeHorizFrame([lblDescr], STYLE_RAISED) + + # HLINE + + self.scrollFragInput = QScrollArea() + self.scrollFragInput.setWidgetResizable(True) + self.scrollFragInput.setMinimumHeight(150) + lblFragList = QRichLabel(tr('Input Fragments Below:'), doWrap=False, bold=True) + self.btnAddFrag = QPushButton(tr('+Frag')) + self.btnRmFrag = QPushButton(tr('-Frag')) + self.btnRmFrag.setVisible(False) + self.connect(self.btnAddFrag, SIGNAL(CLICKED), self.addFragment) + self.connect(self.btnRmFrag, SIGNAL(CLICKED), self.removeFragment) + self.chkEncrypt = QCheckBox('Encrypt Restored Wallet') + self.chkEncrypt.setChecked(True) + frmAddRm = makeHorizFrame([self.chkEncrypt, STRETCH, self.btnRmFrag, self.btnAddFrag]) + self.fragDataMap = {} + self.tableSize = 2 + self.wltType = UNKNOWN + self.fragIDPrefix = UNKNOWN + doItText = tr('Test Backup' if thisIsATest else 'Restore from Fragments') + btnExit = QPushButton(tr('Cancel')) + self.btnRestore = QPushButton(doItText) + self.connect(btnExit, SIGNAL(CLICKED), self.reject) + self.connect(self.btnRestore, SIGNAL(CLICKED), self.processFrags) + frmBtns = makeHorizFrame([btnExit, STRETCH, self.btnRestore]) -################################################################################ -class DlgSimpleBackup(ArmoryDialog): - def __init__(self, parent, main, wlt): - super(DlgSimpleBackup, self).__init__(parent, main) - - self.wlt = wlt + self.lblRightFrm = QRichLabel('', hAlign=Qt.AlignHCenter) + self.lblSecureStr = QRichLabel(tr('SecurePrint\xe2\x84\xa2 Code:'), \ + hAlign=Qt.AlignHCenter, \ + color='TextWarn') + self.displaySecureString = QLineEdit() + self.imgPie = QRichLabel('', hAlign=Qt.AlignHCenter) + self.lblReqd = QRichLabel('', hAlign=Qt.AlignHCenter) + self.lblWltID = QRichLabel('', doWrap=False, hAlign=Qt.AlignHCenter) + self.lblFragID = QRichLabel('', doWrap=False, hAlign=Qt.AlignHCenter) + self.lblSecureStr.setVisible(False) + self.displaySecureString.setVisible(False) + self.displaySecureString.setMaximumWidth(relaxedSizeNChar(self.displaySecureString, 16)[0]) + # The Secure String is now edited in DlgEnterOneFrag, It is only displayed here + self.displaySecureString.setEnabled(False) + frmSecPair = makeVertFrame([self.lblSecureStr, self.displaySecureString]) + frmSecCtr = makeHorizFrame([STRETCH, frmSecPair, STRETCH]) + + frmWltInfo = makeVertFrame([STRETCH, + self.lblRightFrm, + self.imgPie, + self.lblReqd, + self.lblWltID, + self.lblFragID, + HLINE(), + frmSecCtr, + 'Strut(200)', + STRETCH], STYLE_SUNKEN) - lblDescrTitle = QRichLabel( tr(""" - Protect Your Bitcoins -- Make a Wallet Backup!""")) - lblDescr = QRichLabel( tr(""" - A failed hard-drive or forgotten passphrase will lead to - permanent loss of bitcoins! Luckily, Armory wallets only - need to be backed up one time, and protect you in both - of these events. If you've ever forgotten a password or had - a hardware failure, make a backup! """)) + fragmentsLayout = QGridLayout() + fragmentsLayout.addWidget(frmDescr, 0, 0, 1, 2) + fragmentsLayout.addWidget(frmAddRm, 1, 0, 1, 1) + fragmentsLayout.addWidget(self.scrollFragInput, 2, 0, 1, 1) + fragmentsLayout.addWidget(frmWltInfo, 1, 1, 2, 1) + setLayoutStretchCols(fragmentsLayout, 1, 0) - ### Paper - lblPaper = QRichLabel( tr(""" - Use a printer or pen-and-paper to write down your wallet "seed." """)) - btnPaper = QPushButton( tr('Make Paper Backup')) + walletRestoreTabs = QTabWidget() + fragmentsFrame = QFrame() + fragmentsFrame.setLayout(fragmentsLayout) + walletRestoreTabs.addTab(fragmentsFrame, "Fragments") + self.advancedOptionsTab = AdvancedOptionsFrame(parent, main) + walletRestoreTabs.addTab(self.advancedOptionsTab, "Advanced Options") - ### Digital - lblDigital = QRichLabel( tr(""" - Create an unencrypted copy of your wallet file, including imported - addresses.""")) - btnDigital = QPushButton(tr('Make Digital Backup')) + self.chkEncrypt.setChecked(not thisIsATest) + self.chkEncrypt.setVisible(not thisIsATest) + self.advancedOptionsTab.setEnabled(not thisIsATest) + if not thisIsATest: + self.connect(self.chkEncrypt, SIGNAL(CLICKED), self.onEncryptCheckboxChange) - ### Other - lblOther = QRichLabel( tr(""" """)) - btnOther = QPushButton(tr('See Other Backup Options')) + layout = QVBoxLayout() + layout.addWidget(walletRestoreTabs) + layout.addWidget(frmBtns) + self.setLayout(layout) + self.setMinimumWidth(650) + self.setMinimumHeight(465) + self.setWindowTitle(tr('Restore wallet from fragments')) - def backupDigital(): - if self.main.digitalBackupWarning(): - self.main.makeWalletCopy(self, self.wlt, 'Decrypt', 'decrypt') - self.accept() + self.makeFragInputTable() + self.checkRestoreParams() - def backupPaper(): - OpenPaperBackupWindow('Single', self, self.main, self.wlt) - self.accept() + ############################################################################# + # Hide advanced options whenver the restored wallet is unencrypted + def onEncryptCheckboxChange(self): + self.advancedOptionsTab.setEnabled(self.chkEncrypt.isChecked()) - def backupOther(): - self.accept() - DlgBackupCenter(self, self.main, self.wlt).exec_() + def makeFragInputTable(self, addCount=0): - self.connect(btnPaper, SIGNAL('clicked()'), backupPaper ) - self.connect(btnDigital, SIGNAL('clicked()'), backupDigital ) - self.connect(btnOther, SIGNAL('clicked()'), backupOther ) + self.tableSize += addCount + newLayout = QGridLayout() + newFrame = QFrame() + self.fragsDone = [] + newLayout.addWidget(HLINE(), 0, 0, 1, 5) + for i in range(self.tableSize): + btnEnter = QPushButton(tr('Type Data')) + btnLoad = QPushButton(tr('Load File')) + btnClear = QPushButton(tr('Clear')) + lblFragID = QRichLabel('', doWrap=False) + lblSecure = QLabel('') + if i in self.fragDataMap: + M, fnum, wltID, doMask, fid = ReadFragIDLineBin(self.fragDataMap[i][0]) + self.fragsDone.append(fnum) + lblFragID.setText('' + fid + '') + if doMask: + lblFragID.setText('' + fid + '', color='TextWarn') - layout = QGridLayout() - layout.addWidget( lblPaper, 0,0) - layout.addWidget( btnPaper, 0,2) + self.connect(btnEnter, SIGNAL(CLICKED), \ + functools.partial(self.dataEnter, fnum=i)) + self.connect(btnLoad, SIGNAL(CLICKED), \ + functools.partial(self.dataLoad, fnum=i)) + self.connect(btnClear, SIGNAL(CLICKED), \ + functools.partial(self.dataClear, fnum=i)) - layout.addWidget( HLINE(), 1,0, 1,3) - layout.addWidget( lblDigital, 2,0) - layout.addWidget( btnDigital, 2,2) + newLayout.addWidget(btnEnter, 2 * i + 1, 0) + newLayout.addWidget(btnLoad, 2 * i + 1, 1) + newLayout.addWidget(btnClear, 2 * i + 1, 2) + newLayout.addWidget(lblFragID, 2 * i + 1, 3) + newLayout.addWidget(lblSecure, 2 * i + 1, 4) + newLayout.addWidget(HLINE(), 2 * i + 2, 0, 1, 5) - layout.addWidget( HLINE(), 3,0, 1,3) - - layout.addWidget( makeHorizFrame(['Stretch', btnOther,'Stretch']), 4,0, 1,3) + btnFrame = QFrame() + btnFrame.setLayout(newLayout) - #layout.addWidget( VLINE(), 0,1, 5,1) + frmFinal = makeVertFrame([btnFrame, STRETCH], STYLE_SUNKEN) + self.scrollFragInput.setWidget(frmFinal) - layout.setContentsMargins(10,5,10,5) - setLayoutStretchRows(layout, 1,0,1,0,0) - setLayoutStretchCols(layout, 1,0,0) + self.btnAddFrag.setVisible(self.tableSize < 12) + self.btnRmFrag.setVisible(self.tableSize > 2) - frmGrid = QFrame() - frmGrid.setFrameStyle(STYLE_PLAIN) - frmGrid.setLayout(layout) - - btnClose = QPushButton('Done') - self.connect(btnClose, SIGNAL('clicked()'), self.accept) - frmClose = makeHorizFrame(['Stretch', btnClose]) - - frmAll = makeVertFrame([lblDescrTitle, lblDescr, frmGrid, frmClose]) - layoutAll = QVBoxLayout() - layoutAll.addWidget(frmAll) - self.setLayout(layoutAll) - self.sizeHint = lambda: QSize(400,250) - self.setWindowTitle(tr('Backup Options')) + ############################################################################# + def addFragment(self): + self.makeFragInputTable(1) + ############################################################################# + def removeFragment(self): + self.makeFragInputTable(-1) + toRemove = [] + for key, val in self.fragDataMap.iteritems(): + if key >= self.tableSize: + toRemove.append(key) + # Have to do this in a separate loop, cause you can't remove items + # from a map while you are iterating over them + for key in toRemove: + self.dataClear(key) -################################################################################ -class DlgFragBackup(ArmoryDialog): ############################################################################# - def __init__(self, parent, main, wlt): - super(DlgFragBackup, self).__init__(parent, main) - - self.wlt = wlt - self.randpass = None - self.binCrypt32 = None + def dataEnter(self, fnum): + dlg = DlgEnterOneFrag(self, self.main, self.fragsDone, self.wltType, self.displaySecureString.text()) + if dlg.exec_(): + LOGINFO('Good data from enter_one_frag exec! %d', fnum) + self.displaySecureString.setText(dlg.editSecurePrint.text()) + self.addFragToTable(fnum, dlg.fragData) + self.makeFragInputTable() - lblDescrTitle = QRichLabel( tr(""" - Create M-of-N Fragmented Backup\xe2\x84\xa2 of "%s" (%s)""") % \ - (wlt.labelName, wlt.uniqueIDB58), doWrap=False) - lblDescrTitle.setContentsMargins(5,5,5,5) - self.lblAboveFrags = QRichLabel('') - self.lblAboveFrags.setContentsMargins(10,0,10,0) + ############################################################################# + def dataLoad(self, fnum): + LOGINFO('Loading data for entry, %d', fnum) + toLoad = unicode(self.main.getFileLoad(tr('Load Fragment File'), \ + [tr('Wallet Fragments (*.frag)')])) - frmDescr = makeVertFrame([lblDescrTitle, self.lblAboveFrags], \ - STYLE_RAISED) + if len(toLoad) == 0: + return - - self.maxM = 5 if not self.main.usermode==USERMODE.Expert else 8 - self.maxN = 6 if not self.main.usermode==USERMODE.Expert else 12 - self.currMinN = 2 - self.maxmaxN = 12 + if not os.path.exists(toLoad): + LOGERROR('File just chosen does not exist! %s', toLoad) + QMessageBox.critical(self, tr('File Does Not Exist'), tr(""" + The file you select somehow does not exist...? +

    %s

    Try a different file""") % toLoad, \ + QMessageBox.Ok) - self.comboM = QComboBox() - self.comboN = QComboBox() + fragMap = {} + with open(toLoad, 'r') as fin: + allData = [line.strip() for line in fin.readlines()] + fragMap = {} + for line in allData: + if line[:2].lower() in ['id', 'x1', 'x2', 'x3', 'x4', \ + 'y1', 'y2', 'y3', 'y4', \ + 'f1', 'f2', 'f3', 'f4']: + fragMap[line[:2].lower()] = line[3:].strip().replace(' ', '') + + + cList, nList = [], [] + if len(fragMap) == 9: + cList, nList = ['x', 'y'], ['1', '2', '3', '4'] + elif len(fragMap) == 5: + cList, nList = ['f'], ['1', '2', '3', '4'] + elif len(fragMap) == 3: + cList, nList = ['f'], ['1', '2'] + else: + LOGERROR('Unexpected number of lines in the frag file, %d', len(fragMap)) + return - for M in range(2,self.maxM+1): - self.comboM.addItem(str(M)) + fragData = [] + fragData.append(hex_to_binary(fragMap['id'])) + for c in cList: + for n in nList: + mapKey = c + n + rawBin, err = readSixteenEasyBytes(fragMap[c + n]) + if err == 'Error_2+': + QMessageBox.critical(self, tr('Fragment Error'), tr(""" + There was an unfixable error in the fragment file: +

    File: %s
    Line: %s
    """) % (toLoad, mapKey), \ + QMessageBox.Ok) + return + fragData.append(SecureBinaryData(rawBin)) + rawBin = None - for N in range(self.currMinN, self.maxN+1): - self.comboN.addItem(str(N)) + self.addFragToTable(fnum, fragData) + self.makeFragInputTable() - self.comboM.setCurrentIndex(1) - self.comboN.setCurrentIndex(2) - def updateM(): - self.updateComboN() - self.createFragDisplay() + ############################################################################# + def dataClear(self, fnum): + if not fnum in self.fragDataMap: + return - updateN = self.createFragDisplay + for i in range(1, 3): + self.fragDataMap[fnum][i].destroy() + del self.fragDataMap[fnum] + self.makeFragInputTable() + self.checkRestoreParams() - self.connect(self.comboM, SIGNAL('activated(int)'), updateM) - self.connect(self.comboN, SIGNAL('activated(int)'), updateN) - self.comboM.setMinimumWidth(30) - self.comboN.setMinimumWidth(30) - btnAccept = QPushButton(tr('Close')) - self.connect(btnAccept, SIGNAL('clicked()'), self.accept) - frmBottomBtn = makeHorizFrame(['Stretch', btnAccept]) + ############################################################################# + def checkRestoreParams(self): + showRightFrm = False + self.btnRestore.setEnabled(False) + self.lblRightFrm.setText(tr(""" + Start entering fragments into the table to left...""")) + for row, data in self.fragDataMap.iteritems(): + showRightFrm = True + M, fnum, wltIDBin, doMask, idBase58 = ReadFragIDLineBin(data[0]) + self.lblRightFrm.setText('Wallet Being Restored:') + self.imgPie.setPixmap(QPixmap(':/frag%df.png' % M)) + self.lblReqd.setText(tr('Frags Needed: %d') % M) + self.lblWltID.setText(tr('Wallet: %s') % binary_to_base58(wltIDBin)) + self.lblFragID.setText(tr('Fragments: %s') % idBase58.split('-')[0]) + self.btnRestore.setEnabled(len(self.fragDataMap) >= M) + break - # We will hold all fragments here, in SBD objects. Destroy all of them - # before the dialog exits - self.secureRoot = self.wlt.addrMap['ROOT'].binPrivKey32_Plain.copy() - self.secureChain = self.wlt.addrMap['ROOT'].chaincode.copy() - self.secureMtrx = [] + anyMask = False + for row, data in self.fragDataMap.iteritems(): + M, fnum, wltIDBin, doMask, idBase58 = ReadFragIDLineBin(data[0]) + if doMask: + anyMask = True + break + # If all of the rows with a Mask have been removed clear the securePrintCode + if not anyMask: + self.displaySecureString.setText('') + self.lblSecureStr.setVisible(anyMask) + self.displaySecureString.setVisible(anyMask) - testChain = DeriveChaincodeFromRootKey(self.secureRoot) - if testChain == self.secureChain: - self.noNeedChaincode = True - self.securePrint = self.secureRoot - else: - self.securePrint = self.secureRoot + self.secureChain + if not showRightFrm: + self.fragIDPrefix = UNKNOWN + self.wltType = UNKNOWN - self.chkSecurePrint = QCheckBox( tr(""" - Use SecurePrint\xe2\x84\xa2 to prevent exposing keys to other devices""")) + self.imgPie.setVisible(showRightFrm) + self.lblReqd.setVisible(showRightFrm) + self.lblWltID.setVisible(showRightFrm) + self.lblFragID.setVisible(showRightFrm) - self.scrollArea = QScrollArea() - self.createFragDisplay() - self.scrollArea.setWidgetResizable(True) - self.ttipSecurePrint = self.main.createToolTipWidget( tr(""" - SecurePrint\xe2\x84\xa2 encrypts your backup with a code displayed on - the screen, so that no other devices or processes has access to the - unencrypted private keys (either network devices when printing, or - other applications if you save a fragment to disk or USB device). - You must keep the SecurePrint\xe2\x84\xa2 code with the backup!""")) - self.lblSecurePrint = QRichLabel(tr(""" - IMPORTANT: You must keep the - SecurePrint\xe2\x84\xa2 encryption code with your backup! - Your SecurePrint\xe2\x84\xa2 code is - %s. - All fragments for a given wallet use the - same code.""") % \ - (htmlColor('TextWarn'), htmlColor('TextBlue'), self.randpass.toBinStr(), \ - htmlColor('TextWarn'))) - self.connect(self.chkSecurePrint, SIGNAL('clicked()'), self.clickChkSP) - self.chkSecurePrint.setChecked(False) - self.lblSecurePrint.setVisible(False) - frmChkSP = makeHorizFrame([self.chkSecurePrint, self.ttipSecurePrint, 'Stretch']) + ############################################################################# + def addFragToTable(self, tableIndex, fragData): - dlgLayout = QVBoxLayout() - dlgLayout.addWidget(frmDescr) - dlgLayout.addWidget(self.scrollArea) - dlgLayout.addWidget(self.chkSecurePrint) - dlgLayout.addWidget(self.lblSecurePrint) - dlgLayout.addWidget(frmBottomBtn) - setLayoutStretch(dlgLayout, 0,1,0,0,0) + if len(fragData) == 9: + currType = '0' + elif len(fragData) == 5: + currType = BACKUP_TYPE_135A + elif len(fragData) == 3: + currType = BACKUP_TYPE_135C + else: + LOGERROR('How\'d we get fragData of size: %d', len(fragData)) + return - self.setLayout(dlgLayout) - self.setMinimumWidth(650) - self.setMinimumHeight(450) - self.setWindowTitle('Create Backup Fragments') + if self.wltType == UNKNOWN: + self.wltType = currType + elif not self.wltType == currType: + QMessageBox.critical(self, tr('Mixed fragment types'), tr(""" + You entered a fragment for a different wallet type. Please check + that all fragments are for the same wallet, of the same version, + and require the same number of fragments."""), QMessageBox.Ok) + LOGERROR('Mixing frag types! How did that happen?') + return - ############################################################################# - def clickChkSP(self): - self.lblSecurePrint.setVisible( self.chkSecurePrint.isChecked()) - self.createFragDisplay() + M, fnum, wltIDBin, doMask, idBase58 = ReadFragIDLineBin(fragData[0]) + # If we don't know the Secure String Yet we have to get it + if doMask and len(str(self.displaySecureString.text()).strip()) == 0: + dlg = DlgEnterSecurePrintCode(self, self.main) + if dlg.exec_(): + self.displaySecureString.setText(dlg.editSecurePrint.text()) + else: + return + + if self.fragIDPrefix == UNKNOWN: + self.fragIDPrefix = idBase58.split('-')[0] + elif not self.fragIDPrefix == idBase58.split('-')[0]: + QMessageBox.critical(self, tr('Multiple Walletss'), tr(""" + The fragment you just entered is actually for a different wallet + than the previous fragments you entered. Please double-check that + all the fragments you are entering belong to the same wallet and + have the "number of needed fragments" (M-value, in M-of-N)."""), \ + QMessageBox.Ok) + LOGERROR('Mixing fragments of different wallets! %s', idBase58) + return + + if not self.verifyNonDuplicateFrag(fnum): + QMessageBox.critical(self, tr('Duplicate Fragment'), tr(""" + You just input fragment #%d, but that fragment has already been + entered!""") % fnum, QMessageBox.Ok) + return - ############################################################################# - def updateComboN(self): - M = int(str(self.comboM.currentText())) - oldN = int(str(self.comboN.currentText())) - self.currMinN = M - self.comboN.clear() - for i,N in enumerate(range(self.currMinN, self.maxN+1)): - self.comboN.addItem(str(N)) - if M>oldN: - self.comboN.setCurrentIndex(0) - else: - for i,N in enumerate(range(self.currMinN, self.maxN+1)): - if N==oldN: - self.comboN.setCurrentIndex(i) - - + if currType == '0': + X = SecureBinaryData(''.join([fragData[i].toBinStr() for i in range(1, 5)])) + Y = SecureBinaryData(''.join([fragData[i].toBinStr() for i in range(5, 9)])) + elif currType == BACKUP_TYPE_135A: + X = SecureBinaryData(int_to_binary(fnum + 1, widthBytes=64, endOut=BIGENDIAN)) + Y = SecureBinaryData(''.join([fragData[i].toBinStr() for i in range(1, 5)])) + elif currType == BACKUP_TYPE_135C: + X = SecureBinaryData(int_to_binary(fnum + 1, widthBytes=32, endOut=BIGENDIAN)) + Y = SecureBinaryData(''.join([fragData[i].toBinStr() for i in range(1, 3)])) - ############################################################################# - def createFragDisplay(self): - self.recomputeFragData() - M = int(str(self.comboM.currentText())) - N = int(str(self.comboN.currentText())) + self.fragDataMap[tableIndex] = [fragData[0][:], X.copy(), Y.copy()] + X.destroy() + Y.destroy() + self.checkRestoreParams() - - lblAboveM = QRichLabel(tr('Required Fragments '), hAlign=Qt.AlignHCenter, doWrap=False) - lblAboveN = QRichLabel(tr('Total Fragments '), hAlign=Qt.AlignHCenter) - frmComboM = makeHorizFrame(['Stretch', QLabel('M:'), self.comboM, 'Stretch']) - frmComboN = makeHorizFrame(['Stretch', QLabel('N:'), self.comboN, 'Stretch']) + ############################################################################# + def verifyNonDuplicateFrag(self, fnum): + for row, data in self.fragDataMap.iteritems(): + rowFrag = ReadFragIDLineBin(data[0])[1] + if fnum == rowFrag: + return False - btnPrintAll = QPushButton('Print All Fragments') - self.connect(btnPrintAll, SIGNAL('clicked()'), self.clickPrintAll) - leftFrame = makeVertFrame(['Stretch', \ - lblAboveM, \ - frmComboM, \ - lblAboveN, \ - frmComboN, \ - 'Stretch', \ - HLINE(), \ - btnPrintAll, \ - 'Stretch'], STYLE_STYLED) + return True - layout = QHBoxLayout() - layout.addWidget(leftFrame) - for f in range(N): - layout.addWidget(self.createFragFrm(f)) + ############################################################################# + def processFrags(self): + if self.chkEncrypt.isChecked() and self.advancedOptionsTab.getKdfSec() == -1: + QMessageBox.critical(self, 'Invalid Target Compute Time', \ + 'You entered Target Compute Time incorrectly.\n\nEnter: (ms, s)', QMessageBox.Ok) + return + if self.chkEncrypt.isChecked() and self.advancedOptionsTab.getKdfBytes() == -1: + QMessageBox.critical(self, 'Invalid Max Memory Usage', \ + 'You entered Max Memory Usage incorrectly.\n\nnter: (kb, mb)', QMessageBox.Ok) + return + SECPRINT = HardcodedKeyMaskParams() + pwd, ekey = '', '' + if self.displaySecureString.isVisible(): + pwd = str(self.displaySecureString.text()).strip() + maskKey = SECPRINT['FUNC_KDF'](pwd) - frmScroll = QFrame() - frmScroll.setFrameStyle(STYLE_SUNKEN) - frmScroll.setStyleSheet('QFrame { background-color : %s }' % \ - htmlColor('SlightBkgdDark')) - frmScroll.setLayout(layout) - self.scrollArea.setWidget(frmScroll) + fragMtrx, M = [], -1 + for row, trip in self.fragDataMap.iteritems(): + M, fnum, wltID, doMask, fid = ReadFragIDLineBin(trip[0]) + X, Y = trip[1], trip[2] + if doMask: + LOGINFO('Row %d needs unmasking' % row) + Y = SECPRINT['FUNC_UNMASK'](Y, ekey=maskKey) + else: + LOGINFO('Row %d is already unencrypted' % row) + fragMtrx.append([X.toBinStr(), Y.toBinStr()]) - BLUE = htmlColor('TextBlue') - self.lblAboveFrags.setText( tr(""" - Any %d of these - %d - fragments are sufficient to restore your wallet, and each fragment - has the ID, %s. All fragments with the - same fragment ID are compatible with each other! - Click - here to read more about our backup system.
    """) % \ - (BLUE, M, BLUE, N, BLUE, self.fragPrefixStr) ) + typeToBytes = {'0': 64, BACKUP_TYPE_135A: 64, BACKUP_TYPE_135C: 32} + nBytes = typeToBytes[self.wltType] - ############################################################################# - def createFragFrm(self, idx): - - doMask = self.chkSecurePrint.isChecked() - M = int(str(self.comboM.currentText())) - N = int(str(self.comboN.currentText())) - - lblFragID = QRichLabel('Fragment ID:
    %s-%d
    ' % \ - (self.fragPrefixStr, idx+1)) - #lblWltID = QRichLabel('(%s)' % self.wlt.uniqueIDB58) - lblFragPix = QImageLabel(self.fragPixmapFn, size=(72,72)) - if doMask: - ys = self.secureMtrxCrypt[idx][1].toBinStr()[:42] - else: - ys = self.secureMtrx[idx][1].toBinStr()[:42] + if self.thisIsATest and len(fragMtrx) > M: + self.testFragSubsets(fragMtrx, M) + return - easyYs1 = makeSixteenBytesEasy(ys[:16 ]) - easyYs2 = makeSixteenBytesEasy(ys[ 16:32]) - - binID = self.wlt.uniqueIDBin - ID = ComputeFragIDLineHex(M, idx, binID, doMask, addSpaces=True) - fragPreview = 'ID: %s...
    ' % ID[:12] - fragPreview += 'F1: %s...
    ' % easyYs1[:12] - fragPreview += 'F2: %s... ' % easyYs2[:12] - lblPreview = QRichLabel(fragPreview) - lblPreview.setFont( GETFONT('Fixed', 9)) - - lblFragIdx = QRichLabel('#%d' % (idx+1), size=4, color='TextBlue', \ - hAlign=Qt.AlignHCenter) + SECRET = ReconstructSecret(fragMtrx, M, nBytes) + for i in range(len(fragMtrx)): + fragMtrx[i] = [] - frmTopLeft = makeVertFrame([lblFragID, lblFragIdx, 'Stretch']) - frmTopRight = makeVertFrame([lblFragPix, 'Stretch']) + LOGINFO('Final length of frag mtrx: %d', len(fragMtrx)) + LOGINFO('Final length of secret: %d', len(SECRET)) - frmPaper = makeVertFrame([lblPreview]) - frmPaper.setStyleSheet('QFrame { background-color : #ffffff }') + priv, chain = '', '' + if len(SECRET) == 64: + priv = SecureBinaryData(SECRET[:32 ]) + chain = SecureBinaryData(SECRET[ 32:]) + elif len(SECRET) == 32: + priv = SecureBinaryData(SECRET) + chain = DeriveChaincodeFromRootKey(priv) - fnPrint = lambda: self.clickPrintFrag(idx) - fnSave = lambda: self.clickSaveFrag(idx) - btnPrintFrag = QPushButton('View/Print') - btnSaveFrag = QPushButton('Save to File') - self.connect(btnPrintFrag, SIGNAL('clicked()'), fnPrint) - self.connect(btnSaveFrag, SIGNAL('clicked()'), fnSave) - frmButtons = makeHorizFrame([btnPrintFrag, btnSaveFrag]) - + # If we got here, the data is valid, let's create the wallet and accept the dlg + # Now we should have a fully-plaintext rootkey and chaincode + root = PyBtcAddress().createFromPlainKeyData(priv) + root.chaincode = chain - layout = QGridLayout() - layout.addWidget(frmTopLeft, 0,0, 1,1) - layout.addWidget(frmTopRight, 0,1, 1,1) - layout.addWidget(frmPaper, 1,0, 1,2) - layout.addWidget(frmButtons, 2,0, 1,2) - layout.setSizeConstraint(QLayout.SetFixedSize) + first = root.extendAddressChain() + newWltID = binary_to_base58((ADDRBYTE + first.getAddr160()[:5])[::-1]) - outFrame = QFrame() - outFrame.setFrameStyle(STYLE_STYLED) - outFrame.setLayout(layout) - return outFrame - + # If this is a test, then bail + if self.thisIsATest: + verifyRecoveryTestID(self, newWltID, self.testWltID) + return - ############################################################################# - def clickPrintAll(self): - self.clickPrintFrag(range(int(str(self.comboN.currentText())))) - - ############################################################################# - def clickPrintFrag(self, zindex): - if not isinstance(zindex, (list,tuple)): - zindex = [zindex] - fragData = {} - fragData['M'] = int(str(self.comboM.currentText())) - fragData['N'] = int(str(self.comboN.currentText())) - fragData['FragIDStr'] = self.fragPrefixStr - fragData['FragPixmap'] = self.fragPixmapFn - fragData['Range'] = zindex - fragData['Secure'] = self.chkSecurePrint.isChecked() - dlg = DlgPrintBackup(self, self.main, self.wlt, 'Fragments', \ - self.secureMtrx, self.secureMtrxCrypt, fragData, \ - self.secureRoot, self.secureChain) - dlg.exec_() + dlgOwnWlt = None + if self.main.walletMap.has_key(newWltID): + dlgOwnWlt = DlgReplaceWallet(newWltID, self.parent, self.main) - ############################################################################# - def clickSaveFrag(self, zindex): - saveMtrx = self.secureMtrx; - doMask = False - if self.chkSecurePrint.isChecked(): - response = QMessageBox.question(self, 'Secure Backup?', tr(""" - You have selected to use SecurePrint\xe2\x84\xa2 for the printed - backups, which can also be applied to fragments saved to file. - Doing so will require you store the SecurePrint\xe2\x84\xa2 - code with the backup, but it will prevent unencrypted key data from - touching any disks.

    Do you want to encrypt the fragment - file with the same SecurePrint\xe2\x84\xa2 code?"""), \ - QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) - - if response==QMessageBox.Yes: - saveMtrx = self.secureMtrxCrypt; - doMask = True - elif response==QMessageBox.No: - pass + if (dlgOwnWlt.exec_()): + if dlgOwnWlt.output == 0: + return else: + self.reject() return - - wid = self.wlt.uniqueIDB58 - pref = self.fragPrefixStr - fnum = zindex+1 - M = self.M - sec = 'secure.' if doMask else '' - defaultFn = 'wallet_%s_%s_num%d_need%d.%sfrag' % (wid,pref,fnum,M, sec) - print 'FragFN:', defaultFn - savepath = self.main.getFileSave(tr('Save Fragment'), \ - [tr('Wallet Fragments (*.frag)')],\ - defaultFn) - - if len(toUnicode(savepath))==0: + reply = QMessageBox.question(self, tr('Verify Wallet ID'), tr(""" + The data you entered corresponds to a wallet with a wallet + ID:
    {}
    Does this ID + match the "Wallet Unique ID" printed on your paper backup? + If not, click "No" and reenter key and chain-code data + again.""").format(newWltID), QMessageBox.Yes | QMessageBox.No) + if reply == QMessageBox.No: return - fout = open(savepath, 'w') - fout.write('Wallet ID: %s\n' % wid) - fout.write('Create Date: %s\n' % unixTimeToFormatStr(RightNow())) - fout.write('Fragment ID: %s-#%d\n' % (pref,fnum)) - fout.write('Frag Needed: %d\n' % M) - fout.write('\n\n') - try: - yBin = saveMtrx[zindex][1].toBinStr() - binID = self.wlt.uniqueIDBin - IDLine = ComputeFragIDLineHex(M, zindex, binID, doMask, addSpaces=True) - if len(yBin)==32: - fout.write('ID: ' + IDLine + '\n') - fout.write('F1: ' + makeSixteenBytesEasy(yBin[:16 ]) + '\n') - fout.write('F2: ' + makeSixteenBytesEasy(yBin[ 16:]) + '\n') - elif len(yBin)==64: - fout.write('ID: ' + IDLine + '\n') - fout.write('F1: ' + makeSixteenBytesEasy(yBin[:16 ]) + '\n') - fout.write('F2: ' + makeSixteenBytesEasy(yBin[ 16:32 ]) + '\n') - fout.write('F3: ' + makeSixteenBytesEasy(yBin[ 32:48 ]) + '\n') - fout.write('F4: ' + makeSixteenBytesEasy(yBin[ 48:]) + '\n') + passwd = [] + if self.chkEncrypt.isChecked(): + dlgPasswd = DlgChangePassphrase(self, self.main) + if dlgPasswd.exec_(): + passwd = SecureBinaryData(str(dlgPasswd.edtPasswd1.text())) else: - LOGERROR('yBin is not 32 or 64 bytes! It is %s bytes', len(yBin)) - finally: - yBin = None + QMessageBox.critical(self, tr('Cannot Encrypt'), tr(""" + You requested your restored wallet be encrypted, but no + valid passphrase was supplied. Aborting wallet + recovery."""), QMessageBox.Ok) + return - fout.close() - - qmsg = tr(""" - The fragment was successfully saved to the following location: -

    %s

    """) % savepath + shortl = '' + longl = '' + nPool = 1000 - if doMask: - qmsg += tr(""" - Important: - The fragment was encrypted with the - SecurePrint\xe2\x84\xa2 encryption code. You must keep this - code with the backup in order to use it! The code is - case-sensitive! -

    %s""") % \ - (htmlColor('TextWarn'), htmlColor('TextBlue'), self.randpass.toBinStr()) + if dlgOwnWlt is not None: + if dlgOwnWlt.Meta is not None: + shortl = ' - %s' % (dlgOwnWlt.Meta['shortLabel']) + longl = dlgOwnWlt.Meta['longLabel'] + nPool = max(nPool, dlgOwnWlt.Meta['naddress']) - QMessageBox.information(self, 'Success', qmsg, QMessageBox.Ok) - - + if passwd: + self.newWallet = PyBtcWallet().createNewWallet(\ + plainRootKey=priv, \ + chaincode=chain, \ + shortLabel='Restored - %s%s' % (newWltID, shortl), \ + longLabel=longl, \ + withEncrypt=True, \ + securePassphrase=passwd, \ + kdfTargSec=self.advancedOptionsTab.getKdfSec(), \ + kdfMaxMem=self.advancedOptionsTab.getKdfBytes(), + isActuallyNew=False, \ + doRegisterWithBDM=False) + else: + self.newWallet = PyBtcWallet().createNewWallet(\ + plainRootKey=priv, \ + chaincode=chain, \ + shortLabel='Restored - %s%s' % (newWltID, shortl), \ + longLabel=longl, \ + withEncrypt=False, \ + isActuallyNew=False, \ + doRegisterWithBDM=False) + + + # Will pop up a little "please wait..." window while filling addr pool + fillAddrPoolProgress = DlgProgress(self, self.parent, HBar=1, + Title="Computing New Addresses") + fillAddrPoolProgress.exec_(self.newWallet.fillAddressPool, nPool) + + if dlgOwnWlt is not None: + if dlgOwnWlt.Meta is not None: + from armoryengine.PyBtcWallet import WLT_UPDATE_ADD + for n_cmt in range(0, dlgOwnWlt.Meta['ncomments']): + entrylist = [] + entrylist = list(dlgOwnWlt.Meta[n_cmt]) + self.newWallet.walletFileSafeUpdate([[WLT_UPDATE_ADD, entrylist[2], entrylist[1], entrylist[0]]]) + + self.newWallet = PyBtcWallet().readWalletFile(dlgOwnWlt.wltPath) + self.accept() ############################################################################# - def destroyFrags(self): - if len(self.secureMtrx)==0: - return + def testFragSubsets(self, fragMtrx, M): + # If the user entered multiple fragments + fragMap = {} + for x, y in fragMtrx: + fragMap[binary_to_int(x, BIGENDIAN) - 1] = [x, y] + typeToBytes = {'0': 64, BACKUP_TYPE_135A: 64, BACKUP_TYPE_135C: 32} - if isinstance(self.secureMtrx[0], (list,tuple)): - for sbdList in self.secureMtrx: - for sbd in sbdList: - sbd.destroy() - for sbdList in self.secureMtrxCrypt: - for sbd in sbdList: - sbd.destroy() - else: - for sbd in self.secureMtrx: - sbd.destroy() - for sbd in self.secureMtrxCrypt: - sbd.destroy() + isRandom, results = testReconstructSecrets(fragMap, M, 100) + def privAndChainFromRow(secret): + priv, chain = None, None + if len(secret) == 64: + priv = SecureBinaryData(secret[:32 ]) + chain = SecureBinaryData(secret[ 32:]) + return (priv, chain) + elif len(secret) == 32: + priv = SecureBinaryData(secret) + chain = DeriveChaincodeFromRootKey(priv) + return (priv, chain) + else: + LOGERROR('Root secret is %s bytes ?!' % len(secret)) + raise KeyDataError - self.secureMtrx = [] - self.secureMtrxCrypt = [] - + results = [(row[0], privAndChainFromRow(row[1])) for row in results] + subsAndIDs = [(row[0], calcWalletIDFromRoot(*row[1])) for row in results] - ############################################################################# - def destroyEverything(self): - self.secureRoot.destroy() - self.secureChain.destroy() - self.securePrint.destroy() - self.destroyFrags() + DlgShowTestResults(self, isRandom, subsAndIDs, \ + M, len(fragMtrx), self.testWltID).exec_() + + +########################################################################## +class DlgShowTestResults(ArmoryDialog): + ####################################################################### + def __init__(self, parent, isRandom, subsAndIDs, M, nFrag, expectID): + super(DlgShowTestResults, self).__init__(parent, parent.main) + + accumSet = set() + for sub, ID in subsAndIDs: + accumSet.add(ID) - ############################################################################# - def recomputeFragData(self): - """ - Only M is needed, since N doesn't change - """ - - M = int(str(self.comboM.currentText())) - N = int(str(self.comboN.currentText())) - # Make sure only local variables contain non-SBD data - self.destroyFrags() - insecureData = SplitSecret(self.securePrint, M, self.maxmaxN) - for x,y in insecureData: - self.secureMtrx.append([SecureBinaryData(x), SecureBinaryData(y)]) - insecureData,x,y = None,None,None + allEqual = (len(accumSet) == 1) + allCorrect = True + testID = expectID + if not testID: + testID = subsAndIDs[0][1] - ##### - # Now we compute the SecurePrint(TM) versions of the fragments - SECPRINT = HardcodedKeyMaskParams() - MASK = lambda x: SECPRINT['FUNC_MASK'](x, ekey=self.binCrypt32) - if not self.randpass or not self.binCrypt32: - self.randpass = SECPRINT['FUNC_PWD'](self.secureRoot + self.secureChain) - self.binCrypt32 = SECPRINT['FUNC_KDF'](self.randpass) - self.secureMtrxCrypt = [] - for sbdX,sbdY in self.secureMtrx: - self.secureMtrxCrypt.append( [sbdX.copy(), MASK(sbdY)] ) - ##### + allCorrect = testID == subsAndIDs[0][1] - self.M,self.N = M,N - self.fragPrefixStr = ComputeFragIDBase58(self.M, self.wlt.uniqueIDBin) - self.fragPixmapFn = ':/frag%df.png' % M + descr = '' + nSubs = len(subsAndIDs) + fact = lambda x: math.factorial(x) + total = fact(nFrag) / (fact(M) * fact(nFrag - M)) + if isRandom: + descr = tr(""" + The total number of fragment subsets (%d) is too high + to test and display. Instead, %d subsets were tested + at random. The results are below """) % (total, nSubs) + else: + descr = tr(""" + For the fragments you entered, there are a total of + %d possible subsets that can restore your wallet. + The test results for all subsets are shown below""") % total + lblDescr = QRichLabel(descr) - ############################################################################# - def accept(self): - self.destroyEverything() - super(DlgFragBackup, self).accept() + lblWltIDDescr = QRichLabel(tr(""" + The wallet ID is computed from the first + address in your wallet based on the root key data (and the + "chain code"). Therefore, a matching wallet ID proves that + the wallet will produce identical addresses.""")) - ############################################################################# - def reject(self): - self.destroyEverything() - super(DlgFragBackup, self).reject() + frmResults = QFrame() + layout = QGridLayout() + row = 0 + for sub, ID in subsAndIDs: + subStrs = [str(s) for s in sub] + subText = ', '.join(subStrs[:-1]) + dispTxt = tr(""" + Fragments %s and %s produce a + wallet with ID "%s" """) % (subText, subStrs[-1], ID) + chk = lambda: QPixmap(':/checkmark32.png').scaled(20, 20) + _X_ = lambda: QPixmap(':/red_X.png').scaled(16, 16) + lblTxt = QRichLabel(dispTxt) + lblTxt.setWordWrap(False) + lblPix = QLabel('') + lblPix.setPixmap(chk() if ID == testID else _X_()) + layout.addWidget(lblTxt, row, 0) + layout.addWidget(lblPix, row, 1) + row += 1 -################################################################################ -class DlgUniversalRestoreSelect(ArmoryDialog): - ############################################################################# - def __init__(self, parent, main): - super(DlgUniversalRestoreSelect, self).__init__(parent, main) + scrollResults = QScrollArea() + frmResults = QFrame() + frmResults.setLayout(layout) + scrollResults.setWidget(frmResults) - lblDescrTitle = QRichLabel( tr(""" - Restore Wallet from Backup""")) - lblDescr = QRichLabel( tr(""" - You can restore any kind of backup ever created by Armory using - one of the options below. If you have a list of private keys - you should open the target wallet and select "Import/Sweep - Private Keys." """)) + btnOkay = QPushButton('Ok') + buttonBox = QDialogButtonBox() + buttonBox.addButton(btnOkay, QDialogButtonBox.AcceptRole) + self.connect(btnOkay, SIGNAL(CLICKED), self.accept) - lblRestore = QRichLabel(tr("""I am restoring a...""")) + mainLayout = QVBoxLayout() + mainLayout.addWidget(lblDescr) + mainLayout.addWidget(scrollResults) + mainLayout.addWidget(lblWltIDDescr) + mainLayout.addWidget(buttonBox) + self.setLayout(mainLayout) - self.rdoSingle = QRadioButton(tr('Single-Sheet Backup (printed)')) - self.rdoFragged = QRadioButton(tr('Fragmented Backup\xe2\x84\xa2 (incl. mix of paper and files)')) - self.rdoDigital = QRadioButton(tr('Import digital backup or watching-only wallet')) - self.chkTest = QCheckBox(tr('This is a test recovery to make sure my backup works')) - btngrp = QButtonGroup(self) - btngrp.addButton(self.rdoSingle) - btngrp.addButton(self.rdoFragged) - btngrp.addButton(self.rdoDigital) - btngrp.setExclusive(True) + self.setWindowTitle('Fragment Test Results') + self.setMinimumWidth(500) - self.rdoSingle.setChecked(True) - self.connect(self.rdoSingle, SIGNAL('clicked()'), self.clickedRadio) - self.connect(self.rdoFragged, SIGNAL('clicked()'), self.clickedRadio) - self.connect(self.rdoDigital, SIGNAL('clicked()'), self.clickedRadio) +################################################################################ +class DlgEnterSecurePrintCode(ArmoryDialog): + + def __init__(self, parent, main): + super(DlgEnterSecurePrintCode, self).__init__(parent, main) - self.btnOkay = QPushButton('Continue') - self.btnCancel = QPushButton('Cancel') + lblSecurePrintCodeDescr = QRichLabel(tr(""" + This fragment file requires a SecurePrint\xe2\x84\xa2 code. + You will only have to enter this code once since it is the same + on all fragments.""")) + lblSecurePrintCodeDescr.setMinimumWidth(440) + self.lblSP = QRichLabel(tr('SecurePrint\xe2\x84\xa2 Code: '), doWrap=False) + self.editSecurePrint = QLineEdit() + spFrame = makeHorizFrame([self.lblSP, self.editSecurePrint, STRETCH]) + + self.btnAccept = QPushButton("Done") + self.btnCancel = QPushButton("Cancel") + self.connect(self.btnAccept, SIGNAL(CLICKED), self.verifySecurePrintCode) + self.connect(self.btnCancel, SIGNAL(CLICKED), self.reject) buttonBox = QDialogButtonBox() - buttonBox.addButton(self.btnOkay, QDialogButtonBox.AcceptRole) + buttonBox.addButton(self.btnAccept, QDialogButtonBox.AcceptRole) buttonBox.addButton(self.btnCancel, QDialogButtonBox.RejectRole) - self.connect(self.btnOkay, SIGNAL('clicked()'), self.clickedOkay) - self.connect(self.btnCancel, SIGNAL('clicked()'), self.reject) - - + layout = QVBoxLayout() - layout.addWidget(lblDescrTitle) - layout.addWidget(lblDescr) - layout.addWidget(HLINE()) - layout.addWidget(self.rdoSingle) - layout.addWidget(self.rdoFragged) - layout.addWidget(self.rdoDigital) - layout.addWidget(HLINE()) - layout.addWidget(self.chkTest) + layout.addWidget(lblSecurePrintCodeDescr) + layout.addWidget(spFrame) layout.addWidget(buttonBox) self.setLayout(layout) - self.setMinimumWidth(450) - - def clickedRadio(self): - if self.rdoDigital.isChecked(): - self.chkTest.setChecked(False) - self.chkTest.setEnabled(False) - else: - self.chkTest.setEnabled(True) - - def clickedOkay(self): - ### Test backup option + self.setWindowTitle(tr('Enter Secure Print Code')) + + def verifySecurePrintCode(self): + # Prepare the key mask parameters + SECPRINT = HardcodedKeyMaskParams() + securePrintCode = str(self.editSecurePrint.text()).strip() + if len(securePrintCode) < 9 or \ + sum([1 if c in BASE58CHARS else 0 for c in securePrintCode]) < len(securePrintCode): + QMessageBox.critical(self, tr('Invalid Code'), tr(""" + You didn't enter a full SecurePrint\xe2\x84\xa2 code. This + code is needed to decrypt your backup file."""), QMessageBox.Ok) + return + if not SECPRINT['FUNC_CHKPWD'](securePrintCode): + QMessageBox.critical(self, tr('Bad Encryption Code'), tr(""" + The SecurePrint\xe2\x84\xa2 code you entered has an error + in it. Note that the code is case-sensitive. Please verify + you entered it correctly and try again."""), QMessageBox.Ok) + return - doTest = self.chkTest.isChecked() + self.accept() - if self.rdoSingle.isChecked(): - self.accept() - dlg = DlgRestoreSingle(self.parent, self.main, doTest) - if dlg.exec_(): - self.main.addWalletToAppAndAskAboutRescan(dlg.newWallet) - LOGINFO('Wallet Restore Complete!') - #self.main.startRescanBlockchain() - #TheBDM.rescanBlockchain('AsNeeded', wait=False) - - elif self.rdoFragged.isChecked(): - self.accept() - dlg = DlgRestoreFragged(self.parent, self.main, doTest) - if dlg.exec_(): - self.main.addWalletToAppAndAskAboutRescan(dlg.newWallet) - LOGINFO('Wallet Restore Complete!') - #TheBDM.main.startRescanBlockchain() - elif self.rdoDigital.isChecked(): - self.main.execGetImportWltName() - self.accept() +################################################################################ +class DlgEnterOneFrag(ArmoryDialog): + def __init__(self, parent, main, fragList=[], wltType=UNKNOWN, securePrintCode=None): + super(DlgEnterOneFrag, self).__init__(parent, main) + self.fragData = [] + BLUE = htmlColor('TextBlue') + already = '' + if len(fragList) > 0: + strList = ['%d' % (BLUE, f) for f in fragList] + replStr = '[' + ','.join(strList[:]) + ']' + already = tr(""" You have entered fragments %s, so far. """) % replStr -################################################################################ -# Create a special QLineEdit with a masked input -# Forces the cursor to start at position 0 whenever there is no input -class MaskedInputLineEdit(QLineEdit): - - def __init__(self, inputMask): - super(MaskedInputLineEdit, self).__init__() - self.setInputMask(inputMask) - fixFont = GETFONT('Fix', 9) - self.setFont(fixFont) - self.setMinimumWidth( tightSizeStr(fixFont, inputMask)[0]+10) - self.connect(self, SIGNAL('cursorPositionChanged(int,int)'), self.controlCursor) - - def controlCursor(self, oldpos, newpos): - if newpos != 0 and len(str(self.text()).strip())==0: - self.setCursorPosition(0) + lblDescr = QRichLabel(tr(""" + Enter Another Fragment...

    %s + The fragments can be entered in any order, as long as you provide + enough of them to restore the wallet. If any fragments use a + SecurePrint\xe2\x84\xa2 code, please enter it once on the + previous window, and it will be applied to all fragments that + require it.""") % already) + self.version0Button = QRadioButton(BACKUP_TYPE_0_TEXT, self) + self.version135aButton = QRadioButton(BACKUP_TYPE_135a_TEXT, self) + self.version135aSPButton = QRadioButton(BACKUP_TYPE_135a_SP_TEXT, self) + self.version135cButton = QRadioButton(BACKUP_TYPE_135c_TEXT, self) + self.version135cSPButton = QRadioButton(BACKUP_TYPE_135c_SP_TEXT, self) + self.backupTypeButtonGroup = QButtonGroup(self) + self.backupTypeButtonGroup.addButton(self.version0Button) + self.backupTypeButtonGroup.addButton(self.version135aButton) + self.backupTypeButtonGroup.addButton(self.version135aSPButton) + self.backupTypeButtonGroup.addButton(self.version135cButton) + self.backupTypeButtonGroup.addButton(self.version135cSPButton) + self.version135cButton.setChecked(True) + self.connect(self.backupTypeButtonGroup, SIGNAL('buttonClicked(int)'), self.changeType) -################################################################################ -class DlgRestoreSingle(ArmoryDialog): - ############################################################################# - def __init__(self, parent, main, thisIsATest=False, expectWltID=None): - super(DlgRestoreSingle, self).__init__(parent, main) + # This value will be locked after the first fragment is entered. + if wltType == UNKNOWN: + self.version135cButton.setChecked(True) + elif wltType == '0': + self.version0Button.setChecked(True) + self.version135aButton.setEnabled(False) + self.version135aSPButton.setEnabled(False) + self.version135cButton.setEnabled(False) + self.version135cSPButton.setEnabled(False) + elif wltType == BACKUP_TYPE_135A: + # Could be 1.35a with or without SecurePrintCode so remove the rest + self.version0Button.setEnabled(False) + self.version135cButton.setEnabled(False) + self.version135cSPButton.setEnabled(False) + if securePrintCode: + self.version135aSPButton.setChecked(True) + else: + self.version135aButton.setChecked(True) + elif wltType == BACKUP_TYPE_135C: + # Could be 1.35c with or without SecurePrintCode so remove the rest + self.version0Button.setEnabled(False) + self.version135aButton.setEnabled(False) + self.version135aSPButton.setEnabled(False) + if securePrintCode: + self.version135cSPButton.setChecked(True) + else: + self.version135cButton.setChecked(True) - self.thisIsATest = thisIsATest - self.testWltID = expectWltID - headerStr = '' - if thisIsATest: - lblDescr = QRichLabel( tr(""" - Test a Paper Backup -

    - Use this window to test a single-sheet paper backup. If your - backup includes imported keys, those will not be covered by this test. """)) - else: - lblDescr = QRichLabel( tr(""" - Restore a Wallet from Paper Backup -

    - Use this window to restore a single-sheet paper backup. - If your backup includes extra pages with - imported keys, please restore the base wallet first, then - double-click the restored wallet and select "Import Private - Keys" from the right-hand menu. -

    - If your backup consists of multiple "fragments," then cancel - out of this window and choose "Fragmented Backup" from the - "Restore Wallet" menu.""")) - + lblType = QRichLabel(tr("""Backup Type:"""), doWrap=False) - lblType = QRichLabel( tr("""Backup Type:"""), doWrap=False) + layoutRadio = QVBoxLayout() + layoutRadio.addWidget(self.version0Button) + layoutRadio.addWidget(self.version135aButton) + layoutRadio.addWidget(self.version135aSPButton) + layoutRadio.addWidget(self.version135cButton) + layoutRadio.addWidget(self.version135cSPButton) + layoutRadio.setSpacing(0) - self.comboBackupType = QComboBox() - self.comboBackupType.clear() - self.comboBackupType.addItem( tr('Version 1.35')) - self.comboBackupType.addItem( tr('Version 1.35a (Unencrypted)')) - self.comboBackupType.addItem( tr('Version 1.35a (with SecurePrint\xe2\x84\xa2)')) - self.comboBackupType.addItem( tr('Version 1.35c (Unencrypted)')) - self.comboBackupType.addItem( tr('Version 1.35c (with SecurePrint\xe2\x84\xa2)')) - self.comboBackupType.setCurrentIndex(3) - + radioButtonFrame = QFrame() + radioButtonFrame.setLayout(layoutRadio) - self.connect(self.comboBackupType, SIGNAL('activated(int)'), self.changeType) - frmCombo = makeHorizFrame([lblType, 'Space(20)', self.comboBackupType, 'Stretch']) + frmBackupType = makeVertFrame([lblType, radioButtonFrame]) - self.lblSP = QRichLabel(tr('SecurePrint\xe2\x84\xa2 Code:'), doWrap=False) - self.edtSP = QLineEdit() - self.prfxList = [QLabel(tr('Root Key:')), QLabel(''), QLabel(tr('Chaincode:')), QLabel('')] - + self.prfxList = ['x1:', 'x2:', 'x3:', 'x4:', \ + 'y1:', 'y2:', 'y3:', 'y4:', \ + 'F1:', 'F2:', 'F3:', 'F4:'] + self.prfxList = [QLabel(p) for p in self.prfxList] inpMask = '
    - The error occured on line #%d.""") % i, \ - QMessageBox.Ok) - LOGERROR('Error in wallet restore field') - self.prfxList[i].setText(''+str(self.prfxList[i].text())+'') - return + self.frmSP.setVisible(sel == self.backupTypeButtonGroup.id(self.version135aSPButton) or \ + sel == self.backupTypeButtonGroup.id(self.version135cSPButton)) + for i in range(12): + self.prfxList[i].setVisible(visList[i] == 1) + self.edtList[ i].setVisible(visList[i] == 1) - inputLines.append(rawBin) - if nError>0: - pluralStr = 'error' if nError==1 else 'errors' - QMessageBox.question(self, 'Errors Corrected!', \ - 'Detected ' + str(nError) + ' ' + pluralStr + ' ' - 'in the data you entered. Armory attempted to fix the ' + - pluralStr + ' but it is not always right. Be sure ' - 'to verify the "Wallet Unique ID" closely on the next window.', \ - QMessageBox.Ok) - privKey = SecureBinaryData(''.join(inputLines[:2])) - if self.isLongForm: - chain = SecureBinaryData(''.join(inputLines[2:])) - + ############################################################################# + def destroyFragData(self): + for line in self.fragData: + if not isinstance(line, basestring): + # It's an SBD Object. Destroy it. + line.destroy() + ############################################################################# + def isSecurePrintID(self): + return hex_to_int(str(self.edtID.text()[:2])) > 127 + + ############################################################################# + def verifyUserInput(self): + self.fragData = [] + nError = 0 + rawBin = None - if self.doMask: + sel = self.backupTypeButtonGroup.checkedId() + rng = [-1] + if sel == self.backupTypeButtonGroup.id(self.version0Button): + rng = range(8) + elif sel == self.backupTypeButtonGroup.id(self.version135aButton) or \ + sel == self.backupTypeButtonGroup.id(self.version135aSPButton): + rng = range(8, 12) + elif sel == self.backupTypeButtonGroup.id(self.version135cButton) or \ + sel == self.backupTypeButtonGroup.id(self.version135cSPButton): + rng = range(8, 10) + + + if sel == self.backupTypeButtonGroup.id(self.version135aSPButton) or \ + sel == self.backupTypeButtonGroup.id(self.version135cSPButton): # Prepare the key mask parameters SECPRINT = HardcodedKeyMaskParams() - pwd = str(self.edtSP.text()).strip() - if len(pwd)<9: + securePrintCode = str(self.editSecurePrint.text()).strip() + if len(securePrintCode) < 9 or \ + sum([1 if c in BASE58CHARS else 0 for c in securePrintCode]) < len(securePrintCode): QMessageBox.critical(self, 'Invalid Code', tr(""" You didn't enter a full SecurePrint\xe2\x84\xa2 code. This - code is needed to decrypt your backup. If this backup is + code is needed to decrypt your backup. If this backup is actually unencrypted and there is no code, then choose the appropriate backup type from the drop-down box"""), QMessageBox.Ok) return - if not SECPRINT['FUNC_CHKPWD'](pwd): + if not SECPRINT['FUNC_CHKPWD'](securePrintCode): QMessageBox.critical(self, 'Bad Encryption Code', tr(""" - The SecurePrint\xe2\x84\xa2 code you entered has an error + The SecurePrint\xe2\x84\xa2 code you entered has an error in it. Note that the code is case-sensitive. Please verify you entered it correctly and try again."""), QMessageBox.Ok) return - - maskKey = SECPRINT['FUNC_KDF'](pwd) - privKey = SECPRINT['FUNC_UNMASK'](privKey, ekey=maskKey) - if self.isLongForm: - chain = SECPRINT['FUNC_UNMASK'](chain, ekey=maskKey) - - if not self.isLongForm: - chain = DeriveChaincodeFromRootKey(privKey) + elif self.isSecurePrintID(): + QMessageBox.critical(self, 'Bad Encryption Code', tr(""" + The ID field indicates that this is a SecurePrint\xe2\x84\xa2 + Backup Type. You have either entered the ID incorrectly or + have chosen an incorrect Backup Type."""), QMessageBox.Ok) + return + for i in rng: + hasError = False + try: + rawEntry = str(self.edtList[i].text()) + rawBin, err = readSixteenEasyBytes(rawEntry.replace(' ', '')) + if err == 'Error_2+': + hasError = True + elif err == 'Fixed_1': + nError += 1 + except KeyError: + hasError = True - # If we got here, the data is valid, let's create the wallet and accept the dlg - # Now we should have a fully-plaintext rootkey and chaincode - root = PyBtcAddress().createFromPlainKeyData(privKey) - root.chaincode = chain + if hasError: + reply = QMessageBox.critical(self, tr('Verify Wallet ID'), tr(""" + There is an error in the data you entered that could not be + fixed automatically. Please double-check that you entered the + text exactly as it appears on the wallet-backup page.

    + The error occured on the "%s" line.""") % \ + str(self.prfxList[i].text()), QMessageBox.Ok) + LOGERROR('Error in wallet restore field') + self.prfxList[i].setText('' + str(self.prfxList[i].text()) + '') + self.destroyFragData() + return - first = root.extendAddressChain() - newWltID = binary_to_base58((ADDRBYTE + first.getAddr160()[:5])[::-1]) + self.fragData.append(SecureBinaryData(rawBin)) + rawBin = None - # Stop here if this was just a test - if self.thisIsATest: - verifyRecoveryTestID(self, newWltID, self.testWltID) - return - + idLine = str(self.edtID.text()).replace(' ', '') + self.fragData.insert(0, hex_to_binary(idLine)) + M, fnum, wltID, doMask, fid = ReadFragIDLineBin(self.fragData[0]) - if self.main.walletMap.has_key(newWltID): - QMessageBox.question(self, 'Duplicate Wallet!', \ - 'The data you entered is for a wallet with a ID: \n\n \t' + - newWltID + '\n\nYou already own this wallet! \n ' - 'Nothing to do...', QMessageBox.Ok) - self.reject() - return - - - - reply = QMessageBox.question(self, 'Verify Wallet ID', \ - 'The data you entered corresponds to a wallet with a wallet ID: \n\n \t' + - newWltID + '\n\nDoes this ID match the "Wallet Unique ID" ' - 'printed on your paper backup? If not, click "No" and reenter ' - 'key and chain-code data again.', \ - QMessageBox.Yes | QMessageBox.No) - if reply==QMessageBox.No: - return + reply = QMessageBox.question(self, tr('Verify Fragment ID'), tr(""" + The data you entered is for fragment: +

    %s

    + Does this ID match the "Fragment:" field displayed on your backup? + If not, click "No" and re-enter the fragment data.""") % \ + (htmlColor('TextBlue'), fid), QMessageBox.Yes | QMessageBox.No) - passwd = [] - if self.chkEncrypt.isChecked(): - dlgPasswd = DlgChangePassphrase(self, self.main) - if dlgPasswd.exec_(): - passwd = SecureBinaryData(str(dlgPasswd.edtPasswd1.text())) - else: - QMessageBox.critical(self, 'Cannot Encrypt', \ - 'You requested your restored wallet be encrypted, but no ' - 'valid passphrase was supplied. Aborting wallet recovery.', \ - QMessageBox.Ok) - return + if reply == QMessageBox.Yes: + self.accept() - if passwd: - self.newWallet = PyBtcWallet().createNewWallet( \ - plainRootKey=privKey, \ - chaincode=chain, \ - shortLabel='Restored - %s'%newWltID, \ - withEncrypt=True, \ - securePassphrase=passwd, \ - kdfTargSec=0.25, \ - kdfMaxMem=32*1024*1024, \ - isActuallyNew=False, \ - doRegisterWithBDM=False) - else: - self.newWallet = PyBtcWallet().createNewWallet( \ - plainRootKey=privKey, \ - chaincode=chain, \ - shortLabel='Restored - %s'%newWltID, \ - withEncrypt=False,\ - isActuallyNew=False, \ - doRegisterWithBDM=False) - def fillAddrPoolAndAccept(): - self.newWallet.fillAddressPool() - self.accept() - # Will pop up a little "please wait..." window while filling addr pool - DlgExecLongProcess(fillAddrPoolAndAccept, "Recovering wallet...", self, self.main).exec_() +################################################################################ +def verifyRecoveryTestID(parent, computedWltID, expectedWltID=None): + + if expectedWltID == None: + # Testing an arbitrary paper backup + yesno = QMessageBox.question(parent, tr('Recovery Test'), tr(""" + From the data you entered, Armory calculated the following + wallet ID: %s +

    + Does this match the wallet ID on the backup you are + testing?""") % computedWltID, QMessageBox.Yes | QMessageBox.No) + + if yesno == QMessageBox.No: + QMessageBox.critical(parent, tr('Bad Backup!'), tr(""" + If this is your only backup and you are sure that you entered + the data correctly, then it is highly recommended you stop using + this wallet! If this wallet currently holds any funds, + you should move the funds to a wallet that does + have a working backup. +



    + Wallet ID of the data you entered: %s
    """) % computedWltID, \ + QMessageBox.Ok) + elif yesno == QMessageBox.Yes: + MsgBoxCustom(MSGBOX.Good, tr('Backup is Good!'), tr(""" + Your backup works! +

    + The wallet ID is computed from a combination of the root + private key, the "chaincode" and the first address derived + from those two pieces of data. A matching wallet ID + guarantees it will produce the same chain of addresses as + the original.""")) + else: # an expected wallet ID was supplied + if not computedWltID == expectedWltID: + QMessageBox.critical(parent, tr('Bad Backup!'), tr(""" + If you are sure that you entered the backup information + correctly, then it is highly recommended you stop using + this wallet! If this wallet currently holds any funds, + you should move the funds to a wallet that does + have a working backup. +

    + Computed wallet ID: %s
    + Expected wallet ID: %s

    + Is it possible that you loaded a different backup than the + one you just made? """ % (computedWltID, expectedWltID)), \ + QMessageBox.Ok) + else: + MsgBoxCustom(MSGBOX.Good, tr('Backup is Good!'), tr(""" + Your backup works! +

    + The wallet ID computed from the data you entered matches + the expected ID. This confirms that the backup produces + the same sequence of private keys as the original wallet! +

    + Computed wallet ID: %s
    + Expected wallet ID: %s
    +
    + """ % (computedWltID, expectedWltID))) +################################################################################ +def finishPrintingBackup(parent, btype=None): + openTestDlg = False + msg = tr(""" + Please make sure that any printed backups you create (excluding any "ID" lines) have nine + columns of four letters each + each. + If you just made a paper backup, it is important that you test it + to make sure that it was printed or copied correctly. Most importantly, + """) + + if btype == None: + QMessageBox.warning(parent, tr('Test Your Backup!'), tr(""" + """)) ################################################################################ -class DlgRestoreFragged(ArmoryDialog): - def __init__(self, parent, main, thisIsATest=False, expectWltID=None): - super(DlgRestoreFragged, self).__init__(parent, main) +class DlgReplaceWallet(ArmoryDialog): - self.thisIsATest = thisIsATest - self.testWltID = expectWltID - headerStr = '' - if thisIsATest: - headerStr = tr("""Testing a - Fragmented Backup\xe2\x84\xa2""") - else: - headerStr = tr('Restore Wallet from Fragments') + ############################################################################# + def __init__(self, WalletID, parent, main): + super(DlgReplaceWallet, self).__init__(parent, main) - descr = tr(""" - %s

    - Use this form to enter all the fragments to be restored. Fragments - can be stored on a mix of paper printouts, and saved files. - If any of the fragments require a SecurePrint\xe2\x84\xa2 code, - you will only have to enter it once, since that code is the same for - all fragments of any given wallet. """) % headerStr + lblDesc = QLabel(tr(""" + You already have this wallet loaded!
    + You can choose to:
    + - Cancel wallet restore operation
    + - Set new password and fix any errors
    + - Overwrite old wallet (delete comments & labels)
    + """)) - if self.thisIsATest: - descr += tr("""

    - For testing purposes, you may enter more fragments than needed - and Armory will test all subsets of the entered fragments to verify - that each one still recovers the wallet successfully.""") + self.WalletID = WalletID + self.main = main + self.Meta = None + self.output = 0 - lblDescr = QRichLabel( descr ) + self.wltPath = main.walletMap[WalletID].walletPath - frmDescr = makeHorizFrame([lblDescr], STYLE_RAISED) + self.btnAbort = QPushButton('Cancel') + self.btnReplace = QPushButton('Overwrite') + self.btnSaveMeta = QPushButton('Set New Password') - # HLINE + self.connect(self.btnAbort, SIGNAL('clicked()'), self.reject) + self.connect(self.btnReplace, SIGNAL('clicked()'), self.Replace) + self.connect(self.btnSaveMeta, SIGNAL('clicked()'), self.SaveMeta) - self.scrollFragInput = QScrollArea() - self.scrollFragInput.setWidgetResizable(True) - self.scrollFragInput.setMinimumHeight(150) + layoutDlg = QGridLayout() - lblFragList = QRichLabel(tr('Input Fragments Below:'), doWrap=False, bold=True) - self.btnAddFrag = QPushButton(tr('+Frag')) - self.btnRmFrag = QPushButton(tr('-Frag')) - self.btnRmFrag.setVisible(False) - self.connect(self.btnAddFrag, SIGNAL('clicked()'), self.addFragment) - self.connect(self.btnRmFrag, SIGNAL('clicked()'), self.removeFragment) - self.chkEncrypt = QCheckBox('Encrypt Restored Wallet') - self.chkEncrypt.setChecked(True) - frmAddRm = makeHorizFrame([self.chkEncrypt, 'Stretch', self.btnRmFrag, self.btnAddFrag]) + layoutDlg.addWidget(lblDesc, 0, 0, 4, 4) + layoutDlg.addWidget(self.btnAbort, 4, 0, 1, 1) + layoutDlg.addWidget(self.btnSaveMeta, 4, 1, 1, 1) + layoutDlg.addWidget(self.btnReplace, 4, 2, 1, 1) - if thisIsATest: - self.chkEncrypt.setChecked(False) - self.chkEncrypt.setVisible(False) + self.setLayout(layoutDlg) + self.setWindowTitle('Wallet already exists') - self.fragDataMap = {} - self.tableSize = 2 - self.wltType = UNKNOWN - self.fragIDPrefix = UNKNOWN + ######### + def Replace(self): + self.main.removeWalletFromApplication(self.WalletID) + + datestr = RightNowStr('%Y-%m-%d-%H%M') + homedir = os.path.dirname(self.wltPath) - doItText = tr('Test Backup' if thisIsATest else 'Restore from Fragments') + oldpath = os.path.join(homedir, self.WalletID, datestr) + try: + if not os.path.exists(oldPath): + os.makedirs(oldpath) + except: + LOGEXCEPT('Cannot create new folder in dataDir! Missing credentials?') + self.reject() + return - btnExit = QPushButton(tr('Cancel')) - self.btnRestore = QPushButton(doItText) - self.connect(btnExit, SIGNAL('clicked()'), self.reject) - self.connect(self.btnRestore, SIGNAL('clicked()'), self.processFrags) - frmBtns = makeHorizFrame([btnExit, 'Stretch', self.btnRestore]) + oldname = os.path.basename(self.wltPath) + self.newname = os.path.join(oldpath, '%s_old.wallet' % (oldname[0:-7])) - self.lblRightFrm = QRichLabel('', hAlign=Qt.AlignHCenter ) - self.lblSecureStr = QRichLabel(tr('SecurePrint\xe2\x84\xa2 Code:'), \ - hAlign=Qt.AlignHCenter, \ - color='TextWarn') - self.edtSecureStr = QLineEdit() - self.imgPie = QRichLabel('', hAlign=Qt.AlignHCenter) - self.lblReqd = QRichLabel('', hAlign=Qt.AlignHCenter) - self.lblWltID = QRichLabel('', doWrap=False, hAlign=Qt.AlignHCenter) - self.lblFragID = QRichLabel('', doWrap=False, hAlign=Qt.AlignHCenter) - self.lblSecureStr.setVisible(False) - self.edtSecureStr.setVisible(False) - self.edtSecureStr.setMaximumWidth( relaxedSizeNChar(self.edtSecureStr, 16)[0]) - frmSecPair = makeVertFrame([self.lblSecureStr, self.edtSecureStr]) - frmSecCtr = makeHorizFrame(['Stretch', frmSecPair, 'Stretch']) + os.rename(self.wltPath, self.newname) - frmWltInfo = makeVertFrame( ['Stretch', - self.lblRightFrm, - self.imgPie, - self.lblReqd, - self.lblWltID, - self.lblFragID, - HLINE(), - frmSecCtr, - 'Strut(200)', - 'Stretch'], STYLE_SUNKEN) + backup = '%s_backup.wallet' % (self.wltPath[0:-7]) + if os.path.exists(backup): + os.remove(backup) - - layout = QGridLayout() - layout.addWidget(frmDescr, 0,0, 1,2) - layout.addWidget(frmAddRm, 1,0, 1,1) - layout.addWidget(self.scrollFragInput, 2,0, 1,1) - layout.addWidget(frmWltInfo, 1,1, 2,1) - layout.addWidget(frmBtns, 3,0, 1,2) - setLayoutStretchCols(layout, 1,0) - self.setLayout(layout) - self.setMinimumWidth(650) - self.setMinimumHeight(465) - self.setWindowTitle(tr('Restore wallet from fragments')) - - self.makeFragInputTable() - self.checkRestoreParams() + self.output =1 + self.accept() + ######### + def SaveMeta(self): + from armoryengine.PyBtcWalletRecovery import PyBtcWalletRecovery - def makeFragInputTable(self, addCount=0): - - self.tableSize += addCount - newLayout = QGridLayout() - newFrame = QFrame() - self.fragsDone = [] - newLayout.addWidget(HLINE(), 0,0, 1,5) - for i in range(self.tableSize): - btnEnter = QPushButton(tr('Type Data')) - btnLoad = QPushButton(tr('Load File')) - btnClear = QPushButton(tr('Clear')) - lblFragID = QRichLabel('', doWrap=False) - lblSecure = QLabel('') - if i in self.fragDataMap: - M,fnum,wltID,doMask,fid = ReadFragIDLineBin(self.fragDataMap[i][0]) - self.fragsDone.append(fnum) - lblFragID.setText(''+fid+'') - if doMask: - lblFragID.setText(''+fid+'', color='TextWarn') - + metaProgress = DlgProgress(self, self.main, Title='Ripping Meta Data') + getMeta = PyBtcWalletRecovery() + self.Meta = metaProgress.exec_(getMeta.ProcessWallet, + WalletPath=self.wltPath, + Mode=RECOVERMODE.Meta, + Progress=metaProgress.UpdateText) + self.Replace() - self.connect(btnEnter, SIGNAL('clicked()'), \ - functools.partial(self.dataEnter, fnum=i)) - self.connect(btnLoad, SIGNAL('clicked()'), \ - functools.partial(self.dataLoad, fnum=i)) - self.connect(btnClear, SIGNAL('clicked()'), \ - functools.partial(self.dataClear, fnum=i)) +############################################################################### +class DlgWltRecoverWallet(ArmoryDialog): + def __init__(self, parent=None, main=None): + super(DlgWltRecoverWallet, self).__init__(parent, main) + + self.edtWalletPath = QLineEdit() + self.edtWalletPath.setFont(GETFONT('Fixed', 9)) + edtW,edtH = tightSizeNChar(self.edtWalletPath, 50) + self.edtWalletPath.setMinimumWidth(edtW) + self.btnWalletPath = QPushButton('Browse File System') + + self.connect(self.btnWalletPath, SIGNAL('clicked()'), self.selectFile) + + lblDesc = QRichLabel(tr(""" + Wallet Recovery Tool: +
    + This tool will recover data from damaged or inconsistent + wallets. Specify a wallet file and Armory will analyze the + wallet and fix any errors with it. +

    + If any problems are found with the specified + wallet, Armory will provide explanation and instructions to + transition to a new wallet. """) % htmlColor('TextWarn')) + lblDesc.setScaledContents(True) + + lblWalletPath = QRichLabel(tr('Wallet Path:')) + + self.selectedWltID = None + + def doWltSelect(): + dlg = DlgWalletSelect(self, self.main, tr('Select Wallet...'), '') + if dlg.exec_(): + self.selectedWltID = dlg.selectedID + wlt = self.parent.walletMap[dlg.selectedID] + self.edtWalletPath.setText(wlt.walletPath) + + self.btnWltSelect = QPushButton(tr("Select Loaded Wallet")) + self.connect(self.btnWltSelect, SIGNAL(CLICKED), doWltSelect) - newLayout.addWidget(btnEnter, 2*i+1,0) - newLayout.addWidget(btnLoad, 2*i+1,1) - newLayout.addWidget(btnClear, 2*i+1,2) - newLayout.addWidget(lblFragID, 2*i+1,3) - newLayout.addWidget(lblSecure, 2*i+1,4) - newLayout.addWidget(HLINE(), 2*i+2,0, 1,5) + layoutMgmt = QGridLayout() + wltSltQF = QFrame() + wltSltQF.setFrameStyle(STYLE_SUNKEN) + + layoutWltSelect = QGridLayout() + layoutWltSelect.addWidget(lblWalletPath, 0,0, 1, 1) + layoutWltSelect.addWidget(self.edtWalletPath, 0,1, 1, 3) + layoutWltSelect.addWidget(self.btnWltSelect, 1,0, 1, 2) + layoutWltSelect.addWidget(self.btnWalletPath, 1,2, 1, 2) + layoutWltSelect.setColumnStretch(0, 0) + layoutWltSelect.setColumnStretch(1, 1) + layoutWltSelect.setColumnStretch(2, 1) + layoutWltSelect.setColumnStretch(3, 0) + + wltSltQF.setLayout(layoutWltSelect) + + layoutMgmt.addWidget(makeHorizFrame([lblDesc], STYLE_SUNKEN), 0,0, 2,4) + layoutMgmt.addWidget(wltSltQF, 2, 0, 3, 4) + + self.rdbtnStripped = QRadioButton('', parent=self) + self.connect(self.rdbtnStripped, SIGNAL('event()'), self.rdClicked) + lblStripped = QLabel('Stripped Recovery
    Only attempts to \ + recover the wallet\'s rootkey and chaincode') + layout_StrippedH = QGridLayout() + layout_StrippedH.addWidget(self.rdbtnStripped, 0, 0, 1, 1) + layout_StrippedH.addWidget(lblStripped, 0, 1, 2, 19) + + self.rdbtnBare = QRadioButton('') + lblBare = QLabel('Bare Recovery
    Attempts to recover all private key related data') + layout_BareH = QGridLayout() + layout_BareH.addWidget(self.rdbtnBare, 0, 0, 1, 1) + layout_BareH.addWidget(lblBare, 0, 1, 2, 19) + + self.rdbtnFull = QRadioButton('') + self.rdbtnFull.setChecked(True) + lblFull = QLabel('Full Recovery
    Attempts to recover as much data as possible') + layout_FullH = QGridLayout() + layout_FullH.addWidget(self.rdbtnFull, 0, 0, 1, 1) + layout_FullH.addWidget(lblFull, 0, 1, 2, 19) + + self.rdbtnCheck = QRadioButton('') + lblCheck = QLabel('Consistency Check
    Checks wallet consistency. Works will both full and watch only
    wallets.' + ' Unlocking of encrypted wallets is not mandatory') + layout_CheckH = QGridLayout() + layout_CheckH.addWidget(self.rdbtnCheck, 0, 0, 1, 1) + layout_CheckH.addWidget(lblCheck, 0, 1, 3, 19) + + + layoutMode = QGridLayout() + layoutMode.addLayout(layout_StrippedH, 0, 0, 2, 4) + layoutMode.addLayout(layout_BareH, 2, 0, 2, 4) + layoutMode.addLayout(layout_FullH, 4, 0, 2, 4) + layoutMode.addLayout(layout_CheckH, 6, 0, 3, 4) + + + #self.rdnGroup = QButtonGroup() + #self.rdnGroup.addButton(self.rdbtnStripped) + #self.rdnGroup.addButton(self.rdbtnBare) + #self.rdnGroup.addButton(self.rdbtnFull) + #self.rdnGroup.addButton(self.rdbtnCheck) - btnFrame = QFrame() - btnFrame.setLayout(newLayout) - frmFinal = makeVertFrame([btnFrame, 'Stretch'], STYLE_SUNKEN) - self.scrollFragInput.setWidget(frmFinal) + layoutMgmt.addLayout(layoutMode, 5, 0, 9, 4) + """ + wltModeQF = QFrame() + wltModeQF.setFrameStyle(STYLE_SUNKEN) + wltModeQF.setLayout(layoutMode) + + layoutMgmt.addWidget(wltModeQF, 5, 0, 9, 4) + wltModeQF.setVisible(False) + + + btnShowAllOpts = QLabelButton(tr("All Recovery Options>>>")) + frmBtn = makeHorizFrame(['Stretch', btnShowAllOpts, 'Stretch'], STYLE_SUNKEN) + layoutMgmt.addWidget(frmBtn, 5, 0, 9, 4) + + def expandOpts(): + wltModeQF.setVisible(True) + btnShowAllOpts.setVisible(False) + self.connect(btnShowAllOpts, SIGNAL('clicked()'), expandOpts) + + if not self.main.usermode==USERMODE.Expert: + frmBtn.setVisible(False) + """ + + self.btnRecover = QPushButton('Recover') + self.btnCancel = QPushButton('Cancel') + layout_btnH = QHBoxLayout() + layout_btnH.addWidget(self.btnRecover, 1) + layout_btnH.addWidget(self.btnCancel, 1) + + layoutMgmt.addLayout(layout_btnH, 14, 1, 1, 2) + + self.connect(self.btnRecover, SIGNAL('clicked()'), self.accept) + self.connect(self.btnCancel , SIGNAL('clicked()'), self.reject) + + self.setLayout(layoutMgmt) + self.layout().setSizeConstraint(QLayout.SetFixedSize) + self.setWindowTitle('Wallet Recovery Tool') + self.setMinimumWidth(550) + + def rdClicked(self): + print "cliocked" + + def promptWalletRecovery(self): + """ + Prompts the user with a window asking for wallet path and recovery mode. + Proceeds to Recover the wallet. Prompt for password if the wallet is locked + """ + if self.exec_(): + path = str(self.edtWalletPath.text()) + mode = RECOVERMODE.Bare + if self.rdbtnStripped.isChecked(): + mode = RECOVERMODE.Stripped + elif self.rdbtnFull.isChecked(): + mode = RECOVERMODE.Full + elif self.rdbtnCheck.isChecked(): + mode = RECOVERMODE.Check + + if mode==RECOVERMODE.Full and self.selectedWltID: + # Funnel all standard, full recovery operations through the + # inconsistent-wallet-dialog. + wlt = self.main.walletMap[self.selectedWltID] + dlgRecoveryUI = DlgCorruptWallet(wlt, [], self.main, self, False) + dlgRecoveryUI.exec_(dlgRecoveryUI.doFixWallets()) + else: + # This is goatpig's original behavior - preserved for any + # non-loaded wallets or non-full recovery operations. + if self.selectedWltID: + wlt = self.main.walletMap[self.selectedWltID] + else: + wlt = path + + dlgRecoveryUI = DlgCorruptWallet(wlt, [], self.main, self, False) + dlgRecoveryUI.exec_(dlgRecoveryUI.ProcessWallet(mode)) + else: + return False - self.btnAddFrag.setVisible(self.tableSize<12) - self.btnRmFrag.setVisible(self.tableSize>2) + def selectFile(self): + # Had to reimplement the path selection here, because the way this was + # implemented doesn't let me access self.main.getFileLoad + ftypes = 'Wallet files (*.wallet);; All files (*)' + if not OS_MACOSX: + pathSelect = unicode(QFileDialog.getOpenFileName(self, \ + tr('Recover Wallet'), \ + ARMORY_HOME_DIR, \ + ftypes)) + else: + pathSelect = unicode(QFileDialog.getOpenFileName(self, \ + tr('Recover Wallet'), \ + ARMORY_HOME_DIR, \ + ftypes, \ + options=QFileDialog.DontUseNativeDialog)) + self.edtWalletPath.setText(pathSelect) - ############################################################################# - def addFragment(self): - self.makeFragInputTable(1) - ############################################################################# - def removeFragment(self): - self.makeFragInputTable(-1) - toRemove = [] - for key,val in self.fragDataMap.iteritems(): - if key >= self.tableSize: - toRemove.append(key) +############################################################################### +class DlgProgress(ArmoryDialog): + """ + Progress bar dialog. The dialog is guaranteed to be created from the main + thread. + + The dialog is modal, meaning all other windows are barred from user + interaction as long as this dialog is within its message loop. + The message loop is entered either through exec_(side_thread), which will + which will lock the main threa and the caller thread, and join on the + side thread + + The dialog reject() signal is overloaded to render it useless. The dialog + cannot be killed through regular means. To kill the Dialog, call Kill() + or end the side thread. Either will release the main thread. The caller + will still join on the side thread if you only call Kill() + + To make a progress dialog that can be killed by the user (before the process + is complete), pass a string to Interrupt. It will add a push button with + that text, that will kill the progress dialog on click. The caller will + still be joining on the side thread. + + Passing a string to Title will draw a title. + Passing an integer to HBar will draw a progress bar with a Max value set to + that integer. It can be updated through UpdateHBar(int) + Passing a string TProgress will draw a label with that string. It can be + updated through UpdateText(str) + """ + def __init__(self, parent=None, main=None, Interrupt=None, HBar=None, + Title=None, TProgress=None): - # Have to do this in a separate loop, cause you can't remove items - # from a map while you are iterating over them - for key in toRemove: - self.dataClear(key) + self.running = 1 + self.Done = 0 + self.status = 0 + self.main = main + self.parent = parent + self.Interrupt = Interrupt + self.HBar = HBar + self.Title = Title + self.TProgress = None + self.procressDone = False + + self.lock = threading.Lock() + self.condVar = threading.Condition(self.lock) + self.btnStop = None - ############################################################################# - def dataEnter(self, fnum): - dlg = DlgEnterOneFrag(self, self.main, self.fragsDone, self.wltType) - if dlg.exec_(): - LOGINFO('Good data from enter_one_frag exec! %d', fnum) - self.addFragToTable(fnum, dlg.fragData) - self.makeFragInputTable() + if main is not None: + main.emit(SIGNAL('initTrigger'), self) + else: return + while self.status == 0: + time.sleep(0.01) - ############################################################################# - def dataLoad(self, fnum): - LOGINFO('Loading data for entry, %d', fnum) - toLoad = unicode(self.main.getFileLoad(tr('Load Fragment File'), \ - [tr('Wallet Fragments (*.frag)')])) + self.connectDlg() - if len(toLoad)==0: - return + def connectDlg(self): + self.connect(self, SIGNAL('Update'), self.UpdateDlg) + self.connect(self, SIGNAL('PromptPassphrase'), self.PromptPassphrase) + self.connect(self, SIGNAL('Exit'), self.Exit) - if not os.path.exists(toLoad): - LOGERROR('File just chosen does not exist! %s', toLoad) - QMessageBox.critical(self, tr('File Does Not Exist'), tr(""" - The file you select somehow does not exist...? -

    %s

    Try a different file""") % toLoad, \ - QMessageBox.Ok) + def UpdateDlg(self, text=None, HBar=None, Title=None): + if text is not None: self.lblDesc.setText(text) + if HBar is not None: self.hbarProgress.setValue(HBar) - fragMap = {} - with open(toLoad,'r') as fin: - allData = [line.strip() for line in fin.readlines()] - fragMap = {} - for line in allData: - if line[:2].lower() in ['id','x1','x2','x3','x4',\ - 'y1','y2','y3','y4',\ - 'f1','f2','f3','f4']: - fragMap[line[:2].lower()] = line[3:].strip().replace(' ','') - - - cList,nList = [],[] - if len(fragMap)==9: - cList,nList = ['x','y'], ['1','2','3','4'] - elif len(fragMap)==5: - cList,nList = ['f'], ['1','2','3','4'] - elif len(fragMap)==3: - cList,nList = ['f'], ['1','2'] - else: - LOGERROR('Unexpected number of lines in the frag file, %d', len(fragMap)) - return + if self.Done == 1: + if self.btnStop is not None: + self.btnStop.setText('Close') + else: self.Kill() - fragData = [] - fragData.append( hex_to_binary( fragMap['id'] )) - for c in cList: - for n in nList: - rawBin,err = readSixteenEasyBytes(fragMap[c+n]) - if err=='Error_2+': - QMessageBox.critical(self, tr('Fragment Error'), tr(""" - There was an unfixable error in the fragment file: -

    File: %s
    Line: %s
    """) % (toLoad, mapKey), \ - QMessageBox.Ok) - return - fragData.append( SecureBinaryData(rawBin) ) - rawBin = None - - M, findex, wltIDBin, doMask, idBase58 = ReadFragIDLineBin(fragMap['id']) - self.addFragToTable(fnum, fragData) - self.makeFragInputTable() + def UpdateText(self, updatedText, endProgress=False): + self.Done = endProgress + if self.main is None: return self.running + self.emit(SIGNAL('Update'), updatedText, None) + return self.running - ############################################################################# - def dataClear(self, fnum): - if not fnum in self.fragDataMap: - return + def UpdateHBar(self, value, maxVal, endProgress=False): + self.Done = endProgress + if self.main is None: return self.running - for i in range(1,3): - self.fragDataMap[fnum][i].destroy() - del self.fragDataMap[fnum] - self.makeFragInputTable() - self.checkRestoreParams() - + progressVal = 100*value/maxVal - ############################################################################# - def checkRestoreParams(self): - showRightFrm = False - self.btnRestore.setEnabled(False) - self.lblRightFrm.setText( tr(""" - Start entering fragments into the table to left...""")) - for row,data in self.fragDataMap.iteritems(): - showRightFrm = True - M, fnum, wltIDBin, doMask, idBase58 = ReadFragIDLineBin(data[0]) - self.lblRightFrm.setText('Wallet Being Restored:') - self.imgPie.setPixmap(QPixmap(':/frag%df.png' % M)) - self.lblReqd.setText(tr('Frags Needed: %d') % M) - self.lblWltID.setText(tr('Wallet: %s') % binary_to_base58(wltIDBin)) - self.lblFragID.setText(tr('Fragments: %s') % idBase58.split('-')[0]) - self.btnRestore.setEnabled(len(self.fragDataMap) >= M) - break + self.emit(SIGNAL('Update'), None, self.HBarCount*100 +progressVal) + if progressVal >= 100: + self.HBarCount = self.HBarCount + 1 + return self.running - anyMask = False - for row,data in self.fragDataMap.iteritems(): - M, fnum, wltIDBin, doMask, idBase58 = ReadFragIDLineBin(data[0]) - if doMask: - anyMask = True - break - - self.lblSecureStr.setVisible(anyMask) - self.edtSecureStr.setVisible(anyMask) + def AskUnlock(self, wll): + self.condVar.acquire() + self.emit(SIGNAL('PromptPassphrase'), wll) + self.condVar.wait() + self.condVar.release() - if not showRightFrm: - self.fragIDPrefix = UNKNOWN - self.wltType = UNKNOWN - - self.imgPie.setVisible(showRightFrm) - self.lblReqd.setVisible(showRightFrm) - self.lblWltID.setVisible(showRightFrm) - self.lblFragID.setVisible(showRightFrm) - + return self.Passphrase - ############################################################################# - def addFragToTable(self, tableIndex, fragData): + def PromptPassphrase(self, wll): + self.condVar.acquire() + dlg = DlgUnlockWallet(wll, self, self.main, "Enter Passphrase", + returnPassphrase=True) - if len(fragData) == 9: - currType = '0' - elif len(fragData) == 5: - currType = '1.35a' - elif len(fragData) == 3: - currType = '1.35c' + self.Passphrase = None + self.GotPassphrase = 0 + if dlg.exec_(): + #grab plain passphrase + if dlg.Accepted == 1: + self.Passphrase = dlg.securePassphrase.copy() + dlg.securePassphrase.destroy() + + self.condVar.notify() + self.condVar.release() + + def Kill(self): + if self.main: self.emit(SIGNAL('Exit')) + + def Exit(self): + self.running = 0 + self.done(0) + + def exec_(self, *args, **kwargs): + ''' + If args[0] is a function, it will be called in exec_thread + args[1:] is the argument list for that function + will return the functions output in exec_thread.output, which is then + returned by exec_ + ''' + exec_thread = PyBackgroundThread(self.exec_async, *args, **kwargs) + exec_thread.start() + + self.main.emit(SIGNAL('execTrigger'), self) + exec_thread.join() + + if exec_thread.didThrowError(): + exec_thread.raiseLastError() else: - LOGERROR('How\'d we get fragData of size: %d', len(fragData)) - return - - if self.wltType==UNKNOWN: - self.wltType = currType - elif not self.wltType==currType: - QMessageBox.critical(self, tr('Mixed fragment types'), tr(""" - You entered a fragment for a different wallet type. Please check - that all fragments are for the same wallet, of the same version, - and require the same number of fragments."""), QMessageBox.Ok) - LOGERROR('Mixing frag types! How did that happen?') - return - - - M, fnum, wltIDBin, doMask, idBase58 = ReadFragIDLineBin(fragData[0]) - if self.fragIDPrefix == UNKNOWN: - self.fragIDPrefix = idBase58.split('-')[0] - elif not self.fragIDPrefix == idBase58.split('-')[0]: - QMessageBox.critical(self, tr('Multiple Walletss'), tr(""" - The fragment you just entered is actually for a different wallet - than the previous fragments you entered. Please double-check that - all the fragments you are entering belong to the same wallet and - have the "number of needed fragments" (M-value, in M-of-N)."""), \ - QMessageBox.Ok) - LOGERROR('Mixing fragments of different wallets! %s', idBase58) - return - - - if not self.verifyNonDuplicateFrag(fnum): - QMessageBox.critical(self, tr('Duplicate Fragment'), tr(""" - You just input fragment #%d, but that fragment has already been - entered!""")%fnum, QMessageBox.Ok) - return - + return exec_thread.output - - if currType=='0': - X = SecureBinaryData(''.join([fragData[i].toBinStr() for i in range(1,5)])) - Y = SecureBinaryData(''.join([fragData[i].toBinStr() for i in range(5,9)])) - elif currType=='1.35a': - X = SecureBinaryData( int_to_binary(fnum+1, widthBytes=64, endOut=BIGENDIAN) ) - Y = SecureBinaryData(''.join([fragData[i].toBinStr() for i in range(1,5)])) - elif currType=='1.35c': - X = SecureBinaryData( int_to_binary(fnum+1, widthBytes=32, endOut=BIGENDIAN) ) - Y = SecureBinaryData(''.join([fragData[i].toBinStr() for i in range(1,3)])) + def exec_async(self, *args, **kwargs): + if len(args) > 0 and hasattr(args[0], '__call__'): + func = args[0] - self.fragDataMap[tableIndex] = [fragData[0][:], X.copy(), Y.copy()] - - X.destroy() - Y.destroy() - self.checkRestoreParams() + if not 'Progress' in kwargs: + if self.HBar > 0: kwargs['Progress'] = self.UpdateHBar + else: kwargs['Progress'] = self.UpdateText - ############################################################################# - def verifyNonDuplicateFrag(self, fnum): - for row,data in self.fragDataMap.iteritems(): - rowFrag = ReadFragIDLineBin(data[0])[1] - if fnum==rowFrag: - return False + try: + rt = func(*args[1:], **kwargs) + except Exception as e: + self.Kill() + raise e + pass - return True - - - - ############################################################################# - def processFrags(self): - - SECPRINT = HardcodedKeyMaskParams() - pwd,ekey = '','' - if self.edtSecureStr.isVisible(): - pwd = str(self.edtSecureStr.text()).strip() - if len(pwd)<9: - QMessageBox.critical(self, tr('Invalid Code'), tr(""" - You didn't enter a full SecurePrint\xe2\x84\xa2 code. This - code is needed to decrypt your backup. If this backup is - actually unencrypted and there is no code, then choose the - correct "Unencrypted" option from the version drop-down - box."""), QMessageBox.Ok) - return - if not SECPRINT['FUNC_CHKPWD'](pwd): - QMessageBox.critical(self, tr('Bad Encryption Code'), tr(""" - The SecurePrint\xe2\x84\xa2 code you entered has an error - in it. Note that the code is case-sensitive. Please verify - you entered it correctly and try again."""), QMessageBox.Ok) - return - maskKey = SECPRINT['FUNC_KDF'](pwd) + self.Kill() + + return rt - fragMtrx,M = [], -1 - for row,trip in self.fragDataMap.iteritems(): - M,fnum,wltID,doMask,fid = ReadFragIDLineBin(trip[0]) - X,Y = trip[1],trip[2] - if doMask: - LOGINFO('Row %d needs unmasking' % row) - Y = SECPRINT['FUNC_UNMASK'](Y, ekey=maskKey) - else: - LOGINFO('Row %d is already unencrypted' % row) - fragMtrx.append( [X.toBinStr(), Y.toBinStr()]) - - typeToBytes = {'0': 64, '1.35a': 64, '1.35c': 32} - nBytes = typeToBytes[self.wltType] + def reject(self): + return + def setup(self, parent=None): + super(DlgProgress, self).__init__(parent, self.main) - if self.thisIsATest and len(fragMtrx)>M: - self.testFragSubsets(fragMtrx, M) - return + css = """ + QDialog{ border:1px solid rgb(0, 0, 0); } + QProgressBar{ text-align: center; font-weight: bold; } + """ + self.setStyleSheet(css) + layoutMgmt = QVBoxLayout() + self.lblDesc = QLabel('') - SECRET = ReconstructSecret(fragMtrx, M, nBytes) - for i in range(len(fragMtrx)): - fragMtrx[i] = [] - - LOGINFO( 'Final length of frag mtrx: %d', len(fragMtrx)) - LOGINFO( 'Final length of secret: %d', len(SECRET)) + if self.Title is not None: + if not self.HBar: + self.lblTitle = QLabel(self.Title) + self.lblTitle.setAlignment(Qt.AlignCenter) + layoutMgmt.addWidget(self.lblTitle) - priv,chain = '','' - if len(SECRET)==64: - priv = SecureBinaryData(SECRET[:32 ]) - chain = SecureBinaryData(SECRET[ 32:]) - elif len(SECRET)==32: - priv = SecureBinaryData(SECRET) - chain = DeriveChaincodeFromRootKey(priv) - - # If we got here, the data is valid, let's create the wallet and accept the dlg - # Now we should have a fully-plaintext rootkey and chaincode - root = PyBtcAddress().createFromPlainKeyData(priv) - root.chaincode = chain + if self.HBar is not None: + self.hbarProgress = QProgressBar(self) + self.hbarProgress.setMaximum(self.HBar*100) + self.hbarProgress.setMinimum(0) + self.hbarProgress.setValue(0) + self.hbarProgress.setMinimumWidth(250) + layoutMgmt.addWidget(self.hbarProgress) + self.HBarCount = 0 - first = root.extendAddressChain() - newWltID = binary_to_base58((ADDRBYTE + first.getAddr160()[:5])[::-1]) + if self.HBar: + self.hbarProgress.setFormat(self.Title +': %p%') + else: + layoutMgmt.addWidget(self.lblDesc) - # If this is a test, then bail - if self.thisIsATest: - verifyRecoveryTestID(self, newWltID, self.testWltID) - return + if self.Interrupt is not None: + self.btnStop = QPushButton(self.Interrupt) + self.connect(self.btnStop, SIGNAL('clicked()'), self.Kill) + layout_btnG = QGridLayout() + layout_btnG.setColumnStretch(0, 1) + layout_btnG.setColumnStretch(4, 1) + layout_btnG.addWidget(self.btnStop, 0, 1, 1, 3) + layoutMgmt.addLayout(layout_btnG) - if self.main.walletMap.has_key(newWltID): - QMessageBox.question(self, 'Duplicate Wallet!', \ - 'The data you entered is for a wallet with a ID: \n\n \t' + - newWltID + '\n\nYou already own this wallet! \n ' - 'Nothing to do...', QMessageBox.Ok) - self.reject() - return - - - - reply = QMessageBox.question(self, tr('Verify Wallet ID'), tr(""" - The data you entered corresponds to a wallet with a wallet - ID: \n\n \t'""") + newWltID + tr(""" \n\nDoes this ID - match the "Wallet Unique ID" printed on your paper backup? - If not, click "No" and reenter key and chain-code data - again."""), QMessageBox.Yes | QMessageBox.No) - if reply==QMessageBox.No: - return + self.minimize = None + self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint) + self.setModal(True) + self.setLayout(layoutMgmt) + self.adjustSize() - passwd = [] - if self.chkEncrypt.isChecked(): - dlgPasswd = DlgChangePassphrase(self, self.main) - if dlgPasswd.exec_(): - passwd = SecureBinaryData(str(dlgPasswd.edtPasswd1.text())) - else: - QMessageBox.critical(self, tr('Cannot Encrypt'), tr(""" - You requested your restored wallet be encrypted, but no - valid passphrase was supplied. Aborting wallet - recovery."""), QMessageBox.Ok) - return + btmRight = self.parent.rect().bottomRight() + topLeft = self.parent.rect().topLeft() + globalbtmRight = self.parent.mapToGlobal((btmRight+topLeft)/2) - if passwd: - self.newWallet = PyBtcWallet().createNewWallet( \ - plainRootKey=priv, \ - chaincode=chain, \ - shortLabel='Restored - %s'%newWltID, \ - withEncrypt=True, \ - securePassphrase=passwd, \ - kdfTargSec=0.25, \ - kdfMaxMem=32*1024*1024, \ - isActuallyNew=False, \ - doRegisterWithBDM=False) + self.move(globalbtmRight - QPoint(self.width()/2, self.height())) + if self.Title: + self.setWindowTitle(self.Title) else: - self.newWallet = PyBtcWallet().createNewWallet( \ - plainRootKey=priv, \ - chaincode=chain, \ - shortLabel='Restored - %s'%newWltID, \ - withEncrypt=False,\ - isActuallyNew=False, \ - doRegisterWithBDM=False) + self.setWindowTitle('Progress Bar') - def fillAddrPoolAndAccept(): - self.newWallet.fillAddressPool() - self.accept() + self.hide() - # Will pop up a little "please wait..." window while filling addr pool - DlgExecLongProcess(fillAddrPoolAndAccept, \ - tr("Recovering wallet..."), \ - self, self.main).exec_() +################################################################################# +class DlgCorruptWallet(DlgProgress): + def __init__(self, wallet, status, main=None, parent=None, alreadyFailed=True): + super(DlgProgress, self).__init__(parent, main) - ############################################################################# - def testFragSubsets(self, fragMtrx, M): - # If the user entered multiple fragments - fragMap = {} - for x,y in fragMtrx: - fragMap[binary_to_int(x,BIGENDIAN)-1] = [x,y] - typeToBytes = {'0': 64, '1.35a': 64, '1.35c': 32} - - isRandom, results = testReconstructSecrets(fragMap, M, 100) - def privAndChainFromRow(secret): - priv,chain = None,None - if len(secret)==64: - priv = SecureBinaryData(secret[:32 ]) - chain = SecureBinaryData(secret[ 32:]) - return (priv,chain) - elif len(secret)==32: - priv = SecureBinaryData(secret) - chain = DeriveChaincodeFromRootKey(priv) - return (priv,chain) - else: - LOGERROR('Root secret is %s bytes ?!' % len(secret)) - raise KeyDataError + self.connectDlg() - results = [(row[0], privAndChainFromRow(row[1])) for row in results] - subsAndIDs = [(row[0], calcWalletIDFromRoot(*row[1])) for row in results] + self.main = main + self.walletList = [] + self.logDirs = [] - DlgShowTestResults(self, isRandom, subsAndIDs, \ - M, len(fragMtrx), self.testWltID).exec_() + self.running = 1 + self.status = 1 + self.isFixing = False + self.needToSubmitLogs = False + self.checkMode = RECOVERMODE.NotSet + + self.lock = threading.Lock() + self.condVar = threading.Condition(self.lock) + mainLayout = QVBoxLayout() -########################################################################## -class DlgShowTestResults(ArmoryDialog): - ####################################################################### - def __init__(self, parent, isRandom, subsAndIDs, M, nFrag, expectID): - super(DlgShowTestResults, self).__init__(parent, parent.main) + self.connect(self, SIGNAL('UCF'), self.UCF) + self.connect(self, SIGNAL('Show'), self.show) + self.connect(self, SIGNAL('Exec'), self.run_lock) + self.connect(self, SIGNAL('SNP'), self.setNewProgress) + self.connect(self, SIGNAL('LFW'), self.LFW) + self.connect(self, SIGNAL('SRD'), self.SRD) - accumSet = set() - for sub,ID in subsAndIDs: - accumSet.add(ID) + if alreadyFailed: + titleStr = tr('Wallet Consistency Check Failed!') + else: + titleStr = tr('Perform Wallet Consistency Check') - allEqual = (len(accumSet)==1) - allCorrect = True - testID = expectID - if not testID: - testID = subsAndIDs[0][1] + lblDescr = QRichLabel(tr(""" + %s +

    + Armory software now detects and prevents certain kinds of + hardware errors that could lead to problems with your wallet. +
    """) % (htmlColor('TextWarn'), titleStr)) + + lblDescr.setAlignment(Qt.AlignCenter) + + + if alreadyFailed: + self.lblFirstMsg = QRichLabel(tr(""" + Armory has detected that wallet file Wallet "%s" (%s) + is inconsistent and should be further analyzed to ensure that your + funds are protected. +

    + This error will pop up every time you start + Armory until the wallet has been analyzed and fixed!""") % \ + (wallet.labelName, wallet.uniqueIDB58, htmlColor('TextWarn'))) + elif isinstance(wallet, PyBtcWallet): + self.lblFirstMsg = QRichLabel(tr(""" + Armory will perform a consistency check on Wallet "%s" (%s) + and determine if any further action is required to keep your funds + protected. This check is normally performed on startup on all + your wallets, but you can click below to force another + check.""") % (wallet.labelName, wallet.uniqueIDB58)) + else: + self.lblFirstMsg = QRichLabel('') - allCorrect = testID == subsAndIDs[0][1] + self.QDS = QDialog() + self.lblStatus = QLabel('') + self.addStatus(wallet, status) + self.QDSlo = QVBoxLayout() + self.QDS.setLayout(self.QDSlo) - descr = '' - nSubs = len(subsAndIDs) - fact = lambda x: math.factorial(x) - total = fact(nFrag) / (fact(M) * fact(nFrag-M)) - if isRandom: - descr = tr(""" - The total number of fragment subsets (%d) is too high - to test and display. Instead, %d subsets were tested - at random. The results are below """) % (total, nSubs) - else: - descr = tr(""" - For the fragments you entered, there are a total of - %d possible subsets that can restore your wallet. - The test results for all subsets are shown below""") % total + self.QDSlo.addWidget(self.lblFirstMsg) + self.QDSlo.addWidget(self.lblStatus) - lblDescr = QRichLabel(descr) - - lblWltIDDescr = QRichLabel( tr(""" - The wallet ID is computed from the first - address in your wallet based on the root key data (and the - "chain code"). Therefore, a matching wallet ID proves that - the wallet will produce identical addresses.""")) + self.lblStatus.setVisible(False) + self.lblFirstMsg.setVisible(True) + saStatus = QScrollArea() + saStatus.setWidgetResizable(True) + saStatus.setWidget(self.QDS) + saStatus.setMinimumHeight(250) + saStatus.setMinimumWidth(500) - frmResults = QFrame() - layout = QGridLayout() - row=0 - for sub,ID in subsAndIDs: - subStrs = [str(s) for s in sub] - subText = ', '.join(subStrs[:-1]) - dispTxt = tr(""" - Fragments %s and %s produce a - wallet with ID "%s" """) % (subText, subStrs[-1], ID) - chk = lambda: QPixmap(':/checkmark32.png').scaled(20,20) - _X_ = lambda: QPixmap(':/red_X.png').scaled(16,16) + layoutButtons = QGridLayout() + layoutButtons.setColumnStretch(0, 1) + layoutButtons.setColumnStretch(4, 1) + self.btnClose = QPushButton('Hide') + self.btnFixWallets = QPushButton('Run Analysis and Recovery Tool') + self.btnFixWallets.setDisabled(True) + self.connect(self.btnFixWallets, SIGNAL('clicked()'), self.doFixWallets) + self.connect(self.btnClose, SIGNAL('clicked()'), self.hide) + layoutButtons.addWidget(self.btnClose, 0, 1, 1, 1) + layoutButtons.addWidget(self.btnFixWallets, 0, 2, 1, 1) - lblTxt = QRichLabel(dispTxt) - lblTxt.setWordWrap(False) - lblPix = QLabel('') - lblPix.setPixmap(chk() if ID==testID else _X_()) - layout.addWidget( lblTxt, row, 0) - layout.addWidget( lblPix, row, 1) - row += 1 + self.lblDescr2 = QRichLabel('') + self.lblDescr2.setAlignment(Qt.AlignCenter) - + self.lblFixRdy = QRichLabel(tr(""" + Your wallets will be ready to fix once the scan is over
    + You can hide this window until then
    """)) - scrollResults = QScrollArea() - frmResults = QFrame() - frmResults.setLayout(layout) - scrollResults.setWidget(frmResults) + self.lblFixRdy.setAlignment(Qt.AlignCenter) + + self.frmBottomMsg = makeVertFrame(['Space(5)', + HLINE(), + self.lblDescr2, + self.lblFixRdy, + HLINE()]) + + self.frmBottomMsg.setVisible(False) - btnOkay = QPushButton('Ok') - buttonBox = QDialogButtonBox() - buttonBox.addButton(btnOkay, QDialogButtonBox.AcceptRole) - self.connect(btnOkay, SIGNAL('clicked()'), self.accept) - mainLayout = QVBoxLayout() mainLayout.addWidget(lblDescr) - mainLayout.addWidget(scrollResults) - mainLayout.addWidget(lblWltIDDescr) - mainLayout.addWidget(buttonBox) - self.setLayout(mainLayout) + mainLayout.addWidget(saStatus) + mainLayout.addWidget(self.frmBottomMsg) + mainLayout.addLayout(layoutButtons) - self.setWindowTitle('Fragment Test Results') - self.setMinimumWidth(500) + self.setLayout(mainLayout) + self.layout().setSizeConstraint(QLayout.SetFixedSize) + self.setWindowTitle('Wallet Error') + + def addStatus(self, wallet, status): + if wallet: + strStatus = ''.join(status) + str(self.lblStatus.text()) + self.lblStatus.setText(strStatus) + + self.walletList.append(wallet) + + def show(self): + super(DlgCorruptWallet, self).show() + self.activateWindow() + + def run_lock(self): + self.btnClose.setVisible(False) + self.hide() + super(DlgProgress, self).exec_() + self.walletList = None + + def UpdateCanFix(self, conditions, canFix=False): + self.emit(SIGNAL('UCF'), conditions, canFix) + + def UCF(self, conditions, canFix=False): + self.lblFixRdy.setText('') + if canFix: + self.btnFixWallets.setEnabled(True) + self.btnClose.setText('Close') + self.btnClose.setVisible(False) + self.connect(self.btnClose, SIGNAL('clicked()'), self.reject) + self.hide() + + def doFixWallets(self): + self.lblFixRdy.hide() + self.adjustSize() + + self.lblStatus.setVisible(True) + self.lblFirstMsg.setVisible(False) + self.frmBottomMsg.setVisible(False) + + from armoryengine.PyBtcWalletRecovery import FixWalletList + self.btnClose.setDisabled(True) + self.btnFixWallets.setDisabled(True) + self.isFixing = True + + self.lblStatus.hide() + self.QDSlo.removeWidget(self.lblStatus) + + for wlt in self.walletList: + self.main.removeWalletFromApplication(wlt.uniqueIDB58) + + FixWalletList(self.walletList, self, Progress=self.UpdateText, async=True) + self.adjustSize() + + def ProcessWallet(self, mode=RECOVERMODE.Full): + ''' + Serves as the entry point for non processing wallets that arent loaded + or fully processed. Only takes 1 wallet at a time + ''' + if len(self.walletList) > 0: + wlt = None + wltPath = '' + + if isinstance(self.walletList[0], str): + wltPath = self.walletList[0] + else: + wlt = self.walletList[0] + + self.lblDesc = QLabel('') + self.QDSlo.addWidget(self.lblDesc) + + self.lblFixRdy.hide() + self.adjustSize() + + self.frmBottomMsg.setVisible(False) + self.lblStatus.setVisible(True) + self.lblFirstMsg.setVisible(False) + + from armoryengine.PyBtcWalletRecovery import ParseWallet + self.btnClose.setDisabled(True) + self.btnFixWallets.setDisabled(True) + self.isFixing = True + + self.checkMode = mode + ParseWallet(wltPath, wlt, mode, self, + Progress=self.UpdateText, async=True) + + def UpdateDlg(self, text=None, HBar=None, Title=None): + if text is not None: self.lblDesc.setText(text) + self.adjustSize() + def accept(self): + self.main.emit(SIGNAL('checkForNegImports')) + super(DlgCorruptWallet, self).accept() + def reject(self): + if not self.isFixing: + super(DlgProgress, self).reject() + self.main.emit(SIGNAL('checkForNegImports')) + + def sigSetNewProgress(self, status): + self.emit(SIGNAL('SNP'), status) + + def setNewProgress(self, status): + self.lblDesc = QLabel('') + self.QDSlo.addWidget(self.lblDesc) + #self.QDS.adjustSize() + status[0] = 1 + + def setRecoveryDone(self, badWallets, goodWallets, fixedWallets, fixers): + self.emit(SIGNAL('SRD'), badWallets, goodWallets, fixedWallets, fixers) + + def SRD(self, badWallets, goodWallets, fixedWallets, fixerObjs): + self.btnClose.setEnabled(True) + self.btnClose.setVisible(True) + self.btnClose.setText('Continue') + self.btnFixWallets.setVisible(False) + self.btnClose.disconnect(self, SIGNAL('clicked()'), self.hide) + self.btnClose.connect(self, SIGNAL('clicked()'), self.accept) + self.isFixing = False + self.frmBottomMsg.setVisible(True) + + anyNegImports = False + for fixer in fixerObjs: + if len(fixer.negativeImports) > 0: + anyNegImports = True + break -################################################################################ -class DlgEnterOneFrag(ArmoryDialog): + if len(badWallets) > 0: + self.lblDescr2.setText(tr(""" + Failed to fix wallets!""") % \ + htmlColor('TextWarn')) + self.main.statusBar().showMessage('Failed to fix wallets!', 150000) + elif len(goodWallets) == len(fixedWallets) and not anyNegImports: + pluralStr = ' is' if len(goodWallets)==1 else 's are' + self.lblDescr2.setText(tr(""" + Wallet%s consistent, nothing to + fix.""") % (htmlColor("TextBlue"), pluralStr)) + self.main.statusBar().showMessage(tr(""" Wallet%s consistent!""") % \ + pluralStr, 15000) + elif len(fixedWallets) > 0 or anyNegImports: + if self.checkMode != RECOVERMODE.Check: + self.lblDescr2.setText(tr(""" + + There may still be issues with your + wallet! +
    + It is important that you send us the recovery logs + and an email address so the Armory team can check for + further risk to your funds!
    """) % \ + (htmlColor('TextWarn'))) + #self.main.statusBar().showMessage('Wallets fixed!', 15000) + else: + self.lblDescr2.setText('

    \ + Consistency check failed!

    ') + self.adjustSize() - ############################################################################# - def __init__(self, parent, main, fragList=[], wltType=UNKNOWN): - super(DlgEnterOneFrag, self).__init__(parent, main) + def loadFixedWallets(self, wallets): + self.emit(SIGNAL('LFW'), wallets) - self.fragData = [] + def LFW(self, wallets): + for wlt in wallets: + newWallet = PyBtcWallet().readWalletFile(wlt) + self.main.addWalletToApplication(newWallet, walletIsNew=True) - BLUE = htmlColor('TextBlue') - already = '' - if len(fragList)>0: - strList = ['%d' % (BLUE, f) for f in fragList] - replStr = '[' + ','.join(strList[:]) + ']' - already = tr(""" You have entered fragments %s, so far. """) % replStr + if TheBDM.getBDMState() in ('Uninitialized', 'Offline'): + TheBDM.registerWallet(newWallet, isFresh=True, wait=False) + else: + self.main.newWalletList.append([newWallet, True]) + + self.main.emit(SIGNAL('checkForkedImport')) - lblDescr = QRichLabel( tr(""" - Enter Another Fragment...

    %s - The fragments can be entered in any order, as long as you provide - enough of them to restore the wallet. If any fragments use a - SecurePrint\xe2\x84\xa2 code, please enter it once on the - previous window, and it will be applied to all fragments that - require it.""") % already) - - - - self.comboBackupType = QComboBox() - self.comboBackupType.clear() - self.comboBackupType.addItem( tr('Version 0 (from script, 9 lines)')) - self.comboBackupType.addItem( tr('Version 1.35a (5 lines)')) - self.comboBackupType.addItem( tr('Version 1.35c (3 lines)')) + # Decided that we can just add all the logic to + #def checkForkedSubmitLogs(self): + #forkedImports = [] + #for wlt in self.walletMap: + #if self.walletMap[wlt].hasForkedImports: + #dlgIWR = DlgInconsistentWltReport(self, self.main, self.logDirs) + #if dlgIWR.exec_(): + #return + #return - # If a wallet type hasn't been determined yet, allow the user to select it - # This value will be locked after the first fragment is entered. - if wltType==UNKNOWN: - self.comboBackupType.setCurrentIndex(2) - self.comboBackupType.setEnabled(True) - elif wltType=='0': - self.comboBackupType.setCurrentIndex(0) - self.comboBackupType.setEnabled(False) - elif wltType=='1.35a': - self.comboBackupType.setCurrentIndex(1) - self.comboBackupType.setEnabled(False) - elif wltType=='1.35c': - self.comboBackupType.setCurrentIndex(2) - self.comboBackupType.setEnabled(False) - - lblType = QRichLabel( tr("""Backup Type:"""), doWrap=False) - self.connect(self.comboBackupType, SIGNAL('activated(int)'), self.changeType) - frmCombo = makeHorizFrame([lblType, 'Space(20)', self.comboBackupType, 'Stretch']) - - self.prfxList = ['x1:','x2:','x3:','x4:', \ - 'y1:','y2:','y3:','y4:', \ - 'F1:','F2:','F3:','F4:'] - self.prfxList = [QLabel(p) for p in self.prfxList] - inpMask = 'Armory Factory Reset
    +

    + It is strongly recommended that you make backups of your + wallets before continuing, though wallet files will never be + intentionally deleted! All Armory + wallet files, and the wallet.dat file used by Bitcoin-Qt/bitcoind + should remain untouched in their current locations. All Armory + wallets will automatically be detected and loaded after the reset. +

    + If you are not sure which option to pick, try the "lightest option" + first, and see if your problems are resolved before trying the more + extreme options.""")) + + + + self.rdoSettings = QRadioButton() + self.lblSettingsText = QRichLabel(tr(""" + Delete settings and rescan (lightest option)""")) + self.lblSettings = QRichLabel(tr(""" + Only delete the settings file and transient network data. The + databases built by Armory will be rescanned (about 5-45 minutes)""")) + + self.rdoArmoryDB = QRadioButton() + self.lblArmoryDBText = QRichLabel(tr(""" + Also delete databases and rebuild""")) + self.lblArmoryDB = QRichLabel(tr(""" + Will delete settings, network data, and delete and Armory's databases, + forcing a rebuild and rescan (45 min to 3 hours)""")) + + self.rdoBitcoinDB = QRadioButton() + self.lblBitcoinDBText = QRichLabel(tr(""" + Also re-download the blockchain (most extreme)""")) + self.lblBitcoinDB = QRichLabel(tr(""" + This will delete settings, network data, Armory's databases, + and the Bitcoin software databases. Bitcoin-Qt/bitcoind will + have to download the 15+ GB blockchain again. Only use this if you + suspect blockchain corruption, such as receiving StdOut/StdErr errors + on the dashboard (8-72 hours depending on your connection)""")) + + + optFrames = [] + for rdo,txt,lbl in [ \ + [self.rdoSettings, self.lblSettingsText, self.lblSettings], \ + [self.rdoArmoryDB, self.lblArmoryDBText, self.lblArmoryDB], \ + [self.rdoBitcoinDB, self.lblBitcoinDBText, self.lblBitcoinDB]]: + + optLayout = QGridLayout() + txt.setWordWrap(False) + optLayout.addWidget(makeHorizFrame([rdo, txt, 'Stretch'])) + optLayout.addWidget(lbl, 1,0, 1,3) + optFrames.append(QFrame()) + optFrames[-1].setLayout(optLayout) + optFrames[-1].setFrameStyle(STYLE_RAISED) + + + self.rdoSettings.setChecked(True) - self.btnAccept = QPushButton("Done") - self.btnCancel = QPushButton("Cancel") - self.connect(self.btnAccept, SIGNAL('clicked()'), self.verifyUserInput) - self.connect(self.btnCancel, SIGNAL('clicked()'), self.reject) + btngrp = QButtonGroup(self) + btngrp.addButton(self.rdoSettings) + btngrp.addButton(self.rdoArmoryDB) + btngrp.addButton(self.rdoBitcoinDB) + + frmDescr = makeHorizFrame([lblDescr], STYLE_SUNKEN) + frmOptions = makeVertFrame(optFrames, STYLE_SUNKEN) + + self.btnOkay = QPushButton(tr('Continue')) + self.btnCancel = QPushButton(tr('Cancel')) buttonBox = QDialogButtonBox() - buttonBox.addButton(self.btnAccept, QDialogButtonBox.AcceptRole) + buttonBox.addButton(self.btnOkay, QDialogButtonBox.AcceptRole) buttonBox.addButton(self.btnCancel, QDialogButtonBox.RejectRole) + self.connect(self.btnOkay, SIGNAL(CLICKED), self.clickedOkay) + self.connect(self.btnCancel, SIGNAL(CLICKED), self.reject) layout = QVBoxLayout() - layout.addWidget(lblDescr) - layout.addWidget(HLINE()) - layout.addWidget(frmCombo) - layout.addWidget(frmAllInputs) + layout.addWidget(frmDescr) + layout.addWidget(frmOptions) layout.addWidget(buttonBox) + self.setLayout(layout) + self.setMinimumWidth(600) + self.setWindowTitle(tr('Factory Reset')) + self.setWindowIcon(QIcon(self.main.iconfile)) - self.setWindowTitle('Restore Single-Sheet Backup') - self.setMinimumWidth(500) - self.layout().setSizeConstraint(QLayout.SetFixedSize) - self.changeType() - - - ############################################################################# - def changeType(self): - sel = self.comboBackupType.currentIndex() - # |-- X --| |-- Y --| |-- F --| - if sel==0: visList = [1,1,1,1, 1,1,1,1, 0,0,0,0] - elif sel==1: visList = [0,0,0,0, 0,0,0,0, 1,1,1,1] - elif sel==2: visList = [0,0,0,0, 0,0,0,0, 1,1,0,0] - else: - LOGERROR('What the heck backup type is selected? %d', sel) - return - for i in range(12): - self.prfxList[i].setVisible( visList[i]==1) - self.edtList[ i].setVisible( visList[i]==1) + ### + def clickedOkay(self): - self.isLongForm = (sel in [0,1]) - - ############################################################################# - def destroyFragData(self): - for line in self.fragData: - if not isinstance(line, basestring): - # It's an SBD Object. Destroy it. - line.destroy() - + if self.rdoSettings.isChecked(): + reply = QMessageBox.warning(self, tr('Confirmation'), tr(""" + You are about to delete your settings and force Armory to rescan + its databases. Are you sure you want to do this?"""), \ + QMessageBox.Cancel | QMessageBox.Ok) - ############################################################################# - def verifyUserInput(self): - self.fragData = [] - nError = 0 - rawBin = None - - sel = self.comboBackupType.currentIndex() - rng = [-1] - if sel==0: rng = range(8) - elif sel==1: rng = range(8,12) - elif sel==2: rng = range(8,10) + if not reply==QMessageBox.Ok: + self.reject() + return + touchFile( os.path.join(ARMORY_HOME_DIR, 'rescan.flag') ) + touchFile( os.path.join(ARMORY_HOME_DIR, 'clearmempool.flag')) + touchFile( os.path.join(ARMORY_HOME_DIR, 'delsettings.flag')) + self.accept() - for i in rng: - hasError=False - try: - rawEntry = str(self.edtList[i].text()) - rawBin,err = readSixteenEasyBytes( rawEntry.replace(' ','') ) - if err=='Error_2+': - hasError=True - elif err=='Fixed_1': - nError += 1 - except KeyError: - hasError=True - - if hasError: - reply = QMessageBox.critical(self, tr('Verify Wallet ID'), tr(""" - There is an error in the data you entered that could not be - fixed automatically. Please double-check that you entered the - text exactly as it appears on the wallet-backup page.

    - The error occured on the "%s" line.""") % \ - str(self.prfxList[i].text()), QMessageBox.Ok) - LOGERROR('Error in wallet restore field') - self.prfxList[i].setText(''+str(self.prfxList[i].text())+'') - self.destroyFragData() + elif self.rdoArmoryDB.isChecked(): + reply = QMessageBox.warning(self, tr('Confirmation'), tr(""" + You are about to delete your settings and force Armory to delete + and rebuild its databases. Are you sure you want to do this?"""), \ + QMessageBox.Cancel | QMessageBox.Ok) + + if not reply==QMessageBox.Ok: + self.reject() return - self.fragData.append(SecureBinaryData(rawBin)) - rawBin = None - + touchFile( os.path.join(ARMORY_HOME_DIR, 'rebuild.flag') ) + touchFile( os.path.join(ARMORY_HOME_DIR, 'clearmempool.flag')) + touchFile( os.path.join(ARMORY_HOME_DIR, 'delsettings.flag')) + self.accept() - idLine = str(self.edtID.text()).replace(' ','') - self.fragData.insert(0, hex_to_binary(idLine)) + elif self.rdoBitcoinDB.isChecked(): + reply = QMessageBox.warning(self, tr('Confirmation'), tr(""" + You are about to delete your settings and delete all + blockchain databases on your system. The Bitcoin software will + have to redownload 15+ GB of blockchain data over the peer-to-peer + network again which can take from 8 to 72 hours depending on + your system speed and connection.

    Are you absolutely + sure you want to do this?"""), \ + QMessageBox.Cancel | QMessageBox.Yes) + + if not reply==QMessageBox.Yes: + QMessageBox.warning(self, tr('Aborted'), tr(""" + You canceled the factory reset operation. No changes were + made."""), QMessageBox.Ok) + self.reject() + return - M,fnum,wltID,doMask,fid = ReadFragIDLineBin(self.fragData[0]) - reply = QMessageBox.question(self, tr('Verify Fragment ID'), tr(""" - The data you entered is for fragment: -

    %s

    - Does this ID match the "Fragment:" field displayed on your backup? - If not, click "No" and re-enter the fragment data.""") % \ - (htmlColor('TextBlue'), fid), QMessageBox.Yes | QMessageBox.No) + if not self.main.settings.get('ManageSatoshi'): + # Must have user shutdown Bitcoin sw now, and delete DBs now + reply = MsgBoxCustom(MSGBOX.Warning, tr('Restart Armory'), tr(""" + Bitcoin-Qt (or bitcoind) must be closed to do the reset! + Please close all Bitcoin software, right now, + before clicking "Continue". +

    + Armory will now close. Please restart Bitcoin-Qt/bitcoind + first and wait for it to finish synchronizing before restarting + Armory."""), wCancel=True, yesStr="Continue") + + if not reply: + QMessageBox.warning(self, tr('Aborted'), tr(""" + You canceled the factory-reset operation. No changes were + made."""), QMessageBox.Ok) + self.reject() + return - if reply==QMessageBox.Yes: + # Do the delete operation now + deleteBitcoindDBs() + else: + reply = QMessageBox.warning(self, tr('Restart Armory'), tr(""" + Armory will now close to apply the requested changes. Please + restart it when you are ready to start the blockchain download + again."""), QMessageBox.Ok) + + if not reply == QMessageBox.Ok: + QMessageBox.warning(self, tr('Aborted'), tr(""" + You canceled the factory reset operation. No changes were + made."""), QMessageBox.Ok) + self.reject() + return + + touchFile( os.path.join(ARMORY_HOME_DIR, 'redownload.flag') ) + + # Always flag the rebuild, and del mempool and settings + touchFile( os.path.join(ARMORY_HOME_DIR, 'rebuild.flag') ) + touchFile( os.path.join(ARMORY_HOME_DIR, 'clearmempool.flag')) + touchFile( os.path.join(ARMORY_HOME_DIR, 'delsettings.flag')) self.accept() - -################################################################################ -def verifyRecoveryTestID(parent, computedWltID, expectedWltID=None): - - if expectedWltID==None: - # Testing an arbitrary paper backup - yesno = QMessageBox.question(parent, tr('Recovery Test'), tr(""" - From the data you entered, Armory calculated the following - wallet ID: %s -

    - Does this match the wallet ID on the backup you are - testing?""") % computedWltID, QMessageBox.Yes | QMessageBox.No) + QMessageBox.information(self, tr('Restart Armory'), tr(""" + Armory will now close so that the requested changes can + be applied."""), QMessageBox.Ok) + self.accept() - if yesno==QMessageBox.No: - QMessageBox.critical(parent, tr('Bad Backup!'), tr(""" - If this is your only backup and you are sure that you entered - the data correctly, then it is highly recommened you stop using - this wallet! If this wallet currently holds any funds, - you should move the funds to a wallet that does - have a working backup. -



    - Wallet ID of the data you entered: %s
    """) % computedWltID, \ - QMessageBox.Ok) - elif yesno==QMessageBox.Yes: - MsgBoxCustom(MSGBOX.Good, tr('Backup is Good!'), tr(""" - Your backup works! + +################################################################################# +class DlgForkedImports(ArmoryDialog): + def __init__(self, walletList, main=None, parent=None): + super(DlgForkedImports, self).__init__(parent, main) + + descr1 = '

    Forked imported addresses have been \ + detected in your wallets!!!

    ' + + descr2 = 'The following wallets have forked imported addresses:

    ' + \ + '
    '.join(walletList) + '
    ' + + descr3 = 'When you fix a corrupted wallet, any damaged private keys will be off \ + the determinstic chain. It means these private keys cannot be recreated \ + by your paper backup. If such private keys are encountered, Armory saves \ + them as forked imported private keys after it fixes the relevant wallets.' + + descr4 = '

    - Do not accept payments to these wallets anymore
    \ + - Do not delete or overwrite these wallets.
    \ + - Transfer all funds to a fresh and backed up wallet

    ' + + lblDescr1 = QRichLabel(descr1) + lblDescr2 = QRichLabel(descr2) + lblDescr3 = QRichLabel(descr3) + lblDescr4 = QRichLabel(descr4) + + layout2 = QVBoxLayout() + layout2.addWidget(lblDescr2) + frame2 = QFrame() + frame2.setLayout(layout2) + frame2.setFrameStyle(QFrame.StyledPanel) + + layout4 = QVBoxLayout() + layout4.addWidget(lblDescr4) + frame4 = QFrame() + frame4.setLayout(layout4) + frame4.setFrameStyle(QFrame.StyledPanel) + + + self.btnOk = QPushButton('Ok') + self.connect(self.btnOk, SIGNAL('clicked()'), self.accept) + + + layout = QVBoxLayout() + layout.addWidget(lblDescr1) + layout.addWidget(frame2) + layout.addWidget(lblDescr3) + layout.addWidget(frame4) + layout.addWidget(self.btnOk) + + + self.setLayout(layout) + self.setMinimumWidth(600) + self.setWindowTitle('Forked Imported Addresses') +### + + +################################################################################# +class DlgPrivacyPolicy(ArmoryDialog): + def __init__(self, main=None, parent=None, popupType='generic'): + super(DlgPrivacyPolicy, self).__init__(parent, main) + + lblHeader = QRichLabel(tr(""" + Armory Technologies, Inc. Privacy + Policy"""), hAlign=Qt.AlignCenter) + + if popupType=='generic': + descrTxt = tr(""" + Unless explicitly disabled, Armory periodically contacts ATI + servers for alerts and software updates. These checks expose + your IP address, software version, and operating system + to ATI servers, as well as any other servers + in-route. No other information is collected without your + explicit permission, which will be obtained when such + information is requested, such as submitting a bug report + with your log files.

    - The wallet ID is computed from a combination of the root - private key, the "chaincode" and the first address derived - from those two pieces of data. A matching wallet ID - guarantees it will produce the same chain of addresses as - the original.""")) - else: # an expected wallet ID was supplied - if not computedWltID == expectedWltID: - QMessageBox.critical(parent, tr('Bad Backup!'), tr(""" - If you are sure that you entered the backup information - correctly, then it is highly recommened you stop using - this wallet! If this wallet currently holds any funds, - you should move the funds to a wallet that does - have a working backup. + By using this software and submitting this information to + ATI, you are agreeing to the ATI privacy policy at the + link below. The page also includes information + about changing Armory's default privacy settings.""") + elif popupType=='submitbug': + descrTxt = tr(""" + You are submitting a bug report to ATI servers. Your log + files will be included unless you explicitly unselected it + from the bug submission screen. Armory log files do not + contain any security-sensitive + information, but some users may consider the information to be + privacy-sensitive. The log files may identify some + addresses and transactions that are related to your wallets. + No signing keys are ever written to the log file that would + allow another party to move or spend your funds.

    - Computed wallet ID: %s
    - Expected wallet ID: %s

    - Is it possible that you loaded a different backup than the - on you just made? """ % (computedWltID, expectedWltID)), \ - QMessageBox.Ok) + + By using this software and submitting this information to + ATI, you are agreeing to the ATI privacy policy at the + link below.""") else: - MsgBoxCustom(MSGBOX.Good, tr('Backup is Good!'), tr(""" - Your backup works! -

    - The wallet ID computed from the data you entered matches - the expected ID. This confirms that the backup produces - the same sequence of private keys as the original wallet! -

    - Computed wallet ID: %s
    - Expected wallet ID: %s
    -
    - """ % (computedWltID, expectedWltID))) + LOGERROR("Unknown popup type: %s", popupType) + descrTxt = tr(""" + By using this software and submitting this information to + ATI, you are agreeing to the ATI privacy policy at the + link below. The page also includes information about + changing Armory's default privacy settings. +

    """) + + + lblURL = QRichLabel(tr("""
    %s

    """) % \ + (PRIVACY_URL, PRIVACY_URL), hAlign=Qt.AlignHCenter) + lblURL.setOpenExternalLinks(True) + - -################################################################################ -def finishPrintingBackup(parent, btype=None): - openTestDlg = False - msg = tr(""" - Please make sure that any printed backups you create (excluding any "ID" lines) have nine - columns of four letters each - each. - If you just made a paper backup, it is important that you test it - to make sure that it was printed or copied correctly. Most importantly, - """) + lblDescr = QRichLabel(tr("""
    %s""") % descrTxt) + + self.chkUserAgrees = QCheckBox(tr(""" + I have read and agree to the ATI privacy policy""")) + + + self.btnContinue = QPushButton('') + self.connect(self.chkUserAgrees, SIGNAL('toggled(bool)'), + self.btnContinue.setEnabled) + self.connect(self.btnContinue, SIGNAL('clicked()'), self.accept) + + + frmBtn = makeHorizFrame(['Stretch', self.btnContinue]) + mainLayout = QVBoxLayout() + mainLayout.addWidget(lblHeader) + mainLayout.addWidget(lblDescr) + mainLayout.addWidget(lblURL) + mainLayout.addWidget(self.chkUserAgrees) + mainLayout.addWidget(frmBtn) + self.setLayout(mainLayout) + + if popupType=='submitbug': + self.chkUserAgrees.setVisible(True) + self.chkUserAgrees.setChecked(False) + self.btnContinue.setEnabled(False) + self.btnContinue.setText(tr('Continue')) + else: + self.chkUserAgrees.setVisible(False) + self.chkUserAgrees.setChecked(False) + self.btnContinue.setEnabled(True) + self.btnContinue.setText(tr('Ok')) - if btype==None: - QMessageBox.warning(parent, tr('Test Your Backup!'), tr(""" - """)) - - - + self.setWindowTitle(tr("Privacy Policy")) + + + + +# Put circular imports at the end +from ui.WalletFrames import SelectWalletFrame, WalletBackupFrame,\ + AdvancedOptionsFrame +from ui.TxFrames import SendBitcoinsFrame, SignBroadcastOfflineTxFrame,\ + ReviewOfflineTxFrame + + + diff --git a/r-pi/crosscompile.py b/r-pi/crosscompile.py new file mode 100644 index 000000000..e60a67ede --- /dev/null +++ b/r-pi/crosscompile.py @@ -0,0 +1,51 @@ +import os +import subprocess +import time + + +def setEnv(var, val): + print 'Setting environment var: $%s to %s' % (var,val) + os.environ[str(var)] = str(val) + + +def popen(cmdList): + print '*'*80 + print 'Executing: "%s"' % ' '.join(cmdList) + proc = subprocess.Popen(cmdList) + while proc.poll() is None: + time.sleep(1) + print 'Done with: "%s"' % ' '.join(cmdList) + print '*'*80 + + +verStr = '' +for line in open('armoryengine/ArmoryUtils.py','r').readlines(): + if line.startswith('BTCARMORY_VERSION'): + c0 = line.find('(')+1 + c1 = line.find(')') + vquad = [int(istr.strip()) for istr in line[c0:c1].split(',')] + verStr = '%d.%02d' % tuple(vquad[:2]) + if (vquad[2] > 0 or vquad[3] > 0): + verStr += '.%d' % vquad[2] + if vquad[3] > 0: + verStr += '.%d' % vquad[3] + break + +print 'Armory version:', verStr + + +setEnv('ARMROOT', '/tmp/armroot') +setEnv('DESTDIR', 'armory_rpi') +setEnv('CXX', 'r-pi/tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian/bin/arm-linux-gnueabihf-c++') +setEnv('PYVER', 'python2.7') +setEnv('PYTHON_INCLUDE', '$ARMROOT/usr/include/python2.7/') +setEnv('PYTHON_LIB', '-L$ARMROOT/usr/lib/python2.7/config-arm-linux-gnueabihf') + +deststr = 'DESTDIR=%s' % os.environ['DESTDIR'] +tarFN = 'armory_%s_raspbian.tar.gz' % verStr + +popen(['make']) +popen(['make', 'install', deststr]) +popen(['tar','-zcf', tarFN, os.environ['DESTDIR']]) + + diff --git a/r-pi/r-pi_dl_packages.sh b/r-pi/r-pi_dl_packages.sh new file mode 100755 index 000000000..21beb0607 --- /dev/null +++ b/r-pi/r-pi_dl_packages.sh @@ -0,0 +1,38 @@ +#! /bin/bash + +packages="libqtcore4 libqt4-dev python-qt4 python-twisted libconfig-file-perl libqt4-designer + libqt4-scripttools libqt4-help libqt4-test libqtassistantclient4 libqtwebkit4 libqt4-declarative libqt4-script + libqt4-xmlpatterns libqt4-dev-bin libqt4-qt3support libqt4-sql qt4-linguist-tools + qt4-qmake + python-psutil python-pyasn1 python-sip python-crypto python-openssl + python-twisted-conch python-twisted-lore python-twisted-mail python-twisted-news python-twisted-runner + python-twisted-words python-twisted-core python-twisted-web python-twisted-names python-twisted-bin + python-zope.interface + python-pkg-resources + " + +arch="i386" + +for i +do + arg=$i +done + +if [[ "$arg" == -* ]]; then + arch=${i:1} +fi + +if [ ! -d "$arch" ]; then + mkdir $arch +fi + +cd $arch + +for pkg in $packages; +do + apt-get -o APT::Architecture=$arch download $pkg +done + +cd .. + +exit diff --git a/release_scripts/README.txt b/release_scripts/README.txt new file mode 100644 index 000000000..82e59810e --- /dev/null +++ b/release_scripts/README.txt @@ -0,0 +1,259 @@ +Build & Release Process for Armory + +This directory contains a variety of scripts that will be used to compile, +bundle, sign, and upload new releases of Armory. There are three scripts +because it is assumed that the signing keys are offline, requiring something +similar to an offline transaction: create everything online, take offline +for signing, take online again to broadcast (upload to Amazon S3). + + +The following is assumed to have been done already before starting this +process: + + - Local & remote machines and VMs have compiled & bundled installers + - master_list.py file that returns nested dictionary of all release/installer + information (see example at end of this doc) + - Each remote system has our public key in its authorized_keys file + - Offline computer has GPG key & Armory wallet spec'd at top of Step2 script + - All announce files are updated (except for dllinks which will be updated + by the script itself once files are signed and hashes are known) + - The Step2 script contains an accurate list of everything file/installer + - The computer running Step3 has write-access to the git repo, and a + configuration file with API key for uploading results to Amazon S3 + - Directories on the offline computer containing dependencies for each + OS-specific offline-bundle + - Already have an installed version of Armory offline in the /usr/lib/armory + directory, to be used for creating signature blocks + +The output of this process will be: + + - Signed git tag that can be pushed to the repo + - All .deb installers will be signed using dpkg-sig + - Offline bundles using the signed deb files + - GPG-signed hashes file including all regular installers and offline bundles + - Append URLs and hashes to dllinks.txt + - New announce.txt file that contains URLs and hashes of all notify files + signed by offline BITCOIN private key + - Full list of URLs of uploaded installers & bundles in HTML and forum + markdown, for easy updating of website and forum posts + + +----- +Step1 Script: + +Fetch all the installers, and do a fresh checkout of the +repo. It should also include updates to the announcement files +After that, put everything into a single directory that +can be copied to a USB key to be taken to the offline computer. + + Script Arguments (* is optional) + argv[0] <> + argv[1] version string, "0.91.1" + argv[2] version type, "-testing", "-beta", "" + argv[3]* output directory (default ~ ./exportToOffline) + argv[4]* unsigned announce dir (default ~ ./unsignedannounce) + argv[5]* Bitcoin Core SHA256SUMS.asc (default ~ "None") + argv[7]* use testing settings (default ~ "0") + + Script Output: + + /BitcoinArmory (clone of repo) + /release_scripts (copy of release_scripts dir from repo) + /installers (all non-offline-bundle packages) + /unsignedannounce (all unsigned announcement files) + /SHA256SUMS.asc (if present) + +Note the release_scripts dir is itself copied because it has master_list.py +which is needed by all three steps. Plus, we most likely made tweaks to +these Step* scripts to support the current release, and it wouldn't be in +the cloned repo yet. After the release is successful, we commit the updated +scripts as the basis for the next release. + + + + +----- +Step2 Script: + +This script will be executed from the release_scripts directory above -- +we will copy the directory to the offline computer, then cd into it then run +Step2 script from there. It does not depend on the cloned repo -- it adds +/usr/lib/armory to sys.path, to use the currently installed version +of Armory for any non-generic-python operations. + + Script Arguments (* is optional) + argv[0] <> + argv[1] inputDir (from Step1) + argv[2] outputDir (for Step3) + argv[3] bundleDir + argv[6]* git branch to tag (default ~ "master") + argv[7]* use testing settings (default ~ 0) + + Script Output: + + /BitcoinArmory (same repo but with signed tag v0.91.1) + /release_scripts (same copy, unmodified) + /installers (signed, now includes offline bundles) + /announceFiles (all txt signed, added announce.txt) + +The "bundleDir" should contain one directory for everything in the master +package list that "hasBundle==True". It's "BundleDeps" will be the name +of the directory that the Step 2 script will look for. The bundle deps dir +will contain all dependencies (all of them .debs, as of this writing), as +well as a script that installs everything including the package itself. +i.e. Step2 script will make a copy of the bundle deps, it will copy the +signed installer into the copy, and then tar it all up. The bundle deps +dir should have, in addition to the deps themselves, a script that will +install everything in that directory including the signed package. + +The testing settings use a different GPG key, BTC key, and different bucket +for uploading + + + +----- +Step 3 Script: + +Will expect to find the three directories above with signed data. It will +actually execute verification of all signatures, though you will have to +manually verify the output before continuing. After that, it will attempt +to upload everything. + +It expects to find the .s3cmd configuration file, already setup with your +S3 API key to be able to upload files to the BitoinArmory-releases bucket. +It will do the following: + + + Script Arguments (* is optional) + argv[0] <> + argv[1] inputDir (from Step2) + argv[2]* isDryRun (default ~ False) + + Script Output: + + -- Upload all installers and offline bundles to BitcoinArmory-releases + -- Upload all announce files to BitcoinArmory-media bucket + -- Ask if you'd like to push the latest git tag (if this is a testing + version, you may not want to push the tag) + + + + +################################################################################ +# # +# Example master_list.py file # +# # +# Provides dictionaries "getReleaseParams" and "getMasterPackageList" # +# # +################################################################################ + +#! /usr/bin/python +import os + + +def getReleaseParams(doTest=False): + rparams = {} + rparams['Builder'] = 'Armory Technologies, Inc.' + rparams['GitUser'] = 'Armory Technologies, Inc.' + rparams['GitEmail'] = 'contact@bitcoinarmory.com' + + if not doTest: + rparams['SignAddr'] = '1NWvhByxfTXPYNT4zMBmEY3VL8QJQtQoei' + rparams['AnnounceFile'] = 'announce.txt' + rparams['BucketAnnounce'] = 'https://s3.amazonaws.com/bitcoinarmory-media/' + rparams['BucketReleases'] = 'https://s3.amazonaws.com/bitcoinarmory-releases/' + rparams['GPGKeyID'] = '98832223' + rparams['BTCWltID'] = '2DTq89wvw' + else: + rparams['SignAddr'] = '1PpAJyNoocJt38Vcf4AfPffaxo76D4AAEe' + rparams['AnnounceFile'] = 'testannounce.txt' + rparams['BucketAnnounce'] = 'https://s3.amazonaws.com/bitcoinarmory-testing/' + rparams['BucketReleases'] = 'https://s3.amazonaws.com/bitcoinarmory-testing/' + rparams['GPGKeyID'] = 'FB596985' + rparams['BTCWltID'] = '2XqAdZZ8B' + + return rparams + + + +def getMasterPackageList(): + masterPkgList = {} + m = masterPkgList + + pkg = 'Windows (All)' + m[pkg] = {} + m[pkg]['FetchFrom'] = ['cp', '~/windows_share/armory_%s_win32.exe'] + m[pkg]['FileSuffix'] = 'winAll.exe' + m[pkg]['OSNameDisp'] = 'Windows' + m[pkg]['OSVarDisp'] = 'XP, Vista, 7, 8+' + m[pkg]['OSArchDisp'] = '32- and 64-bit' + m[pkg]['OSNameLink'] = 'Windows' + m[pkg]['OSVarLink'] = 'XP,Vista,7,8,8.1' + m[pkg]['OSArchLink'] = '32,64' + m[pkg]['HasBundle'] = False + + pkg = 'MacOSX (All)' + m[pkg] = {} + m[pkg]['FetchFrom'] = ['scp', 'joeschmode', '192.168.1.22', 22, '~/BitcoinArmory/osxbuild/armory_%s_osx.tar.gz'] + m[pkg]['FileSuffix'] = 'osx.tar.gz' + m[pkg]['OSNameDisp'] = 'MacOSX' + m[pkg]['OSVarDisp'] = '10.7+' + m[pkg]['OSArchDisp'] = '64bit' + m[pkg]['OSNameLink'] = 'MacOSX' + m[pkg]['OSVarLink'] = '10.7,10.8,10.9,10.9.1,10.9.2' + m[pkg]['OSArchLink'] = '64'' + m[pkg]['HasBundle'] = False + + + pkg = 'Ubuntu 12.04-32bit' + m[pkg] = {} + m[pkg]['FetchFrom'] = ['scp', 'guest', '192.168.1.23', 5111, '~/buildenv/armory_%s-1_i386.deb'] + m[pkg]['FileSuffix'] = 'ubuntu-32bit.deb' + m[pkg]['OSNameDisp'] = 'Ubuntu' + m[pkg]['OSVarDisp'] = '12.04+' + m[pkg]['OSArchDisp'] = '32bit' + m[pkg]['OSNameLink'] = 'Ubuntu' + m[pkg]['OSVarLink'] = '12.04,12.10,13.04,13.10,14.04' + m[pkg]['OSArchLink'] = '32' + m[pkg]['HasBundle'] = True + m[pkg]['BundleDeps'] = 'offline_deps_ubuntu32' + m[pkg]['BundleSuffix'] = 'offline_ubuntu_12.04-32.tar.gz' + m[pkg]['BundleOSVar'] = '12.04' + + + pkg = 'Ubuntu 12.04-64bit' + m[pkg] = {} + m[pkg]['FetchFrom'] = ['scp', 'joe', '192.168.1.80', 5111, '~/buildenv/armory_%s-1_amd64_osx.deb'] + m[pkg]['FileSuffix'] = 'ubuntu-64bit.deb' + m[pkg]['OSNameDisp'] = 'Ubuntu' + m[pkg]['OSVarDisp'] = '12.04+' + m[pkg]['OSArchDisp'] = '64bit' + m[pkg]['OSNameLink'] = 'Ubuntu' + m[pkg]['OSVarLink'] = '12.04,12.10,13.04,13.10,14.04' + m[pkg]['OSArchLink'] = '64' + m[pkg]['HasBundle'] = True + m[pkg]['BundleDeps'] = 'offline_deps_ubuntu64' + m[pkg]['BundleSuffix'] = 'offline_ubuntu_12.04-64.tar.gz' + m[pkg]['BundleOSVar'] = '12.04' + + pkg = 'RaspberryPi' + m[pkg] = {} + m[pkg]['FetchFrom'] = ['cp', '~/buildenv/rpibuild/armory_%s-1.tar.gz'] + m[pkg]['FileSuffix'] = 'raspbian-armhf.tar.gz' + m[pkg]['OSNameDisp'] = 'Raspbian' + m[pkg]['OSVarDisp'] = None + m[pkg]['OSArchDisp'] = 'armhf' + m[pkg]['OSNameLink'] = 'RaspberryPi' + m[pkg]['OSVarLink'] = None + m[pkg]['OSArchLink'] = '32' + m[pkg]['HasBundle'] = True + m[pkg]['BundleDeps'] = 'offline_deps_raspbian' + m[pkg]['BundleSuffix'] = 'rpi_bundle.tar.gz' + m[pkg]['BundleOSVar'] = None + + + return masterPkgList + + + + diff --git a/release_scripts/Step1_Online_PrepareForSigning.py b/release_scripts/Step1_Online_PrepareForSigning.py index d352c7e85..a1a3a14c1 100644 --- a/release_scripts/Step1_Online_PrepareForSigning.py +++ b/release_scripts/Step1_Online_PrepareForSigning.py @@ -1 +1,79 @@ # stub +import subprocess +import os +import time +import shutil +import ast +from sys import argv +from release_utils import * + +##### +from release_settings import getReleaseParams, getMasterPackageList +##### + +masterPkgList = getMasterPackageList() + +CLONE_URL = 'https://github.com/etotheipi/BitcoinArmory.git' + +if len(argv)<3: + import textwrap + print textwrap.dedent(""" + Script Arguments (* is optional) + argv[0] "python %s" + argv[1] version string, "0.91.1" + argv[2] version type, "-testing", "-beta", "" + argv[3]* output directory (default ~ ./exportToOffline) + argv[4]* unsigned announce dir (default ~ ./unsignedannounce) + argv[5]* Bitcoin Core SHA256SUMS.asc (default ~ "None") + """) % argv[0] + exit(1) + +print argv + +verStr = argv[1] +verType = argv[2] +outDir = argv[3] if len(argv)>3 else './exportToOffline' +annSrc = argv[4] if len(argv)>4 else './unsignedannounce' +shaCore = argv[5] if len(argv)>5 else None + +instDst = os.path.join(outDir, 'installers') +cloneDir = os.path.join(outDir, 'BitcoinArmory') +rscrDir = os.path.join(outDir, 'release_scripts') +annDst = os.path.join(outDir, 'unsignedannounce') + +if os.path.exists(outDir): + shutil.rmtree(outDir) +os.makedirs(instDst) # will create outdir and installers dir + +# Check that unsigned announcements dir exists +checkExists(annSrc) + +# Throw in the Bitcoin Core hashes file if supplied +if shaCore is not None: + checkExists(shaCore) + shutil.copy(shaCore, os.path.join(outDir,'SHA256SUMS.asc')) + +# Grab the list of scp and cp commands from fetchlist +for pkgName,pkgInfo in masterPkgList.iteritems(): + fetchCmd = pkgInfo['FetchFrom'] + cmd,cmdArgs = fetchCmd[0],fetchCmd[1:] + localFn = 'armory_%s%s_%s' % (verStr, verType, pkgInfo['FileSuffix']) + copyTo = os.path.join(instDst, localFn) + if cmd=='cp': + assert(len(cmdArgs)==1) + copyFrom = checkExists(cmdArgs[0] % verStr) + print 'Copying: %s --> %s' % (copyFrom, copyTo) + shutil.copy(copyFrom, copyTo) + if cmd=='scp': + assert(len(cmdArgs)==4) + usr,ip,port,src = cmdArgs + remoteSrc = src % verStr + hostPath = '%s@%s:%s' % (usr, ip, remoteSrc) + execAndWait(['scp', '-P', str(port), hostPath, copyTo]) + + +execAndWait(['git', 'clone', CLONE_URL, cloneDir]) +shutil.copytree('../release_scripts', rscrDir) +shutil.copytree(annSrc, annDst) + + diff --git a/release_scripts/Step2_Offline_PackageSigning.py b/release_scripts/Step2_Offline_PackageSigning.py index 0ecb79d38..d13a0919e 100644 --- a/release_scripts/Step2_Offline_PackageSigning.py +++ b/release_scripts/Step2_Offline_PackageSigning.py @@ -2,190 +2,384 @@ # Take a directory full of things to be signed, and do the right thing. # Make sure you cert-sign the windows installers, first -import sys import os import time import shutil +import getpass +from sys import argv from subprocess import Popen, PIPE from release_utils import * -#uploadlog = open('step2_log_%d.txt' % long(time.time()), 'w') -uploadlog = open('step2_log.txt', 'w') -def logprint(s): - print s - uploadlog.write(s + '\n') +##### +from release_settings import getReleaseParams, getMasterPackageList +##### + + +if len(argv)<4: + import textwrap + print textwrap.dedent(""" + Script Arguments (* is optional) + argv[0] "python %s" + argv[1] inputDir (from Step1) + argv[2] outputDir (for Step3) + argv[3] bundleDir + argv[4] isTestingRelease (default ~ "0") + argv[5]* git branch to tag (default ~ "master") + argv[6]* use testing settings (default ~ "0") + """) % argv[0] + exit(1) +# Process CLI args +inDir = checkExists(argv[1]) +outDir = argv[2] +bundleDir = argv[3] +isTestRelease = (len(argv)>4 and not argv[4]=="0") +gitBranch = 'master' if len(argv)<5 else argv[5] +testParams = (len(argv)>6 and not argv[6]=="0") +outDir = makeOutputDir(outDir, wipe=False) -################################################################################ -# DEFAULTS FOR RUNNING THIS SCRIPT -instDir = '.' -verType = 'testing' -gpgKeyID = 'FB596985' -builder = 'Armory Technologies, Inc.' - -gitRepo = './BitcoinArmory' -gitBranch = 'testing' -gituser = 'Armory Technologies, Inc.' -gitemail = 'support@bitcoinarmory.com' - -# We expect to find one file with each of the following suffixes -suffixes = set(['_10.04_amd64.deb', \ - '_10.04_i386.deb', \ - '_12.04_amd64.deb', \ - '_12.04_i386.deb', \ - '_win32.exe', \ - '.app.tar.gz']) - -# We may have more installers than we want to make bundles. -# Specify just the ones you want bundled, here (and we expect -# to find an "armory_deps_XX.XX_arch" dir with the dependencies) -offlinebundles = [ \ - ['10.04', 'i386' ], \ - ['10.04', 'amd64'], \ - ['12.04', 'i386' ], \ - ['12.04', 'amd64'] \ - ] + +masterPkgList = getMasterPackageList() +RELEASE = getReleaseParams(testParams) + + +# Other defaults -- same for all Armory releases +builder = RELEASE['Builder'] +gituser = RELEASE['GitUser'] +gitemail = RELEASE['GitEmail'] +signAddress = RELEASE['SignAddr'] +announceName = RELEASE['AnnounceFile'] +bucketPrefix = RELEASE['BucketPrefix'] +bucketAnnounce = bucketPrefix + RELEASE['BucketAnnounce'] +bucketReleases = bucketPrefix + RELEASE['BucketReleases'] +gpgKeyID = RELEASE['GPGKeyID'] +btcWltID = RELEASE['BTCWltID'] + + +# Setup dual writing to console and log file +writelog = open('step2_log.txt', 'w') +def logprint(s): + print s + writelog.write(s + '\n') + + +# Check that all the paths expected from step 1 actually exist +srcGitRepo = checkExists(os.path.join(inDir, 'BitcoinArmory')) +srcInstalls = checkExists(os.path.join(inDir, 'installers')) +srcAnnounce = checkExists(os.path.join(inDir, 'unsignedannounce')) +srcCoreSHA = checkExists(os.path.join(inDir, 'SHA256SUMS.asc'), 'skip') + +# Check that all the paths expected from step 1 actually exist +dstGitRepo = os.path.join(outDir, 'BitcoinArmory') +dstInstalls = makeOutputDir(os.path.join(outDir, 'installers')) +dstAnnounce = makeOutputDir(os.path.join(outDir, 'signedannounce')) + +# Scan the list of files in installers dir to get latest +instList = [fn for fn in os.listdir(srcInstalls)] +topVerInt,topVerStr,topVerType = getLatestVerFromList(instList) + +# A shortcut to get the full path of the installer filename for a given pkg +def getSrcPath(pkgName, suffixStr='FileSuffix'): + pkgSuffix = masterPkgList[pkgName][suffixStr] + fname = 'armory_%s%s_%s' % (topVerStr, topVerType, pkgSuffix) + return os.path.join(srcInstalls, fname) + +def getDstPath(pkgName, suffixStr='FileSuffix'): + if pkgName=='SHAFILE_TXT': + pkgSuffix = 'sha256sum.txt' + elif pkgName=='SHAFILE_ASC': + pkgSuffix = 'sha256sum.txt.asc' + else: + pkgSuffix = masterPkgList[pkgName][suffixStr] + + fname = 'armory_%s%s_%s' % (topVerStr, topVerType, pkgSuffix) + return os.path.join(dstInstalls, fname) + +# Check that all the master packages exist, as well as any bundle dirs +for pkgName,pkgInfo in masterPkgList.iteritems(): + checkExists(getSrcPath(pkgName)) + logprint('Regular pacakge "%s": %s' % (pkgName.ljust(20), getSrcPath(pkgName))) + + if pkgInfo['HasBundle']: + logprint('Offline bundle "%s": %s' % (pkgName.ljust(20), pkgInfo['BundleDeps'])) + checkExists(os.path.join(bundleDir, pkgInfo['BundleDeps'])) + +# Check for wallet in ARMORY_HOME_DIR +wltPath = checkExists('~/.armory/armory_%s_.wallet' % btcWltID) logprint('*'*80) logprint('Please confirm all parameters before continuing:') -logprint(' Dir with all the installers: "%s"' % instDir) -logprint(' This is release of type : "%s"' % verType) +logprint(' Dir with all the installers: "%s"' % srcInstalls) +logprint(' Dir with announcement data : "%s"' % srcAnnounce) +logprint(' Dir with fresh git checkout: "%s"' % srcGitRepo) +logprint(' Path to Bitcoin Core SHA256: "%s"' % str(srcCoreSHA)) +logprint(' Detected Version Integer : "%s"' % topVerInt) +logprint(' Detected Version String : "%s"' % topVerStr) +logprint(' This is release of type : "%s"' % topVerType) logprint(' Use the following GPG key : "%s"' % gpgKeyID) +logprint(' Use the following wallet : "%s"' % wltPath) logprint(' Builder for signing deb : "%s"' % builder) -logprint(' Git repo to be signed is : "%s", branch: "%s"' % (gitRepo,gitBranch)) +logprint(' Git branch to be signed is : "%s"' % gitBranch) logprint(' Git user to tag release : "%s" / <%s>' % (gituser, gitemail)) -logprint('') -logprint(' Expected files to find :') -for suf in suffixes: - logprint(' ' + suf) -logprint('') -logprint(' Expected offline bundles to create: ') -for bndl in offlinebundles: - logprint(' ' + str(bndl)) -logprint('') -logprint('Make sure all non-deb files are ready before continuing...') -logprint('') + +if srcCoreSHA: + logprint('\n'*2) + logprint('*'*80) + logprint('Output of gpg-verify on SHA256SUMS') + logprint(execAndWait('gpg -v %s' % srcCoreSHA)[0]) + logprint('*'*80) + logprint('Contents of SHA256SUM.asc:') + logprint(open(srcCoreSHA,'r').read()) + logprint('*'*80) + logprint('Contents of dllinks.txt (to be signed):') + logprint(open(os.path.join(srcAnnounce, 'dllinks.txt'),'r').read()) + logprint('\n') + if raw_input('Visually verify -- good? [Y/n]: ').lower().startswith('n'): + exit(1) + reply = raw_input('Does all this look correct? [Y/n]') -if not reply.lower().startswith('y'): +if reply.lower().startswith('n'): logprint('User aborted') exit(0) logprint('*'*80) logprint('') + -################################################################################ -# Do some sanity checks to make sure things are in order before continuing -if len(sys.argv) > 1: - instDir = sys.argv[1] - -if not os.path.exists(instDir): - logprint('Installers dir does not exist! ' + instDir) - exit(1) +# First thing: sign all debs +logprint('Signing all .deb files:') +for pkgName in masterPkgList: + pkgSrc = getSrcPath(pkgName) + pkgDst = getDstPath(pkgName) + shutil.copy(pkgSrc, pkgDst) + if pkgDst.endswith('.deb'): + logprint('Signing: ' + pkgDst) + cmd = 'dpkg-sig -s builder -m "%s" -k %s %s' % (builder, gpgKeyID, pkgDst) + logprint('EXEC: ' + cmd) + execAndWait(cmd) + logprint(execAndWait('dpkg-sig --verify %s' % pkgDst)[0]) -if not os.path.exists(gitRepo): - logprint('Git repo does not exist! ' + gitRepo) - exit(1) -# Check that we have offline bundle dependencies dirs for each -depsdirs = ['./armory_deps_%s_%s' % (dist,arch) for dist,arch in offlinebundles] -for fn in depsdirs: - if not os.path.exists(fn): - logprint('Directory does not exist: ' + fn) - exit(1) +################################################################################ +logprint('Creating bundles:') +for pkgName,pkgInfo in masterPkgList.iteritems(): + if not pkgInfo['HasBundle']: + continue -# Grab the latest file version from the list -latestVerInt,latestVerStr = getLatestVerFromList(os.listdir(instDir)) + bpath = getDstPath(pkgName, 'BundleSuffix') + tempDir = 'OfflineBundle' + if os.path.exists(tempDir): + logprint('Removing temp bundle directory: ' + tempDir) + shutil.rmtree(tempDir) + + logprint('\tCopying bundle dependencies') + shutil.copytree(os.path.join(bundleDir, pkgInfo['BundleDeps']), tempDir) + shutil.copy(getSrcPath(pkgName), tempDir) + execAndWait('tar -zcf %s %s/*' % (bpath, tempDir)) + + if os.path.exists(tempDir): + logprint('Removing temp bundle directory: ' + tempDir) + shutil.rmtree(tempDir) + + +# Finally, create the signed hashes file +filesToSign = [] +for pkgName,pkgInfo in masterPkgList.iteritems(): + filesToSign.append(getDstPath(pkgName)) + if pkgInfo['HasBundle']: + filesToSign.append(getDstPath(pkgName, 'BundleSuffix')) + + +logprint('All files to be included in hashes file:') +for f in filesToSign: + logprint(' ' + f) + +newHashes = getAllHashes(filesToSign) +#hashname = 'armory_%s%s_sha256sum.txt' % (topVerStr, topVerType) +hashname = getDstPath('SHAFILE_TXT') +hashpath = os.path.join(dstInstalls, hashname) +with open(hashpath, 'w') as hashfile: + for fn,sha2 in newHashes: + basefn = os.path.basename(fn) + logprint(' ' + sha2 + ' ' + basefn) + hashfile.write('%s %s\n' % (sha2,basefn)) + hashfile.write('\n') -logprint('\nHighest version number found: ("%s", %d)' % (latestVerStr, latestVerInt)) +execAndWait('gpg -sa --clearsign --digest-algo SHA256 %s ' % hashpath) +os.remove(hashpath) -# Verify we have at least one installer of each type at the same, latest version -for fn in os.listdir(instDir): - fivevals = parseInstallerName(fn) - if fivevals == None: - continue - verint = fivevals[-2] - for suf in suffixes: - if fn.endswith(suf) and (verint == latestVerInt): - suffixes.remove(suf) - break -if not len(suffixes)==0: - logprint('Not all installers found; remaining %s' % str(suffixes)) +################################################################################ +################################################################################ +# Now update the announcements (require armoryengine) +sys.path.append('/usr/lib/armory') +from armoryengine.ALL import PyBtcWallet, binary_to_hex, hex_to_binary, \ + SecureBinaryData, addrStr_to_hash160, sha256, \ + ADDRBYTE +from jasvet import ASv1CS, readSigBlock, verifySignature + +origDLFile = os.path.join(srcAnnounce, 'dllinks.txt') +newDLFile = os.path.join(srcAnnounce, 'dllinks_temp.txt') +announcePath = os.path.join(dstAnnounce, announceName) + +# Checking that wallet has signing key, and user can unlock wallet +wlt = PyBtcWallet().readWalletFile(wltPath) +if not wlt.hasAddr(signAddress): + print 'Supplied wallet does not have the correct signing key' exit(1) - -instFiles = [] -debbundles = [] -for fn in os.listdir(instDir): - try: - osStr,subOS,bits,verint,verstr = parseInstallerName(fn) - except: - continue - - if not verint == latestVerInt: +print 'Must unlock wallet to sign the announce file...' +while True: + passwd = SecureBinaryData(getpass.getpass('Wallet passphrase: ')) + if not wlt.verifyPassphrase(passwd): + print 'Invalid passphrase!' continue - - instFiles.append(fn) - if osStr == 'Linux': - arch = 'i386' if bits==32 else 'amd64' - if [subOS, arch] in offlinebundles: - newBundleDir = 'armory_%s-%s_OfflineBundle_%s-%dbit' % (verstr, verType, subOS, bits) - debbundles.append( [fn, 'armory_deps_%s_%s'%(subOS, arch), newBundleDir]) - + break + +wlt.unlock(securePassphrase=passwd) +passwd.destroy() +addrObj = wlt.getAddrByHash160(addrStr_to_hash160(signAddress)[1]) + +def doSignFile(inFile, outFile): + with open(inFile, 'rb') as f: + sigBlock = ASv1CS(addrObj.binPrivKey32_Plain.toBinStr(), f.read()) + + with open(outFile, 'wb') as f: + f.write(sigBlock) + +def getFileHash(baseDir, fname): + fullpath = os.path.join(baseDir, fn) + with open(fullpath, 'rb') as fdata: + return binary_to_hex(sha256(fdata.read())) + +# Now compute the hashes of the files in the signed-installer dir, write out +fnew = open(newDLFile, 'w') +fnew.write(open(origDLFile, 'r').read()) +fnew.write('\n') +typeSuffix = 'Testing' if isTestRelease else '' +for pkgName,pkgInfo in masterPkgList.iteritems(): + fn = 'armory_%s%s_%s' % (topVerStr, topVerType, pkgInfo['FileSuffix']) + outputStr = ['Armory%s' % typeSuffix, + topVerStr, + pkgInfo['OSNameLink'], + pkgInfo['OSVarLink'], + pkgInfo['OSArchLink'], + os.path.join(bucketReleases, fn), + getFileHash(dstInstalls, fn)] + fnew.write(' '.join(outputStr) + '\n') + + if pkgInfo['HasBundle']: + # Note different 4th arg for OSVar -- because bundles have different reqts + fn = 'armory_%s%s_%s' % (topVerStr, topVerType, pkgInfo['BundleSuffix']) + outputStr = ['ArmoryOffline%s' % typeSuffix, + topVerStr, + pkgInfo['OSNameLink'], + pkgInfo['BundleOSVar'], + pkgInfo['OSArchLink'], + os.path.join(bucketReleases, fn), + getFileHash(dstInstalls, fn)] + fnew.write(' '.join(outputStr) + '\n') + +fnew.write('\n') +fnew.close() + + + +fileMappings = {} +longestID = 0 +longestURL = 0 +print 'Reading file mapping...' +with open('announcemap.txt','r') as f: + for line in f.readlines(): + fname, fid = line.strip().split() + inputPath = os.path.join(srcAnnounce, fname) + if not os.path.exists(inputPath): + print 'ERROR: Could not find %s-file (%s)' % (fid, inputPath) + exit(1) + print ' Map: %s --> %s' % (fname, fid) + + + +print 'Signing and copying files to %s directory...' % dstAnnounce +with open('announcemap.txt','r') as f: + for line in f.readlines(): + fname, fid = line.strip().split() + + inputPath = os.path.join(srcAnnounce, fname) + outputPath = os.path.join(dstAnnounce, fname) + + # If we're using a modified DL file + if fname=='dllinks.txt': + inputPath = newDLFile + + if fname.endswith('.txt'): + doSignFile(inputPath, outputPath) + else: + shutil.copy(inputPath, outputPath) + + fdata = open(outputPath, 'rb').read() + fhash = binary_to_hex(sha256(fdata)) + fileMappings[fname] = [fid, fhash] + longestID = max(longestID, len(fid)) + longestURL = max(longestURL, len(bucketAnnounce + fname)) + + + +print 'Creating digest file...' +digestFile = open(announcePath, 'w') + +### +for fname,vals in fileMappings.iteritems(): + fid = vals[0].ljust(longestID + 3) + url = (bucketAnnounce + fname).ljust(longestURL + 3) + fhash = vals[1] + digestFile.write('%s %s %s\n' % (fid, url, fhash)) +digestFile.close() + + +print '' +print '------' +with open(announcePath, 'r') as f: + dfile = f.read() + print dfile +print '------' + +print 'Please verify the above data to your satisfaction:' +raw_input('Hit when ready: ') -logprint('\nAll installers to be hashed and signed:') -for fn in instFiles: - logprint(' ' + fn.ljust(45) + ':' + str(parseInstallerName(fn))) -logprint('\nAll debian bundles to create:') -for deb in debbundles: - logprint(' ' + str(deb)) +doSignFile(announcePath, os.path.join(dstAnnounce, announceName)) -for fn in instFiles: - if fn.endswith('.deb'): - cmd = 'dpkg-sig -s builder -m "%s" -k %s %s' % (builder,gpgKeyID,fn) - logprint('EXEC: ' + cmd) - execAndWait(cmd) - logprint(execAndWait('dpkg-sig --verify %s' % fn)[0]) - - -for fn,depsdir,bundledir in debbundles: - if os.path.exists(bundledir): - logprint('Removing old bundle directory: ' + bundledir) - shutil.rmtree(bundledir) +print '*'*80 +print open(announcePath, 'r').read() +print '*'*80 - targz = '%s.tar.gz' % bundledir - shutil.copytree(depsdir, bundledir) - shutil.copy(fn, bundledir) - execAndWait('tar -zcf %s %s/*' % (targz, bundledir)) - instFiles.append(targz) -instFiles.sort() +print '' +print 'Verifying files' +for fname,vals in fileMappings.iteritems(): + if 'bootstrap' in fname: + continue + with open(os.path.join(dstAnnounce, fname), 'rb') as f: + sig,msg = readSigBlock(f.read()) + addrB58 = verifySignature(sig, msg, 'v1', ord(ADDRBYTE)) + print 'Sign addr for:', vals[0].ljust(longestID+3), addrB58 + -newHashes = getAllHashes(instFiles) -hashfilename = os.path.join(instDir, 'armory_%s-%s_sha256sum.txt' % (latestVerStr, verType)) -hashfile = open(hashfilename, 'w') -for hline in newHashes: - fn,h = hline - basefn = os.path.basename(fn) - logprint(' ' + h + ' ' + basefn) - hashfile.write('%s %s\n' % (h,basefn)) -hashfile.write('\n') -hashfile.close() -execAndWait('gpg -sa --clearsign --digest-algo SHA256 %s ' % hashfilename) -os.remove(hashfilename) +print 'Done!' +################################################################################ # GIT SIGN -gittag = 'v%s-%s' % (latestVerStr, verType) +gittag = 'v%s%s' % (topVerStr, topVerType) logprint('*'*80) logprint('About to tag and sign git repo with:') logprint(' Tag: ' + gittag) @@ -193,27 +387,29 @@ def logprint(s): logprint(' Email: ' + gitemail) gitmsg = raw_input('Put your commit message here: ') - -prevDir = os.getcwd() -os.chdir(gitRepo) -execAndWait('git checkout %s' % gitBranch) -execAndWait('git config user.name "%s"' % gituser) -execAndWait('git config user.email %s' % gitemail) -execAndWait('git tag -s %s -u %s -m "%s"' % (gittag, gpgKeyID, gitmsg)) - -out,err = execAndWait('git tag -v %s' % gittag) +if os.path.exists(dstGitRepo): + shutil.rmtree(dstGitRepo) +shutil.copytree(srcGitRepo, dstGitRepo) +os.chdir(dstGitRepo) +execAndWait('git checkout %s' % gitBranch, cwd=dstGitRepo) +execAndWait('git config user.name "%s"' % gituser, cwd=dstGitRepo) +execAndWait('git config user.email %s' % gitemail, cwd=dstGitRepo) +execAndWait('git tag -s %s -u %s -m "%s"' % (gittag, gpgKeyID, gitmsg), cwd=dstGitRepo) + +out,err = execAndWait('git tag -v %s' % gittag, cwd=dstGitRepo) logprint(out) logprint(err) -os.chdir(prevDir) - logprint('*'*80) -logprint('CLEAN UP & BUNDLE EVERYTHING TOGETHER') -toExport = instFiles[:] -toExport.append(hashfilename + '.asc') -toExport.append("%s" % gitRepo) +#logprint('CLEAN UP & BUNDLE EVERYTHING TOGETHER') +#toExport = instFiles[:] +#toExport.append(hashpath + '.asc') +#toExport.append("%s" % dstGitRepo) + +#execAndWait('tar -zcf signed_release_%s%s.tar.gz %s' % (topVerStr, topVerType, ' '.join(toExport))) + + -execAndWait('tar -zcf signed_release_%s-%s.tar.gz %s' % (latestVerStr, verType, ' '.join(toExport))) diff --git a/release_scripts/Step3_Online_VerifyAndUpload.py b/release_scripts/Step3_Online_VerifyAndUpload.py index 09a0d3eab..46cb38a66 100644 --- a/release_scripts/Step3_Online_VerifyAndUpload.py +++ b/release_scripts/Step3_Online_VerifyAndUpload.py @@ -5,19 +5,47 @@ # dpkg-sig --verify *.deb # gpg -v *.asc # cd BitcoinArmory; git tag -v v0.90-beta (or whatever the tag is) -import sys +from sys import argv import os import time import shutil from release_utils import * +##### +from release_settings import getReleaseParams, getMasterPackageList +##### + +if len(argv)<2: + import textwrap + print textwrap.dedent(""" + Script Arguments (* is optional) + argv[0] "python %s" + argv[1] inputDir (from Step2) + argv[2]* isDryRun (default ~ 0) + argv[3]* useTestParams (default ~ 0) + """) % argv[0] + exit(1) + + +# Parse CLI args +inDir = checkExists(argv[1]) +isDryRun = (len(argv)>2 and not argv[2]=='0') +testParams = (len(argv)>3 and not argv[3]=='0') + + +masterPkgList = getMasterPackageList() +RELEASE = getReleaseParams(testParams) + +signAddress = RELEASE['SignAddr'] +announceName = RELEASE['AnnounceFile'] +bucketPrefix = RELEASE['BucketPrefix'] +htmlRelease = bucketPrefix + RELEASE['BucketReleases'] +htmlAnnounce = bucketPrefix + RELEASE['BucketAnnounce'] +s3Release = 's3://%s' % RELEASE['BucketReleases'] +s3Announce = 's3://%s' % RELEASE['BucketAnnounce'] +gpgKeyID = RELEASE['GPGKeyID'] +btcWltID = RELEASE['BTCWltID'] -startDir = os.getcwd() -latestVerInt, latestVerStr, latestRelease = 0,'','' -unpackDir = 'signed_release_unpack' -bucket = 'bitcoinarmory-releases' -buckets3 = 's3://%s' % bucket -bucketdl = 'https://s3.amazonaws.com/%s' % bucket #uploadlog = open('step3_log_%d.txt' % long(time.time()), 'w') uploadlog = open('step3_log.txt', 'w') @@ -25,51 +53,29 @@ def logprint(s): print s uploadlog.write(s + '\n') -# Find the latest signed_release archive -for fn in os.listdir('.'): - if not fn.startswith('signed_release'): - continue +srcGitRepo = checkExists(os.path.join(inDir, 'BitcoinArmory')) +srcInstalls = checkExists(os.path.join(inDir, 'installers')) +srcAnnounce = checkExists(os.path.join(inDir, 'signedannounce')) - fivevals = parseInstallerName(fn, ignoreExt=True) +# Scan the list of files in installers dir to get latest +instList = [fn for fn in os.listdir(srcInstalls)] +topVerInt,topVerStr,topVerType = getLatestVerFromList(instList) - if fivevals==None: - continue - vi,vs = fivevals[-2],fivevals[-1] - if vi>latestVerInt: - latestVerInt = vi - latestVerStr = vs - latestRelease = fn - - -# Get the version type "testing", "beta", etc -locver = latestRelease.index(latestVerStr) -lenver = len(latestVerStr) -locdot = latestRelease.index('.', locver+lenver+1) -verType = latestRelease[locver+lenver+1:locdot] -verFullStr = latestVerStr + '-' + verType +def getPkgFilename(pkgName, offline=False): + if pkgName=='SHAFILE_TXT': + suffix = 'sha256sum.txt' + elif pkgName=='SHAFILE_ASC': + suffix = 'sha256sum.txt.asc' + else: + suffixType = 'BundleSuffix' if offline else 'FileSuffix' + suffix = masterPkgList[pkgName][suffixType] + return 'armory_%s%s_%s' % (topVerStr, topVerType, suffix) -logprint('') -logprint('*'*80) -logprint('Detected Release Parameters:') -logprint(' Version type: ' + verType) -logprint(' Release file: ' + latestRelease) -logprint(' Full version: ' + verFullStr) -logprint(' S3 Bucket : ' + buckets3) -logprint(' DL Links : ' + bucketdl) -logprint('') - -################################################################################ -# Now unpack and start doing our thing -if os.path.exists(unpackDir): - logprint('Removing previous unpack-directory: %s' % unpackDir) - shutil.rmtree(unpackDir) - -os.mkdir(unpackDir) -execAndWait('tar -zxf %s -C %s' % (latestRelease, unpackDir)) # Create [relpath, filename, isBundle, isHashes, [pentuple]] +""" uploads = [] ascfile = '' for fn in os.listdir(unpackDir): @@ -78,7 +84,8 @@ def logprint(s): if fivevals==None or os.path.isdir(fullfn): continue - isBundle = ("OfflineBundle" in fn) + osName,verStr,verInt,verType,suffix = fivevals[:] + isBundle = suffix.lower().startswith('offline') isHashes = ("sha256" in fn) uploads.append( [fullfn, fn, isBundle, isHashes, fivevals] ) @@ -136,38 +143,90 @@ def logprint(s): raw_input('\nConfirm all signature checks passed...[press enter when done]') +pkgMap = {} +pkgMap['osx'] = ['MacOSX', '(All)', '(64bit)'] +pkgMap['winAll'] = ['Windows', '(All)', '(32- and 64-bit)'] +pkgMap['raspbian'] = ['Raspberry Pi', '', '(armhf)' ] +pkgMap['ubuntu32'] = ['Ubuntu', '12.04+', '(32bit)' ] +pkgMap['ubuntu64'] = ['Ubuntu', '12.04+', '(64bit)' ] -uploads.sort(key=lambda x: x[1]) +""" + +#uploads.sort(key=lambda x: x[1]) # Now actually execute the uploads and make them public forumTextList = [] htmlTextList = [] rawUrlList = [] s3cmdList = [] -for fullfn, fn, isbundle, ishash, fivevals in uploads: - osStr, subOS, bits, vi, vs = fivevals + +uploads = [] +for pkgName,pkgInfo in masterPkgList.iteritems(): + pkgDict = {} + pkgDict['SrcFile'] = getPkgFilename(pkgName) + pkgDict['SrcPath'] = os.path.join(inDir, 'installers', pkgDict['SrcFile']) + pkgDict['OSName'] = pkgInfo['OSNameDisp'] + pkgDict['OSVar'] = pkgInfo['OSVarDisp'] + pkgDict['OSArch'] = pkgInfo['OSArchDisp'] + pkgDict['IsHash'] = False + pkgDict['IsBundle'] = False + pkgDict['DstUpload'] = '%s%s' % (s3Release, pkgDict['SrcFile']) + pkgDict['DstHtml'] = '%s%s' % (htmlRelease, pkgDict['SrcFile']) + uploads.append(pkgDict) + + if pkgInfo['HasBundle']: + pkgDict = {} + pkgDict['SrcFile'] = getPkgFilename(pkgName, offline=True) + pkgDict['SrcPath'] = os.path.join(inDir, 'installers', pkgDict['SrcFile']) + pkgDict['OSName'] = pkgInfo['OSNameDisp'] + pkgDict['OSVar'] = pkgInfo['BundleOSVar'] + pkgDict['OSArch'] = pkgInfo['OSArchDisp'] + pkgDict['IsHash'] = False + pkgDict['IsBundle'] = True + pkgDict['DstUpload'] = '%s%s' % (s3Release, pkgDict['SrcFile']) + pkgDict['DstHtml'] = '%s%s' % (htmlRelease, pkgDict['SrcFile']) + uploads.append(pkgDict) + + +ascDict = {} +ascDict['SrcFile'] = getPkgFilename('SHAFILE_ASC') +ascDict['SrcPath'] = os.path.join(inDir, 'installers', ascDict['SrcFile']) +ascDict['IsHash'] = True +ascDict['IsBundle'] = False +ascDict['DstUpload'] = '%s%s' % (s3Release, ascDict['SrcFile']) +ascDict['DstHtml'] = '%s%s' % (htmlRelease, ascDict['SrcFile']) +uploads.append(ascDict) + + +for upl in uploads: + print 'Going to upload:' + for key,val in upl.iteritems(): + print ' ', key.ljust(10), ':', val + print '' + + + +for pkgDict in uploads: + + #osStr, subOS, bits, vi, vs = fivevals #print fullfn, fn, isbundle, ishash, osStr, subOS, bits, vi, vs + verFullStr = topVerStr + topVerType + humanText = 'Armory %s' % verFullStr - if isbundle: + if pkgDict['IsBundle']: humanText += ' Offline Bundle' - if ishash: + if pkgDict['IsHash']: humanText += ': Signed hashes of all installers ' else: - if osStr == 'Linux' or 'OfflineBundle' in fn: - humanText += ' for Ubuntu/Debian %s-%dbit' % (subOS, bits) - if osStr == 'Win': - humanText += ' for Windows Vista, 7, 8 (Both 32- and 64-bit)' - if osStr == 'Mac': - humanText += ' for Mac/OSX 10.8 and 10.9' + osParams = [pkgDict[a] for a in ['OSName', 'OSVar', 'OSArch']] + humanText += ' for %s %s %s' % tuple(osParams) - - uploadurl = '%s/%s' % (buckets3, fn) - linkurl = '%s/%s' % (bucketdl, fn) + uploadurl = pkgDict['DstUpload'] + linkurl = pkgDict['DstHtml'] - s3cmd = 's3cmd put --acl-public %s %s' % (fullfn, uploadurl) - #s3cmd = 's3cmd put %s %s' % (fullfn, uploadurl) + s3cmd = 's3cmd put --acl-public %s %s' % (pkgDict['SrcPath'], uploadurl) forumText = '[url=%s]%s[/url]' % (linkurl, humanText) htmlText = '%s' % (linkurl, humanText) @@ -175,8 +234,16 @@ def logprint(s): htmlTextList.append(htmlText) rawUrlList.append(linkurl) s3cmdList.append(s3cmd) - + +announceDir = os.path.join(inDir, 'signedannounce') +for fn in os.listdir(announceDir): + fpath = os.path.join(announceDir, fn) + uploadurl = '%s%s' % (s3Announce, fn) + s3cmd = 's3cmd put --acl-public %s %s' % (fpath, uploadurl) + s3cmdList.append(s3cmd) + + logprint('\nRAW URL LIST') for txt in rawUrlList: logprint(' '+txt) @@ -193,18 +260,21 @@ def logprint(s): for txt in s3cmdList: logprint(' '+txt) -logprint('') -yn = raw_input('Continue with upload? [y/N]') -if yn.lower().startswith('y'): - logprint('STARTING UPLOADS') - for s3cmd in s3cmdList: - logprint('Uploading: ' + s3cmd.split()[-1].strip()) - execAndWait(s3cmd, usepipes=False) +if not isDryRun: + + logprint('') + yn = raw_input('Continue with upload? [y/N]') + + if yn.lower().startswith('y'): + logprint('STARTING UPLOADS') + for s3cmd in s3cmdList: + logprint('Uploading: ' + s3cmd.split()[-1].strip()) + execAndWait(s3cmd, usepipes=False) + logprint('') + logprint('Not actually pushing the signed tag; do it manually --') + logprint('Copy the following command to push the tag:') + logprint(' cd %s/BitcoinArmory; git push origin v%s' % (inDir, verFullStr)) -logprint('') -logprint('Not actually pushing the signed tag; do it manually --') -logprint('Copy the following command to push the tag:') -logprint(' cd %s/BitcoinArmory; git push origin v%s' % (unpackDir, verFullStr)) diff --git a/release_scripts/announcemap.txt b/release_scripts/announcemap.txt new file mode 100644 index 000000000..efa209471 --- /dev/null +++ b/release_scripts/announcemap.txt @@ -0,0 +1,4 @@ +bootstrap.dat.torrent bootstrap +changelog.txt changelog +dllinks.txt downloads +notify.txt notify diff --git a/release_scripts/dlmap.txt b/release_scripts/dlmap.txt new file mode 100644 index 000000000..a14e5e309 --- /dev/null +++ b/release_scripts/dlmap.txt @@ -0,0 +1,7 @@ +Armory 0.91 https://s3.amazonaws.com/bitcoinarmory-releases/ + +armory_%ver-beta_64bit.deb Ubuntu 12.04,12.10,13.04,13.10,14.10 64 +armory_%ver-beta_32bit.deb Ubuntu 12.04,12.10,13.04,13.10,14.10 32 +armory_%ver-beta_osx.tar.gz MacOSX 10.7,10.8,10.9,10.9.1,10.9.2 64 +armory_%ver-beta_raspbian.tar.gz RaspberryPi Raspbian(armhf) 32 +armory_%ver-beta_winAll.exe Windows XP,Vista,7,8,8.1 32,64 diff --git a/release_scripts/release_utils.py b/release_scripts/release_utils.py index cbe5330e8..9f460dc80 100644 --- a/release_scripts/release_utils.py +++ b/release_scripts/release_utils.py @@ -6,16 +6,19 @@ from subprocess import Popen, PIPE ################################################################################ -def execAndWait(cli_str, timeout=0, usepipes=True): +def execAndWait(cli_str, timeout=0, usepipes=True, cwd=None): """ There may actually still be references to this function where check_output would've been more appropriate. But I didn't know about check_output at the time... """ + if isinstance(cli_str, (list, tuple)): + cli_str = ' '.join(cli_str) + print 'Executing:', '"' + cli_str + '"' if usepipes: - process = Popen(cli_str, stdin=PIPE, stdout=PIPE, stderr=PIPE, shell=True) + process = Popen(cli_str, stdin=PIPE, stdout=PIPE, stderr=PIPE, shell=True, cwd=cwd) else: - process = Popen(cli_str, shell=True) + process = Popen(cli_str, shell=True, cwd=cwd) pid = process.pid start = time.time() @@ -57,64 +60,49 @@ def getVersionInt(vquad, numPieces=4): ################################################################################ -# Extract [osStr, subOS, armoryVersion, bits] -def parseInstallerName(fn, ignoreExt=False): - if ignoreExt or \ - fn[-4:] in ('.msi', '.exe', '.deb', '.app', '.dmg') or \ - fn.endswith('.app.tar.gz'): - - try: - pieces = fn.replace('-','_').split('_') - osStr, subOS, bits, armVerInt, armVerStr = None,'',32,None,None - for pc in pieces: - if 'win' in pc.lower(): - osStr = 'Win' - elif pc.endswith('.deb'): - osStr = 'Linux' - elif 'osx' in pc.lower(): - osStr = 'Mac' - - try: - verpieces = [int(a) for a in pc.split('.')] - # Could be Armory version or Ubuntu version, or nothing - if verpieces[0]>=10: - subOS = pc - else: - while len(verpieces)<4: - verpieces.append(0) - armVerInt = getVersionInt(verpieces) - armVerStr = pc - except Exception as e: - pass - - if 'amd64' in pc or 'win64' in pc or '64bit' in pc: - bits = 64 - - return osStr,subOS,bits,armVerInt,armVerStr - - except: - print 'WARNING: Could not parse installer filename: %s' % fn - - - return None - +# Extract [osName, verStr, verInt, verType, ext] +# Example ['winAll', '0.91.1', 91001000, 'rc1', '.exe'] +def parseInstallerName(fn): + pcs = fn.split('_') + if not len(pcs)==3 or not pcs[0]=='armory': + return None + + temp,verWhole,osNameExt = pcs[:] + vpcs = verWhole.split('-') + verStr = vpcs[0] + verType = ('-'+vpcs[1]) if len(vpcs)>1 else '' + epcs = osNameExt.split('.') + osName = epcs[0] + osExt = '.'.join(epcs[1:]) + + verQuad = readVersionString(verStr) + verInt = getVersionInt(verQuad) + return [osName, verStr, verInt, verType, osExt] + +################################################################################ +# Parse filenames to return the latest version number present (and assoc type) def getLatestVerFromList(filelist): - latestVerInt = 0 latestVerStr = '' + verType = '' # Find the highest version number for fn in filelist: fivevals = parseInstallerName(fn) - if fivevals == None: + if fivevals is None: continue; - verint,verstr = fivevals[-2], fivevals[-1] + + verstr,verint,vertype = fivevals[1:4] if verint>latestVerInt: - latestVerInt = verint - latestVerStr = verstr + latestVerInt = verint + latestVerStr = verstr + latestVerType = vertype + + return (latestVerInt, latestVerStr, latestVerType) + + - return (latestVerInt, latestVerStr) ################################################################################ @@ -125,3 +113,29 @@ def getAllHashes(fnlist): hashes.append([fn, out.strip().split()[0]]) return hashes + +################################################################################ +def checkExists(fullPath, onDNE='exit'): + fullPath = os.path.expanduser(fullPath) + if os.path.exists(fullPath): + print 'Found file: %s' % fullPath + else: + print 'Path does not exist: %s' % fullPath + if onDNE=='skip': + return None + elif onDNE=='exit': + exit(1) + + return fullPath + + +def makeOutputDir(dirpath, wipe=True): + if os.path.exists(dirpath) and wipe: + shutil.rmtree(dirpath) + + if not os.path.exists(dirpath): + os.makedirs(dirpath) + return dirpath + + + diff --git a/release_scripts/signannounce.py b/release_scripts/signannounce.py new file mode 100644 index 000000000..3e63b96f5 --- /dev/null +++ b/release_scripts/signannounce.py @@ -0,0 +1,225 @@ +import sys +import os +import getpass +import shutil +sys.path.append('..') +sys.path.append('/usr/lib/armory') + +from armoryengine.ALL import * +from jasvet import ASv1CS, readSigBlock, verifySignature + + +def signAnnounceFiles(wltPath): + + if len(CLI_ARGS)<1: + print 'Must specify a wallet file containing the signing key' + exit(1) + + wltPath = CLI_ARGS[0] + if not os.path.exists(wltPath): + print 'Wallet file was not found (%s)' % wltPath + exit(1) + + + inDir = 'unsignedAnnounce' + outDir = 'signedAnnounce' + + origDLFile = os.path.join(inDir, 'dllinks.txt') + newDLFile = os.path.join(inDir, 'dllinks_temp.txt') + + ################################################################################ + ################################################################################ + # We may need to compute some installer hashes + doComputeDLLinks = (len(CLI_ARGS)>1) + if doComputeDLLinks: + instDir = CLI_ARGS[1] + if not os.path.exists(instDir): + print 'Installers dir does not exist!' + exit(1) + + instFileInfo = {} + with open('dlmap.txt','r') as f: + dlmapLines = [line.strip() for line in f.readlines()] + verName, verStr, urlPrefix = dlmapLines[0].split() + + for line in dlmapLines[2:]: + fn,opsys,osver,osarch = line.replace('%ver',verStr).split() + instFileInfo[fn] = [opsys, osver, osarch] + + OS,OSVER,OSARCH = range(3) + with open(newDLFile, 'w') as f: + # Write the existing data from rawfiles/dllinks into temp file + f.write(open(origDLFile, 'r').read()) + f.write('\n') + + # Now compute hashes of each installer and write info to temp file + for fn in os.listdir(instDir): + fullpath = os.path.join(instDir, fn) + if not fn in instFileInfo: + print 'File in installer directory does not match any in info map' + print ' File:', fullpath + print ' InfoMap:' + for filename in instFileInfo: + print ' ', filename + exit(1) + + fdata = open(fullpath, 'rb').read() + fhash = binary_to_hex(sha256(fdata)) + outputStr = [verName, + verStr, + instFileInfo[fn][OS], + instFileInfo[fn][OSVER], + instFileInfo[fn][OSARCH], + os.path.join(urlPrefix, fn), + fhash] + f.write(' '.join(outputStr) + '\n') + ################################################################################ + + + + + + ### + if CLI_OPTIONS.testAnnounceCode: + signAddress = '1PpAJyNoocJt38Vcf4AfPffaxo76D4AAEe' + announceName = 'testannounce.txt' + pathPrefix = 'https://s3.amazonaws.com/bitcoinarmory-testing/' + else: + signAddress = '1NWvhByxfTXPYNT4zMBmEY3VL8QJQtQoei' + announceName = 'announce.txt' + pathPrefix = 'https://s3.amazonaws.com/bitcoinarmory-media/' + + + announcePath = os.path.join(outDir, announceName) + + ### + wlt = PyBtcWallet().readWalletFile(wltPath) + if not wlt.hasAddr(signAddress): + print 'Supplied wallet does not have the correct signing key' + exit(1) + + + + print 'Must unlock wallet to sign the announce file...' + while True: + passwd = SecureBinaryData(getpass.getpass('Wallet passphrase: ')) + if not wlt.verifyPassphrase(passwd): + print 'Invalid passphrase!' + continue + break + + wlt.unlock(securePassphrase=passwd) + passwd.destroy() + addrObj = wlt.getAddrByHash160(addrStr_to_hash160(signAddress)[1]) + + def doSignFile(inFile, outFile): + with open(inFile, 'rb') as f: + sigBlock = ASv1CS(addrObj.binPrivKey32_Plain.toBinStr(), f.read()) + + with open(outFile, 'wb') as f: + f.write(sigBlock) + + + + fileMappings = {} + longestID = 0 + longestURL = 0 + print 'Reading file mapping...' + with open('announcemap.txt','r') as f: + for line in f.readlines(): + fname, fid = line.strip().split() + inputPath = os.path.join(inDir, fname) + if not os.path.exists(inputPath): + print 'ERROR: Could not find %s-file (%s)' % (fid, inputPath) + exit(1) + print ' Map: %s --> %s' % (fname, fid) + + + + if os.path.exists(outDir): + print 'Wiping old, announced files...' + shutil.rmtree(outDir) + os.mkdir(outDir) + + + + print 'Signing and copying files to %s directory...' % outDir + with open('announcemap.txt','r') as f: + for line in f.readlines(): + fname, fid = line.strip().split() + + inputPath = os.path.join(inDir, fname) + outputPath = os.path.join(outDir, fname) + + # If we're using a modified DL file + if fname=='dllinks.txt' and doComputeDLLinks: + inputPath = newDLFile + + if fname.endswith('.txt'): + doSignFile(inputPath, outputPath) + else: + shutil.copy(inputPath, outputPath) + + + fdata = open(outputPath, 'rb').read() + fhash = binary_to_hex(sha256(fdata)) + fileMappings[fname] = [fid, fhash] + longestID = max(longestID, len(fid)) + longestURL = max(longestURL, len(pathPrefix+fname)) + + + + + + print 'Creating digest file...' + digestFile = open(announcePath, 'w') + + ### + for fname,vals in fileMappings.iteritems(): + fid = vals[0].ljust(longestID + 3) + url = (pathPrefix + fname).ljust(longestURL + 3) + fhash = vals[1] + digestFile.write('%s %s %s\n' % (fid, url, fhash)) + digestFile.close() + + + print '' + print '------' + with open(announcePath, 'r') as f: + dfile = f.read() + print dfile + print '------' + + print 'Please verify the above data to your satisfaction:' + raw_input('Hit when ready: ') + + + + + doSignFile(announcePath, os.path.join(outDir, announceName)) + + + print '*'*80 + print open(announcePath, 'r').read() + print '*'*80 + + + print '' + print 'Verifying files' + for fname,vals in fileMappings.iteritems(): + if 'bootstrap' in fname: + continue + with open(os.path.join(outDir, fname), 'rb') as f: + sig,msg = readSigBlock(f.read()) + addrB58 = verifySignature(sig, msg, 'v1', ord(ADDRBYTE)) + print 'Sign addr for:', vals[0].ljust(longestID+3), addrB58 + + + + print 'Done!' + + + + + + diff --git a/setup.py b/setup.py index 0ba3acb1c..4cebb36ee 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,20 @@ -#! /usr/bin/python -from distutils.core import setup -import py2exe - - -opts = {"py2exe":{ - "dll_excludes":["MSWSOCK.dll", "IPHLPAPI.dll", "MSWSOCK.dll", "WINNSI.dll", "WTSAPI32.dll"] - }} - -setup( options = opts, windows = ['../../ArmoryQt.py']) - - +#! /usr/bin/python +from distutils.core import setup +import py2exe + + +opts = {"py2exe":{ + "dll_excludes":["MSWSOCK.dll", "IPHLPAPI.dll", "MSWSOCK.dll", "WINNSI.dll", "WTSAPI32.dll"] + }} + +setup( options = opts, windows = [ + { + "script": "../../ArmoryQt.py", + "icon_resources": [(1, "../../img/armory256x256.ico"), + (1, "../../img/armory64x64.ico"), + (1, "../../img/armory48x48.ico")] + } + ] + ) + + diff --git a/subprocess_win.py b/subprocess_win.py new file mode 100644 index 000000000..f1c2f0d36 --- /dev/null +++ b/subprocess_win.py @@ -0,0 +1,1654 @@ +# subprocess - Subprocesses with accessible I/O streams +# +# For more information about this module, see PEP 324. +# +# This module should remain compatible with Python 2.2, see PEP 291. +# +# Copyright (c) 2003-2005 by Peter Astrand +# +# Licensed to PSF under a Contributor Agreement. +# See http://www.python.org/2.4/license for licensing details. + +r"""subprocess - Subprocesses with accessible I/O streams + +This module allows you to spawn processes, connect to their +input/output/error pipes, and obtain their return codes. This module +intends to replace several other, older modules and functions, like: + +os.system +os.spawn* +os.popen* +popen2.* +commands.* + +Information about how the subprocess module can be used to replace these +modules and functions can be found below. + + + +Using the subprocess module +=========================== +This module defines one class called Popen: + +class Popen(args, bufsize=0, executable=None, + stdin=None, stdout=None, stderr=None, + preexec_fn=None, close_fds=False, shell=False, + cwd=None, env=None, universal_newlines=False, + startupinfo=None, creationflags=0): + + +Arguments are: + +args should be a string, or a sequence of program arguments. The +program to execute is normally the first item in the args sequence or +string, but can be explicitly set by using the executable argument. + +On UNIX, with shell=False (default): In this case, the Popen class +uses os.execvp() to execute the child program. args should normally +be a sequence. A string will be treated as a sequence with the string +as the only item (the program to execute). + +On UNIX, with shell=True: If args is a string, it specifies the +command string to execute through the shell. If args is a sequence, +the first item specifies the command string, and any additional items +will be treated as additional shell arguments. + +On Windows: the Popen class uses CreateProcess() to execute the child +program, which operates on strings. If args is a sequence, it will be +converted to a string using the list2cmdline method. Please note that +not all MS Windows applications interpret the command line the same +way: The list2cmdline is designed for applications using the same +rules as the MS C runtime. + +bufsize, if given, has the same meaning as the corresponding argument +to the built-in open() function: 0 means unbuffered, 1 means line +buffered, any other positive value means use a buffer of +(approximately) that size. A negative bufsize means to use the system +default, which usually means fully buffered. The default value for +bufsize is 0 (unbuffered). + +stdin, stdout and stderr specify the executed programs' standard +input, standard output and standard error file handles, respectively. +Valid values are PIPE, an existing file descriptor (a positive +integer), an existing file object, and None. PIPE indicates that a +new pipe to the child should be created. With None, no redirection +will occur; the child's file handles will be inherited from the +parent. Additionally, stderr can be STDOUT, which indicates that the +stderr data from the applications should be captured into the same +file handle as for stdout. + +If preexec_fn is set to a callable object, this object will be called +in the child process just before the child is executed. + +If close_fds is true, all file descriptors except 0, 1 and 2 will be +closed before the child process is executed. + +if shell is true, the specified command will be executed through the +shell. + +If cwd is not None, the current directory will be changed to cwd +before the child is executed. + +If env is not None, it defines the environment variables for the new +process. + +If universal_newlines is true, the file objects stdout and stderr are +opened as a text files, but lines may be terminated by any of '\n', +the Unix end-of-line convention, '\r', the Macintosh convention or +'\r\n', the Windows convention. All of these external representations +are seen as '\n' by the Python program. Note: This feature is only +available if Python is built with universal newline support (the +default). Also, the newlines attribute of the file objects stdout, +stdin and stderr are not updated by the communicate() method. + +The startupinfo and creationflags, if given, will be passed to the +underlying CreateProcess() function. They can specify things such as +appearance of the main window and priority for the new process. +(Windows only) + + +This module also defines some shortcut functions: + +call(*popenargs, **kwargs): + Run command with arguments. Wait for command to complete, then + return the returncode attribute. + + The arguments are the same as for the Popen constructor. Example: + + retcode = call(["ls", "-l"]) + +check_call(*popenargs, **kwargs): + Run command with arguments. Wait for command to complete. If the + exit code was zero then return, otherwise raise + CalledProcessError. The CalledProcessError object will have the + return code in the returncode attribute. + + The arguments are the same as for the Popen constructor. Example: + + check_call(["ls", "-l"]) + +check_output(*popenargs, **kwargs): + Run command with arguments and return its output as a byte string. + + If the exit code was non-zero it raises a CalledProcessError. The + CalledProcessError object will have the return code in the returncode + attribute and output in the output attribute. + + The arguments are the same as for the Popen constructor. Example: + + output = check_output(["ls", "-l", "/dev/null"]) + + +Exceptions +---------- +Exceptions raised in the child process, before the new program has +started to execute, will be re-raised in the parent. Additionally, +the exception object will have one extra attribute called +'child_traceback', which is a string containing traceback information +from the childs point of view. + +The most common exception raised is OSError. This occurs, for +example, when trying to execute a non-existent file. Applications +should prepare for OSErrors. + +A ValueError will be raised if Popen is called with invalid arguments. + +check_call() and check_output() will raise CalledProcessError, if the +called process returns a non-zero return code. + + +Security +-------- +Unlike some other popen functions, this implementation will never call +/bin/sh implicitly. This means that all characters, including shell +metacharacters, can safely be passed to child processes. + + +Popen objects +============= +Instances of the Popen class have the following methods: + +poll() + Check if child process has terminated. Returns returncode + attribute. + +wait() + Wait for child process to terminate. Returns returncode attribute. + +communicate(input=None) + Interact with process: Send data to stdin. Read data from stdout + and stderr, until end-of-file is reached. Wait for process to + terminate. The optional input argument should be a string to be + sent to the child process, or None, if no data should be sent to + the child. + + communicate() returns a tuple (stdout, stderr). + + Note: The data read is buffered in memory, so do not use this + method if the data size is large or unlimited. + +The following attributes are also available: + +stdin + If the stdin argument is PIPE, this attribute is a file object + that provides input to the child process. Otherwise, it is None. + +stdout + If the stdout argument is PIPE, this attribute is a file object + that provides output from the child process. Otherwise, it is + None. + +stderr + If the stderr argument is PIPE, this attribute is file object that + provides error output from the child process. Otherwise, it is + None. + +pid + The process ID of the child process. + +returncode + The child return code. A None value indicates that the process + hasn't terminated yet. A negative value -N indicates that the + child was terminated by signal N (UNIX only). + + +Replacing older functions with the subprocess module +==================================================== +In this section, "a ==> b" means that b can be used as a replacement +for a. + +Note: All functions in this section fail (more or less) silently if +the executed program cannot be found; this module raises an OSError +exception. + +In the following examples, we assume that the subprocess module is +imported with "from subprocess import *". + + +Replacing /bin/sh shell backquote +--------------------------------- +output=`mycmd myarg` +==> +output = Popen(["mycmd", "myarg"], stdout=PIPE).communicate()[0] + + +Replacing shell pipe line +------------------------- +output=`dmesg | grep hda` +==> +p1 = Popen(["dmesg"], stdout=PIPE) +p2 = Popen(["grep", "hda"], stdin=p1.stdout, stdout=PIPE) +output = p2.communicate()[0] + + +Replacing os.system() +--------------------- +sts = os.system("mycmd" + " myarg") +==> +p = Popen("mycmd" + " myarg", shell=True) +pid, sts = os.waitpid(p.pid, 0) + +Note: + +* Calling the program through the shell is usually not required. + +* It's easier to look at the returncode attribute than the + exitstatus. + +A more real-world example would look like this: + +try: + retcode = call("mycmd" + " myarg", shell=True) + if retcode < 0: + print >>sys.stderr, "Child was terminated by signal", -retcode + else: + print >>sys.stderr, "Child returned", retcode +except OSError, e: + print >>sys.stderr, "Execution failed:", e + + +Replacing os.spawn* +------------------- +P_NOWAIT example: + +pid = os.spawnlp(os.P_NOWAIT, "/bin/mycmd", "mycmd", "myarg") +==> +pid = Popen(["/bin/mycmd", "myarg"]).pid + + +P_WAIT example: + +retcode = os.spawnlp(os.P_WAIT, "/bin/mycmd", "mycmd", "myarg") +==> +retcode = call(["/bin/mycmd", "myarg"]) + + +Vector example: + +os.spawnvp(os.P_NOWAIT, path, args) +==> +Popen([path] + args[1:]) + + +Environment example: + +os.spawnlpe(os.P_NOWAIT, "/bin/mycmd", "mycmd", "myarg", env) +==> +Popen(["/bin/mycmd", "myarg"], env={"PATH": "/usr/bin"}) + + +Replacing os.popen* +------------------- +pipe = os.popen("cmd", mode='r', bufsize) +==> +pipe = Popen("cmd", shell=True, bufsize=bufsize, stdout=PIPE).stdout + +pipe = os.popen("cmd", mode='w', bufsize) +==> +pipe = Popen("cmd", shell=True, bufsize=bufsize, stdin=PIPE).stdin + + +(child_stdin, child_stdout) = os.popen2("cmd", mode, bufsize) +==> +p = Popen("cmd", shell=True, bufsize=bufsize, + stdin=PIPE, stdout=PIPE, close_fds=True) +(child_stdin, child_stdout) = (p.stdin, p.stdout) + + +(child_stdin, + child_stdout, + child_stderr) = os.popen3("cmd", mode, bufsize) +==> +p = Popen("cmd", shell=True, bufsize=bufsize, + stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) +(child_stdin, + child_stdout, + child_stderr) = (p.stdin, p.stdout, p.stderr) + + +(child_stdin, child_stdout_and_stderr) = os.popen4("cmd", mode, + bufsize) +==> +p = Popen("cmd", shell=True, bufsize=bufsize, + stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True) +(child_stdin, child_stdout_and_stderr) = (p.stdin, p.stdout) + +On Unix, os.popen2, os.popen3 and os.popen4 also accept a sequence as +the command to execute, in which case arguments will be passed +directly to the program without shell intervention. This usage can be +replaced as follows: + +(child_stdin, child_stdout) = os.popen2(["/bin/ls", "-l"], mode, + bufsize) +==> +p = Popen(["/bin/ls", "-l"], bufsize=bufsize, stdin=PIPE, stdout=PIPE) +(child_stdin, child_stdout) = (p.stdin, p.stdout) + +Return code handling translates as follows: + +pipe = os.popen("cmd", 'w') +... +rc = pipe.close() +if rc is not None and rc % 256: + print "There were some errors" +==> +process = Popen("cmd", 'w', shell=True, stdin=PIPE) +... +process.stdin.close() +if process.wait() != 0: + print "There were some errors" + + +Replacing popen2.* +------------------ +(child_stdout, child_stdin) = popen2.popen2("somestring", bufsize, mode) +==> +p = Popen(["somestring"], shell=True, bufsize=bufsize + stdin=PIPE, stdout=PIPE, close_fds=True) +(child_stdout, child_stdin) = (p.stdout, p.stdin) + +On Unix, popen2 also accepts a sequence as the command to execute, in +which case arguments will be passed directly to the program without +shell intervention. This usage can be replaced as follows: + +(child_stdout, child_stdin) = popen2.popen2(["mycmd", "myarg"], bufsize, + mode) +==> +p = Popen(["mycmd", "myarg"], bufsize=bufsize, + stdin=PIPE, stdout=PIPE, close_fds=True) +(child_stdout, child_stdin) = (p.stdout, p.stdin) + +The popen2.Popen3 and popen2.Popen4 basically works as subprocess.Popen, +except that: + +* subprocess.Popen raises an exception if the execution fails +* the capturestderr argument is replaced with the stderr argument. +* stdin=PIPE and stdout=PIPE must be specified. +* popen2 closes all filedescriptors by default, but you have to specify + close_fds=True with subprocess.Popen. +""" + +import sys +mswindows = (sys.platform == "win32") + +import os +import types +import traceback +import gc +import signal +import errno +import ctypes + +# Exception classes used by this module. +class CalledProcessError(Exception): + """This exception is raised when a process run by check_call() or + check_output() returns a non-zero exit status. + The exit status will be stored in the returncode attribute; + check_output() will also store the output in the output attribute. + """ + def __init__(self, returncode, cmd, output=None): + self.returncode = returncode + self.cmd = cmd + self.output = output + def __str__(self): + return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode) + + +if mswindows: + import threading + import msvcrt + import _subprocess + class STARTUPINFO: + dwFlags = 0 + hStdInput = None + hStdOutput = None + hStdError = None + wShowWindow = 0 + class pywintypes: + error = IOError +else: + import select + _has_poll = hasattr(select, 'poll') + import fcntl + import pickle + + # When select or poll has indicated that the file is writable, + # we can write up to _PIPE_BUF bytes without risk of blocking. + # POSIX defines PIPE_BUF as >= 512. + _PIPE_BUF = getattr(select, 'PIPE_BUF', 512) + + +__all__ = ["Popen", "PIPE", "STDOUT", "call", "check_call", + "check_output", "CalledProcessError"] + +if mswindows: + from _subprocess import (CREATE_NEW_CONSOLE, CREATE_NEW_PROCESS_GROUP, + STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, + STD_ERROR_HANDLE, SW_HIDE, + STARTF_USESTDHANDLES, STARTF_USESHOWWINDOW) + + __all__.extend(["CREATE_NEW_CONSOLE", "CREATE_NEW_PROCESS_GROUP", + "STD_INPUT_HANDLE", "STD_OUTPUT_HANDLE", + "STD_ERROR_HANDLE", "SW_HIDE", + "STARTF_USESTDHANDLES", "STARTF_USESHOWWINDOW"]) +try: + MAXFD = os.sysconf("SC_OPEN_MAX") +except: + MAXFD = 256 + +_active = [] + +def _cleanup(): + for inst in _active[:]: + res = inst._internal_poll(_deadstate=sys.maxint) + if mswindows: + if inst._handle is not None: + ctypes.windll.kernel32.CloseHandle(inst._handle) + if res is not None: + try: + _active.remove(inst) + except ValueError: + # This can happen if two threads create a new Popen instance. + # It's harmless that it was already removed, so ignore. + pass + +PIPE = -1 +STDOUT = -2 + + +def _eintr_retry_call(func, *args): + while True: + try: + return func(*args) + except (OSError, IOError) as e: + if e.errno == errno.EINTR: + continue + raise + + +# XXX This function is only used by multiprocessing and the test suite, +# but it's here so that it can be imported when Python is compiled without +# threads. + +def _args_from_interpreter_flags(): + """Return a list of command-line arguments reproducing the current + settings in sys.flags and sys.warnoptions.""" + flag_opt_map = { + 'debug': 'd', + # 'inspect': 'i', + # 'interactive': 'i', + 'optimize': 'O', + 'dont_write_bytecode': 'B', + 'no_user_site': 's', + 'no_site': 'S', + 'ignore_environment': 'E', + 'verbose': 'v', + 'bytes_warning': 'b', + 'hash_randomization': 'R', + 'py3k_warning': '3', + } + args = [] + for flag, opt in flag_opt_map.items(): + v = getattr(sys.flags, flag) + if v > 0: + args.append('-' + opt * v) + for opt in sys.warnoptions: + args.append('-W' + opt) + return args + + +def call(*popenargs, **kwargs): + """Run command with arguments. Wait for command to complete, then + return the returncode attribute. + + The arguments are the same as for the Popen constructor. Example: + + retcode = call(["ls", "-l"]) + """ + return Popen(*popenargs, **kwargs).wait() + + +def check_call(*popenargs, **kwargs): + """Run command with arguments. Wait for command to complete. If + the exit code was zero then return, otherwise raise + CalledProcessError. The CalledProcessError object will have the + return code in the returncode attribute. + + The arguments are the same as for the Popen constructor. Example: + + check_call(["ls", "-l"]) + """ + retcode = call(*popenargs, **kwargs) + if retcode: + cmd = kwargs.get("args") + if cmd is None: + cmd = popenargs[0] + raise CalledProcessError(retcode, cmd) + return 0 + + +def check_output(*popenargs, **kwargs): + r"""Run command with arguments and return its output as a byte string. + + If the exit code was non-zero it raises a CalledProcessError. The + CalledProcessError object will have the return code in the returncode + attribute and output in the output attribute. + + The arguments are the same as for the Popen constructor. Example: + + >>> check_output(["ls", "-l", "/dev/null"]) + 'crw-rw-rw- 1 root root 1, 3 Oct 18 2007 /dev/null\n' + + The stdout argument is not allowed as it is used internally. + To capture standard error in the result, use stderr=STDOUT. + + >>> check_output(["/bin/sh", "-c", + ... "ls -l non_existent_file ; exit 0"], + ... stderr=STDOUT) + 'ls: non_existent_file: No such file or directory\n' + """ + if 'stdout' in kwargs: + raise ValueError('stdout argument not allowed, it will be overridden.') + process = Popen(stdout=PIPE, *popenargs, **kwargs) + output, unused_err = process.communicate() + retcode = process.poll() + if retcode: + cmd = kwargs.get("args") + if cmd is None: + cmd = popenargs[0] + raise CalledProcessError(retcode, cmd, output=output) + return output + + +def list2cmdline(seq): + """ + Translate a sequence of arguments into a command line + string, using the same rules as the MS C runtime: + + 1) Arguments are delimited by white space, which is either a + space or a tab. + + 2) A string surrounded by double quotation marks is + interpreted as a single argument, regardless of white space + contained within. A quoted string can be embedded in an + argument. + + 3) A double quotation mark preceded by a backslash is + interpreted as a literal double quotation mark. + + 4) Backslashes are interpreted literally, unless they + immediately precede a double quotation mark. + + 5) If backslashes immediately precede a double quotation mark, + every pair of backslashes is interpreted as a literal + backslash. If the number of backslashes is odd, the last + backslash escapes the next double quotation mark as + described in rule 3. + """ + + # See + # http://msdn.microsoft.com/en-us/library/17w5ykft.aspx + # or search http://msdn.microsoft.com for + # "Parsing C++ Command-Line Arguments" + result = [] + needquote = False + for arg in seq: + bs_buf = [] + + # Add a space to separate this argument from the others + if result: + result.append(' ') + + needquote = (" " in arg) or ("\t" in arg) or not arg + if needquote: + result.append('"') + + for c in arg: + if c == '\\': + # Don't know if we need to double yet. + bs_buf.append(c) + elif c == '"': + # Double backslashes. + result.append('\\' * len(bs_buf)*2) + bs_buf = [] + result.append('\\"') + else: + # Normal char + if bs_buf: + result.extend(bs_buf) + bs_buf = [] + result.append(c) + + # Add remaining backslashes, if any. + if bs_buf: + result.extend(bs_buf) + + if needquote: + result.extend(bs_buf) + result.append('"') + + return ''.join(result) + + +if mswindows: + class STARTUP_INFO(ctypes.Structure): + _fields_ = [ + ("cb", ctypes.c_uint), + ("lpReserved", ctypes.c_uint), + ("lpDesktop", ctypes.c_uint), + ("lpTitle", ctypes.c_uint), + ("dwX", ctypes.c_uint), + ("dwY", ctypes.c_uint), + ("dwXSize", ctypes.c_uint), + ("dwYSize", ctypes.c_uint), + ("dwXCountChars", ctypes.c_uint), + ("dwYCountChars", ctypes.c_uint), + ("dwFillAttribute", ctypes.c_uint), + ("dwFlags", ctypes.c_uint), + ("wShowWindow", ctypes.c_uint16), + ("cbReserved2", ctypes.c_uint16), + ("lpReserved2", ctypes.c_uint), + ("hStdInput", ctypes.c_uint), + ("hStdOutput", ctypes.c_uint), + ("hStdError", ctypes.c_uint)] + + class PROCESS_INFORMATION(ctypes.Structure): + _fields_ = [ + ("hProcess", ctypes.c_uint), + ("hThread", ctypes.c_uint), + ("dwProcessId", ctypes.c_uint), + ("dwThreadId", ctypes.c_uint)] + +class Popen(object): + def __init__(self, args, bufsize=0, executable=None, + stdin=None, stdout=None, stderr=None, + preexec_fn=None, close_fds=False, shell=False, + cwd=None, env=None, universal_newlines=False, + startupinfo=None, creationflags=0): + """Create new Popen instance.""" + _cleanup() + + self._child_created = False + if not isinstance(bufsize, (int, long)): + raise TypeError("bufsize must be an integer") + + if mswindows: + if preexec_fn is not None: + raise ValueError("preexec_fn is not supported on Windows " + "platforms") + if close_fds and (stdin is not None or stdout is not None or + stderr is not None): + raise ValueError("close_fds is not supported on Windows " + "platforms if you redirect stdin/stdout/stderr") + else: + # POSIX + if startupinfo is not None: + raise ValueError("startupinfo is only supported on Windows " + "platforms") + if creationflags != 0: + raise ValueError("creationflags is only supported on Windows " + "platforms") + + self.stdin = None + self.stdout = None + self.stderr = None + self.pid = None + self.returncode = None + self.universal_newlines = universal_newlines + + # Input and output objects. The general principle is like + # this: + # + # Parent Child + # ------ ----- + # p2cwrite ---stdin---> p2cread + # c2pread <--stdout--- c2pwrite + # errread <--stderr--- errwrite + # + # On POSIX, the child objects are file descriptors. On + # Windows, these are Windows file handles. The parent objects + # are file descriptors on both platforms. The parent objects + # are None when not using PIPEs. The child objects are None + # when not redirecting. + + (p2cread, p2cwrite, + c2pread, c2pwrite, + errread, errwrite) = self._get_handles(stdin, stdout, stderr) + + try: + self._execute_child(args, executable, preexec_fn, close_fds, + cwd, env, universal_newlines, + startupinfo, creationflags, shell, + p2cread, p2cwrite, + c2pread, c2pwrite, + errread, errwrite) + except Exception: + # Preserve original exception in case os.close raises. + exc_type, exc_value, exc_trace = sys.exc_info() + + to_close = [] + # Only close the pipes we created. + if stdin == PIPE: + to_close.extend((p2cread, p2cwrite)) + if stdout == PIPE: + to_close.extend((c2pread, c2pwrite)) + if stderr == PIPE: + to_close.extend((errread, errwrite)) + + for fd in to_close: + try: + os.close(fd) + except EnvironmentError: + pass + + raise exc_type, exc_value, exc_trace + + if mswindows: + if p2cwrite is not None: + p2cwrite = msvcrt.open_osfhandle(p2cwrite.Detach(), 0) + if c2pread is not None: + c2pread = msvcrt.open_osfhandle(c2pread.Detach(), 0) + if errread is not None: + errread = msvcrt.open_osfhandle(errread.Detach(), 0) + + if p2cwrite is not None: + self.stdin = os.fdopen(p2cwrite, 'wb', bufsize) + if c2pread is not None: + if universal_newlines: + self.stdout = os.fdopen(c2pread, 'rU', bufsize) + else: + self.stdout = os.fdopen(c2pread, 'rb', bufsize) + if errread is not None: + if universal_newlines: + self.stderr = os.fdopen(errread, 'rU', bufsize) + else: + self.stderr = os.fdopen(errread, 'rb', bufsize) + + + def _translate_newlines(self, data): + data = data.replace("\r\n", "\n") + data = data.replace("\r", "\n") + return data + + + def __del__(self, _maxint=sys.maxint, _active=_active): + # If __init__ hasn't had a chance to execute (e.g. if it + # was passed an undeclared keyword argument), we don't + # have a _child_created attribute at all. + if not getattr(self, '_child_created', False): + # We didn't get to successfully create a child process. + return + # In case the child hasn't been waited on, check if it's done. + self._internal_poll(_deadstate=_maxint) + if self.returncode is None and _active is not None: + # Child is still running, keep us alive until we can wait on it. + _active.append(self) + else: + if mswindows: + if self._handle is not None: + ctypes.windll.kernel32.CloseHandle(self._handle) + self._handle = None + + + def communicate(self, input=None): + """Interact with process: Send data to stdin. Read data from + stdout and stderr, until end-of-file is reached. Wait for + process to terminate. The optional input argument should be a + string to be sent to the child process, or None, if no data + should be sent to the child. + + communicate() returns a tuple (stdout, stderr).""" + + # Optimization: If we are only using one pipe, or no pipe at + # all, using select() or threads is unnecessary. + if [self.stdin, self.stdout, self.stderr].count(None) >= 2: + stdout = None + stderr = None + if self.stdin: + if input: + try: + self.stdin.write(input) + except IOError as e: + if e.errno != errno.EPIPE and e.errno != errno.EINVAL: + raise + self.stdin.close() + elif self.stdout: + stdout = _eintr_retry_call(self.stdout.read) + self.stdout.close() + elif self.stderr: + stderr = _eintr_retry_call(self.stderr.read) + self.stderr.close() + self.wait() + return (stdout, stderr) + + return self._communicate(input) + + + def poll(self): + return self._internal_poll() + + + if mswindows: + # + # Windows methods + # + def _get_handles(self, stdin, stdout, stderr): + """Construct and return tuple with IO objects: + p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite + """ + if stdin is None and stdout is None and stderr is None: + return (None, None, None, None, None, None) + + p2cread, p2cwrite = None, None + c2pread, c2pwrite = None, None + errread, errwrite = None, None + + if stdin is None: + p2cread = _subprocess.GetStdHandle(_subprocess.STD_INPUT_HANDLE) + if p2cread is None: + p2cread, _ = _subprocess.CreatePipe(None, 0) + elif stdin == PIPE: + p2cread, p2cwrite = _subprocess.CreatePipe(None, 0) + elif isinstance(stdin, int): + p2cread = msvcrt.get_osfhandle(stdin) + else: + # Assuming file-like object + p2cread = msvcrt.get_osfhandle(stdin.fileno()) + p2cread = self._make_inheritable(p2cread) + + if stdout is None: + c2pwrite = _subprocess.GetStdHandle(_subprocess.STD_OUTPUT_HANDLE) + if c2pwrite is None: + _, c2pwrite = _subprocess.CreatePipe(None, 0) + elif stdout == PIPE: + c2pread, c2pwrite = _subprocess.CreatePipe(None, 0) + elif isinstance(stdout, int): + c2pwrite = msvcrt.get_osfhandle(stdout) + else: + # Assuming file-like object + c2pwrite = msvcrt.get_osfhandle(stdout.fileno()) + c2pwrite = self._make_inheritable(c2pwrite) + + if stderr is None: + errwrite = _subprocess.GetStdHandle(_subprocess.STD_ERROR_HANDLE) + if errwrite is None: + _, errwrite = _subprocess.CreatePipe(None, 0) + elif stderr == PIPE: + errread, errwrite = _subprocess.CreatePipe(None, 0) + elif stderr == STDOUT: + errwrite = c2pwrite + elif isinstance(stderr, int): + errwrite = msvcrt.get_osfhandle(stderr) + else: + # Assuming file-like object + errwrite = msvcrt.get_osfhandle(stderr.fileno()) + errwrite = self._make_inheritable(errwrite) + + return (p2cread, p2cwrite, + c2pread, c2pwrite, + errread, errwrite) + + + def _make_inheritable(self, handle): + """Return a duplicate of handle, which is inheritable""" + return _subprocess.DuplicateHandle(_subprocess.GetCurrentProcess(), + handle, _subprocess.GetCurrentProcess(), 0, 1, + _subprocess.DUPLICATE_SAME_ACCESS) + + + def _find_w9xpopen(self): + """Find and return absolut path to w9xpopen.exe""" + w9xpopen = os.path.join( + os.path.dirname(_subprocess.GetModuleFileName(0)), + "w9xpopen.exe") + if not os.path.exists(w9xpopen): + # Eeek - file-not-found - possibly an embedding + # situation - see if we can locate it in sys.exec_prefix + w9xpopen = os.path.join(os.path.dirname(sys.exec_prefix), + "w9xpopen.exe") + if not os.path.exists(w9xpopen): + raise RuntimeError("Cannot locate w9xpopen.exe, which is " + "needed for Popen to work with your " + "shell or platform.") + return w9xpopen + + + def _execute_child(self, args, executable, preexec_fn, close_fds, + cwd, env, universal_newlines, + startupinfo, creationflags, shell, + p2cread, p2cwrite, + c2pread, c2pwrite, + errread, errwrite): + """Execute program (MS Windows version)""" + + if not isinstance(args, types.StringTypes): + args = list2cmdline(args) + + # Process startup details + startup_info = STARTUP_INFO() + if startupinfo is None: + startupinfo = STARTUPINFO() + if None not in (p2cread, c2pwrite, errwrite): + startup_info.dwFlags = 0x00000100 + + startup_info.hStdInput = p2cread + startup_info.hStdOutput = c2pwrite + startup_info.hStdError = errwrite + + startup_info.dwFlags |= startupinfo.dwFlags + + if shell: + #startup_info.dwFlags |= 0x00000001 + startup_info.wShowWindow = 0 + comspec = os.environ.get("COMSPEC", "cmd.exe") + w9args = '{} /c "{}"'.format (comspec, args) + if (_subprocess.GetVersion() >= 0x80000000 or + os.path.basename(comspec).lower() == "command.com"): + # Win9x, or using command.com on NT. We need to + # use the w9xpopen intermediate program. For more + # information, see KB Q150956 + # (http://web.archive.org/web/20011105084002/http://support.microsoft.com/support/kb/articles/Q150/9/56.asp) + w9xpopen = self._find_w9xpopen() + w9args = '"%s" %s' % (w9xpopen, w9args) + # Not passing CREATE_NEW_CONSOLE has been known to + # cause random failures on win9x. Specifically a + # dialog: "Your program accessed mem currently in + # use at xxx" and a hopeful warning about the + # stability of your system. Cost is Ctrl+C wont + # kill children. + #creationflags |= _subprocess.CREATE_NEW_CONSOLE + creationflags = 0x00040000 + + # Start the process + + p_i = PROCESS_INFORMATION() + try: + args_u16 = unicode(args) + efg = ctypes.windll.kernel32.CreateProcessW(executable, args_u16, + #hp, ht, pid, tid = _subprocess.CreateProcess(executable, args, + # no special security + None, None, + 0, + creationflags, + env, + cwd, + ctypes.byref(startup_info), + ctypes.byref(p_i)) + except pywintypes.error, e: + # Translate pywintypes.error to WindowsError, which is + # a subclass of OSError. FIXME: We should really + # translate errno using _sys_errlist (or similar), but + # how can this be done from Python? + raise WindowsError(*e.args) + finally: + # Child is launched. Close the parent's copy of those pipe + # handles that only the child should have open. You need + # to make sure that no handles to the write end of the + # output pipe are maintained in this process or else the + # pipe will not close when the child process exits and the + # ReadFile will hang. + if p2cread is not None: + p2cread.Close() + if c2pwrite is not None: + c2pwrite.Close() + if errwrite is not None: + errwrite.Close() + + # Retain the process handle, but close the thread handle + self._child_created = True + self._handle = p_i.hProcess + self.pid = p_i.dwProcessId + ctypes.windll.kernel32.CloseHandle(p_i.hThread) + + def _internal_poll(self, _deadstate=None, + _WaitForSingleObject=_subprocess.WaitForSingleObject, + _WAIT_OBJECT_0=_subprocess.WAIT_OBJECT_0, + _GetExitCodeProcess=_subprocess.GetExitCodeProcess): + """Check if child process has terminated. Returns returncode + attribute. + + This method is called by __del__, so it can only refer to objects + in its local scope. + + """ + if self.returncode is None: + if _WaitForSingleObject(self._handle, 0) == _WAIT_OBJECT_0: + self.returncode = _GetExitCodeProcess(self._handle) + return self.returncode + + + def wait(self): + """Wait for child process to terminate. Returns returncode + attribute.""" + if self.returncode is None: + _subprocess.WaitForSingleObject(self._handle, + _subprocess.INFINITE) + self.returncode = _subprocess.GetExitCodeProcess(self._handle) + return self.returncode + + + def _readerthread(self, fh, buffer): + buffer.append(fh.read()) + + + def _communicate(self, input): + stdout = None # Return + stderr = None # Return + + if self.stdout: + stdout = [] + stdout_thread = threading.Thread(target=self._readerthread, + args=(self.stdout, stdout)) + stdout_thread.setDaemon(True) + stdout_thread.start() + if self.stderr: + stderr = [] + stderr_thread = threading.Thread(target=self._readerthread, + args=(self.stderr, stderr)) + stderr_thread.setDaemon(True) + stderr_thread.start() + + if self.stdin: + if input is not None: + try: + self.stdin.write(input) + except IOError as e: + if e.errno != errno.EPIPE: + raise + self.stdin.close() + + if self.stdout: + stdout_thread.join() + if self.stderr: + stderr_thread.join() + + # All data exchanged. Translate lists into strings. + if stdout is not None: + stdout = stdout[0] + if stderr is not None: + stderr = stderr[0] + + # Translate newlines, if requested. We cannot let the file + # object do the translation: It is based on stdio, which is + # impossible to combine with select (unless forcing no + # buffering). + if self.universal_newlines and hasattr(file, 'newlines'): + if stdout: + stdout = self._translate_newlines(stdout) + if stderr: + stderr = self._translate_newlines(stderr) + + self.wait() + return (stdout, stderr) + + def send_signal(self, sig): + """Send a signal to the process + """ + if sig == signal.SIGTERM: + self.terminate() + elif sig == signal.CTRL_C_EVENT: + os.kill(self.pid, signal.CTRL_C_EVENT) + elif sig == signal.CTRL_BREAK_EVENT: + os.kill(self.pid, signal.CTRL_BREAK_EVENT) + else: + raise ValueError("Unsupported signal: {}".format(sig)) + + def terminate(self): + """Terminates the process + """ + try: + _subprocess.TerminateProcess(self._handle, 1) + except OSError as e: + # ERROR_ACCESS_DENIED (winerror 5) is received when the + # process already died. + if e.winerror != 5: + raise + rc = _subprocess.GetExitCodeProcess(self._handle) + if rc == _subprocess.STILL_ACTIVE: + raise + self.returncode = rc + + kill = terminate + + else: + # + # POSIX methods + # + def _get_handles(self, stdin, stdout, stderr): + """Construct and return tuple with IO objects: + p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite + """ + p2cread, p2cwrite = None, None + c2pread, c2pwrite = None, None + errread, errwrite = None, None + + if stdin is None: + pass + elif stdin == PIPE: + p2cread, p2cwrite = self.pipe_cloexec() + elif isinstance(stdin, int): + p2cread = stdin + else: + # Assuming file-like object + p2cread = stdin.fileno() + + if stdout is None: + pass + elif stdout == PIPE: + c2pread, c2pwrite = self.pipe_cloexec() + elif isinstance(stdout, int): + c2pwrite = stdout + else: + # Assuming file-like object + c2pwrite = stdout.fileno() + + if stderr is None: + pass + elif stderr == PIPE: + errread, errwrite = self.pipe_cloexec() + elif stderr == STDOUT: + errwrite = c2pwrite + elif isinstance(stderr, int): + errwrite = stderr + else: + # Assuming file-like object + errwrite = stderr.fileno() + + return (p2cread, p2cwrite, + c2pread, c2pwrite, + errread, errwrite) + + + def _set_cloexec_flag(self, fd, cloexec=True): + try: + cloexec_flag = fcntl.FD_CLOEXEC + except AttributeError: + cloexec_flag = 1 + + old = fcntl.fcntl(fd, fcntl.F_GETFD) + if cloexec: + fcntl.fcntl(fd, fcntl.F_SETFD, old | cloexec_flag) + else: + fcntl.fcntl(fd, fcntl.F_SETFD, old & ~cloexec_flag) + + + def pipe_cloexec(self): + """Create a pipe with FDs set CLOEXEC.""" + # Pipes' FDs are set CLOEXEC by default because we don't want them + # to be inherited by other subprocesses: the CLOEXEC flag is removed + # from the child's FDs by _dup2(), between fork() and exec(). + # This is not atomic: we would need the pipe2() syscall for that. + r, w = os.pipe() + self._set_cloexec_flag(r) + self._set_cloexec_flag(w) + return r, w + + + def _close_fds(self, but): + if hasattr(os, 'closerange'): + os.closerange(3, but) + os.closerange(but + 1, MAXFD) + else: + for i in xrange(3, MAXFD): + if i == but: + continue + try: + os.close(i) + except: + pass + + + def _execute_child(self, args, executable, preexec_fn, close_fds, + cwd, env, universal_newlines, + startupinfo, creationflags, shell, + p2cread, p2cwrite, + c2pread, c2pwrite, + errread, errwrite): + """Execute program (POSIX version)""" + + if isinstance(args, types.StringTypes): + args = [args] + else: + args = list(args) + + if shell: + args = ["/bin/sh", "-c"] + args + if executable: + args[0] = executable + + if executable is None: + executable = args[0] + + # For transferring possible exec failure from child to parent + # The first char specifies the exception type: 0 means + # OSError, 1 means some other error. + errpipe_read, errpipe_write = self.pipe_cloexec() + try: + try: + gc_was_enabled = gc.isenabled() + # Disable gc to avoid bug where gc -> file_dealloc -> + # write to stderr -> hang. http://bugs.python.org/issue1336 + gc.disable() + try: + self.pid = os.fork() + except: + if gc_was_enabled: + gc.enable() + raise + self._child_created = True + if self.pid == 0: + # Child + try: + # Close parent's pipe ends + if p2cwrite is not None: + os.close(p2cwrite) + if c2pread is not None: + os.close(c2pread) + if errread is not None: + os.close(errread) + os.close(errpipe_read) + + # When duping fds, if there arises a situation + # where one of the fds is either 0, 1 or 2, it + # is possible that it is overwritten (#12607). + if c2pwrite == 0: + c2pwrite = os.dup(c2pwrite) + if errwrite == 0 or errwrite == 1: + errwrite = os.dup(errwrite) + + # Dup fds for child + def _dup2(a, b): + # dup2() removes the CLOEXEC flag but + # we must do it ourselves if dup2() + # would be a no-op (issue #10806). + if a == b: + self._set_cloexec_flag(a, False) + elif a is not None: + os.dup2(a, b) + _dup2(p2cread, 0) + _dup2(c2pwrite, 1) + _dup2(errwrite, 2) + + # Close pipe fds. Make sure we don't close the + # same fd more than once, or standard fds. + closed = set(None) + for fd in [p2cread, c2pwrite, errwrite]: + if fd not in closed and fd > 2: + os.close(fd) + closed.add(fd) + + # Close all other fds, if asked for + if close_fds: + self._close_fds(but=errpipe_write) + + if cwd is not None: + os.chdir(cwd) + + if preexec_fn: + preexec_fn() + + if env is None: + os.execvp(executable, args) + else: + os.execvpe(executable, args, env) + + except: + exc_type, exc_value, tb = sys.exc_info() + # Save the traceback and attach it to the exception object + exc_lines = traceback.format_exception(exc_type, + exc_value, + tb) + exc_value.child_traceback = ''.join(exc_lines) + os.write(errpipe_write, pickle.dumps(exc_value)) + + # This exitcode won't be reported to applications, so it + # really doesn't matter what we return. + os._exit(255) + + # Parent + if gc_was_enabled: + gc.enable() + finally: + # be sure the FD is closed no matter what + os.close(errpipe_write) + + if p2cread is not None and p2cwrite is not None: + os.close(p2cread) + if c2pwrite is not None and c2pread is not None: + os.close(c2pwrite) + if errwrite is not None and errread is not None: + os.close(errwrite) + + # Wait for exec to fail or succeed; possibly raising exception + # Exception limited to 1M + data = _eintr_retry_call(os.read, errpipe_read, 1048576) + finally: + # be sure the FD is closed no matter what + os.close(errpipe_read) + + if data != "": + try: + _eintr_retry_call(os.waitpid, self.pid, 0) + except OSError as e: + if e.errno != errno.ECHILD: + raise + child_exception = pickle.loads(data) + #if child_exception is not None: raise child_exception + #else: + # class ChildProcessException(Exception): pass + #raise ChildProcessException + raise child_exception + + + def _handle_exitstatus(self, sts, _WIFSIGNALED=os.WIFSIGNALED, + _WTERMSIG=os.WTERMSIG, _WIFEXITED=os.WIFEXITED, + _WEXITSTATUS=os.WEXITSTATUS): + # This method is called (indirectly) by __del__, so it cannot + # refer to anything outside of its local scope.""" + if _WIFSIGNALED(sts): + self.returncode = -_WTERMSIG(sts) + elif _WIFEXITED(sts): + self.returncode = _WEXITSTATUS(sts) + else: + # Should never happen + raise RuntimeError("Unknown child exit status!") + + + def _internal_poll(self, _deadstate=None, _waitpid=os.waitpid, + _WNOHANG=os.WNOHANG, _os_error=os.error, _ECHILD=errno.ECHILD): + """Check if child process has terminated. Returns returncode + attribute. + + This method is called by __del__, so it cannot reference anything + outside of the local scope (nor can any methods it calls). + + """ + if self.returncode is None: + try: + pid, sts = _waitpid(self.pid, _WNOHANG) + if pid == self.pid: + self._handle_exitstatus(sts) + except _os_error as e: + if _deadstate is not None: + self.returncode = _deadstate + if e.errno == _ECHILD: + # This happens if SIGCLD is set to be ignored or + # waiting for child processes has otherwise been + # disabled for our process. This child is dead, we + # can't get the status. + # http://bugs.python.org/issue15756 + self.returncode = 0 + return self.returncode + + + def wait(self): + """Wait for child process to terminate. Returns returncode + attribute.""" + while self.returncode is None: + try: + pid, sts = _eintr_retry_call(os.waitpid, self.pid, 0) + except OSError as e: + if e.errno != errno.ECHILD: + raise + # This happens if SIGCLD is set to be ignored or waiting + # for child processes has otherwise been disabled for our + # process. This child is dead, we can't get the status. + pid = self.pid + sts = 0 + # Check the pid and loop as waitpid has been known to return + # 0 even without WNOHANG in odd situations. issue14396. + if pid == self.pid: + self._handle_exitstatus(sts) + return self.returncode + + + def _communicate(self, input): + if self.stdin: + # Flush stdio buffer. This might block, if the user has + # been writing to .stdin in an uncontrolled fashion. + self.stdin.flush() + if not input: + self.stdin.close() + + if _has_poll: + stdout, stderr = self._communicate_with_poll(input) + else: + stdout, stderr = self._communicate_with_select(input) + + # All data exchanged. Translate lists into strings. + if stdout is not None: + stdout = ''.join(stdout) + if stderr is not None: + stderr = ''.join(stderr) + + # Translate newlines, if requested. We cannot let the file + # object do the translation: It is based on stdio, which is + # impossible to combine with select (unless forcing no + # buffering). + if self.universal_newlines and hasattr(file, 'newlines'): + if stdout: + stdout = self._translate_newlines(stdout) + if stderr: + stderr = self._translate_newlines(stderr) + + self.wait() + return (stdout, stderr) + + + def _communicate_with_poll(self, input): + stdout = None # Return + stderr = None # Return + fd2file = {} + fd2output = {} + + poller = select.poll() + def register_and_append(file_obj, eventmask): + poller.register(file_obj.fileno(), eventmask) + fd2file[file_obj.fileno()] = file_obj + + def close_unregister_and_remove(fd): + poller.unregister(fd) + fd2file[fd].close() + fd2file.pop(fd) + + if self.stdin and input: + register_and_append(self.stdin, select.POLLOUT) + + select_POLLIN_POLLPRI = select.POLLIN | select.POLLPRI + if self.stdout: + register_and_append(self.stdout, select_POLLIN_POLLPRI) + fd2output[self.stdout.fileno()] = stdout = [] + if self.stderr: + register_and_append(self.stderr, select_POLLIN_POLLPRI) + fd2output[self.stderr.fileno()] = stderr = [] + + input_offset = 0 + while fd2file: + try: + ready = poller.poll() + except select.error, e: + if e.args[0] == errno.EINTR: + continue + raise + + for fd, mode in ready: + if mode & select.POLLOUT: + chunk = input[input_offset : input_offset + _PIPE_BUF] + try: + input_offset += os.write(fd, chunk) + except OSError as e: + if e.errno == errno.EPIPE: + close_unregister_and_remove(fd) + else: + raise + else: + if input_offset >= len(input): + close_unregister_and_remove(fd) + elif mode & select_POLLIN_POLLPRI: + data = os.read(fd, 4096) + if not data: + close_unregister_and_remove(fd) + fd2output[fd].append(data) + else: + # Ignore hang up or errors. + close_unregister_and_remove(fd) + + return (stdout, stderr) + + + def _communicate_with_select(self, input): + read_set = [] + write_set = [] + stdout = None # Return + stderr = None # Return + + if self.stdin and input: + write_set.append(self.stdin) + if self.stdout: + read_set.append(self.stdout) + stdout = [] + if self.stderr: + read_set.append(self.stderr) + stderr = [] + + input_offset = 0 + while read_set or write_set: + try: + rlist, wlist, xlist = select.select(read_set, write_set, []) + except select.error, e: + if e.args[0] == errno.EINTR: + continue + raise + + if self.stdin in wlist: + chunk = input[input_offset : input_offset + _PIPE_BUF] + try: + bytes_written = os.write(self.stdin.fileno(), chunk) + except OSError as e: + if e.errno == errno.EPIPE: + self.stdin.close() + write_set.remove(self.stdin) + else: + raise + else: + input_offset += bytes_written + if input_offset >= len(input): + self.stdin.close() + write_set.remove(self.stdin) + + if self.stdout in rlist: + data = os.read(self.stdout.fileno(), 1024) + if data == "": + self.stdout.close() + read_set.remove(self.stdout) + stdout.append(data) + + if self.stderr in rlist: + data = os.read(self.stderr.fileno(), 1024) + if data == "": + self.stderr.close() + read_set.remove(self.stderr) + stderr.append(data) + + return (stdout, stderr) + + + def send_signal(self, sig): + """Send a signal to the process + """ + os.kill(self.pid, sig) + + def terminate(self): + """Terminate the process with SIGTERM + """ + self.send_signal(signal.SIGTERM) + + def kill(self): + """Kill the process with SIGKILL + """ + self.send_signal(signal.SIGKILL) + + +def _demo_posix(): + # + # Example 1: Simple redirection: Get process list + # + plist = Popen(["ps"], stdout=PIPE).communicate()[0] + print "Process list:" + print plist + + # + # Example 2: Change uid before executing child + # + if os.getuid() == 0: + p = Popen(["id"], preexec_fn=lambda: os.setuid(100)) + p.wait() + + # + # Example 3: Connecting several subprocesses + # + print "Looking for 'hda'..." + p1 = Popen(["dmesg"], stdout=PIPE) + p2 = Popen(["grep", "hda"], stdin=p1.stdout, stdout=PIPE) + print repr(p2.communicate()[0]) + + # + # Example 4: Catch execution error + # + print + print "Trying a weird file..." + try: + print Popen(["/this/path/does/not/exist"]).communicate() + except OSError, e: + if e.errno == errno.ENOENT: + print "The file didn't exist. I thought so..." + print "Child traceback:" + print e.child_traceback + else: + print "Error", e.errno + else: + print >>sys.stderr, "Gosh. No error." + + +def _demo_windows(): + # + # Example 1: Connecting several subprocesses + # + print "Looking for 'PROMPT' in set output..." + p1 = Popen("set", stdout=PIPE, shell=True) + p2 = Popen('find "PROMPT"', stdin=p1.stdout, stdout=PIPE) + print repr(p2.communicate()[0]) + + # + # Example 2: Simple execution of program + # + print "Executing calc..." + p = Popen("calc") + p.wait() + + +if __name__ == "__main__": + if mswindows: + _demo_windows() + else: + _demo_posix() diff --git a/test/ArmoryDTest.py b/test/ArmoryDTest.py new file mode 100644 index 000000000..36d9210d3 --- /dev/null +++ b/test/ArmoryDTest.py @@ -0,0 +1,231 @@ +''' +Created on Oct 8, 2013 + +@author: Andy +''' +import os +import sys +import time +import unittest + +sys.argv.append('--nologging') +sys.argv.append('--testnet') +from CppBlockUtils import SecureBinaryData, CryptoECDSA +from armoryd import Armory_Json_Rpc_Server, PrivateKeyNotFound, \ + InvalidBitcoinAddress, WalletUnlockNeeded, Armory_Daemon, AmountToJSON +from armoryengine.ArmoryUtils import ARMORY_HOME_DIR, hex_to_binary, \ + binary_to_base58, binary_to_hex, convertKeyDataToAddress, hash160_to_addrStr,\ + hex_switchEndian, hash160, BIGENDIAN +from armoryengine.BDM import TheBDM +from armoryengine.PyBtcWallet import PyBtcWallet +from armoryengine.Transaction import PyTx + + +TEST_WALLET_NAME = 'Test Wallet Name' +TEST_WALLET_DESCRIPTION = 'Test Wallet Description' +TEST_WALLET_ID = '3VB8XSoY' + +RAW_TX1 = ('01000000081fa335f8aa332693c7bf77c960ac1eb86c50a5f60d8dc6892d4' + '3f89473dc50e4b104000000ffffffff4be787d4a6009ba04534c9b42af46e' + '5442744804e6050ad08e58804ef8f067882601000000ffffffffa20b1cdd9' + '232e335f6200f7d2623d2789a0847dc53faa79387ffab38271307be650400' + '0000ffffffff3a7771644a35dbe67e071f7d3ec8e097ec23817311c0a0d0e' + 'a6a03d6c3a62f1b7000000000ffffffff445d69280e25c34db1159deef391' + '10fb47e7d3091972e4366e3991d52ac38edfec00000000ffffffffc258ede' + 'c7fc46646ab2d4c682abf3757dabdee91b7635aa0e62f0d620cdb97830204' + '000000ffffffff38b93d88c22b56e58489dba7bbf439bb5044e5f76e00969' + 'd591e805636601c886600000000ffffffffdada1b7c73ce39cffb8245f343' + '91c0f0d54f32cb3f05570b1c37635c44179df72700000000ffffffff03f04' + 'b0000000000001976a914be17ec0fc1f8aa029223dbe5f53109d0faf8c797' + '88ac10270000000000001976a91443134735b72a1e9cf5e4c56d910295313' + '2352ba688ac00000000000000000000000000000000000000000000000000' + '00000000000000ffffffff3a4c3754686973206973206120636f6d6d656e7' + '420617474616368656420746f2061207472616e73616374696f6e206f6e20' + '6d61696e6e65742e7500000000') + +TX_ID1 = hex_switchEndian('e0dc8e3d3654c5bfeb1eb077f835179395ee82d623b0c0d3c7074fc2d4c0706f') + +TX_ID1_OUTPUT0_VALUE = 63000 +TX_ID1_OUTPUT1_VALUE = 139367000 + +PASSPHRASE1 = 'abcde' +UNLOCK_TIMEOUT = 5 + +class ArmoryDTest(unittest.TestCase): + def removeFileList(self, fileList): + for f in fileList: + if os.path.exists(f): + os.remove(f) + + @classmethod + def setUpClass(self): + # This is not a UI so no need to worry about the main thread being blocked. + # Any UI that uses this Daemon can put the call to the Daemon on it's own thread. + TheBDM.Reset() + TheBDM.setBlocking(True) + TheBDM.setOnlineMode(True) + while not TheBDM.getBDMState()=='BlockchainReady': + time.sleep(2) + + def setUp(self): + self.fileA = os.path.join(ARMORY_HOME_DIR, 'armory_%s_.wallet' % TEST_WALLET_ID) + self.fileB = os.path.join(ARMORY_HOME_DIR, 'armory_%s_backup.wallet' % TEST_WALLET_ID) + self.fileAupd = os.path.join(ARMORY_HOME_DIR, 'armory_%s_backup_unsuccessful.wallet' % TEST_WALLET_ID) + self.fileBupd = os.path.join(ARMORY_HOME_DIR, 'armory_%s_update_unsuccessful.wallet' % TEST_WALLET_ID) + + self.removeFileList([self.fileA, self.fileB, self.fileAupd, self.fileBupd]) + + # We need a controlled test, so we script the all the normally-random stuff + self.privKey = SecureBinaryData('\xaa'*32) + self.privKey2 = SecureBinaryData('\x33'*32) + self.chainstr = SecureBinaryData('\xee'*32) + theIV = SecureBinaryData(hex_to_binary('77'*16)) + self.passphrase = SecureBinaryData('A self.passphrase') + self.passphrase2 = SecureBinaryData('A new self.passphrase') + + self.wallet = PyBtcWallet().createNewWallet(withEncrypt=False, \ + plainRootKey=self.privKey, \ + chaincode=self.chainstr, \ + IV=theIV, \ + shortLabel=TEST_WALLET_NAME, \ + longLabel=TEST_WALLET_DESCRIPTION) + self.jsonServer = Armory_Json_Rpc_Server(self.wallet) + TheBDM.registerWallet(self.wallet) + + def tearDown(self): + self.removeFileList([self.fileA, self.fileB, self.fileAupd, self.fileBupd]) + + + # Can't test with actual transactions in this environment. See ARMORY-34. + # This wallet has no txs + # def testListunspent(self): + # actualResult = self.jsonServer.jsonrpc_listunspent() + # self.assertEqual(actualResult, []) + + def testImportprivkey(self): + originalLength = len(self.wallet.linearAddr160List) + self.jsonServer.jsonrpc_importprivkey(self.privKey2) + self.assertEqual(len(self.wallet.linearAddr160List), originalLength+1) + + def testGettxout(self): + txOut = self.jsonServer.jsonrpc_gettxout(TX_ID1, 0) + self.assertEquals(TX_ID1_OUTPUT0_VALUE, txOut.value) + txOut = self.jsonServer.jsonrpc_gettxout(TX_ID1, 1) + self.assertEquals(TX_ID1_OUTPUT1_VALUE, txOut.value) + + # Cannot unit test actual balances. Only verify that getreceivedbyaddress return a 0 result. + def testGetreceivedbyaddress(self): + a160 = hash160(self.wallet.getNextUnusedAddress().binPublicKey65.toBinStr()) + testAddr = hash160_to_addrStr(a160) + result = self.jsonServer.jsonrpc_getreceivedbyaddress(testAddr) + self.assertEqual(result, 0) + + def testGetrawtransaction(self): + actualRawTx = self.jsonServer.jsonrpc_getrawtransaction(TX_ID1) + pyTx = PyTx().unserialize(hex_to_binary(actualRawTx)) + self.assertEquals(TX_ID1, binary_to_hex(pyTx.getHash(), BIGENDIAN)) + + def testBackupWallet(self): + backupTestPath = os.path.join(ARMORY_HOME_DIR, 'armory_%s_.wallet.backup.test' % TEST_WALLET_ID) + # Remove backupTestPath in case it exists + backupFileList = [backupTestPath, self.fileB] + self.removeFileList(backupFileList) + # Remove the backup test path that is to be created after tear down. + self.addCleanup(self.removeFileList, backupFileList) + self.jsonServer.jsonrpc_backupwallet(backupTestPath) + self.assertTrue(os.path.exists(backupTestPath)) + self.wallet.backupWalletFile() + self.assertTrue(os.path.exists(self.fileB)) + + def testDecoderawtransaction(self): + actualDD = self.jsonServer.jsonrpc_decoderawtransaction(RAW_TX1) + # Test specific values pulled from bitcoin daemon's output for the test raw TX + expectScriptStr = 'OP_DUP OP_HASH160 PUSHDATA(20) [be17ec0fc1f8aa029223dbe5f53109d0faf8c797] OP_EQUALVERIFY OP_CHECKSIG' + self.assertEqual(actualDD['locktime'], 0) + self.assertEqual(actualDD['version'], 1) + self.assertEqual(actualDD['vin'][0]['sequence'], 4294967295L) + self.assertEqual(actualDD['vin'][0]['scriptSig']['hex'], '') + self.assertEqual(actualDD['vin'][0]['scriptSig']['asm'], '') + self.assertEqual(actualDD['vin'][0]['vout'], 1201) + self.assertEqual(actualDD['vin'][0]['txid'], 'e450dc7394f8432d89c68d0df6a5506cb81eac60c977bfc7932633aaf835a31f') + self.assertEqual(actualDD['vin'][1]['vout'], 294) + self.assertEqual(actualDD['vin'][1]['txid'], '8867f0f84e80588ed00a05e604487442546ef42ab4c93445a09b00a6d487e74b') + self.assertEqual(actualDD['vout'][0]['value'], 0.0001944) + self.assertEqual(actualDD['vout'][0]['n'], 0) + self.assertEqual(actualDD['vout'][0]['scriptPubKey']['reqSigs'], 1) + self.assertEqual(actualDD['vout'][0]['scriptPubKey']['hex'], '76a914be17ec0fc1f8aa029223dbe5f53109d0faf8c79788ac') + self.assertEqual(actualDD['vout'][0]['scriptPubKey']['addresses'], ['mxr5Le3bt7dfbFqmpK6saUYPt5xtcDB7Yw']) + self.assertEqual(actualDD['vout'][0]['scriptPubKey']['asm'], expectScriptStr) + self.assertEqual(actualDD['vout'][0]['scriptPubKey']['type'], 'Standard (PKH)') + self.assertEqual(actualDD['vout'][1]['scriptPubKey']['type'], 'Standard (PKH)') + self.assertEqual(actualDD['vout'][2]['scriptPubKey']['type'], 'Non-Standard') + + + def testDumpprivkey(self): + + testPrivKey = self.privKey.toBinStr() + hash160 = convertKeyDataToAddress(testPrivKey) + addr58 = hash160_to_addrStr(hash160) + + # Verify that a bogus addrss Raises InvalidBitcoinAddress Exception + self.assertRaises(InvalidBitcoinAddress, self.jsonServer.jsonrpc_dumpprivkey, 'bogus') + + # verify that the root private key is not found + self.assertRaises(PrivateKeyNotFound, self.jsonServer.jsonrpc_dumpprivkey, addr58) + + # verify that the first private key can be found + firstHash160 = self.wallet.getNextUnusedAddress().getAddr160() + firstAddr58 = hash160_to_addrStr(firstHash160) + actualPrivateKey = self.jsonServer.jsonrpc_dumpprivkey(firstAddr58) + expectedPrivateKey = self.wallet.getAddrByHash160(firstHash160).serializePlainPrivateKey() + self.assertEqual(actualPrivateKey, expectedPrivateKey) + + # Verify that a locked wallet Raises WalletUnlockNeeded Exception + kdfParams = self.wallet.computeSystemSpecificKdfParams(0.1) + self.wallet.changeKdfParams(*kdfParams) + self.wallet.changeWalletEncryption( securePassphrase=self.passphrase ) + self.wallet.lock() + self.assertRaises(WalletUnlockNeeded, self.jsonServer.jsonrpc_dumpprivkey, addr58) + + def testEncryptwallet(self): + kdfParams = self.wallet.computeSystemSpecificKdfParams(0.1) + self.wallet.changeKdfParams(*kdfParams) + self.jsonServer.jsonrpc_encryptwallet(PASSPHRASE1) + self.assertTrue(self.wallet.isLocked) + + # Verify that a locked wallet Raises WalletUnlockNeeded Exception + self.assertRaises(WalletUnlockNeeded, self.jsonServer.jsonrpc_encryptwallet, PASSPHRASE1) + + def testUnlockwallet(self): + kdfParams = self.wallet.computeSystemSpecificKdfParams(0.1) + self.wallet.changeKdfParams(*kdfParams) + self.jsonServer.jsonrpc_encryptwallet(PASSPHRASE1) + self.assertTrue(self.wallet.isLocked) + self.jsonServer.jsonrpc_unlockwallet(PASSPHRASE1, UNLOCK_TIMEOUT) + self.assertFalse(self.wallet.isLocked) + time.sleep(UNLOCK_TIMEOUT+1) + self.wallet.checkWalletLockTimeout() + self.assertTrue(self.wallet.isLocked) + + def testGetWalletInfo(self): + wltInfo = self.jsonServer.jsonrpc_getwalletinfo() + self.assertEqual(wltInfo['name'], TEST_WALLET_NAME) + self.assertEqual(wltInfo['description'], TEST_WALLET_DESCRIPTION) + self.assertEqual(wltInfo['balance'], AmountToJSON(self.wallet.getBalance('Spend'))) + self.assertEqual(wltInfo['keypoolsize'], self.wallet.addrPoolSize) + self.assertEqual(wltInfo['numaddrgen'], len(self.wallet.addrMap)) + self.assertEqual(wltInfo['highestusedindex'], self.wallet.highestUsedChainIndex) + + # This should always return 0 balance + # Need to create our own test net to test with balances + def testGetBalance(self): + for ballanceType in ['spendable','spend', 'unconf', \ + 'unconfirmed', 'total', 'ultimate','unspent', 'full']: + self.assertEqual(self.jsonServer.jsonrpc_getbalance(ballanceType), + AmountToJSON(self.wallet.getBalance(ballanceType))) + self.assertEqual(self.jsonServer.jsonrpc_getbalance('bogus'), -1) + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/test/ArmoryEngineUtilsTest.py b/test/ArmoryEngineUtilsTest.py new file mode 100644 index 000000000..de314cc3e --- /dev/null +++ b/test/ArmoryEngineUtilsTest.py @@ -0,0 +1,518 @@ +''' +Created on Jul 29, 2013 + +@author: Andy +''' +import hashlib +import locale +from random import shuffle +import sys +import time +import unittest + +sys.path.append('..') + +from armoryengine.ArmoryUtils import * +from armoryengine.BinaryPacker import * +from armoryengine.BinaryUnpacker import * +import armoryengine.ArmoryUtils +from armoryengine import ArmoryUtils + + +#sys.argv.append('--nologging') + +UNICODE_STRING = u'unicode string' +NON_ASCII_STRING = '\xff\x00 Non-ASCII string \xff\x00' +ASCII_STRING = 'ascii string' +LONG_TEST_NUMBER = 98753178900 + + +################################################################################ +################################################################################ +class ArmoryEngineTest(unittest.TestCase): + + + ############################################################################# + def testIsASCII(self): + self.assertTrue(isASCII(ASCII_STRING)) + self.assertFalse(isASCII(NON_ASCII_STRING)) + + ############################################################################# + def testToBytes(self): + self.assertEqual(toBytes(UNICODE_STRING), UNICODE_STRING.encode('utf-8')) + self.assertEqual(toBytes(ASCII_STRING), ASCII_STRING) + self.assertEqual(toBytes(NON_ASCII_STRING), NON_ASCII_STRING) + self.assertEqual(toBytes(5), None) + + ############################################################################# + def testToUnicode(self): + self.assertEqual(toUnicode(ASCII_STRING), unicode(ASCII_STRING, 'utf-8')) + self.assertEqual(toUnicode(UNICODE_STRING), UNICODE_STRING) + self.assertEqual(toUnicode(5),None) + + ############################################################################# + def testToPreferred(self): + self.assertEqual(toPreferred(ASCII_STRING), toUnicode(ASCII_STRING).encode(locale.getpreferredencoding())) + + ############################################################################# + def testLenBytes(self): + self.assertEqual(lenBytes(ASCII_STRING), len(ASCII_STRING)) + + ############################################################################# + def testBasicUtils(self): + addr = '1Ncui8YjT7JJD91tkf42dijPnqywbupf7w' # Sam Rushing's BTC address + i = 4093 + hstr = 'fd0f' + bstr = '\xfd\x0f' + + self.callTestFunction('int_to_hex', hstr, i ) + self.callTestFunction('int_to_hex', hstr, long(i)) + self.callTestFunction('int_to_hex', hstr + "00", i, 3) + self.callTestFunction('hex_to_int', i, hstr) + self.callTestFunction('int_to_binary', bstr, i ) + self.callTestFunction('binary_to_int', i, bstr) + self.callTestFunction('hex_to_binary', bstr, hstr) + self.callTestFunction('binary_to_hex', hstr, bstr) + self.callTestFunction('hex_switchEndian', '67452301', '01234567') + + hstr = '0ffd' + bstr = '\x0f\xfd' + + self.callTestFunction('int_to_hex', hstr, i , 2, BIGENDIAN) + self.callTestFunction('hex_to_int', i, hstr, BIGENDIAN) + self.callTestFunction('int_to_binary', bstr, i , 2, BIGENDIAN) + self.callTestFunction('binary_to_int', i, bstr, BIGENDIAN) + + #h = '00000123456789abcdef000000' + #ans = 'aaaaabcdeghjknrsuwxyaaaaaa' + #self.callTestFunction('binary_to_typingBase16', ans, h ) + #self.callTestFunction('typingBase16_to_binary', h, ans) + + blockhead = '010000001d8f4ec0443e1f19f305e488c1085c95de7cc3fd25e0d2c5bb5d0000000000009762547903d36881a86751f3f5049e23050113f779735ef82734ebf0b4450081d8c8c84db3936a1a334b035b' + blockhash = '1195e67a7a6d0674bbd28ae096d602e1f038c8254b49dfe79d47000000000000' + blockhashBE = '000000000000479de7df494b25c838f0e102d696e08ad2bb74066d7a7ae69511' + blockhashBEDifficulty = 3.6349e-48 + allF = 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' + + self.callTestFunction('ubtc_to_floatStr', '12.05600000', 1205600000) + self.callTestFunction('floatStr_to_ubtc', 1205600000, '12.056') + self.callTestFunction('float_to_btc', 1205600000, 12.056) + + self.callTestFunction('packVarInt', ['A',1], 65) + self.callTestFunction('packVarInt', ['\xfd\xff\x00', 3], 255) + self.callTestFunction('packVarInt', ['\xfe\x00\x00\x01\x00', 5], 65536) + self.callTestFunction('packVarInt', ['\xff\x00\x10\xa5\xd4\xe8\x00\x00\x00', 9], 10**12) + + self.callTestFunction('unpackVarInt', [65,1], 'A') + self.callTestFunction('unpackVarInt', [255, 3], '\xfd\xff\x00') + self.callTestFunction('unpackVarInt', [65536, 5], '\xfe\x00\x00\x01\x00') + self.callTestFunction('unpackVarInt', [10**12, 9], '\xff\x00\x10\xa5\xd4\xe8\x00\x00\x00') + + + data = hex_to_binary('11' + 'aa'*31) + dataBE = hex_to_binary('11' + 'aa'*31, endIn=LITTLEENDIAN, endOut=BIGENDIAN) + dataE1 = hex_to_binary('11' + 'aa'*30 + 'ab') + dataE2 = hex_to_binary('11' + 'aa'*29 + 'abab') + dchk = hash256(data)[:4] + self.callTestFunction('verifyChecksum', data, data, dchk) + self.callTestFunction('verifyChecksum', data, dataBE, dchk, beQuiet=True) + self.callTestFunction('verifyChecksum', '', dataE1, dchk, hash256, False, True) # don't fix + self.callTestFunction('verifyChecksum', data, dataE1, dchk, hash256, True, True) # try fix + self.callTestFunction('verifyChecksum', '', dataE2, dchk, hash256, False, True) # don't fix + self.callTestFunction('verifyChecksum', '', dataE2, dchk, hash256, True, True) # try fix + + + verTuple = (0,50,0,0) + verInt = 5000000 + verStr = '0.50' + self.callTestFunction('getVersionString', verStr, verTuple) + self.callTestFunction('getVersionInt', verInt, verTuple) + self.callTestFunction('readVersionString', verTuple, verStr) + self.callTestFunction('readVersionInt', verTuple, verInt) + + verTuple = (1,0,12,0) + verInt = 10012000 + verStr = '1.00.12' + self.callTestFunction('getVersionString', verStr, verTuple) + self.callTestFunction('getVersionInt', verInt, verTuple) + self.callTestFunction('readVersionString', verTuple, verStr) + self.callTestFunction('readVersionInt', verTuple, verInt) + + verTuple = (0,20,0,108) + verInt = 2000108 + verStr = '0.20.0.108' + self.callTestFunction('getVersionString', verStr, verTuple) + self.callTestFunction('getVersionInt', verInt, verTuple) + self.callTestFunction('readVersionString', verTuple, verStr) + self.callTestFunction('readVersionInt', verTuple, verInt) + + miniKey = 'S4b3N3oGqDqR5jNuxEvDwf' + miniPriv = hex_to_binary('0c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d') + self.callTestFunction('decodeMiniPrivateKey', miniPriv, miniKey) + + self.callTestFunction('coin2str',' 0.0000', 0, 4) + + self.callTestFunction('coin2str',' 987.5318', LONG_TEST_NUMBER, 4) + self.callTestFunction('coin2str','987.53178900', LONG_TEST_NUMBER, 8, False) + self.callTestFunction('coin2str','987.5317890000', LONG_TEST_NUMBER, 12, False, 10) + self.callTestFunction('coin2strNZ',' 987.531789 ', LONG_TEST_NUMBER) + self.callTestFunction('coin2strNZS','987.531789', LONG_TEST_NUMBER) + self.callTestFunction('coin2str_approx',' 988 ', LONG_TEST_NUMBER) + self.callTestFunction('coin2str_approx',' -988 ', LONG_TEST_NUMBER * -1) + self.callTestFunction('str2coin', LONG_TEST_NUMBER, '987.53178900') + self.assertRaises(ValueError, str2coin, ' ') + self.assertRaises(NegativeValueError, str2coin, '-1', False) + self.callTestFunction('str2coin', -100000000, '-1', True) + self.assertRaises(NegativeValueError, str2coin, '-1.1', False) + self.assertRaises(TooMuchPrecisionError, str2coin, '.1111', True, 2, False) + self.callTestFunction('str2coin', 11110000, '.1111', True, 8, True) + self.callTestFunction('sha1', hashlib.new('sha1', bstr).digest(), bstr) + self.callTestFunction('sha512', hashlib.new('sha512', bstr).digest(), bstr) + self.callTestFunction('ripemd160', hex_to_binary('13988143ae67128f883765a4a4b19d77c1ea1ee9'), bstr) + self.callTestFunction('hash160', hex_to_binary('d418dd224e11e1d3b37b5f46b072ccf4e4e26203'), bstr) + self.callTestFunction('binaryBits_to_difficulty', blockhashBEDifficulty, blockhashBE) + + ############################################################################# + def callTestFunction(self, fnName, expectedOutput, *args, **kwargs): + """ + Provide a function name, inputs and some known outputs + Prints a pass/fail string if the outputs match + """ + fn = getattr(ArmoryUtils, fnName) + actualOutput = fn(*args,**kwargs) + self.assertAlmostEqual(expectedOutput, actualOutput, 4, \ + '\n\t' + '___Inputs___:' + str(args) + '\n\t' + '___ExpOut___:' + \ + str(expectedOutput) + '\n\t' + '___ActOut___:' + str(actualOutput)) + + + ############################################################################# + def testPluralsBasic(self): + ##### Test the basic replacePlurals function + strIn = 'Hello my @{kitty|2 kitties}@. I love you@{| guys}@!' + strOut = ['','',''] + strOut[1] = 'Hello my kitty. I love you!' + strOut[2] = 'Hello my 2 kitties. I love you guys!' + + # Test singular + for i in range(1,3): + replOut = replacePlurals(strIn, i) + self.assertEqual(replOut, strOut[i]) + + replOut = replacePlurals(strIn, i, i) + self.assertEqual(replOut, strOut[i]) + + + noStr = 'No replacements' + self.assertEqual( replacePlurals(noStr), noStr) + + + # Not enough arguments + self.assertRaises(IndexError, replacePlurals, strIn) + self.assertRaises(IndexError, replacePlurals, strIn + ' @{A cat|many cats}@', 1,1) + + # Too many arguments + self.assertRaises(TypeError, replacePlurals, strIn, 1,1,1) + + # No format specifiers + self.assertRaises(TypeError, replacePlurals, noStr, 1) + + + + ############################################################################# + def testFormatWithPlurals(self): + ##### Test the formatWithPlurals function + strIn = 'Hello my @{kitty|%d kitties}@. I love you@{| guys}@!' + strOut = ['','','','',''] + strOut[1] = 'Hello my kitty. I love you!' + strOut[2] = 'Hello my 2 kitties. I love you guys!' + strOut[3] = 'Hello my kitty. I love you guys!' + strOut[4] = 'Hello my 2 kitties. I love you!' + + ## Test singular + for i in range(1,3): + replOut = formatWithPlurals(strIn, i, i) + self.assertEqual(replOut, strOut[i]) + + replOut = formatWithPlurals(strIn, [i], [i]) + self.assertEqual(replOut, strOut[i]) + + replOut = formatWithPlurals(strIn, [i], [i, i]) + self.assertEqual(replOut, strOut[i]) + + replOut = formatWithPlurals(strIn, i, [i]) + self.assertEqual(replOut, strOut[i]) + + replOut = formatWithPlurals(strIn, i, [i, i]) + self.assertEqual(replOut, strOut[i]) + + + replOut = formatWithPlurals(strIn, 2, [1,2]) + self.assertEqual(replOut, strOut[3]) + + replOut = formatWithPlurals(strIn, 2, [2,1]) + self.assertEqual(replOut, strOut[4]) + + + + + ############################################################################# + def testPyBackgroundThread(self): + + # This will be used for testing + def doLongOperation(throwError=False): + j = '\xab'*32 + n = 20000 + for i in xrange(n): + j = hash160(j) + if i==n/2 and throwError: + raise ValueError('This is a forced error') + return j + + # On completion, the following should be the output + ans = '\\\xea\xef\xc6:\xd4\xd7\xed\xee_YM\xf1\xa1aV\x81\x03Y\xc1' + + # Test proper thread execution + thr = PyBackgroundThread(doLongOperation, False) + thr.start() + while not thr.isFinished(): + thr.join(0.1) + + self.assertTrue(thr.isFinished()) + self.assertEqual(thr.getOutput(), ans) + self.assertFalse(thr.didThrowError()) + self.assertEqual(thr.getErrorType(), None) + self.assertEqual(thr.getErrorMsg(), '') + + + # Test error thrown + thr = PyBackgroundThread(doLongOperation, True) + thr.start() + while not thr.isFinished(): + thr.join(0.1) + + self.assertTrue(thr.isFinished()) + self.assertEqual(thr.getOutput(), None) + self.assertTrue(thr.didThrowError()) + self.assertEqual(thr.getErrorType(), ValueError) + self.assertEqual(thr.getErrorMsg(), 'This is a forced error') + self.assertRaises(ValueError, thr.raiseLastError) + + ############################################################################# + def test_read_address(self): + hashVal = hex_to_binary('c3a9eb6753c449c88ac193e9ddf7ab3a0be8c5ad') + addrStr00 = hash160_to_addrStr(hashVal) + addrStr05 = hash160_to_p2shStr(hashVal) + addrStrA3 = '28tvtpKmqbKUVaGHshsVKWTaRHJ5yG36xvv' + addrStrBad = '1JqaKdBsruwgGcZkiVCNU2DBh56DHBsEb1' + + prefix, a160 = addrStr_to_hash160(addrStr00) + self.assertEqual(prefix, ADDRBYTE) + self.assertEqual(a160, hashVal) + self.assertFalse(addrStr_is_p2sh(addrStr00)) + + prefix, a160 = addrStr_to_hash160(addrStr05) + self.assertEqual(prefix, P2SHBYTE) + self.assertEqual(a160, hashVal) + self.assertTrue(addrStr_is_p2sh(addrStr05)) + + self.assertRaises(BadAddressError, addrStr_to_hash160, addrStrA3) + self.assertRaises(ChecksumError, addrStr_to_hash160, addrStrBad) + self.assertRaises(P2SHNotSupportedError, addrStr_to_hash160, addrStr05, False) + + + ############################################################################# + def test_p2pkhash_script(self): + pkh = '\xab'*20 + computed = hash160_to_p2pkhash_script(pkh) + expected = '\x76\xa9\x14' + pkh + '\x88\xac' + self.assertEqual(computed, expected) + + # Make sure it raises an error on non-20-byte inputs + self.assertRaises(InvalidHashError, hash160_to_p2pkhash_script, '\xab'*21) + + + ############################################################################# + def test_p2sh_script(self): + scriptHash = '\xab'*20 + computed = hash160_to_p2sh_script(scriptHash) + expected = '\xa9\x14' + scriptHash + '\x87' + self.assertEqual(computed, expected) + + # Make sure it raises an error on non-20-byte inputs + self.assertRaises(InvalidHashError, hash160_to_p2sh_script, '\xab'*21) + + + ############################################################################# + def test_p2pk_script(self): + pubkey = ['\x3a'*33, \ + '\x3a'*65] + + expected = ['\x21' + '\x3a'*33 + '\xac', \ + '\x41' + '\x3a'*65 + '\xac'] + + for i in range(2): + pk = pubkey[i] + exp = expected[i] + self.assertEqual( pubkey_to_p2pk_script(pk), exp) + + + # Make sure it raises an error on non-[33,65]-byte inputs + self.assertRaises(KeyDataError, pubkey_to_p2pk_script, '\xab'*21) + + + ############################################################################# + def test_script_to_p2sh(self): + script = '\xab'*10 + scriptHash = hex_to_binary('fc7eb079a69ac4a98e49ea373c91f62b8b9cebe2') + expected = '\xa9\x14' + scriptHash + '\x87' + self.assertEqual( script_to_p2sh_script(script), expected) + + ############################################################################# + def test_pklist_to_multisig(self): + pk1 = 'x01'*33 + pk2 = 'xfa'*33 + pk3 = 'x33'*33 + pk4 = 'x01'*65 + pk5 = 'xfa'*65 + pk6 = 'x33'*65 + pkNot = 'x33'*100 + pkList1 = [pk1, pk2, pk3] + pkList2 = [pk4, pk5, pk6] + pkList3 = [pk1, pk5, pk3] + pkList4 = [pk1, pkNot, pk3] # error + + #self.assertTrue(False) # STUB + + + ############################################################################# + def testCppScrAddr(self): + + ##### Pay to hash(pubkey) + script = hex_to_binary("76a914a134408afa258a50ed7a1d9817f26b63cc9002cc88ac") + a160 = hex_to_binary( "a134408afa258a50ed7a1d9817f26b63cc9002cc") + scraddr = hex_to_binary( "00a134408afa258a50ed7a1d9817f26b63cc9002cc") + + self.assertEqual(script, scrAddr_to_script(scraddr)) + self.assertEqual(scraddr, script_to_scrAddr(script)) # this uses C++ + # Go round trip to avoid dependency on the network. Works in both main-net or testnet + self.assertEqual(scraddr, addrStr_to_scrAddr(scrAddr_to_addrStr(scraddr))) + + + ##### Pay to PubKey65 + script = hex_to_binary( "4104" + "b0bd634234abbb1ba1e986e884185c61" + "cf43e001f9137f23c2c409273eb16e65" + "37a576782eba668a7ef8bd3b3cfb1edb" + "7117ab65129b8a2e681f3c1e0908ef7b""ac") + a160 = hex_to_binary( "e24b86bff5112623ba67c63b6380636cbdf1a66d") + scraddr = hex_to_binary("00e24b86bff5112623ba67c63b6380636cbdf1a66d") + self.assertEqual(scraddr, script_to_scrAddr(script)) + self.assertEqual(scraddr, addrStr_to_scrAddr(scrAddr_to_addrStr(scraddr))) + + + ##### Pay to PubKey33 + script = hex_to_binary( "2102" + "4005c945d86ac6b01fb04258345abea7" + "a845bd25689edb723d5ad4068ddd3036""ac") + a160 = hex_to_binary( "0c1b83d01d0ffb2bccae606963376cca3863a7ce") + scraddr = hex_to_binary("000c1b83d01d0ffb2bccae606963376cca3863a7ce") + + self.assertEqual(scraddr, script_to_scrAddr(script)) + self.assertEqual(scraddr, addrStr_to_scrAddr(scrAddr_to_addrStr(scraddr))) + + ##### Non-standard script + # This was from block 150951 which was erroneously produced by MagicalTux + # This is not only non-standard, it's non-spendable + script = hex_to_binary("76a90088ac") + scraddr = hex_to_binary("ff") + hash160(hex_to_binary("76a90088ac")) + + self.assertEqual(scraddr, script_to_scrAddr(script)) + + + ##### P2SH + script = hex_to_binary("a914d0c15a7d41500976056b3345f542d8c944077c8a87") + a160 = hex_to_binary( "d0c15a7d41500976056b3345f542d8c944077c8a") + scraddr = hex_to_binary("05d0c15a7d41500976056b3345f542d8c944077c8a") + + self.assertEqual(script, scrAddr_to_script(scraddr)) + self.assertEqual(scraddr, script_to_scrAddr(script)) # this uses C++ + self.assertEqual(scraddr, addrStr_to_scrAddr(scrAddr_to_addrStr(scraddr))) + + +################################################################################ +################################################################################ +class BinaryPackerUnpackerTest(unittest.TestCase): + + ############################################################################# + def testBinaryUnpacker(self): + ts = '\xff\xff\xff' + bu = BinaryUnpacker(ts) + self.assertEqual(bu.getSize(), len(ts)) + bu.advance(1) + self.assertEqual(bu.getRemainingSize(), len(ts)-1) + self.assertEqual(bu.getBinaryString(), ts) + self.assertEqual(bu.getRemainingString(), ts[1:]) + bu.rewind(1) + self.assertEqual(bu.getRemainingSize(), len(ts)) + bu.resetPosition(2) + self.assertEqual(bu.getRemainingSize(), len(ts) - 2) + self.assertEqual(bu.getPosition(), 2) + bu.resetPosition() + self.assertEqual(bu.getRemainingSize(), len(ts)) + self.assertEqual(bu.getPosition(), 0) + bu.append(ts) + self.assertEqual(bu.getBinaryString(), ts + ts) + + ############################################################################# + def testBinaryPacker(self): + UNKNOWN_TYPE = 100 + TEST_FLOAT = 1.23456789 + TEST_UINT = 0xff + TEST_INT = -1 + TEST_VARINT = 78 + TEST_STR = 'abc' + TEST_BINARY_PACKER_STR = hex_to_binary('ffff00ff000000ff00000000000000ffffffffffffffffffffffffffffff4e0361626352069e3fffffffffffff00') + FS_FOR_3_BYTES = '\xff\xff\xff' + bp = BinaryPacker() + bp.put(UINT8, TEST_UINT) + bp.put(UINT16, TEST_UINT) + bp.put(UINT32, TEST_UINT) + bp.put(UINT64, TEST_UINT) + bp.put(INT8, TEST_INT) + bp.put(INT16, TEST_INT) + bp.put(INT32, TEST_INT) + bp.put(INT64, TEST_INT) + bp.put(VAR_INT, TEST_VARINT) + bp.put(VAR_STR, TEST_STR) + bp.put(FLOAT, TEST_FLOAT) + bp.put(BINARY_CHUNK, FS_FOR_3_BYTES) + bp.put(BINARY_CHUNK, FS_FOR_3_BYTES, 4) + self.assertRaises(PackerError, bp.put, UNKNOWN_TYPE, TEST_INT) + self.assertRaises(PackerError, bp.put, BINARY_CHUNK, FS_FOR_3_BYTES, 2) + self.assertEqual(bp.getSize(), len(TEST_BINARY_PACKER_STR)) + ts = bp.getBinaryString() + self.assertEqual(ts, TEST_BINARY_PACKER_STR) + bu = BinaryUnpacker(ts) + self.assertEqual(bu.get(UINT8), TEST_UINT) + self.assertEqual(bu.get(UINT16), TEST_UINT) + self.assertEqual(bu.get(UINT32), TEST_UINT) + self.assertEqual(bu.get(UINT64), TEST_UINT) + self.assertEqual(bu.get(INT8), TEST_INT) + self.assertEqual(bu.get(INT16), TEST_INT) + self.assertEqual(bu.get(INT32), TEST_INT) + self.assertEqual(bu.get(INT64), TEST_INT) + self.assertEqual(bu.get(VAR_INT), TEST_VARINT) + self.assertEqual(bu.get(VAR_STR), TEST_STR) + self.assertAlmostEqual(bu.get(FLOAT), TEST_FLOAT, 2) + self.assertEqual(bu.get(BINARY_CHUNK, 3), FS_FOR_3_BYTES) + self.assertEqual(bu.get(BINARY_CHUNK, 4), FS_FOR_3_BYTES+"\x00") + self.assertRaises(UnpackerError, bu.get, BINARY_CHUNK, 1) + self.assertRaises(UnpackerError, bu.get, UNKNOWN_TYPE) + self.assertRaises(UnpackerError, bu.get, BINARY_CHUNK, 1) + + + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testArmoryEngine'] + unittest.main() diff --git a/test/BlockDataManagerThreadTest.py b/test/BlockDataManagerThreadTest.py new file mode 100644 index 000000000..09d2a581e --- /dev/null +++ b/test/BlockDataManagerThreadTest.py @@ -0,0 +1,58 @@ +''' +Created on Jul 29, 2013 + +@author: Andy +''' +import sys +from armoryengine.BDM import BlockDataManagerThread +sys.argv.append('--nologging') +import unittest + +class BlockDataManagerThreadTest(unittest.TestCase): + + def setUp(self): + self.theBDM = BlockDataManagerThread() + + + def testPredictLoadTime(self): + ''' + This whole test and all of the test files are out of date. + + Add optional args to the subject to run this test. + + Update with the latest format that looks like this. + 3 909278307 285 0 0.001 + 3 909278307 37621601 0 4.782 + 3 909278307 74125505 0 9.788 + 3 909278307 108883082 0 14.779 + 3 909278307 145392744 0 19.771 + + # blkfiles1.txt - completed phase 1, should be exactly a quarter complete + # time left is last time(200) - first time(100) * 3 + # There is still phase 2 which 3 times longer than phase 1 + actual = self.theBDM.predictLoadTime('.', 'blkfiles1.txt') + self.assertTrue(actual) + self.assertEqual(1, actual[0]) + self.assertEqual(.25, actual[1]) + self.assertEqual(.0025, actual[2]) + self.assertEqual(300, actual[3]) + + # set values to be about half way through + actual = self.theBDM.predictLoadTime('.', 'blkfiles2.txt') + self.assertTrue(actual) + self.assertAlmostEqual(2, actual[0], 2) + self.assertAlmostEqual(.5, actual[1], 2) + self.assertAlmostEqual(.0025, actual[2], 2) + self.assertAlmostEqual(200.3, actual[3], 2) + + # set values to be all the way through + actual = self.theBDM.predictLoadTime('.', 'blkfiles3.txt') + self.assertTrue(actual) + self.assertEqual(1, actual[1]) + self.assertEqual(0, actual[3]) + ''' + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testArmoryEngine'] + unittest.main() \ No newline at end of file diff --git a/test/DecoratorsTest.py b/test/DecoratorsTest.py new file mode 100644 index 000000000..2bd5652c1 --- /dev/null +++ b/test/DecoratorsTest.py @@ -0,0 +1,36 @@ +################################################################################ +# +# Copyright (C) 2011-2014, Armory Technologies, Inc. +# Distributed under the GNU Affero General Public License (AGPL v3) +# See LICENSE or http://www.gnu.org/licenses/agpl.html +# +################################################################################ +# +# Project: Armory +# Author: Andy Ofiesh +# Website: www.bitcoinarmory.com +# Orig Date: 2 January, 2014 +# +################################################################################ +import unittest +from armoryengine.Decorators import EmailOutput + + +# NOT a real unit test. To verify this test properly +# uncomment the decorator and specify the email arguments +# The email arguments should never be pushed to the repo +# Run the test and check your email +class EmailOutputTest(unittest.TestCase): + + def testEmailOutput(self): + actualResult = someStringOutputFunction("World!") + expectedResult = "Hello World!" + self.assertEqual(actualResult, expectedResult) + +# @EmailOutput(, , , ) +def someStringOutputFunction(inputString): + return "Hello " + inputString + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/test/FragmentedBackupTest.py b/test/FragmentedBackupTest.py new file mode 100644 index 000000000..c3598e5ee --- /dev/null +++ b/test/FragmentedBackupTest.py @@ -0,0 +1,85 @@ +################################################################################ +# # +# Copyright (C) 2011-2014, Armory Technologies, Inc. # +# Distributed under the GNU Affero General Public License (AGPL v3) # +# See LICENSE or http://www.gnu.org/licenses/agpl.html # +# # +################################################################################ +import sys +from armoryengine.ArmoryUtils import SplitSecret, binary_to_hex, ReconstructSecret,\ + FiniteFieldError +import itertools +sys.argv.append('--nologging') +import unittest + + +SECRET = '\x00\x01\x02\x03\x04\x05\x06\x07' + +BAD_SECRET = '\xff\xff\xff\xff\xff\xff\xff\xff' + +# Fragment combination to String abreviated name for debugging purposes +def c2s(combinationMap): + return '\n'.join([' '.join([str(k), binary_to_hex(v[0]), binary_to_hex(v[1])]) \ + for k,v in combinationMap.iteritems()]) + +def splitSecretToFragmentMap(splitSecret): + fragMap = {} + for i,frag in enumerate(splitSecret): + fragMap[i] = frag + return fragMap + + +class Test(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def getNextCombination(self, fragmentMap, m): + combinationIterator = itertools.combinations(fragmentMap.iterkeys(), m) + for keyList in combinationIterator: + combinationMap = {} + for key in keyList: + combinationMap[key] = fragmentMap[key] + yield combinationMap + + + def subtestAllFragmentedBackups(self, secret, m, n): + fragmentMap = splitSecretToFragmentMap(SplitSecret(secret, m, n)) + for combinationMap in self.getNextCombination(fragmentMap, m): + fragmentList = [value for value in combinationMap.itervalues()] + reconSecret = ReconstructSecret(fragmentList, m, len(secret)) + self.assertEqual(reconSecret, secret) + + + def testFragmentedBackup(self): + + self.subtestAllFragmentedBackups(SECRET, 2, 3) + self.subtestAllFragmentedBackups(SECRET, 2, 3) + self.subtestAllFragmentedBackups(SECRET, 3, 4) + self.subtestAllFragmentedBackups(SECRET, 5, 7) + self.subtestAllFragmentedBackups(SECRET, 8, 8) + self.subtestAllFragmentedBackups(SECRET, 2, 12) + + # Secret Too big test + self.assertRaises(FiniteFieldError, SplitSecret, BAD_SECRET, 2,3) + + # More needed than pieces + self.assertRaises(FiniteFieldError, SplitSecret, SECRET, 4,3) + + # Secret Too many needed needed + self.assertRaises(FiniteFieldError, SplitSecret, SECRET, 9, 12) + + # Too few pieces needed + self.assertRaises(FiniteFieldError, SplitSecret, SECRET, 1, 12) + + # Test Reconstuction failures + fragmentList = SplitSecret(SECRET, 3, 5) + reconSecret = ReconstructSecret(fragmentList[:2], 2, len(SECRET)) + self.assertNotEqual(reconSecret, SECRET) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/test/OnlineVersionOfEncryptedWallet.bin b/test/OnlineVersionOfEncryptedWallet.bin new file mode 100644 index 000000000..dda7ad660 Binary files /dev/null and b/test/OnlineVersionOfEncryptedWallet.bin differ diff --git a/test/OnlineVersionOfEncryptedWallet_backup.bin b/test/OnlineVersionOfEncryptedWallet_backup.bin new file mode 100644 index 000000000..f5a516fd7 Binary files /dev/null and b/test/OnlineVersionOfEncryptedWallet_backup.bin differ diff --git a/test/PyBtcAddressTest.py b/test/PyBtcAddressTest.py new file mode 100644 index 000000000..a55254e29 --- /dev/null +++ b/test/PyBtcAddressTest.py @@ -0,0 +1,400 @@ +''' +Created on Aug 6, 2013 + +@author: Andy +''' +import sys +import unittest + +from CppBlockUtils import CryptoECDSA, SecureBinaryData +from armoryengine.ArmoryUtils import hex_to_binary, RightNow, int_to_binary, \ + checkAddrStrValid, hash256, UnserializeError, hash160_to_addrStr +from armoryengine.PyBtcAddress import PyBtcAddress +from armoryengine.BDM import TheBDM + + +sys.argv.append('--nologging') + +INIT_VECTOR = '77'*16 +TEST_ADDR1_PRIV_KEY_ENCR1 = '500c41607d79c766859e6d9726ef1ea0fdf095922f3324454f6c4c34abcb23a5' +TEST_ADDR1_PRIV_KEY_ENCR2 = '7966cf5886494246cc5aaf7f1a4a2777cd6126612e7029d79ef9df47f6d6927d' +TEST_ADDR1_PRIV_KEY_ENCR3 = '0db5c1e9a8d1ebc0525bdb534626033b948804a9a34871d67bf58a3df11d6888' +TEST_ADDR1_PRIV_KEY_ENCR4 = '5db1314a20ae9fc978477ab3fe16ab17b246d813a541ecdd4143fcf082b19407' + +TEST_PUB_KEY1 = '046c35e36776e997883ad4269dcc0696b10d68f6864ae73b8ad6ad03e879e43062a0139095ece3bd653b809fa7e8c7d78ffe6fac75a84c8283d8a000890bfc879d' + +# Create an address to use for all subsequent tests +PRIVATE_KEY = SecureBinaryData(hex_to_binary('aa'*32)) +PRIVATE_CHECKSUM = PRIVATE_KEY.getHash256()[:4] +PUBLIC_KEY = CryptoECDSA().ComputePublicKey(PRIVATE_KEY) +ADDRESS_20 = PUBLIC_KEY.getHash160() + +TEST_BLOCK_NUM = 100 + +# We pretend that we plugged some passphrases through a KDF +FAKE_KDF_OUTPUT1 = SecureBinaryData( hex_to_binary('11'*32) ) +FAKE_KDF_OUTPUT2 = SecureBinaryData( hex_to_binary('22'*32) ) + +class PyBtcAddressTest(unittest.TestCase): + + + def setUp(self): + TheBDM.Reset() + + def tearDown(self): + pass + + + # TODO: This test needs more verification of the results. + def testEncryptedAddress(self): + + + # test serialization and unserialization of an empty PyBtcAddrss + # Should serialize to a string that starts with 20 bytes of zeros + # Unserialize should throw an UnserializeError caused by checksum mismatch + emptyBtcAddr = PyBtcAddress() + emptyBtcAddrSerialized = emptyBtcAddr.serialize() + self.assertEqual(emptyBtcAddrSerialized[:20], hex_to_binary('00'*20)) + self.assertRaises(UnserializeError, PyBtcAddress().unserialize, emptyBtcAddrSerialized) + + # Test non-crashing + testAddr1 = PyBtcAddress().createFromPlainKeyData(PRIVATE_KEY, ADDRESS_20) + testAddr2 = PyBtcAddress().createFromPlainKeyData(PRIVATE_KEY, ADDRESS_20, chksum=PRIVATE_CHECKSUM) + testAddr3 = PyBtcAddress().createFromPlainKeyData(PRIVATE_KEY, ADDRESS_20, publicKey65=PUBLIC_KEY) + testAddr4 = PyBtcAddress().createFromPlainKeyData(PRIVATE_KEY, ADDRESS_20, publicKey65=PUBLIC_KEY, skipCheck=True) + testAddr5 = PyBtcAddress().createFromPlainKeyData(PRIVATE_KEY, ADDRESS_20, skipPubCompute=True) + + testString = testAddr1.toString() + self.assertTrue(len(testString) > 0) + + testAddr = PyBtcAddress().createFromPlainKeyData(PRIVATE_KEY, ADDRESS_20, publicKey65=PUBLIC_KEY) + serializedAddr1 = testAddr.serialize() + retestAddr = PyBtcAddress().unserialize(serializedAddr1) + serializedRetest1 = retestAddr.serialize() + self.assertEqual(serializedAddr1, serializedRetest1) + + theIV = SecureBinaryData(hex_to_binary(INIT_VECTOR)) + testAddr.enableKeyEncryption(theIV) + testAddr.lock(FAKE_KDF_OUTPUT1) + self.assertTrue(testAddr.useEncryption) + self.assertTrue(testAddr.isLocked) + self.assertEqual(testAddr.binPrivKey32_Plain.toHexStr(), '') + self.assertEqual(testAddr.binPrivKey32_Encr.toHexStr(), TEST_ADDR1_PRIV_KEY_ENCR1) + + serializedAddr2 = testAddr.serialize() + retestAddr = PyBtcAddress().unserialize(serializedAddr2) + serializedRetest2 = retestAddr.serialize() + self.assertEqual(serializedAddr2, serializedRetest2) + testAddr.unlock(FAKE_KDF_OUTPUT1) + self.assertFalse(testAddr.isLocked) + self.assertEqual(testAddr.binPrivKey32_Plain.toHexStr(), PRIVATE_KEY.toHexStr()) + + testAddr.changeEncryptionKey(None, FAKE_KDF_OUTPUT1) + self.assertTrue(testAddr.useEncryption) + self.assertFalse(testAddr.isLocked) + self.assertEqual(testAddr.binPrivKey32_Plain.toHexStr(), PRIVATE_KEY.toHexStr()) + self.assertEqual(testAddr.binPrivKey32_Encr.toHexStr(), TEST_ADDR1_PRIV_KEY_ENCR1) + + # Save off this data for a later test + addr20_1 = testAddr.getAddr160() + encryptedKey1 = testAddr.binPrivKey32_Encr + encryptionIV1 = testAddr.binInitVect16 + plainPubKey1 = testAddr.binPublicKey65 + + # OP(Key1 --> Unencrypted) + testAddr.changeEncryptionKey(FAKE_KDF_OUTPUT1, None) + self.assertFalse(testAddr.useEncryption) + self.assertFalse(testAddr.isLocked) + self.assertEqual(testAddr.binPrivKey32_Plain.toHexStr(), PRIVATE_KEY.toHexStr()) + self.assertEqual(testAddr.binPrivKey32_Encr.toHexStr(), '') + + # OP(Unencrypted --> Key2) + if not testAddr.isKeyEncryptionEnabled(): + testAddr.enableKeyEncryption(theIV) + testAddr.changeEncryptionKey(None, FAKE_KDF_OUTPUT2) + self.assertTrue(testAddr.useEncryption) + self.assertFalse(testAddr.isLocked) + self.assertEqual(testAddr.binPrivKey32_Plain.toHexStr(), PRIVATE_KEY.toHexStr()) + self.assertEqual(testAddr.binPrivKey32_Encr.toHexStr(), TEST_ADDR1_PRIV_KEY_ENCR2) + + # Save off this data for a later test + addr20_2 = testAddr.getAddr160() + encryptedKey2 = testAddr.binPrivKey32_Encr + encryptionIV2 = testAddr.binInitVect16 + plainPubKey2 = testAddr.binPublicKey65 + + # OP(Key2 --> Key1) + testAddr.changeEncryptionKey(FAKE_KDF_OUTPUT2, FAKE_KDF_OUTPUT1) + self.assertTrue(testAddr.useEncryption) + self.assertFalse(testAddr.isLocked) + self.assertEqual(testAddr.binPrivKey32_Plain.toHexStr(), PRIVATE_KEY.toHexStr()) + self.assertEqual(testAddr.binPrivKey32_Encr.toHexStr(), TEST_ADDR1_PRIV_KEY_ENCR1) + + # OP(Key1 --> Lock --> Key2) + testAddr.lock(FAKE_KDF_OUTPUT1) + testAddr.changeEncryptionKey(FAKE_KDF_OUTPUT1, FAKE_KDF_OUTPUT2) + self.assertTrue(testAddr.useEncryption) + self.assertTrue(testAddr.isLocked) + self.assertEqual(testAddr.binPrivKey32_Plain.toHexStr(), '') + self.assertEqual(testAddr.binPrivKey32_Encr.toHexStr(), TEST_ADDR1_PRIV_KEY_ENCR2) + + # OP(Key2 --> Lock --> Unencrypted) + testAddr.changeEncryptionKey(FAKE_KDF_OUTPUT2, None) + self.assertFalse(testAddr.useEncryption) + self.assertFalse(testAddr.isLocked) + self.assertEqual(testAddr.binPrivKey32_Plain.toHexStr(), PRIVATE_KEY.toHexStr()) + self.assertEqual(testAddr.binPrivKey32_Encr.toHexStr(), '') + + # Encryption Key Tests: + self.assertEqual(testAddr.serializePlainPrivateKey(), PRIVATE_KEY.toBinStr()) + + # Test loading pre-encrypted key data + testAddr = PyBtcAddress().createFromEncryptedKeyData(addr20_1, encryptedKey1, encryptionIV1) + + self.assertTrue(testAddr.useEncryption) + self.assertTrue(testAddr.isLocked) + self.assertEqual(testAddr.binPrivKey32_Plain.toHexStr(), '') + self.assertEqual(testAddr.binPrivKey32_Encr.toHexStr(), TEST_ADDR1_PRIV_KEY_ENCR1) + + # OP(EncrAddr --> Unlock1) + testAddr.unlock(FAKE_KDF_OUTPUT1) + self.assertTrue(testAddr.useEncryption) + self.assertFalse(testAddr.isLocked) + self.assertEqual(testAddr.binPrivKey32_Plain.toHexStr(), PRIVATE_KEY.toHexStr()) + self.assertEqual(testAddr.binPrivKey32_Encr.toHexStr(), TEST_ADDR1_PRIV_KEY_ENCR1) + + # OP(Unlock1 --> Lock1) + testAddr.lock() + self.assertTrue(testAddr.useEncryption) + self.assertTrue(testAddr.isLocked) + self.assertEqual(testAddr.binPrivKey32_Plain.toHexStr(), '') + self.assertEqual(testAddr.binPrivKey32_Encr.toHexStr(), TEST_ADDR1_PRIV_KEY_ENCR1) + + # OP(Lock1 --> Lock2) + testAddr.changeEncryptionKey(FAKE_KDF_OUTPUT1, FAKE_KDF_OUTPUT2) + self.assertTrue(testAddr.useEncryption) + self.assertTrue(testAddr.isLocked) + self.assertEqual(testAddr.binPrivKey32_Plain.toHexStr(), '') + self.assertEqual(testAddr.binPrivKey32_Encr.toHexStr(), TEST_ADDR1_PRIV_KEY_ENCR2) + + # Test serializing locked wallet from pre-encrypted data' + serializedAddr = testAddr.serialize() + retestAddr = PyBtcAddress().unserialize(serializedAddr) + serializedRetest = retestAddr.serialize() + self.assertEqual(serializedAddr, serializedRetest) + + ############################################################################# + # Now testing chained-key (deterministic) address generation + # Test chained priv key generation + # Starting with plain key data + chaincode = SecureBinaryData(hex_to_binary('ee'*32)) + addr0 = PyBtcAddress().createFromPlainKeyData(PRIVATE_KEY, ADDRESS_20) + addr0.markAsRootAddr(chaincode) + pub0 = addr0.binPublicKey65 + + # Test serializing address-chain-root + serializedAddr = addr0.serialize() + retestAddr = PyBtcAddress().unserialize(serializedAddr) + serializedRetest = retestAddr.serialize() + self.assertEqual(serializedAddr, serializedRetest) + self.assertEqual(retestAddr.binPrivKey32_Plain.toHexStr(), PRIVATE_KEY.toHexStr()) + + # Generate chained PRIVATE key address + # OP(addr[0] --> addr[1]) + addr1 = addr0.extendAddressChain() + self.assertEqual(addr1.binPrivKey32_Plain.toHexStr(), TEST_ADDR1_PRIV_KEY_ENCR3) + + # OP(addr[0] --> addr[1]) [again]' + addr1a = addr0.extendAddressChain() + self.assertEqual(addr1a.binPrivKey32_Plain.toHexStr(), TEST_ADDR1_PRIV_KEY_ENCR3) + + # OP(addr[1] --> addr[2]) + addr2 = addr1.extendAddressChain() + pub2 = addr2.binPublicKey65.copy() + priv2 = addr2.binPrivKey32_Plain.copy() + self.assertEqual(priv2.toHexStr(), TEST_ADDR1_PRIV_KEY_ENCR4) + + # Addr1.PRIVATE_KEY == Addr1a.PRIVATE_KEY:', + self.assertEqual(addr1.binPublicKey65, addr1a.binPublicKey65) + + # Test serializing priv-key-chained', + serializedAddr = addr2.serialize() + retestAddr = PyBtcAddress().unserialize(serializedAddr) + serializedRetest = retestAddr.serialize() + self.assertEqual(serializedAddr, serializedRetest) + + ############################################################################# + # Generate chained PUBLIC key address + # addr[0] + addr0 = PyBtcAddress().createFromPublicKeyData(pub0) + addr0.markAsRootAddr(chaincode) + self.assertEqual(addr0.chainIndex, -1) + self.assertEqual(addr0.chaincode, chaincode) + + # Test serializing pub-key-only-root', + serializedAddr = addr0.serialize() + retestAddr = PyBtcAddress().unserialize(serializedAddr) + serializedRetest = retestAddr.serialize() + self.assertEqual(serializedAddr, serializedRetest) + + # OP(addr[0] --> addr[1])' + addr1 = addr0.extendAddressChain() + self.assertEqual(addr1.binPrivKey32_Plain.toHexStr(), '') + + + # OP(addr[1] --> addr[2])' + addr2 = addr1.extendAddressChain() + pub2a = addr2.binPublicKey65.copy() + self.assertEqual(addr2.binPrivKey32_Plain.toHexStr(), '') + self.assertEqual(pub2a.toHexStr(), TEST_PUB_KEY1) + + # Addr2.PublicKey == Addr2a.PublicKey:' + # Test serializing pub-key-from-chain' + serializedAddr = addr2.serialize() + retestAddr = PyBtcAddress().unserialize(serializedAddr) + serializedRetest = retestAddr.serialize() + self.assertEqual(serializedAddr, serializedRetest) + + ############################################################################# + # Generate chained keys from locked addresses + addr0 = PyBtcAddress().createFromPlainKeyData( PRIVATE_KEY, \ + willBeEncr=True, IV16=theIV) + addr0.markAsRootAddr(chaincode) + # OP(addr[0] plain) + + # Test serializing unlocked addr-chain-root + serializedAddr = addr0.serialize() + retestAddr = PyBtcAddress().unserialize(serializedAddr) + serializedRetest = retestAddr.serialize() + self.assertEqual(serializedAddr, serializedRetest) + self.assertFalse(retestAddr.useEncryption) + + # OP(addr[0] locked) + addr0.lock(FAKE_KDF_OUTPUT1) + self.assertEqual(addr0.binPrivKey32_Plain.toHexStr(), '') + + # OP(addr[0] w/Key --> addr[1]) + addr1 = addr0.extendAddressChain(FAKE_KDF_OUTPUT1, newIV=theIV) + self.assertEqual(addr1.binPrivKey32_Plain.toHexStr(), '') + + # OP(addr[1] w/Key --> addr[2]) + addr2 = addr1.extendAddressChain(FAKE_KDF_OUTPUT1, newIV=theIV) + addr2.unlock(FAKE_KDF_OUTPUT1) + priv2a = addr2.binPrivKey32_Plain.copy() + addr2.lock() + self.assertEqual(addr2.binPrivKey32_Plain.toHexStr(), '') + + # Addr2.priv == Addr2a.priv: + self.assertEqual(priv2, priv2a) + + # Test serializing chained address from locked root + serializedAddr = addr2.serialize() + retestAddr = PyBtcAddress().unserialize(serializedAddr) + serializedRetest = retestAddr.serialize() + self.assertEqual(serializedAddr, serializedRetest) + + ############################################################################# + # Generate chained keys from locked addresses, no unlocking + addr0 = PyBtcAddress().createFromPlainKeyData( PRIVATE_KEY, \ + willBeEncr=True, IV16=theIV) + addr0.markAsRootAddr(chaincode) + # OP(addr[0] locked) + addr0.lock(FAKE_KDF_OUTPUT1) + self.assertEqual(addr0.binPrivKey32_Plain.toHexStr(), '') + + # OP(addr[0] locked --> addr[1] locked)' + addr1 = addr0.extendAddressChain(newIV=theIV) + self.assertEqual(addr1.binPrivKey32_Plain.toHexStr(), '') + + # OP(addr[1] locked --> addr[2] locked) + addr2 = addr1.extendAddressChain(newIV=theIV) + pub2b = addr2.binPublicKey65.copy() + self.assertEqual(addr2.binPrivKey32_Plain.toHexStr(), '') + self.assertEqual(pub2b.toHexStr(), TEST_PUB_KEY1) + + # Addr2.Pub == Addr2b.pub: + # Test serializing priv-key-bearing address marked for unlock + serializedAddr = addr2.serialize() + retestAddr = PyBtcAddress().unserialize(serializedAddr) + serializedRetest = retestAddr.serialize() + self.assertEqual(serializedAddr, serializedRetest) + + addr2.unlock(FAKE_KDF_OUTPUT1) + priv2b = addr2.binPrivKey32_Plain.copy() + # OP(addr[2] locked --> unlocked) + self.assertEqual(priv2b.toHexStr(), TEST_ADDR1_PRIV_KEY_ENCR4) + + + addr2.lock() + # OP(addr[2] unlocked --> locked)' + # Addr2.priv == Addr2b.priv: + self.assertEqual(priv2, priv2b) + + # TODO: Add coverage for condition where TheBDM is in BlockchainReady state. + def testTouch(self): + testAddr = PyBtcAddress().createFromPlainKeyData(PRIVATE_KEY, ADDRESS_20, publicKey65=PUBLIC_KEY) + theIV = SecureBinaryData(hex_to_binary(INIT_VECTOR)) + testAddr.enableKeyEncryption(theIV) + rightNow = RightNow() + testAddr.touch(rightNow) + self.assertEqual(testAddr.timeRange[0], long(rightNow)) + self.assertEqual(testAddr.timeRange[1], long(rightNow)) + testAddr.touch(0) + self.assertEqual(testAddr.timeRange[0], long(0)) + self.assertEqual(testAddr.timeRange[1], long(rightNow)) + testAddr.touch(blkNum=TEST_BLOCK_NUM) + self.assertEqual(testAddr.blkRange[0], TEST_BLOCK_NUM) + self.assertEqual(testAddr.blkRange[1], TEST_BLOCK_NUM) + testAddr.touch(blkNum=0) + self.assertEqual(testAddr.blkRange[0], 0) + self.assertEqual(testAddr.blkRange[1], TEST_BLOCK_NUM) + # Cover the case where the blkRange[0] starts at 0 + testAddr.touch(blkNum=TEST_BLOCK_NUM) + self.assertEqual(testAddr.blkRange[0], TEST_BLOCK_NUM) + self.assertEqual(testAddr.blkRange[1], TEST_BLOCK_NUM) + + def testCopy(self): + testAddr = PyBtcAddress().createFromPlainKeyData(PRIVATE_KEY, ADDRESS_20, publicKey65=PUBLIC_KEY) + testCopy = testAddr.copy() + self.assertEqual(testAddr.binPrivKey32_Plain, testCopy.binPrivKey32_Plain) + self.assertEqual(testAddr.binPrivKey32_Encr, testCopy.binPrivKey32_Encr) + self.assertEqual(testAddr.binPublicKey65, testCopy.binPublicKey65) + self.assertEqual(testAddr.binInitVect16, testCopy.binInitVect16) + self.assertEqual(testAddr.isLocked, testCopy.isLocked) + self.assertEqual(testAddr.useEncryption, testCopy.useEncryption) + self.assertEqual(testAddr.isInitialized, testCopy.isInitialized) + self.assertEqual(testAddr.keyChanged, testCopy.keyChanged) + self.assertEqual(testAddr.walletByteLoc, testCopy.walletByteLoc) + self.assertEqual(testAddr.chaincode, testCopy.chaincode) + self.assertEqual(testAddr.chainIndex, testCopy.chainIndex) + + def testVerifyEncryptionKey(self): + testAddr = PyBtcAddress().createFromPlainKeyData(PRIVATE_KEY, ADDRESS_20, publicKey65=PUBLIC_KEY) + theIV = SecureBinaryData(hex_to_binary(INIT_VECTOR)) + testAddr.enableKeyEncryption(theIV) + self.assertFalse(testAddr.verifyEncryptionKey(FAKE_KDF_OUTPUT1)) + testAddr.lock(FAKE_KDF_OUTPUT1) + self.assertTrue(testAddr.verifyEncryptionKey(FAKE_KDF_OUTPUT1)) + self.assertFalse(testAddr.verifyEncryptionKey(FAKE_KDF_OUTPUT2)) + + def testSimpleAddress(self): + # Execute the tests with Satoshi's public key from the Bitcoin specification page + satoshiPubKeyHex = '04fc9702847840aaf195de8442ebecedf5b095cdbb9bc716bda9110971b28a49e0ead8564ff0db22209e0374782c093bb899692d524e9d6a6956e7c5ecbcd68284' + addrPiece1Hex = '65a4358f4691660849d9f235eb05f11fabbd69fa' + addrPiece1Bin = hex_to_binary(addrPiece1Hex) + satoshiAddrStr = hash160_to_addrStr(addrPiece1Bin) + + saddr = PyBtcAddress().createFromPublicKey( hex_to_binary(satoshiPubKeyHex) ) + print '\tAddr calc from pubkey: ', saddr.calculateAddrStr() + self.assertTrue(checkAddrStrValid(satoshiAddrStr)) + + testAddr = PyBtcAddress().createFromPlainKeyData(PRIVATE_KEY, ADDRESS_20, publicKey65=PUBLIC_KEY) + msg = int_to_binary(39029348428) + theHash = hash256(msg) + derSig = testAddr.generateDERSignature(theHash) + # Testing ECDSA signing & verification -- arbitrary binary strings: + self.assertTrue(testAddr.verifyDERSignature( theHash, derSig)) + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() \ No newline at end of file diff --git a/test/PyBtcWalletRecoveryTest.py b/test/PyBtcWalletRecoveryTest.py new file mode 100644 index 000000000..3f7c7d2f2 --- /dev/null +++ b/test/PyBtcWalletRecoveryTest.py @@ -0,0 +1,136 @@ + +from CppBlockUtils import SecureBinaryData, CryptoECDSA, CryptoAES +from armoryengine.PyBtcAddress import PyBtcAddress +from armoryengine.ArmoryUtils import hex_to_binary, binary_to_hex, HMAC256 +from armoryengine.BinaryUnpacker import BinaryUnpacker +from armoryengine.PyBtcWallet import PyBtcWallet +from armoryengine.PyBtcWalletRecovery import PyBtcWalletRecovery, RECOVERMODE +import unittest +import os + +from armoryengine.ArmoryUtils import SECP256K1_ORDER, binary_to_int, BIGENDIAN + +class PyBtcWalletRecoveryTest(unittest.TestCase): + def setUp(self): + self.corruptWallet = 'corrupt_wallet.wallet' + + self.wltID = self.buildCorruptWallet(self.corruptWallet) + + def tearDown(self): + os.unlink(self.corruptWallet) + os.unlink(self.corruptWallet[:-7] + '_backup.wallet') + + os.unlink('armory_%s_RECOVERED.wallet' % self.wltID) + os.unlink('armory_%s_RECOVERED_backup.wallet' % self.wltID) + + def buildCorruptWallet(self, walletPath): + crpWlt = PyBtcWallet() + crpWlt.createNewWallet(walletPath, securePassphrase='testing', doRegisterWithBDM=False) + #not registering with the BDM, have to fill the wallet address pool manually + crpWlt.fillAddressPool(100) + + #grab the last computed address + lastaddr = crpWlt.addrMap[crpWlt.lastComputedChainAddr160] + + #corrupt the pubkey + PubKey = hex_to_binary('0478d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71a1518063243acd4dfe96b66e3f2ec8013c8e072cd09b3834a19f81f659cc3455') + lastaddr.binPublicKey65 = SecureBinaryData(PubKey) + crpWlt.addrMap[crpWlt.lastComputedChainAddr160] = lastaddr + + crpWlt.fillAddressPool(200) + + #insert a gap and inconsistent encryption + newAddr = PyBtcAddress() + newAddr.chaincode = lastaddr.chaincode + newAddr.chainIndex = 250 + PrivKey = hex_to_binary('e3b0c44298fc1c149afbf4c8996fb92427ae41e5978fe51ca495991b7852b855') + newAddr.binPrivKey32_Plain = SecureBinaryData(PrivKey) + newAddr.binPublicKey65 = CryptoECDSA().ComputePublicKey( \ + newAddr.binPrivKey32_Plain) + newAddr.addrStr20 = newAddr.binPublicKey65.getHash160() + newAddr.isInitialized = True + + crpWlt.addrMap[newAddr.addrStr20] = newAddr + crpWlt.lastComputedChainAddr160 = newAddr.addrStr20 + crpWlt.fillAddressPool(250) + + #TODO: corrupt a private key + #break an address entry at binary level + return crpWlt.uniqueIDB58 + + def testWalletRecovery(self): + #run recovery on broken wallet + recThread = PyBtcWalletRecovery().RecoverWallet(self.corruptWallet, \ + 'testing', RECOVERMODE.Full, \ + returnError = 'Dict') + recThread.join() + brkWltResult = recThread.output + + self.assertTrue(len(brkWltResult['sequenceGaps'])==1, \ + "Sequence Gap Undetected") + self.assertTrue(len(brkWltResult['forkedPublicKeyChain'])==2, \ + "Address Chain Forks Undetected") + self.assertTrue(len(brkWltResult['unmatchedPair'])==100, \ + "Unmatched Priv/Pub Key Undetected") + self.assertTrue(len(brkWltResult['misc'])==50, \ + "Wallet Encryption Inconsistency Undetected") + self.assertTrue(brkWltResult['nErrors']==153, \ + "Unexpected Errors Found") + + #check obfuscated keys yield the valid key + #grab root key + badWlt = PyBtcWallet() + badWlt.readWalletFile(self.corruptWallet, False, False) + rootAddr = badWlt.addrMap['ROOT'] + + SecurePassphrase = SecureBinaryData('testing') + secureKdfOutput = badWlt.kdf.DeriveKey(SecurePassphrase) + + #HMAC Q + rootAddr.unlock(secureKdfOutput) + Q = rootAddr.binPrivKey32_Plain.toBinStr() + + nonce = 0 + while 1: + hmacQ = HMAC256(Q, 'LogMult%d' % nonce) + if binary_to_int(hmacQ, BIGENDIAN) < SECP256K1_ORDER: + hmacQ = SecureBinaryData(hmacQ) + break + nonce = nonce +1 + + #Bad Private Keys + import operator + badKeys = [addrObj for addrObj in (sorted(badWlt.addrMap.values(), + key=operator.attrgetter('chainIndex')))] + + #run through obdsPrivKey + for i in range(0, len(brkWltResult['privMult'])): + obfsPKey = SecureBinaryData( + hex_to_binary(brkWltResult['privMult'][i])) + pKey = CryptoECDSA().ECMultiplyScalars(obfsPKey.toBinStr(), \ + hmacQ.toBinStr()) + + try: + badKeys[i+201].unlock(secureKdfOutput) + except: + continue + + self.assertTrue(binary_to_hex(pKey) == \ + badKeys[i+201].binPrivKey32_Plain.toHexStr(), \ + 'key mult error') + + #run recovery on recovered wallet + recThread = PyBtcWalletRecovery().RecoverWallet( \ + 'armory_%s_RECOVERED.wallet' % self.wltID, \ + 'testing', RECOVERMODE.Full, \ + returnError = 'Dict') + recThread.join() + rcvWltResult = recThread.output + + self.assertTrue(rcvWltResult['nErrors']==0, "Unexpected Errors Found") + self.assertTrue(len(rcvWltResult['negativeImports'])==50, \ + "Missing neg Imports") + +############################################################################### +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/test/PyBtcWalletTest.py b/test/PyBtcWalletTest.py new file mode 100644 index 000000000..77967911b --- /dev/null +++ b/test/PyBtcWalletTest.py @@ -0,0 +1,411 @@ +''' +Created on Aug 14, 2013 + +@author: Andy +''' +import os +import shutil +import sys +import unittest + +from CppBlockUtils import SecureBinaryData +from armoryengine.ArmoryUtils import USE_TESTNET, convertKeyDataToAddress, \ + hash256, binary_to_hex, hex_to_binary, CLI_OPTIONS, ARMORY_HOME_DIR, \ + WalletLockError, InterruptTestError +from armoryengine.PyBtcWallet import PyBtcWallet +from armoryengine.BDM import TheBDM + + +sys.argv.append('--nologging') + + +WALLET_ROOT_ADDR = '5da74ed60a43a7ff11f0ba56cb0192b03518cc56' +NEW_UNUSED_ADDR = 'fb80e6fd042fa24178b897a6a70e1ae7eb56a20a' + +class PyBtcWalletTest(unittest.TestCase): + + def setUp(self): + TheBDM.Reset() + self.shortlabel = 'TestWallet1' + self.wltID ='3VB8XSoY' if USE_TESTNET else '3VB8XSmd' + + self.fileA = os.path.join(ARMORY_HOME_DIR, 'armory_%s_.wallet' % self.wltID) + self.fileB = os.path.join(ARMORY_HOME_DIR, 'armory_%s_backup.wallet' % self.wltID) + self.fileAupd = os.path.join(ARMORY_HOME_DIR, 'armory_%s_backup_unsuccessful.wallet' % self.wltID) + self.fileBupd = os.path.join(ARMORY_HOME_DIR, 'armory_%s_update_unsuccessful.wallet' % self.wltID) + + self.removeFileList([self.fileA, self.fileB, self.fileAupd, self.fileBupd]) + + # We need a controlled test, so we script the all the normally-random stuff + self.privKey = SecureBinaryData('\xaa'*32) + self.privKey2 = SecureBinaryData('\x33'*32) + self.chainstr = SecureBinaryData('\xee'*32) + theIV = SecureBinaryData(hex_to_binary('77'*16)) + self.passphrase = SecureBinaryData('A self.passphrase') + self.passphrase2 = SecureBinaryData('A new self.passphrase') + + self.wlt = PyBtcWallet().createNewWallet(withEncrypt=False, \ + plainRootKey=self.privKey, \ + chaincode=self.chainstr, \ + IV=theIV, \ + shortLabel=self.shortlabel) + + def tearDown(self): + self.removeFileList([self.fileA, self.fileB, self.fileAupd, self.fileBupd]) + + + # ********************************************************************* + # Testing deterministic, encrypted wallet features' + # ********************************************************************* + def removeFileList(self, fileList): + for f in fileList: + if os.path.exists(f): + os.remove(f) + + def testBackupWallet(self): + backupTestPath = os.path.join(ARMORY_HOME_DIR, 'armory_%s_.wallet.backup.test' % self.wltID) + # Remove backupTestPath in case it exists + backupFileList = [backupTestPath, self.fileB] + self.removeFileList(backupFileList) + # Remove the backup test path that is to be created after tear down. + self.addCleanup(self.removeFileList, backupFileList) + self.wlt.backupWalletFile(backupTestPath) + self.assertTrue(os.path.exists(backupTestPath)) + self.wlt.backupWalletFile() + self.assertTrue(os.path.exists(self.fileB)) + + # Remove wallet files, need fresh dir for this test + def testPyBtcWallet(self): + + self.wlt.addrPoolSize = 5 + # No block chain loaded so this should return -1 + self.assertEqual(self.wlt.detectHighestUsedIndex(True), -1) + self.assertEqual(self.wlt.kdfKey, None) + self.assertEqual(binary_to_hex(self.wlt.addrMap['ROOT'].addrStr20), WALLET_ROOT_ADDR ) + + ############################################################################# + # (1) Getting a new address: + newAddr = self.wlt.getNextUnusedAddress() + self.wlt.pprint(indent=' '*5) + self.assertEqual(binary_to_hex(newAddr.addrStr20), NEW_UNUSED_ADDR) + + # (1) Re-reading wallet from file, compare the two wallets + wlt2 = PyBtcWallet().readWalletFile(self.wlt.walletPath) + self.assertTrue(self.wlt.isEqualTo(wlt2)) + + ############################################################################# + # (2)Testing unencrypted wallet import-address' + originalLength = len(self.wlt.linearAddr160List) + self.wlt.importExternalAddressData(privKey=self.privKey2) + self.assertEqual(len(self.wlt.linearAddr160List), originalLength+1) + + # (2) Re-reading wallet from file, compare the two wallets + wlt2 = PyBtcWallet().readWalletFile(self.wlt.walletPath) + self.assertTrue(self.wlt.isEqualTo(wlt2)) + + # (2a)Testing deleteImportedAddress + # Wallet size before delete:', os.path.getsize(self.wlt.walletPath) + # Addresses before delete:', len(self.wlt.linearAddr160List) + toDelete160 = convertKeyDataToAddress(self.privKey2) + self.wlt.deleteImportedAddress(toDelete160) + self.assertEqual(len(self.wlt.linearAddr160List), originalLength) + + + # (2a) Reimporting address for remaining tests + # Wallet size before reimport:', os.path.getsize(self.wlt.walletPath) + self.wlt.importExternalAddressData(privKey=self.privKey2) + self.assertEqual(len(self.wlt.linearAddr160List), originalLength+1) + + + # (2b)Testing ENCRYPTED wallet import-address + privKey3 = SecureBinaryData('\xbb'*32) + privKey4 = SecureBinaryData('\x44'*32) + self.chainstr2 = SecureBinaryData('\xdd'*32) + theIV2 = SecureBinaryData(hex_to_binary('66'*16)) + self.passphrase2= SecureBinaryData('hello') + wltE = PyBtcWallet().createNewWallet(withEncrypt=True, \ + plainRootKey=privKey3, \ + securePassphrase=self.passphrase2, \ + chaincode=self.chainstr2, \ + IV=theIV2, \ + shortLabel=self.shortlabel) + + # We should have thrown an error about importing into a locked wallet... + self.assertRaises(WalletLockError, wltE.importExternalAddressData, privKey=self.privKey2) + + + + wltE.unlock(securePassphrase=self.passphrase2) + wltE.importExternalAddressData(privKey=self.privKey2) + + # (2b) Re-reading wallet from file, compare the two wallets + wlt2 = PyBtcWallet().readWalletFile(wltE.walletPath) + self.assertTrue(wltE.isEqualTo(wlt2)) + + # (2b) Unlocking wlt2 after re-reading locked-import-wallet + wlt2.unlock(securePassphrase=self.passphrase2) + self.assertFalse(wlt2.isLocked) + + ############################################################################# + # Now play with encrypted wallets + # *********************************************************************' + # (3)Testing conversion to encrypted wallet + + kdfParams = self.wlt.computeSystemSpecificKdfParams(0.1) + self.wlt.changeKdfParams(*kdfParams) + + self.assertEqual(self.wlt.kdf.getSalt(), kdfParams[2]) + self.wlt.changeWalletEncryption( securePassphrase=self.passphrase ) + self.assertEqual(self.wlt.kdf.getSalt(), kdfParams[2]) + + # (3) Re-reading wallet from file, compare the two wallets' + wlt2 = PyBtcWallet().readWalletFile(self.wlt.getWalletPath()) + self.assertTrue(self.wlt.isEqualTo(wlt2)) + # NOTE: this isEqual operation compares the serializations + # of the wallet addresses, which only contains the + # encrypted versions of the private keys. However, + # self.wlt is unlocked and contains the plaintext keys, too + # while wlt2 does not. + self.wlt.lock() + for key in self.wlt.addrMap: + self.assertTrue(self.wlt.addrMap[key].isLocked) + self.assertEqual(self.wlt.addrMap[key].binPrivKey32_Plain.toHexStr(), '') + + ############################################################################# + # (4)Testing changing self.passphrase on encrypted wallet', + + self.wlt.unlock( securePassphrase=self.passphrase ) + for key in self.wlt.addrMap: + self.assertFalse(self.wlt.addrMap[key].isLocked) + self.assertNotEqual(self.wlt.addrMap[key].binPrivKey32_Plain.toHexStr(), '') + # ...to same self.passphrase' + origKdfKey = self.wlt.kdfKey + self.wlt.changeWalletEncryption( securePassphrase=self.passphrase ) + self.assertEqual(origKdfKey, self.wlt.kdfKey) + + # (4)And now testing new self.passphrase...' + self.wlt.changeWalletEncryption( securePassphrase=self.passphrase2 ) + self.assertNotEqual(origKdfKey, self.wlt.kdfKey) + + # (4) Re-reading wallet from file, compare the two wallets' + wlt2 = PyBtcWallet().readWalletFile(self.wlt.getWalletPath()) + self.assertTrue(self.wlt.isEqualTo(wlt2)) + + ############################################################################# + # (5)Testing changing KDF on encrypted wallet' + + self.wlt.unlock( securePassphrase=self.passphrase2 ) + + MEMORY_REQT_BYTES = 1024 + NUM_ITER = 999 + SALT_ALL_0 ='00'*32 + self.wlt.changeKdfParams(MEMORY_REQT_BYTES, NUM_ITER, hex_to_binary(SALT_ALL_0), self.passphrase2) + self.assertEqual(self.wlt.kdf.getMemoryReqtBytes(), MEMORY_REQT_BYTES) + self.assertEqual(self.wlt.kdf.getNumIterations(), NUM_ITER) + self.assertEqual(self.wlt.kdf.getSalt().toHexStr(), SALT_ALL_0) + + self.wlt.changeWalletEncryption( securePassphrase=self.passphrase2 ) + # I don't know why this shouldn't be '' + # Commenting out because it's a broken assertion + # self.assertNotEqual(origKdfKey.toHexStr(), '') + + # (5) Get new address from locked wallet' + # Locking wallet' + self.wlt.lock() + for i in range(10): + self.wlt.getNextUnusedAddress() + self.assertEqual(len(self.wlt.addrMap), originalLength+13) + + # (5) Re-reading wallet from file, compare the two wallets' + wlt2 = PyBtcWallet().readWalletFile(self.wlt.getWalletPath()) + self.assertTrue(self.wlt.isEqualTo(wlt2)) + + ############################################################################# + # !!! #forkOnlineWallet() + # (6)Testing forking encrypted wallet for online mode' + self.wlt.forkOnlineWallet('OnlineVersionOfEncryptedWallet.bin') + wlt2.readWalletFile('OnlineVersionOfEncryptedWallet.bin') + for key in wlt2.addrMap: + self.assertTrue(self.wlt.addrMap[key].isLocked) + self.assertEqual(self.wlt.addrMap[key].binPrivKey32_Plain.toHexStr(), '') + # (6)Getting a new addresses from both wallets' + for i in range(self.wlt.addrPoolSize*2): + self.wlt.getNextUnusedAddress() + wlt2.getNextUnusedAddress() + + newaddr1 = self.wlt.getNextUnusedAddress() + newaddr2 = wlt2.getNextUnusedAddress() + self.assertTrue(newaddr1.getAddr160() == newaddr2.getAddr160()) + self.assertEqual(len(wlt2.addrMap), 3*originalLength+14) + + # (6) Re-reading wallet from file, compare the two wallets + wlt3 = PyBtcWallet().readWalletFile('OnlineVersionOfEncryptedWallet.bin') + self.assertTrue(wlt3.isEqualTo(wlt2)) + ############################################################################# + # (7)Testing removing wallet encryption' + # Wallet is locked? ', self.wlt.isLocked + self.wlt.unlock(securePassphrase=self.passphrase2) + self.wlt.changeWalletEncryption( None ) + for key in self.wlt.addrMap: + self.assertFalse(self.wlt.addrMap[key].isLocked) + self.assertNotEqual(self.wlt.addrMap[key].binPrivKey32_Plain.toHexStr(), '') + + # (7) Re-reading wallet from file, compare the two wallets' + wlt2 = PyBtcWallet().readWalletFile(self.wlt.getWalletPath()) + self.assertTrue(self.wlt.isEqualTo(wlt2)) + + ############################################################################# + # \n' + # *********************************************************************' + # (8)Doing interrupt tests to test wallet-file-update recovery' + def hashfile(fn): + f = open(fn,'r') + d = hash256(f.read()) + f.close() + return binary_to_hex(d[:8]) + + def verifyFileStatus(fileAExists = True, fileBExists = True, \ + fileAupdExists = True, fileBupdExists = True): + self.assertEqual(os.path.exists(self.fileA), fileAExists) + self.assertEqual(os.path.exists(self.fileB), fileBExists) + self.assertEqual(os.path.exists(self.fileAupd), fileAupdExists) + self.assertEqual(os.path.exists(self.fileBupd), fileBupdExists) + + correctMainHash = hashfile(self.fileA) + try: + self.wlt.interruptTest1 = True + self.wlt.getNextUnusedAddress() + except InterruptTestError: + # Interrupted!' + pass + self.wlt.interruptTest1 = False + + # (8a)Interrupted getNextUnusedAddress on primary file update' + verifyFileStatus(True, True, False, True) + # (8a)Do consistency check on the wallet' + self.wlt.doWalletFileConsistencyCheck() + verifyFileStatus(True, True, False, False) + self.assertEqual(correctMainHash, hashfile(self.fileA)) + + try: + self.wlt.interruptTest2 = True + self.wlt.getNextUnusedAddress() + except InterruptTestError: + # Interrupted!' + pass + self.wlt.interruptTest2 = False + + # (8b)Interrupted getNextUnusedAddress on between primary/backup update' + verifyFileStatus(True, True, True, True) + # (8b)Do consistency check on the wallet' + self.wlt.doWalletFileConsistencyCheck() + verifyFileStatus(True, True, False, False) + self.assertEqual(hashfile(self.fileA), hashfile(self.fileB)) + # (8c) Try interrupting at state 3' + verifyFileStatus(True, True, False, False) + + try: + self.wlt.interruptTest3 = True + self.wlt.getNextUnusedAddress() + except InterruptTestError: + # Interrupted!' + pass + self.wlt.interruptTest3 = False + + # (8c)Interrupted getNextUnusedAddress on backup file update' + verifyFileStatus(True, True, True, False) + # (8c)Do consistency check on the wallet' + self.wlt.doWalletFileConsistencyCheck() + verifyFileStatus(True, True, False, False) + self.assertEqual(hashfile(self.fileA), hashfile(self.fileB)) + + ############################################################################# + # \n' + # *********************************************************************' + # (9)Checksum-based byte-error correction tests!' + # (9)Start with a good primary and backup file...' + + # (9a)Open primary wallet, change second byte in KDF' + wltfile = open(self.wlt.walletPath,'r+b') + wltfile.seek(326) + wltfile.write('\xff') + wltfile.close() + # (9a)Byte changed, file hashes:' + verifyFileStatus(True, True, False, False) + + # (9a)Try to read wallet from file, should correct KDF error, write fix' + wlt2 = PyBtcWallet().readWalletFile(self.wlt.walletPath) + verifyFileStatus(True, True, False, False) + self.assertNotEqual(hashfile(self.fileA), hashfile(self.fileB)) + + # \n' + # *********************************************************************' + # (9b)Change a byte in each checksummed field in root addr' + wltfile = open(self.wlt.walletPath,'r+b') + wltfile.seek(838); wltfile.write('\xff') + wltfile.seek(885); wltfile.write('\xff') + wltfile.seek(929); wltfile.write('\xff') + wltfile.seek(954); wltfile.write('\xff') + wltfile.seek(1000); wltfile.write('\xff') + wltfile.close() + # (9b) New file hashes...' + verifyFileStatus(True, True, False, False) + + # (9b)Try to read wallet from file, should correct address errors' + wlt2 = PyBtcWallet().readWalletFile(self.wlt.walletPath) + verifyFileStatus(True, True, False, False) + self.assertNotEqual(hashfile(self.fileA), hashfile(self.fileB)) + + # \n' + # *********************************************************************' + # (9c)Change a byte in each checksummed field, of first non-root addr' + wltfile = open(self.wlt.walletPath,'r+b') + wltfile.seek(1261+21+838); wltfile.write('\xff') + wltfile.seek(1261+21+885); wltfile.write('\xff') + wltfile.seek(1261+21+929); wltfile.write('\xff') + wltfile.seek(1261+21+954); wltfile.write('\xff') + wltfile.seek(1261+21+1000); wltfile.write('\xff') + wltfile.close() + # (9c) New file hashes...' + verifyFileStatus(True, True, False, False) + + # (9c)Try to read wallet from file, should correct address errors' + wlt2 = PyBtcWallet().readWalletFile(self.wlt.walletPath) + verifyFileStatus(True, True, False, False) + self.assertNotEqual(hashfile(self.fileA), hashfile(self.fileB)) + + # \n' + # *********************************************************************' + # (9d)Now butcher the CHECKSUM, see if correction works' + wltfile = open(self.wlt.walletPath,'r+b') + wltfile.seek(977); wltfile.write('\xff') + wltfile.close() + # (9d) New file hashes...' + verifyFileStatus(True, True, False, False) + + # (9d)Try to read wallet from file, should correct address errors' + wlt2 = PyBtcWallet().readWalletFile(self.wlt.walletPath) + verifyFileStatus(True, True, False, False) + self.assertNotEqual(hashfile(self.fileA), hashfile(self.fileB)) + + + # *******' + # (9z) Test comment I/O' + comment1 = 'This is my normal unit-testing address.' + comment2 = 'This is fake tx... no tx has this hash.' + comment3 = comment1 + ' Corrected!' + hash1 = '\x1f'*20 # address160 + hash2 = '\x2f'*32 # tx hash + self.wlt.setComment(hash1, comment1) + self.wlt.setComment(hash2, comment2) + self.wlt.setComment(hash1, comment3) + + wlt2 = PyBtcWallet().readWalletFile(self.wlt.walletPath) + c3 = wlt2.getComment(hash1) + c2 = wlt2.getComment(hash2) + self.assertEqual(c3, comment3) + self.assertEqual(c2, comment2) + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() \ No newline at end of file diff --git a/test/PyTXTest.py b/test/PyTXTest.py new file mode 100644 index 000000000..ab87281f4 --- /dev/null +++ b/test/PyTXTest.py @@ -0,0 +1,308 @@ +''' +Created on Aug 4, 2013 + +@author: Andy +''' +import sys +import unittest +from armoryengine.BDM import BlockDataManagerThread, TheBDM +sys.path.append('..') + +from armoryengine.ArmoryUtils import hex_to_binary, binary_to_hex, hex_to_int, \ + ONE_BTC +from armoryengine.BinaryUnpacker import BinaryUnpacker +from armoryengine.Block import PyBlock +from armoryengine.PyBtcAddress import PyBtcAddress +from armoryengine.Script import PyScriptProcessor +from armoryengine.Transaction import PyTx, PyTxIn, PyOutPoint, PyTxOut, \ + PyCreateAndSignTx, getMultisigScriptInfo, BlockComponent + + + +# Unserialize an reserialize +tx1raw = hex_to_binary( \ + '01000000016290dce984203b6a5032e543e9e272d8bce934c7de4d15fa0fe44d' + 'd49ae4ece9010000008b48304502204f2fa458d439f957308bca264689aa175e' + '3b7c5f78a901cb450ebd20936b2c500221008ea3883a5b80128e55c9c6070aa6' + '264e1e0ce3d18b7cd7e85108ce3d18b7419a0141044202550a5a6d3bb81549c4' + 'a7803b1ad59cdbba4770439a4923624a8acfc7d34900beb54a24188f7f0a4068' + '9d905d4847cc7d6c8d808a457d833c2d44ef83f76bffffffff0242582c0a0000' + '00001976a914c1b4695d53b6ee57a28647ce63e45665df6762c288ac80d1f008' + '000000001976a9140e0aec36fe2545fb31a41164fb6954adcd96b34288ac00000000') +tx2raw = hex_to_binary( \ + '0100000001f658dbc28e703d86ee17c9a2d3b167a8508b082fa0745f55be5144' + 'a4369873aa010000008c49304602210041e1186ca9a41fdfe1569d5d807ca7ff' + '6c5ffd19d2ad1be42f7f2a20cdc8f1cc0221003366b5d64fe81e53910e156914' + '091d12646bc0d1d662b7a65ead3ebe4ab8f6c40141048d103d81ac9691cf13f3' + 'fc94e44968ef67b27f58b27372c13108552d24a6ee04785838f34624b294afee' + '83749b64478bb8480c20b242c376e77eea2b3dc48b4bffffffff0200e1f50500' + '0000001976a9141b00a2f6899335366f04b277e19d777559c35bc888ac40aeeb' + '02000000001976a9140e0aec36fe2545fb31a41164fb6954adcd96b34288ac00000000') + +multiTx1raw = hex_to_binary( \ + '0100000004a14fd232f045f0c9f28c6848a22fee393152e901eaa61a9f18438b3ba05c6035010000008a47304402201b19808aa145dbebf775ed11a15d763eaa2' + 'b5df92b20f9835f62c72404918b1b02205aea3e816ac6ac7545254b9c34a00c37f20024793bbe0a64958934343f3c577b014104c0f3d0a4920bb6825769dd6ae1' + 'e36b0ac36581639d605241cdd548c4ef5d46cda5ac21723d478041a63118f192fdb730c4cf76106789824cd68879a7afeb5288ffffffffa14fd232f045f0c9f28' + 'c6848a22fee393152e901eaa61a9f18438b3ba05c6035000000008b4830450220796307d9787b892c8b1ada8511d99e855ea3099e1a76ce0f7aa783ed352a6e59' + '022100fc38d05d7dfbe51e28c36d854dd0dcc938d60a3e406573c3dc39253694d14a12014104630aaf9d5c8d757cb5759428d4075911a2b2ff13dd7208ad7ea1d' + '1682738a7138be93ee526c9d774e0dea03fa2a5fbb68043259ddfb942c0763f9b636b40c43fffffffffa14fd232f045f0c9f28c6848a22fee393152e901eaa61a' + '9f18438b3ba05c6035020000008c493046022100cb423b63197ef3cdbfaed69f61aac59755f0025bd6d7a9d3c78024d897ebcf94022100f3ad14804a3c8042387' + 'eca9b9053abe99e12651a795cae7f546b08e1c08c6464014104649694df12dcd7fdb5a8c54c376b904bd7337891d865b8d306beb5d2e5d8fdf2a537d6f9df65ff' + '44eb0b6042ebfdf9e338bff7f4afacb359dd6c71aea7b9b92dffffffffa14fd232f045f0c9f28c6848a22fee393152e901eaa61a9f18438b3ba05c60350300000' + '08b483045022100fb9f4ddc68497a266362d489abf05184909a2b99aa64803061c88597b725877802207f39cf5a90a305aee45f365cf9e2d258e37cab4da6c123' + 'aa287635cd1fd40dd001410438252055130f3dd242201684931550c4065efc1b87c48192f75868f747e2a9df9a700fed7e90068bd395c58680bd593780c8119e7' + '981dae08c345588f120fcb4ffffffff02e069f902000000001976a914ad00cf2b893e132c33a79a22ae938d6309c780a488ac80f0fa02000000001976a9143155' + '18b646ea65ad148ee1e2f0360233617447e288ac00000000') + +multiTx2raw = hex_to_binary( \ + '0100000004a14fd232f045f0c9f28c6848a22fee393152e901eaa61a9f18438b3ba05c6035010000008a47304402201b19808aa145dbebf775ed11a15d763eaa2' + 'b5df92b20f9835f62c72404918b1b02205aea3e816ac6ac7545254b9c34a00c37f20024793bbe0a64958934343f3c577b014104c0f3d0a4920bb6825769dd6ae1' + 'e36b0ac36581639d605241cdd548c4ef5d46cda5ac21723d478041a63118f192fdb730c4cf76106789824cd68879a7afeb5288ffffffffa14fd232f045f0c9f28' + 'c6848a22fee393152e901eaa61a9f18438b3ba05c6035000000008b4830450220796307d9787b892c8b1ada8511d99e855ea3099e1a76ce0f7aa783ed352a6e59' + '022100fc38d05d7dfbe51e28c36d854dd0dcc938d60a3e406573c3dc39253694d14a12014104630aaf9d5c8d757cb5759428d4075911a2b2ff13dd7208ad7ea1d' + '1682738a7138be93ee526c9d774e0dea03fa2a5fbb68043259ddfb942c0763f9b636b40c43fffffffffa14fd232f045f0c9f28c6848a22fee393152e901eaa61a' + '9f18438b3ba05c6035020000008c493046022100cb423b63197ef3cdbfaed69f61aac59755f0025bd6d7a9d3c78024d897ebcf94022100f3ad14804a3c8042387' + 'eca9b9053abe99e12651a795cae7f546b08e1c08c6464014104649694df12dcd7fdb5a8c54c376b904bd7337891d865b8d306beb5d2e5d8fdf2a537d6f9df65ff' + '44eb0b6042ebfdf9e338bff7f4afacb359dd6c71aea7b9b92dffffffffa14fd232f045f0c9f28c6848a22fee393152e901eaa61a9f18438b3ba05c60350300000' + '08c49304602220000fb9f4ddc68497a266362d489abf05184909a2b99aa64803061c88597b725877802207f39cf5a90a305aee45f365cf9e2d258e37cab4da6c123' + 'aa287635cd1fd40dd001410438252055130f3dd242201684931550c4065efc1b87c48192f75868f747e2a9df9a700fed7e90068bd395c58680bd593780c8119e7' + '981dae08c345588f120fcb4ffffffff02e069f902000000001976a914ad00cf2b893e132c33a79a22ae938d6309c780a488ac80f0fa02000000001976a9143155' + '18b646ea65ad148ee1e2f0360233617447e288ac00000000') + + # Here's a full block, which we should be able to parse and process +hexBlock = ( \ + '01000000eb10c9a996a2340a4d74eaab41421ed8664aa49d18538bab59010000000000005a2f06efa9f2bd804f17877537f2080030cadbfa1eb50e02338117cc' + '604d91b9b7541a4ecfbb0a1a64f1ade70301000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0804cfbb0a1a' + '02360affffffff0100f2052a01000000434104c2239c4eedb3beb26785753463be3ec62b82f6acd62efb65f452f8806f2ede0b338e31d1f69b1ce449558d7061' + 'aa1648ddc2bf680834d3986624006a272dc21cac000000000100000003e8caa12bcb2e7e86499c9de49c45c5a1c6167ea4b894c8c83aebba1b6100f343010000' + '008c493046022100e2f5af5329d1244807f8347a2c8d9acc55a21a5db769e9274e7e7ba0bb605b26022100c34ca3350df5089f3415d8af82364d7f567a6a297f' + 'cc2c1d2034865633238b8c014104129e422ac490ddfcb7b1c405ab9fb42441246c4bca578de4f27b230de08408c64cad03af71ee8a3140b40408a7058a1984a9' + 'f246492386113764c1ac132990d1ffffffff5b55c18864e16c08ef9989d31c7a343e34c27c30cd7caa759651b0e08cae0106000000008c4930460221009ec9aa' + '3e0caf7caa321723dea561e232603e00686d4bfadf46c5c7352b07eb00022100a4f18d937d1e2354b2e69e02b18d11620a6a9332d563e9e2bbcb01cee559680a' + '014104411b35dd963028300e36e82ee8cf1b0c8d5bf1fc4273e970469f5cb931ee07759a2de5fef638961726d04bd5eb4e5072330b9b371e479733c942964bb8' + '6e2b22ffffffff3de0c1e913e6271769d8c0172cea2f00d6d3240afc3a20f9fa247ce58af30d2a010000008c493046022100b610e169fd15ac9f60fe2b507529' + '281cf2267673f4690ba428cbb2ba3c3811fd022100ffbe9e3d71b21977a8e97fde4c3ba47b896d08bc09ecb9d086bb59175b5b9f03014104ff07a1833fd8098b' + '25f48c66dcf8fde34cbdbcc0f5f21a8c2005b160406cbf34cc432842c6b37b2590d16b165b36a3efc9908d65fb0e605314c9b278f40f3e1affffffff0240420f' + '00000000001976a914adfa66f57ded1b655eb4ccd96ee07ca62bc1ddfd88ac007d6a7d040000001976a914981a0c9ae61fa8f8c96ae6f8e383d6e07e77133e88' + 'ac00000000010000000138e7586e0784280df58bd3dc5e3d350c9036b1ec4107951378f45881799c92a4000000008a47304402207c945ae0bbdaf9dadba07bdf' + '23faa676485a53817af975ddf85a104f764fb93b02201ac6af32ddf597e610b4002e41f2de46664587a379a0161323a85389b4f82dda014104ec8883d3e4f7a3' + '9d75c9f5bb9fd581dc9fb1b7cdf7d6b5a665e4db1fdb09281a74ab138a2dba25248b5be38bf80249601ae688c90c6e0ac8811cdb740fcec31dffffffff022f66' + 'ac61050000001976a914964642290c194e3bfab661c1085e47d67786d2d388ac2f77e200000000001976a9141486a7046affd935919a3cb4b50a8a0c233c286c' + '88ac00000000') + +# I made these two tx in a fake blockchain... but they should still work +tx1Fake = PyTx().unserialize(hex_to_binary( ( + '01000000 0163451d 1002611c 1388d5ba 4ddfdf99 196a86b5 990fb5b0 dc786207' + '4fdcb8ee d2000000 004a4930 46022100 cb02fb5a 910e7554 85e3578e 6e9be315' + 'a161540a 73f84ee6 f5d68641 925c59ac 0221007e 530a1826 30b50e2c 12dd09cd' + 'ebfd809f 038be982 bdc2c7e9 d4cbf634 9e088d01 ffffffff 0200ca9a 3b000000' + '001976a9 14cb2abd e8bccacc 32e893df 3a054b9e f7f227a4 ce88ac00 286bee00' + '00000019 76a914ee 26c56fc1 d942be8d 7a24b2a1 001dd894 69398088 ac000000' + '00' ).replace(' ',''))) + +tx2Fake = PyTx().unserialize(hex_to_binary( ( + '01000000 01a5b837 da38b64a 6297862c ba8210d0 21ac59e1 2b7c6d7e 70c355f6' + '972ee7a8 6e010000 008c4930 46022100 89e47100 d88d5f8c 8f62a796 dac3afb8' + 'f090c6fc 2eb0c4af ac7b7567 3a364c01 0221002b f40e554d ae51264b 0a86df17' + '3e45756a 89bbd302 4f166cc4 2cfd1874 13636901 41046868 0737c76d abb801cb' + '2204f57d be4e4579 e4f710cd 67dc1b42 27592c81 e9b5cf02 b5ac9e8b 4c9f49be' + '5251056b 6a6d011e 4c37f6b6 d17ede6b 55faa235 19e2ffff ffff0100 286bee00' + '00000019 76a914c5 22664fb0 e55cdc5c 0cea73b4 aad97ec8 34323288 ac000000' + '00' ).replace(' ',''))) + +ALL_ZERO_OUTPOINT = hex_to_binary('00' * 36) + +class PyTXTest(unittest.TestCase): + + + def setUp(self): + TheBDM.Reset(wait=True) + + def tearDown(self): + pass + + + def testMinimizeDERSignaturePadding(self): + multiTx1 = PyTx().unserialize(multiTx1raw) + paddingMinimizedMulti1, newTxMulti1 = multiTx1.minimizeDERSignaturePadding() + self.assertEqual(multiTx1.inputs[0].binScript, newTxMulti1.inputs[0].binScript) + self.assertEqual(multiTx1.inputs[1].binScript, newTxMulti1.inputs[1].binScript) + self.assertEqual(multiTx1.inputs[2].binScript, newTxMulti1.inputs[2].binScript) + self.assertEqual(multiTx1.inputs[3].binScript, newTxMulti1.inputs[3].binScript) + self.assertFalse(paddingMinimizedMulti1) + + txString = multiTx1.toString() + self.assertTrue(len(txString)> 0) + + multiTx2 = PyTx().unserialize(multiTx2raw) + paddingMinimizedMulti2, newTxMulti2 = multiTx2.minimizeDERSignaturePadding() + self.assertEqual(multiTx2.inputs[0].binScript, newTxMulti2.inputs[0].binScript) + self.assertEqual(multiTx2.inputs[1].binScript, newTxMulti2.inputs[1].binScript) + self.assertEqual(multiTx2.inputs[2].binScript, newTxMulti2.inputs[2].binScript) + # Added 1 extra byte of padding + self.assertEqual(len(multiTx2.inputs[3].binScript)-1, len(newTxMulti2.inputs[3].binScript)) + self.assertTrue(paddingMinimizedMulti2) + + tx1 = PyTx().unserialize(tx1raw) + paddingMinimized1, newTx1 = tx1.minimizeDERSignaturePadding() + self.assertEqual(tx1.inputs[0].binScript, newTx1.inputs[0].binScript) + self.assertFalse(paddingMinimized1) + tx2 = PyTx().unserialize(tx2raw) + paddingMinimized2, newTx2 = tx2.minimizeDERSignaturePadding() + # Old tx had 2 extra bytes of padding one each on the r and s + self.assertEqual(len(tx2.inputs[0].binScript)-2, len(newTx2.inputs[0].binScript)) + self.assertTrue(paddingMinimized2) + + + def testSerializeUnserialize(self): + tx1 = PyTx().unserialize(tx1raw) + tx2 = PyTx().unserialize(BinaryUnpacker(tx2raw)) + tx1again = tx1.serialize() + tx2again = tx2.serialize() + self.assertEqual(tx1again, tx1raw) + self.assertEqual(tx2again, tx2raw) + blk = PyBlock().unserialize( hex_to_binary(hexBlock) ) + blockReHex = binary_to_hex(blk.serialize()) + self.assertEqual(hexBlock, blockReHex) + binRoot = blk.blockData.getMerkleRoot() + self.assertEqual(blk.blockHeader.merkleRoot, blk.blockData.merkleRoot) + + def testCreateTx(self): + addrA = PyBtcAddress().createFromPrivateKey(hex_to_int('aa' * 32)) + addrB = PyBtcAddress().createFromPrivateKey(hex_to_int('bb' * 32)) + + # This TxIn will be completely ignored, so it can contain garbage + txinA = PyTxIn() + txinA.outpoint = PyOutPoint().unserialize(hex_to_binary('00'*36)) + txinA.binScript = hex_to_binary('99'*4) + txinA.intSeq = hex_to_int('ff'*4) + # test binary unpacker in unserialize + testTxIn = PyTxIn().unserialize(txinA.serialize()) + self.assertEqual(txinA.getScript(), testTxIn.getScript()) + self.assertEqual(txinA.intSeq, testTxIn.intSeq) + self.assertEqual(txinA.outpoint.txHash, testTxIn.outpoint.txHash) + txoutA = PyTxOut() + txoutA.value = 50 * ONE_BTC + txoutA.binScript = '\x76\xa9\x14' + addrA.getAddr160() + '\x88\xac' + # Test pprint + print '\nTest pretty print PyTxIn, expect PrevTXHash all 0s' + testTxIn.pprint() + + # test binary unpacker in unserialize + testTxOut = PyTxOut().unserialize(txoutA.serialize()) + self.assertEqual(txoutA.getScript(), testTxOut.getScript()) + self.assertEqual(txoutA.value, testTxOut.getValue()) + # Test pprint + print '\nTest pretty print PyTxOut' + testTxOut.pprint() + + tx1 = PyTx() + tx1.version = 1 + tx1.numInputs = 1 + tx1.inputs = [txinA] + tx1.numOutputs = 1 + tx1.outputs = [txoutA] + tx1.locktime = 0 + tx1hash = tx1.getHash() + recipientList = tx1.makeRecipientsList() + self.assertEqual(len(recipientList), 1) + self.assertEqual(recipientList[0][0], 0) + self.assertEqual(recipientList[0][1], 50 * ONE_BTC) + + self.assertEqual(tx1.getHashHex(), binary_to_hex(tx1hash)) + # Creating transaction to send coins from A to B + tx2 = PyCreateAndSignTx( [[ addrA, tx1, 0 ]], [[addrB, 50*ONE_BTC]]) + psp = PyScriptProcessor() + psp.setTxObjects(tx1, tx2, 0) + self.assertTrue(psp.verifyTransactionValid()) + + + def testVerifyTxFromFakeBlockChain(self): + psp = PyScriptProcessor() + psp.setTxObjects(tx1Fake, tx2Fake, 0) + self.assertTrue(psp.verifyTransactionValid()) + + def test2of2MultiSigTx(self): + tx1 = PyTx().unserialize(hex_to_binary('010000000189a0022c8291b4328338ec95179612b8ebf72067051de019a6084fb97eae0ebe000000004a4930460221009627882154854e3de066943ba96faba02bb8b80c1670a0a30d0408caa49f03df022100b625414510a2a66ebb43fffa3f4023744695380847ee1073117ec90cb60f2c8301ffffffff0210c18d0000000000434104a701496f10db6aa8acbb6a7aa14d62f4925f8da03de7f0262010025945f6ebcc3efd55b6aa4bc6f811a0dc1bbdd2644bdd81c8a63766aa11f650cd7736bbcaf8ac001bb7000000000043526b006b7dac7ca914fc1243972b59c1726735d3c5cca40e415039dce9879a6c936b7dac7ca914375dd72e03e7b5dbb49f7e843b7bef4a2cc2ce9e879a6c936b6c6ca200000000')) + tx2 = PyTx().unserialize(hex_to_binary('01000000011c9608650a912be7fa88eecec664e6fbfa4b676708697fa99c28b3370005f32d01000000fd1701483045022017462c29efc9158cf26f2070d444bb2b087b8a0e6287a9274fa36fad30c46485022100c6d4cc6cd504f768389637df71c1ccd452e0691348d0f418130c31da8cc2a6e8014104e83c1d4079a1b36417f0544063eadbc44833a992b9667ab29b4ff252d8287687bad7581581ae385854d4e5f1fcedce7de12b1aec1cb004cabb2ec1f3de9b2e60493046022100fdc7beb27de0c3a53fbf96df7ccf9518c5fe7873eeed413ce17e4c0e8bf9c06e022100cc15103b3c2e1f49d066897fe681a12e397e87ed7ee39f1c8c4a5fef30f4c2c60141047cf315904fcc2e3e2465153d39019e0d66a8aaec1cec1178feb10d46537427239fd64b81e41651e89b89fefe6a23561d25dddc835395dd3542f83b32a1906aebffffffff01c0d8a700000000001976a914fc1243972b59c1726735d3c5cca40e415039dce988ac00000000')) + # Verify 2-of-2 tx from Testnet + psp = PyScriptProcessor() + psp.setTxObjects(tx1, tx2, 0) + self.assertTrue(psp.verifyTransactionValid()) + + def test2of3MultiSigTx(self): + tx1 = PyTx().unserialize(hex_to_binary('010000000371c06e0639dbe6bc35e6f948da4874ae69d9d91934ec7c5366292d0cbd5f97b0010000008a47304402200117cdd3ec6259af29acea44db354a6f57ac10d8496782033f5fe0febfd77f1b02202ceb02d60dbb43e6d4e03e5b5fbadc031f8bbb3c6c34ad307939947987f600bf01410452d63c092209529ca2c75e056e947bc95f9daffb371e601b46d24377aaa3d004ab3c6be2d6d262b34d736b95f3b0ef6876826c93c4077d619c02ebd974c7facdffffffffa65aa866aa7743ec05ba61418015fc32ecabd99886732056f1d4454c8f762bf8000000008c493046022100ea0a9b41c9372837e52898205c7bebf86b28936a3ee725672d0ca8f434f876f0022100beb7243a51fbc0997e55cb519d3b9cbd59f7aba68d80ba1e8adbb53443cda3c00141043efd1ca3cffc50638031281d227ff347a3a27bc145e2f846891d29f87bc068c27710559c4d9cd71f7e9e763d6e2753172406eb1ed1fadcaf9a8972b4270f05b4ffffffffd866d14151ee1b733a2a7273f155ecb25c18303c31b2c4de5aa6080aef2e0006000000008b483045022052210f95f6b413c74ce12cfc1b14a36cb267f9fa3919fa6e20dade1cd570439f022100b9e5b325f312904804f043d06c6ebc8e4b1c6cd272856c48ab1736b9d562e10c01410423fdddfe7e4d70d762dd6596771e035f4b43d54d28c2231be1102056f81f067914fe4fb6fd6e3381228ee5587ddd2028c846025741e963d9b1d6cf2c2dea0dbcffffffff0210ef3200000000004341048a33e9fd2de28137574cc69fe5620199abe37b7d08a51c528876fe6c5fa7fc28535f5a667244445e79fffc9df85ec3d79d77693b1f37af0e2d7c1fa2e7113a48acc0d454070000000061526b006b7dac7ca9143cd1def404e12a85ead2b4d3f5f9f817fb0d46ef879a6c936b7dac7ca9146a4e7d5f798e90e84db9244d4805459f87275943879a6c936b7dac7ca914486efdd300987a054510b4ce1148d4ad290d911e879a6c936b6c6ca200000000')) + tx2 = PyTx().unserialize(hex_to_binary('01000000012f654d4d1d7246d1a824c5b6c5177c0b5a1983864579aabb88cabd5d05e032e201000000fda0014730440220151ad44e7f78f9e0c4a3f2135c19ca3de8dbbb7c58893db096c0c5f1573d5dec02200724a78c3fa5f153103cb46816df46eb6cfac3718038607ddec344310066161e01410459fd82189b81772258a3fc723fdda900eb8193057d4a573ee5ad39e26b58b5c12c4a51b0edd01769f96ed1998221daf0df89634a7137a8fa312d5ccc95ed8925483045022100ca34834ece5925cff6c3d63e2bda6b0ce0685b18f481c32e70de9a971e85f12f0220572d0b5de0cf7b8d4e28f4914a955e301faaaa42f05feaa1cc63b45f938d75d9014104ce6242d72ee67e867e6f8ec434b95fcb1889c5b485ec3414df407e11194a7ce012eda021b68f1dd124598a9b677d6e7d7c95b1b7347f5c5a08efa628ef0204e1483045022074e01e8225e8c4f9d0b3f86908d42a61e611f406e13817d16240f94f52f49359022100f4c768dd89c6435afd3834ae2c882465ade92d7e1cc5c2c2c3d8d25c41b3ea61014104ce66c9f5068b715b62cc1622572cd98a08812d8ca01563045263c3e7af6b997e603e8e62041c4eb82dfd386a3412c34c334c34eb3c76fb0e37483fc72323f807ffffffff01b0ad5407000000001976a9146a4e7d5f798e90e84db9244d4805459f8727594388ac00000000')) + # Verify 2-of-3 tx from Testnet + psp = PyScriptProcessor() + psp.setTxObjects(tx1, tx2, 0) + self.assertTrue(psp.verifyTransactionValid()) + + def testMultiSig(self): + tx1 = PyTx().unserialize(hex_to_binary('0100000001845ad165bdc0f9b5829cf5a594c4148dfd89e24756303f3a8dabeb597afa589b010000008b483045022063c233df8efa3d1885e069e375a8eabf16b23475ef21bdc9628a513ee4caceb702210090a102c7b602043e72b34a154d495ac19b3b9e42acb962c399451f2baead8f4c014104b38f79037ad25b84a564eaf53ede93dec70b35216e6682aa71a47cefa2996ec49acfbb0a8730577c62ef9a7cc20c740aaaaee75419bef9640a4216c2b49c42d3ffffffff02000c022900000000434104c08c0a71ccbe838403e3870aa1ab871b0ab3a6014b0ba41f6df2b9aefea73134ecaa0b27797620e402a33799e9047f86519d9e43bbd504cf753c293752933f4fac406f40010000000062537a7652a269537a829178a91480677c5392220db736455533477d0bc2fba65502879b69537a829178a91402d7aa2e76d9066fb2b3c41ff8839a5c81bdca19879b69537a829178a91410039ce4fdb5d4ee56148fe3935b9bfbbe4ecc89879b6953ae00000000')) + tx2 = PyTx().unserialize(hex_to_binary('0100000001bb664ff716b9dfc831bcc666c1767f362ad467fcfbaf4961de92e45547daab8701000000fd190100493046022100d73f633f114e0e0b324d87d38d34f22966a03b072803afa99c9408201f6d6dc6022100900e85be52ad2278d24e7edbb7269367f5f2d6f1bd338d017ca460008776614401473044022071fef8ac0aa6318817dbd242bf51fb5b75be312aa31ecb44a0afe7b49fcf840302204c223179a383bb6fcb80312ac66e473345065f7d9136f9662d867acf96c12a42015241048c006ff0d2cfde86455086af5a25b88c2b81858aab67f6a3132c885a2cb9ec38e700576fd46c7d72d7d22555eee3a14e2876c643cd70b1b0a77fbf46e62331ac4104b68ef7d8f24d45e1771101e269c0aacf8d3ed7ebe12b65521712bba768ef53e1e84fff3afbee360acea0d1f461c013557f71d426ac17a293c5eebf06e468253e00ffffffff0280969800000000001976a9140817482d2e97e4be877efe59f4bae108564549f188ac7015a7000000000062537a7652a269537a829178a91480677c5392220db736455533477d0bc2fba65502879b69537a829178a91402d7aa2e76d9066fb2b3c41ff8839a5c81bdca19879b69537a829178a91410039ce4fdb5d4ee56148fe3935b9bfbbe4ecc89879b6953ae00000000')) + # OP_CHECKMULTISIG from Testnet + psp = PyScriptProcessor() + psp.setTxObjects(tx1, tx2, 0) + self.assertTrue(psp.verifyTransactionValid()) + + ''' + def testMultiSigAddrExtraction(self): + script1 = hex_to_binary('4104b54b5fc1917945fff64785d4baaca66a9704e9ed26002f51f53763499643321fbc047683a62be16e114e25404ce6ffdcf625a928002403402bf9f01e5cbd5f3dad4104f576e534f9bbf6d7c5f186ff4c6e0c5442c2755314bdee62fbc656f94d6cbf32c5eb3522da21cf9f954133000ffccb20dbfec030737640cc3315ce09619210d0ac') + expectedBtcAddrList1 = ['1KmV9FdKJEFFCHydZUZGdBL9uKq2T9JUm8','13maaQeK5qSPjHwnHhwNUtNKruK3qYLwvv'] + self.verifyMultiSigAddrExtraction(script1, expectedBtcAddrList1) + + script2 = hex_to_binary('537a7652a269537a829178a91480677c5392220db736455533477d0bc2fba65502879b69537a829178a91402d7aa2e76d9066fb2b3c41ff8839a5c81bdca19879b69537a829178a91410039ce4fdb5d4ee56148fe3935b9bfbbe4ecc89879b6953ae') + expectedBtcAddrList2 = ['1ChwTs5Dmh6y9iDh4pjWyu2X6nAhjre7SV','1G2i31fxRqaoXBfYMuE4YKb9x96uYcHeQ','12Tg96ZPSYc3P2g5c9c4znFFH2whriN9NQ'] + self.verifyMultiSigAddrExtraction(script2, expectedBtcAddrList2) + + script3 = hex_to_binary('527a7651a269527a829178a914731cdb75c88a01cbb96729888f726b3b9f29277a879b69527a829178a914e9b4261c6122f8957683636548923acc069e8141879b6952ae') + expectedBtcAddrList3 = ['1BVfH6iKT1s8fYEVSj39QkJrPqCKN4hv2m','1NJiFfFPZ177Pv96Yt4FCNZFEumyL2eKmt'] + self.verifyMultiSigAddrExtraction(script3, expectedBtcAddrList3) + ''' + + def verifyMultiSigAddrExtraction(self, scr, expectedBtcAddrList): + addrList = getMultisigScriptInfo(scr)[2] + btcAddrList = [] + for a in addrList: + btcAddrList.append(PyBtcAddress().createFromPublicKeyHash160(a).getAddrStr()) + self.assertEqual(btcAddrList, expectedBtcAddrList) + + def testUnpackUnserializePyOutPoint(self): + outpoint = PyOutPoint().unserialize(BinaryUnpacker(ALL_ZERO_OUTPOINT)) + self.assertEqual(outpoint.txHash, hex_to_binary('00'*32)) + self.assertEqual(outpoint.txOutIndex, 0) + + def testCopyPyOutPoint(self): + outpoint = PyOutPoint().unserialize(BinaryUnpacker(ALL_ZERO_OUTPOINT)) + outpointCopy = outpoint.copy() + self.assertEqual(outpoint.txHash, outpointCopy.txHash) + self.assertEqual(outpoint.txOutIndex, outpointCopy.txOutIndex) + + def testPPrintPyOutPoint(self): + # No return value - Should just print 0s + outpoint = PyOutPoint().unserialize(BinaryUnpacker(ALL_ZERO_OUTPOINT)) + print "PyOutPoint PPrint Test. Expect all 0s: " + outpoint.pprint() + + ''' + Does not pass because fromCpp is missing + def testCreateCppFromCppPyOutPoint(self): + outpoint = PyOutPoint().unserialize(BinaryUnpacker(ALL_ZERO_OUTPOINT)) + outpointFromCpp = PyOutPoint().fromCpp(outpoint.createCpp()) + self.assertEqual(outpoint.txHash, outpointFromCpp.txHash) + self.assertEqual(outpoint.txOutIndex, outpointFromCpp.txOutIndex) + ''' + def testBogusBlockComponent(self): + class TestBlockComponent(BlockComponent): + pass + testBlkComp = TestBlockComponent() + self.assertRaises(NotImplementedError, testBlkComp.serialize) + self.assertRaises(NotImplementedError, testBlkComp.unserialize) + + # TODO: Add some tests for the OP_CHECKMULTISIG support in TxDP + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/test/SendTx.py b/test/SendTx.py new file mode 100644 index 000000000..521fb4145 --- /dev/null +++ b/test/SendTx.py @@ -0,0 +1,93 @@ +import unittest + +from armoryengine.ALL import * + +from armoryengine.Networking import ArmoryClientFactory +from twisted.internet import reactor + +from test import Tiab + + +class Test: + def __init__(self): + self.success=False + + def txReturned(self, tx): + # a round trip was completed + # print "got something back" + #if protoObj.serialize() == tx: + self.factory1.stopTrying() + self.factory2.stopTrying() + reactor.stop() + self.success=True + + def timeout(self): + self.factory1.stopTrying() + self.factory2.stopTrying() + reactor.stop() + self.success=False + + + def run(self): + class T: + def port(self, p): + if p == 0: + return 19000 + else: + return 19010 + + #tiab = T() + tiab = Tiab.TiabSession() + # sends 10BTC from Charles's TIAB wallet to mwxN3Xw7P7kfkKY41KC99eD6cHtFYV9fun (also in that wallet) + tx = hex_to_binary("0100000001cce316f49284cb1e7d0c582064df8bb5dad960a3feeeca938cbb9fec7ba75694010000008b483045022100ad0dd567452c9d6d4b668e7847c360d1e866932a49f687927acfce86bf583d470220212028998cec632e41ea8d1bdd330da4e7c9b33f109bef9d9f8ae3fa85448aa80141043636a37759b535cc29ae611d770efdd7dc18830e0fc3a7a67851c5fe41737ae1a8ec90219aaea2684ec443344c16d385090359832d5eb71a6f07fb07bc06dcfdffffffff02f0c6c99d450000001976a9145d07242295d11e2fddb4535e7b0a5cdbea32db6888ac00ca9a3b000000001976a914b4503c9ef81c2d09f13a51ebd1d92e2368912b2d88ac00000000"); + + success=False + + def sendTx(protoObj): + # print "sent" + pytx = PyTx() + pytx.unserialize(tx) + protoObj.sendMessage(PayloadTx(pytx)) + + self.factory1 = ArmoryClientFactory(None, sendTx) + reactor.callWhenRunning( \ + reactor.connectTCP, '127.0.0.1', \ + tiab.port(0), self.factory1) + + self.factory2 = ArmoryClientFactory( None, func_inv=self.txReturned) + reactor.callWhenRunning( \ + reactor.connectTCP, '127.0.0.1', \ + tiab.port(0), self.factory2) + + + reactor.callLater(15, self.timeout) + reactor.run() + + tiab.clean() + + return self.success + + + +class TiabSendTxTest(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_sendtx(self): + self.assertTrue(Test().run()) + + +if not USE_TESTNET: + LOGERROR("Must be run with --testnet") + sys.exit(1) + +if __name__ == "__main__": + s = Test().run() + if not s: + print "Failed" + +# kate: indent-width 3; replace-tabs on; diff --git a/test/SplitSecretTest.py b/test/SplitSecretTest.py new file mode 100644 index 000000000..7c675644b --- /dev/null +++ b/test/SplitSecretTest.py @@ -0,0 +1,98 @@ +''' +Created on Aug 4, 2013 + +@author: Andy +''' +from random import shuffle +import sys +import unittest + +from armoryengine.ArmoryUtils import FiniteField, FiniteFieldError, SplitSecret, \ + hex_to_binary, RightNow, binary_to_hex, ReconstructSecret + + +sys.argv.append('--nologging') +sys.argv.append('--nologging') + +TEST_A = 200 +TEST_B = 100 +TEST_ADD_RESULT = 49 +TEST_SUB_RESULT = 100 +TEST_MULT_RESULT = 171 +TEST_DIV_RESULT = 2 +TEST_MTRX = [[1, 2, 3], [3,4,5], [6,7,8] ] +TEST_VECTER = [1, 2, 3] +TEST_3_BY_2_MTRX = [[1, 2, 3], [3,4,5]] +TEST_2_BY_3_MTRX = [[1, 2], [3,4], [5, 6]] +TEST_RMROW1CO1L_RESULT = [[1, 3], [6, 8]] +TEST_DET_RESULT = 0 +TEST_MULT_VECT_RESULT = [14, 26, 44] +TEST_MULT_VECT_RESULT2 = [5, 11, 17] +TEST_MULT_VECT_RESULT3 = [[7, 10], [15, 22], [23, 34]] +TEST_MULT_VECT_RESULT4 = [[248, 5, 249], [6, 241, 4], [248, 5, 249]] +TEST_MULT_VECT_RESULT5 = [[0, 0, 0], [0, 0, 0], [0, 0, 0]] + +class SplitSecretTest(unittest.TestCase): + + + def testFiniteFieldTest(self): + ff1 = FiniteField(1) + self.assertRaises(FiniteFieldError, FiniteField, 257) + + self.assertEqual(ff1.add(TEST_A, TEST_B), TEST_ADD_RESULT) + self.assertEqual(ff1.subtract(TEST_A, TEST_B), TEST_SUB_RESULT) + self.assertEqual(ff1.mult(TEST_A, TEST_B), TEST_MULT_RESULT) + self.assertEqual(ff1.divide(TEST_A, TEST_B), TEST_DIV_RESULT) + self.assertEqual(ff1.mtrxrmrowcol(TEST_MTRX, 1, 1), TEST_RMROW1CO1L_RESULT) + self.assertEqual(ff1.mtrxrmrowcol(TEST_3_BY_2_MTRX, 1, 1), []) + self.assertEqual(ff1.mtrxdet([[1]]), 1) + self.assertEqual(ff1.mtrxdet(TEST_3_BY_2_MTRX), -1) + self.assertEqual(ff1.mtrxdet(TEST_MTRX), TEST_DET_RESULT) + self.assertEqual(ff1.mtrxmultvect(TEST_MTRX, TEST_VECTER), TEST_MULT_VECT_RESULT) + self.assertEqual(ff1.mtrxmultvect(TEST_3_BY_2_MTRX, TEST_VECTER), TEST_MULT_VECT_RESULT[:2]) + self.assertEqual(ff1.mtrxmultvect(TEST_2_BY_3_MTRX, TEST_VECTER), TEST_MULT_VECT_RESULT2) + self.assertEqual(ff1.mtrxmult(TEST_2_BY_3_MTRX, TEST_3_BY_2_MTRX), TEST_MULT_VECT_RESULT3) + self.assertEqual(ff1.mtrxmult(TEST_2_BY_3_MTRX, TEST_2_BY_3_MTRX), TEST_MULT_VECT_RESULT3) + self.assertEqual(ff1.mtrxadjoint(TEST_MTRX), TEST_MULT_VECT_RESULT4) + self.assertEqual(ff1.mtrxinv(TEST_MTRX), TEST_MULT_VECT_RESULT5) + + def testSplitSecret(self): + self.callSplitSecret('9f', 2,3) + self.callSplitSecret('9f', 3,5) + self.callSplitSecret('9f', 4,7) + self.callSplitSecret('9f', 5,9) + self.callSplitSecret('9f', 6,7) + self.callSplitSecret('9f'*16, 3,5, 16) + self.callSplitSecret('9f'*16, 7,10, 16) + self.assertRaises(FiniteFieldError, SplitSecret, '9f'*16, 3, 5, 8) + self.assertRaises(FiniteFieldError, SplitSecret, '9f', 5,4) + self.assertRaises(FiniteFieldError, SplitSecret, '9f', 1,1) + + + def callSplitSecret(self, secretHex, M, N, nbytes=1): + secret = hex_to_binary(secretHex) + print '\nSplitting secret into %d-of-%d: secret=%s' % (M,N,secretHex) + tstart = RightNow() + out = SplitSecret(secret, M, N) + tsplit = RightNow() - tstart + print 'Fragments:' + for i in range(len(out)): + x = binary_to_hex(out[i][0]) + y = binary_to_hex(out[i][1]) + print ' Fragment %d: [%s, %s]' % (i+1,x,y) + trecon = 0 + print 'Reconstructing secret from various subsets of fragments...' + for i in range(10): + shuffle(out) + tstart = RightNow() + reconstruct = ReconstructSecret(out, M, nbytes) + trecon += RightNow() - tstart + print ' The reconstructed secret is:', binary_to_hex(reconstruct) + self.assertEqual(binary_to_hex(reconstruct), secretHex) + print 'Splitting secret took: %0.5f sec' % tsplit + print 'Reconstructing takes: %0.5f sec' % (trecon/10) + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() \ No newline at end of file diff --git a/test/Tiab.py b/test/Tiab.py new file mode 100644 index 000000000..de9e2ef9b --- /dev/null +++ b/test/Tiab.py @@ -0,0 +1,80 @@ + +import os +import tempfile +import shutil +import subprocess + +# runs a Test In a Box (TIAB) bitcoind session. By copying a prebuilt +# testnet with a known state +# Charles's recommendation is that you keep the TIAB somewhere like ~/.armory/tiab.charles +# and export that path in your .bashrc as ARMORY_TIAB_PATH +class TiabSession: + numInstances=0 + + # create a Test In a Box, initializing it and filling it with data + # the data comes from a path in the environment unless tiabdatadir is set + # tiab_repository is used to name which flavor of box is used if + # tiabdatadir is not used - It is intended to be used for when we + # have multiple testnets in a box with different properties + def __init__(self, tiab_repository="", tiabdatadir=None): + self.tiabdatadir = tiabdatadir + self.processes = [] + if not self.tiabdatadir: + self.tiabdatadir = os.environ['ARMORY_TIAB_PATH'+tiab_repository] + # an obvious race condition lives here + self.directory = tempfile.mkdtemp("armory_tiab") + + self.running = False + + self.restart() + + def __del__(self): + self.clean() + + # exit bitcoind and remove all data + def clean(self): + if not self.running: + return + TiabSession.numInstances -= 1 + try: + for x in self.processes: + x.kill() + for x in self.processes: + x.wait() + self.processes = [] + shutil.rmtree(self.directory) + except: + pass + self.running=False + + # returns the port the first bitcoind is running on + # In future versions of this class, multiple bitcoinds will get different ports, + # so therefor, you should call this function to get the port to connect to + def port(self, instanceNum): + instance = instanceNum + if instance==0: + return 19000 + elif instance==1: + return 19010 + else: + raise RuntimeError("No such instance number") + + # clean() and then start bitcoind again + def restart(self): + self.clean() + if TiabSession.numInstances != 0: + raise RuntimeError("Cannot have more than one Test-In-A-Box session simultaneously (yet)") + + TiabSession.numInstances += 1 + os.rmdir(self.directory) + shutil.copytree(self.tiabdatadir, self.directory) + try: + print "executing in datadir " + self.directory + self.processes.append( subprocess.Popen(["bitcoind", "-datadir=" + self.directory + "/1", "-debugnet", "-debug" ]) ) + self.processes.append( subprocess.Popen(["bitcoind", "-datadir=" + self.directory + "/2", "-debugnet", "-debug" ]) ) + except: + self.clean() + raise + self.running = True + +# kate: indent-width 3; replace-tabs on; diff --git a/test/TransactionTest.py b/test/TransactionTest.py new file mode 100644 index 000000000..3f76656ad --- /dev/null +++ b/test/TransactionTest.py @@ -0,0 +1,30 @@ +''' +Created on Jan 10, 2014 + +@author: Alan +''' +import sys +import unittest + +sys.path.append('..') +from armoryengine.ALL import * + + +# Unserialize an reserialize + +class TransactionTest(unittest.TestCase): + + + def setUp(self): + pass + + + def tearDown(self): + pass + + + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 000000000..ba838d1dd --- /dev/null +++ b/test/__init__.py @@ -0,0 +1,2 @@ +__all__ = ["ArmoryEngineUtilsTest", "BlockDataManagerThreadTest", "PyBtcAddressTest", \ + "SplitSecretTest", "PyTXTest", "PyBtcWalletTest"] \ No newline at end of file diff --git a/test/announcetester.py b/test/announcetester.py new file mode 100644 index 000000000..bad83a93c --- /dev/null +++ b/test/announcetester.py @@ -0,0 +1,103 @@ + +import os +import sys +import time +import unittest + +sys.argv.append('--nologging') +sys.argv.append('--testnet') + +from armoryengine.ALL import * +from announcefetch import AnnounceDataFetcher + + +forceTestURL = 'https://s3.amazonaws.com/bitcoinarmory-testing/testannounce.txt' +fetchDump = './fetchedFiles' + +class AnnouncementTester(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def testNoStart(self): + adf = AnnounceDataFetcher(forceTestURL, fetchDir=fetchDump) + adf.setFetchInterval(20) + + self.assertFalse(adf.isDisabled()) + self.assertFalse(adf.atLeastOneSuccess()) + + def testStart(self): + adf = AnnounceDataFetcher(forceTestURL, fetchDir=fetchDump) + adf.setFetchInterval(20) + + print 'STARTING', + print ' Running:', adf.isRunning(), + print ' OneSuccess:', adf.atLeastOneSuccess(), + print ' #Files',adf.numFiles() + + print 'Attempting to fetch before ADF is started' + d = adf.getAnnounceFile('notify') + print '*****' + print 'LENGTH OF NOTIFY FILE:', (len(d) if d else 0) + print '*****' + print 'Attempting to fetch before ADF is started (forced)' + d = adf.getAnnounceFile('notify', forceCheck=True) + print '*****' + print 'LENGTH OF NOTIFY FILE:', (len(d) if d else 0) + print '*****' + adf.start() + + t = 0 + try: + while True: + time.sleep(0.5) + t += 0.5 + + print ' Running:', adf.isRunning(), + print ' OneSuccess:', adf.atLeastOneSuccess(), + print ' #Files',adf.numFiles() + + if 1040: + adf.shutdown() + + if not adf.isRunning(): + print 'Attempting to fetch after shutdown' + d = adf.getAnnounceFile('notify') + print '*****' + print 'LENGTH OF NOTIFY FILE:', (len(d) if d else 0) + print '*****' + print 'Attempting to fetch after shutdown (forced)' + d = adf.getAnnounceFile('notify', forceCheck=True) + print '*****' + print 'LENGTH OF NOTIFY FILE:', (len(d) if d else 0) + print '*****' + break + + except KeyboardInterrupt: + print 'Exiting...' + + +if __name__ == "__main__": + del sys.argv[1:] + unittest.main() + + diff --git a/test/blkfiles1.txt b/test/blkfiles1.txt new file mode 100644 index 000000000..ed54d0457 --- /dev/null +++ b/test/blkfiles1.txt @@ -0,0 +1,8 @@ +1 0 100000000 100 +1 2238561 100000000 102 +1 4461701 100000000 106 +1 6821964 100000000 110 +1 9242336 100000000 118 +1 21193828 100000000 122 +1 50000000 100000000 174 +1 100000000 100000000 200 \ No newline at end of file diff --git a/test/blkfiles2.txt b/test/blkfiles2.txt new file mode 100644 index 000000000..898623cb0 --- /dev/null +++ b/test/blkfiles2.txt @@ -0,0 +1,16 @@ +1 0 100000000 100 +1 2238561 100000000 102 +1 4461701 100000000 106 +1 6821964 100000000 110 +1 9242336 100000000 118 +1 21193828 100000000 122 +1 50000000 100000000 174 +1 100000000 100000000 200 +2 285 100000000 200 +2 2238561 100000000 209 +2 4461701 100000000 216 +2 6821964 100000000 220 +2 9242336 100000000 228 +2 11938282 100000000 239 +2 16469598 100000000 260 +2 33300000 100000000 300 diff --git a/test/blkfiles3.txt b/test/blkfiles3.txt new file mode 100644 index 000000000..bbec5facf --- /dev/null +++ b/test/blkfiles3.txt @@ -0,0 +1,17 @@ +1 0 100000000 100 +1 2238561 100000000 102 +1 4461701 100000000 106 +1 6821964 100000000 110 +1 9242336 100000000 118 +1 21193828 100000000 122 +1 50000000 100000000 174 +1 100000000 100000000 200 +2 285 100000000 200 +2 2238561 100000000 209 +2 4461701 100000000 216 +2 6821964 100000000 220 +2 9242336 100000000 228 +2 11938282 100000000 239 +2 16469598 100000000 260 +2 33300000 100000000 300 +2 100000000 100000000 10000 diff --git a/test/parseAnnounceTest.py b/test/parseAnnounceTest.py new file mode 100644 index 000000000..8d84906f3 --- /dev/null +++ b/test/parseAnnounceTest.py @@ -0,0 +1,304 @@ +import os +import sys +import unittest + +sys.path.append('..') + +from armoryengine.parseAnnounce import * +from armoryengine.ArmoryUtils import * + +changelogTestText = """ +# This is a comment + + # Nothing to see here +#---------------------------------- + +Version 0.31 +Released January 27, 2014 + + - Major Feature 1 + This is a description of the first major feature. + + - Major Feature 2 + Description of + the second + big feature. + - Major Feature 3 + + Indentations might be + malformed + + +Version 0.30 + + # No release date on this one + - Major Feature 4 + Another multi-line + description + +# I debated whetehr to put this next feature in there... + # In the end I did + - Major Feature 5 + Description of the fifth big feature. + +Version 0.25 +Released April 21, 2013 + + # I realize these feature numbers don't make sense for decreasing + # version numbers + - Major Feature 6 + # Can we put comments + This feature requires + # in the middle of + # the descriptions? + interspersed comments + + - Major Feature 7 + + - Major Feature 8 +""" + + + +fullFeatureLists = \ +[ \ + [ '0.31', 'January 27, 2014', + [ \ + ['Major Feature 1', 'This is a description of the first major feature.'], + ['Major Feature 2', 'Description of the second big feature.'], + ['Major Feature 3', 'Indentations might be malformed'] \ + ] \ + ], + [ '0.30', '', + [ \ + ['Major Feature 4', 'Another multi-line description'], + ['Major Feature 5', 'Description of the fifth big feature.'] \ + ] \ + ], + [ '0.25', 'April 21, 2013', + [ \ + ['Major Feature 6', 'This feature requires interspersed comments'], + ['Major Feature 7', ''], + ['Major Feature 8', ''] \ + ] \ + ] \ +] + + + +downloadTestText = """ +-----BEGIN BITCOIN SIGNED MESSAGE----- + +# Armory for Windows +Armory 0.91 Windows XP 32 http://url/armory_0.91_xp32.exe 3afb9881c32 +Armory 0.91 Windows XP 64 http://url/armory_0.91_xp64.exe 8993ab127cf +Armory 0.91 Windows Vista,7,8 32,64 http://url/armory_0.91.exe 7f3b9964aa3 + + +# Various Ubuntu/Debian versions +Armory 0.91 Ubuntu 10.04,10.10 32 http://url/armory_10.04-32.deb 01339a9469b59a15bedab3b90f0a9c90ff2ff712ffe1b8d767dd03673be8477f +Armory 0.91 Ubuntu 12.10,13.04 32 http://url/armory_12.04-32.deb 5541af39c84 +Armory 0.91 Ubuntu 10.04,10.10 64 http://url/armory_10.04-64.deb 9af7613cab9 +Armory 0.91 Ubuntu 13.10 64 http://url/armory_13.10-64.deb 013fccb961a + +# Offline Bundles +ArmoryOffline 0.90 Ubuntu 10.04 32 http://url/offbundle-32-90.tar.gz 641382c93b9 +ArmoryOffline 0.90 Ubuntu 12.10 32 http://url/offbundle-64-90.tar.gz 5541af39c84 +ArmoryOffline 0.88 Ubuntu 10.04 32 http://url/offbundle-32-88.tar.gz 641382c93b9 +ArmoryOffline 0.88 Ubuntu 12.10 32 http://url/offbundle-64-88.tar.gz 5541af39c84 + +# Windows 32-bit Satoshi (Bitcoin-Qt/bitcoind) +Satoshi 0.9.0 Windows XP,Vista,7,8 32,64 http://btc.org/win0.9.0.exe 837f6cb4981314b323350353e1ffed736badb1c8c0db083da4e5dfc0dd47cdf1 +Satoshi 0.9.0 Ubuntu 10.04 32 http://btc.org/lin0.9.0.deb 2aa3f763c3b +Satoshi 0.9.0 Ubuntu 10.04 64 http://btc.org/lin0.9.0.deb 2aa3f763c3b + +-----BEGIN BITCOIN SIGNATURE----- + +HAZGhRr4U/utHgk9BZVOTqWcAodtHLuIq67TMSdThAiZwcfpdjnYZ6ZwmkUj0c3W +U0zy72vLLx9mpKJQdDmV7k0= +=i8i+ +-----END BITCOIN SIGNATURE----- + +""" + + + + +notifyTestText = """ +-----BEGIN BITCOIN SIGNED MESSAGE----- +Comment: Signed by Bitcoin Armory v0.90.99 + +# PRIORITY VALUES: +# Test announce: 1024 +# General Announcment: 2048 +# Important non-critical: 3072 +# Critical/security sens: 4096 +# Critical++: 5120 + +# + +UNIQUEID: 873fbc11 +VERSION: 0 +STARTTIME: 0 +EXPIRES: 1500111222 +CANCELID: [] +MINVERSION: 0.87.2 +MAXVERSION: 0.88.1 +PRIORITY: 4096 +ALERTTYPE: Security +NOTIFYSEND: False +NOTIFYRECV: True +SHORTDESCR: Until further notice, require 30 confirmations for incoming transactions. +LONGDESCR: + THIS IS A FAKE ALERT FOR TESTING PURPOSES: + + There is some turbulence on the network that may result in some transactions + being accidentally reversed up to 30 confirmations. A clever attacker may + be able to exploit this to scam you. For incoming transactions from + parties with no existing trust, please wait at least 30 confirmations before + considering the coins to be yours. + + By default, Armory will show you a checkmark when a transaction has 6 + confirmations. You can hover your mouse over the checkmark in the ledger + to display the actual number. 30 confirmations corresponds to approximately + 4-6 hours. + ***** + + +UNIQUEID: 113c948a +VERSION: 0 +STARTTIME: 0 +EXPIRES: 1700111222 +CANCELID: [] +MINVERSION: * +MAXVERSION: <0.91.99.8 +PRIORITY: 0 +ALERTTYPE: Upgrade +NOTIFYSEND: False +NOTIFYSEND: False +NOTIFYRECV: False +SHORTDESCR: New 0.92-beta testing version available. Please download 0.91.99.8 +LONGDESCR: + The new version fixes the following bugs: + - Bug A + - Bug B + - Bug C +***** +-----BEGIN BITCOIN SIGNATURE----- + +HAZGhRr4U/utHgk9BZVOTqWcAodtHLuIq67TMSdThAiZwcfpdjnYZ6ZwmkUj0c3W +U0zy72vLLx9mpKJQdDmV7k0= +=i8i+ +-----END BITCOIN SIGNATURE----- + +""" + + + + +class parseChangelogTest(unittest.TestCase): + + + def setUp(self): + self.clh = changelogParser(changelogTestText) + + def tearDown(self): + pass + + + # TODO: This test needs more verification of the results. + def testReadAll(self): + + expectedOutput = fullFeatureLists[:] + testOutput = self.clh.getChangelog() + + for test,expect in zip(testOutput, expectedOutput): + self.assertEqual( test[0], expect[0] ) + + for testFeat, expectFeat in zip(test[1], expect[1]): + self.assertEqual( testFeat, expectFeat ) + + + def testStopAt028(self): + + expectedOutput = fullFeatureLists[:] + testOutput = self.clh.getChangelog(getVersionInt([0,28,0,0])) + + for test,expect in zip(testOutput, expectedOutput): + self.assertEqual( test[0], expect[0] ) + + for testFeat, expectFeat in zip(test[1], expect[1]): + self.assertEqual( testFeat, expectFeat ) + + +################################################################################ +class parseDownloadTest(unittest.TestCase): + + def setUp(self): + self.dl = downloadLinkParser(filetext=downloadTestText) + + def tearDown(self): + pass + + + def testParseDL(self): + + dllink = self.dl.getDownloadLink('Armory','0.91','Windows','XP','32') + self.assertEqual(dllink, ['http://url/armory_0.91_xp32.exe', '3afb9881c32']) + + dllink = self.dl.getDownloadLink('Armory','0.91','Windows','Vista','32') + self.assertEqual(dllink, ['http://url/armory_0.91.exe', '7f3b9964aa3']) + + # This is a real file with a real hash, for testing DL in Armory + dllink = self.dl.getDownloadLink('Satoshi','0.9.0','Windows','7','64') + self.assertEqual(dllink, ['http://btc.org/win0.9.0.exe', + '837f6cb4981314b323350353e1ffed736badb1c8c0db083da4e5dfc0dd47cdf1']) + + dllink = self.dl.getDownloadLink('ArmoryOffline','0.88','Ubuntu','10.04','32') + self.assertEqual(dllink, ['http://url/offbundle-32-88.tar.gz', '641382c93b9']) + + dllink = self.dl.getDownloadLink('Armory','1.01','WIndows','10.04','32') + self.assertEqual(dllink, None) + + + +################################################################################ +class parseNotifyTest(unittest.TestCase): + + def setUp(self): + self.notify = notificationParser(filetext=notifyTestText) + + def tearDown(self): + pass + + def testParseNotify(self): + + notifyMap = self.notify.getNotificationMap() + + + self.assertEqual(notifyMap['873fbc11']['VERSION'], '0') + self.assertEqual(notifyMap['873fbc11']['CANCELID'], '[]') + self.assertEqual(notifyMap['873fbc11']['MAXVERSION'], '0.88.1') + self.assertTrue(notifyMap['873fbc11']['LONGDESCR'].strip().startswith('THIS IS A FAKE')) + + self.assertEqual(notifyMap['113c948a']['VERSION'], '0') + self.assertEqual(notifyMap['113c948a']['CANCELID'], '[]') + # This is a bogus assertion that must fail unless updated on every release: + # self.assertEqual(notifyMap['113c948a']['MAXVERSION'], '0.91.99.7') + self.assertTrue(notifyMap['113c948a']['LONGDESCR'].strip().startswith('The new version')) + + #for nid,fields in notifyMap.iteritems(): + #print 'Notification ID: ', nid + #for key,val in fields.iteritems(): + #print ' %s: %s' % (key.ljust(16), val) + + +if __name__ == "__main__": + + # This is just a fun way to look at the download data + #dl = downloadLinkParser(filetext=downloadTestText) + #dl.printDownloadMap() + + unittest.main() + + diff --git a/ui/TxFrames.py b/ui/TxFrames.py new file mode 100644 index 000000000..5948e1905 --- /dev/null +++ b/ui/TxFrames.py @@ -0,0 +1,1715 @@ +################################################################################ +# # +# Copyright (C) 2011-2014, Armory Technologies, Inc. # +# Distributed under the GNU Affero General Public License (AGPL v3) # +# See LICENSE or http://www.gnu.org/licenses/agpl.html # +# # +################################################################################ + +from PyQt4.Qt import * #@UnusedWildImport +from PyQt4.QtGui import * #@UnusedWildImport + +from armoryengine.BDM import TheBDM +from qtdefines import * #@UnusedWildImport +from armoryengine.Transaction import PyTxDistProposal +from armoryengine.CoinSelection import PySelectCoins, calcMinSuggestedFees,\ + PyUnspentTxOut +from ui.WalletFrames import SelectWalletFrame + + +class SendBitcoinsFrame(ArmoryFrame): + COLS = enum('LblAddr', 'Addr', 'AddrBook', 'LblWltID', 'LblAmt', 'Btc', \ + 'LblUnit', 'BtnMax', 'LblComm', 'Comm') + def __init__(self, parent, main, initLabel='', + wlt=None, prefill=None, wltIDList=None, + selectWltCallback = None, onlyOfflineWallets=False, + sendCallback = None, createUnsignedTxCallback = None): + super(SendBitcoinsFrame, self).__init__(parent, main) + self.maxHeight = tightSizeNChar(GETFONT('var'), 1)[1] + 8 + self.sourceAddrList = None + self.altBalance = None + self.wlt = wlt + self.wltID = wlt.uniqueIDB58 if wlt else None + self.wltIDList = wltIDList + self.selectWltCallback = selectWltCallback + self.sendCallback = sendCallback + self.createUnsignedTxCallback = createUnsignedTxCallback + self.onlyOfflineWallets = onlyOfflineWallets + txFee = self.main.getSettingOrSetDefault('Default_Fee', MIN_TX_FEE) + self.widgetTable = [] + self.scrollRecipArea = QScrollArea() + lblRecip = QRichLabel('Enter Recipients:') + lblRecip.setAlignment(Qt.AlignLeft | Qt.AlignBottom) + + self.freeOfErrors = True + + feetip = self.main.createToolTipWidget(\ + 'Transaction fees go to users who contribute computing power to ' + 'keep the Bitcoin network secure, and in return they get your transaction ' + 'included in the blockchain faster. Most transactions ' + 'do not require a fee but it is recommended anyway ' + 'since it guarantees quick processing and helps the network.') + + self.edtFeeAmt = QLineEdit() + self.edtFeeAmt.setFont(GETFONT('Fixed')) + self.edtFeeAmt.setMinimumWidth(tightSizeNChar(self.edtFeeAmt, 6)[0]) + self.edtFeeAmt.setMaximumWidth(tightSizeNChar(self.edtFeeAmt, 12)[0]) + self.edtFeeAmt.setMaximumHeight(self.maxHeight) + self.edtFeeAmt.setAlignment(Qt.AlignRight) + self.edtFeeAmt.setText(coin2str(txFee, maxZeros=1).strip()) + + # Created a standard wallet chooser frame. Pass the call back method + # for when the user selects a wallet. + coinControlCallback = self.coinControlUpdate if self.main.usermode == USERMODE.Expert else None + self.walletSelector = SelectWalletFrame(parent, main, VERTICAL, self.wltID, \ + wltIDList=self.wltIDList, selectWltCallback=self.setWallet, \ + coinControlCallback=coinControlCallback, onlyOfflineWallets=self.onlyOfflineWallets) + + componentList = [ QLabel('Fee:'), \ + self.edtFeeAmt, \ + feetip, \ + STRETCH] + # Only the Create Unsigned Transaction button if there is a callback for it. + # Otherwise the containing dialog or wizard will provide the offlien tx button + if self.createUnsignedTxCallback: + self.ttipUnsigned = self.main.createToolTipWidget(\ + 'Check this box to create an unsigned transaction to be signed and/or broadcast later.') + self.unsignedCheckbox = QCheckBox('Create Unsigned') + self.connect(self.unsignedCheckbox, SIGNAL(CLICKED), self.unsignedCheckBoxUpdate) + frmUnsigned = makeHorizFrame([self.unsignedCheckbox, self.ttipUnsigned]) + componentList.append(frmUnsigned) + + # Only add the Send Button if there's a callback for it + # Otherwise the containing dialog or wizard will provide the send button + if self.sendCallback: + self.btnSend = QPushButton('Send!') + self.connect(self.btnSend, SIGNAL(CLICKED), self.createTxAndBroadcast) + componentList.append(self.btnSend) + + txFrm = makeLayoutFrame(HORIZONTAL, componentList) + + btnEnterURI = QPushButton('Manually Enter "bitcoin:" Link') + ttipEnterURI = self.main.createToolTipWidget(\ + 'Armory does not always succeed at registering itself to handle ' + 'URL links from webpages and email. ' + 'Click this button to copy a link directly into Armory') + self.connect(btnEnterURI, SIGNAL("clicked()"), self.clickEnterURI) + fromFrameList = [self.walletSelector] + if not USE_TESTNET: + btnDonate = QPushButton("Donate to Armory Developers!") + ttipDonate = self.main.createToolTipWidget(\ + 'Making this software was a lot of work. You can give back ' + 'by adding a small donation to go to the Armory developers. ' + 'You will have the ability to change the donation amount ' + 'before finalizing the transaction.') + self.connect(btnDonate, SIGNAL("clicked()"), self.addDonation) + frmDonate = makeHorizFrame([btnDonate, ttipDonate]) + fromFrameList.append(frmDonate) + + if not self.main.usermode == USERMODE.Standard: + frmEnterURI = makeHorizFrame([btnEnterURI, ttipEnterURI]) + fromFrameList.append(frmEnterURI) + + ######################################################################## + # In Expert usermode, allow the user to modify source addresses + if self.main.usermode == USERMODE.Expert: + self.chkDefaultChangeAddr = QCheckBox('Use an existing address for change') + self.radioFeedback = QRadioButton('Send change to first input address') + self.radioSpecify = QRadioButton('Specify a change address') + self.lblChangeAddr = QRichLabel('Send Change To:') + self.edtChangeAddr = QLineEdit() + self.btnChangeAddr = createAddrBookButton(parent, self.edtChangeAddr, \ + None, 'Send change to') + self.chkRememberChng = QCheckBox('Remember for future transactions') + self.vertLine = VLINE() + + self.ttipSendChange = self.main.createToolTipWidget(\ + 'Most transactions end up with oversized inputs and Armory will send ' + 'the change to the next address in this wallet. You may change this ' + 'behavior by checking this box.') + self.ttipFeedback = self.main.createToolTipWidget(\ + 'Guarantees that no new addresses will be created to receive ' + 'change. This reduces anonymity, but is useful if you ' + 'created this wallet solely for managing imported addresses, ' + 'and want to keep all funds within existing addresses.') + self.ttipSpecify = self.main.createToolTipWidget(\ + 'You can specify any valid Bitcoin address for the change. ' + 'NOTE: If the address you specify is not in this wallet, ' + 'Armory will not be able to distinguish the outputs when it shows ' + 'up in your ledger. The change will look like a second recipient, ' + 'and the total debit to your wallet will be equal to the amount ' + 'you sent to the recipient plus the change.') + + + # Make sure that there can only be one selection + btngrp = QButtonGroup(self) + btngrp.addButton(self.radioFeedback) + btngrp.addButton(self.radioSpecify) + btngrp.setExclusive(True) + self.connect(self.chkDefaultChangeAddr, SIGNAL('toggled(bool)'), self.toggleChngAddr) + self.connect(self.radioSpecify, SIGNAL('toggled(bool)'), self.toggleSpecify) + frmChngLayout = QGridLayout() + i = 0; + frmChngLayout.addWidget(self.chkDefaultChangeAddr, i, 0, 1, 6) + frmChngLayout.addWidget(self.ttipSendChange, i, 6, 1, 2) + i += 1 + frmChngLayout.addWidget(self.radioFeedback, i, 1, 1, 5) + frmChngLayout.addWidget(self.ttipFeedback, i, 6, 1, 2) + i += 1 + frmChngLayout.addWidget(self.radioSpecify, i, 1, 1, 5) + frmChngLayout.addWidget(self.ttipSpecify, i, 6, 1, 2) + i += 1 + sendChangeToFrame = makeHorizFrame([self.lblChangeAddr, + self.edtChangeAddr, + self.btnChangeAddr]) + frmChngLayout.addWidget(sendChangeToFrame, i, 1, 1, 6) + i += 1 + frmChngLayout.addWidget(self.chkRememberChng, i, 1, 1, 7) + + frmChngLayout.addWidget(self.vertLine, 1, 0, i - 1, 1) + frmChngLayout.setColumnStretch(0,1) + frmChngLayout.setColumnStretch(1,1) + frmChngLayout.setColumnStretch(2,1) + frmChngLayout.setColumnStretch(3,1) + frmChngLayout.setColumnStretch(4,1) + frmChngLayout.setColumnStretch(5,1) + frmChngLayout.setColumnStretch(6,1) + frmChangeAddr = QFrame() + frmChangeAddr.setLayout(frmChngLayout) + frmChangeAddr.setFrameStyle(STYLE_SUNKEN) + fromFrameList.append(STRETCH) + fromFrameList.append(frmChangeAddr) + else: + fromFrameList.append(STRETCH) + frmBottomLeft = makeVertFrame(fromFrameList, STYLE_RAISED) + + lblSend = QRichLabel('Sending from Wallet:') + lblSend.setAlignment(Qt.AlignLeft | Qt.AlignBottom) + + + leftFrame = makeVertFrame([lblSend, frmBottomLeft]) + rightFrame = makeVertFrame([lblRecip, self.scrollRecipArea, txFrm]) + layout = QHBoxLayout() + layout.addWidget(leftFrame) + layout.addWidget(rightFrame) + self.setLayout(layout) + + self.makeRecipFrame(1) + self.setWindowTitle('Send Bitcoins') + self.setMinimumHeight(self.maxHeight * 20) + # self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + + loadCount = self.main.settings.get('Load_Count') + alreadyDonated = self.main.getSettingOrSetDefault('DonateAlready', False) + lastPestering = self.main.getSettingOrSetDefault('DonateLastPester', 0) + donateFreq = self.main.getSettingOrSetDefault('DonateFreq', 20) + dnaaDonate = self.main.getSettingOrSetDefault('DonateDNAA', False) + + + if prefill: + get = lambda s: prefill[s] if prefill.has_key(s) else '' + atype, addr160 = addrStr_to_hash160(get('address')) + amount = get('amount') + message = get('message') + label = get('label') + self.addOneRecipient(addr160, amount, message, label) + + elif not self.main == None and loadCount % donateFreq == (donateFreq - 1) and \ + not loadCount == lastPestering and not dnaaDonate: + result = MsgBoxWithDNAA(MSGBOX.Question, 'Please donate!', \ + 'Armory is the result of over 3,000 hours of development ' + 'and dozens of late nights bug-hunting and testing. Yet, this software ' + 'has been given to you for free to benefit the greater Bitcoin ' + 'community! ' + '

    However, continued development may not be possible without ' + 'donations. If you are satisfied with this software, please consider ' + 'donating what you think this software would be worth as a commercial ' + 'application.' + '

    Are you willing to donate to the Armory developers? If you ' + 'select "Yes," a donation field will be added to your ' + 'next transaction. You will have the opportunity to remove or change ' + 'the amount before sending the transaction.', None) + self.main.writeSetting('DonateLastPester', loadCount) + + if result[0] == True: + self.addDonation() + self.makeRecipFrame(2) + + if result[1] == True: + self.main.writeSetting('DonateDNAA', True) + + hexgeom = self.main.settings.get('SendBtcGeometry') + if len(hexgeom) > 0: + geom = QByteArray.fromHex(hexgeom) + self.restoreGeometry(geom) + + # Use this to fire wallet change after the constructor is complete. + # if it's called during construction then self's container may not exist yet. + def fireWalletChange(self): + # Set the wallet in the wallet selector and let all of display components + # react to it. This is at the end so that we can be sure that all of the + # components that react to setting the wallet exist. + self.walletSelector.updateOnWalletChange() + self.unsignedCheckBoxUpdate() + + + ############################################################################# + def unsignedCheckBoxUpdate(self): + if self.unsignedCheckbox.isChecked(): + self.btnSend.setText('Continue') + self.btnSend.setToolTip('Click to create an unsigned transaction!') + else: + self.btnSend.setText('Send!') + self.btnSend.setToolTip('Click to send bitcoins!') + + + ############################################################################# + def addOneRecipient(self, addr160, amt, msg, label=''): + if len(label) > 0: + self.wlt.setComment(addr160, label) + + COLS = self.COLS + lastIsEmpty = True + for col in (COLS.Addr, COLS.Btc, COLS.Comm): + if len(str(self.widgetTable[-1][col].text())) > 0: + lastIsEmpty = False + + if not lastIsEmpty: + self.makeRecipFrame(len(self.widgetTable) + 1) + + if amt: + amt = coin2str(amt, maxZeros=2).strip() + + self.widgetTable[-1][self.COLS.Addr].setText(hash160_to_addrStr(addr160)) + self.widgetTable[-1][self.COLS.Addr].setCursorPosition(0) + self.widgetTable[-1][self.COLS.Btc].setText(amt) + self.widgetTable[-1][self.COLS.Btc].setCursorPosition(0) + self.widgetTable[-1][self.COLS.Comm].setText(msg) + self.widgetTable[-1][self.COLS.Comm].setCursorPosition(0) + + ############################################################################# + # Now that the wallet can change in the context of the send dialog, this + # method is used as a callback for when the wallet changes + # isDoubleClick is unused - do not accept or close dialog on double click + def setWallet(self, wlt, isDoubleClick=False): + self.wlt = wlt + self.wltID = wlt.uniqueIDB58 if wlt else None + if not TheBDM.getBDMState() == 'BlockchainReady': + self.lblSummaryBal.setText('(available when online)', color='DisableFG') + if self.main.usermode == USERMODE.Expert: + # Pre-set values based on settings + chngBehave = self.main.getWltSetting(self.wltID, 'ChangeBehavior') + chngAddr = self.main.getWltSetting(self.wltID, 'ChangeAddr') + if chngBehave == 'Feedback': + self.chkDefaultChangeAddr.setChecked(True) + self.radioFeedback.setChecked(True) + self.radioSpecify.setChecked(False) + self.toggleChngAddr(True) + self.chkRememberChng.setChecked(True) + elif chngBehave == 'Specify': + self.chkDefaultChangeAddr.setChecked(True) + self.radioFeedback.setChecked(False) + self.radioSpecify.setChecked(True) + self.toggleChngAddr(True) + if checkAddrStrValid(chngAddr): + self.edtChangeAddr.setText(chngAddr) + self.edtChangeAddr.setCursorPosition(0) + self.chkRememberChng.setChecked(True) + else: + # Other option is "NewAddr" but in case there's an error, should run + # this branch by default + self.chkDefaultChangeAddr.setChecked(False) + self.radioFeedback.setChecked(False) + self.radioSpecify.setChecked(False) + self.toggleChngAddr(False) + + if (self.chkDefaultChangeAddr.isChecked() and \ + not self.radioFeedback.isChecked() and \ + not self.radioSpecify.isChecked()): + self.radioFeedback.setChecked(True) + # If there is a unsigned then we have a send button and unsigned checkbox to update + if self.createUnsignedTxCallback: + self.unsignedCheckbox.setChecked(wlt.watchingOnly) + self.unsignedCheckbox.setEnabled(not wlt.watchingOnly) + self.unsignedCheckBoxUpdate() + if self.selectWltCallback: + self.selectWltCallback(wlt) + + ############################################################################# + # Update the available source address list and balance based on results from + # coin control. This callback is now necessary because coin control was moved + # to the Select Wallet Frame + def coinControlUpdate(self, sourceAddrList, altBalance): + self.sourceAddrList = sourceAddrList + self.altBalance = altBalance + + ############################################################################# + def validateInputsGetTxDP(self): + COLS = self.COLS + self.freeOfErrors = True + scrAddrs = [] + addrList = [] + self.comments = [] + for i in range(len(self.widgetTable)): + # Verify validity of address strings + addrStr = str(self.widgetTable[i][COLS.Addr].text()).strip() + self.widgetTable[i][COLS.Addr].setText(addrStr) # overwrite w/ stripped + addrIsValid = True + addrList.append(addrStr) + try: + # The addrStr_to_scrAddr method fails if not reg Addr, or P2SH + scrAddrs.append(addrStr_to_scrAddr(addrStr)) + except: + addrIsValid = False + scrAddrs.append('') + self.freeOfErrors = False + self.updateAddrField(i, COLS.Addr, Colors.SlightRed) + + + numChkFail = sum([1 if len(b)==0 else 0 for b in scrAddrs]) + if not self.freeOfErrors: + QMessageBox.critical(self, tr('Invalid Address'), tr(""" + You have entered %d invalid @{address|addresses}@. + The @{error has|errors have}@ been highlighted on the + entry screen.""", numChkFail, numChkFail), QMessageBox.Ok) + + for i in range(len(self.widgetTable)): + try: + atype, a160 = addrStr_to_hash160(addrList[i]) + if atype == -1 or not atype in [ADDRBYTE,P2SHBYTE]: + net = 'Unknown Network' + if NETWORKS.has_key(addrList[i][0]): + net = NETWORKS[addrList[i][0]] + QMessageBox.warning(self, tr('Wrong Network!'), tr(""" + Address %d is for the wrong network! You are on the %s + and the address you supplied is for the the %s!""") % \ + (i+1, NETWORKS[ADDRBYTE], net), QMessageBox.Ok) + except: + pass + + return False + + # Construct recipValuePairs and check that all metrics check out + scraddrValuePairs = [] + totalSend = 0 + for i in range(len(self.widgetTable)): + try: + recipStr = str(self.widgetTable[i][COLS.Addr].text()).strip() + valueStr = str(self.widgetTable[i][COLS.Btc].text()).strip() + value = str2coin(valueStr, negAllowed=False) + if value == 0: + QMessageBox.critical(self, 'Zero Amount', \ + 'You cannot send 0 BTC to any recipients.
    Please enter ' + 'a positive amount for recipient %d.' % (i + 1), QMessageBox.Ok) + return False + + except NegativeValueError: + QMessageBox.critical(self, 'Negative Value', \ + 'You have specified a negative amount for recipient %d.
    Only ' + 'positive values are allowed!.' % (i + 1), QMessageBox.Ok) + return False + except TooMuchPrecisionError: + QMessageBox.critical(self, 'Too much precision', \ + 'Bitcoins can only be specified down to 8 decimal places. ' + 'The smallest value that can be sent is 0.0000 0001 BTC. ' + 'Please enter a new amount for recipient %d.' % (i + 1), QMessageBox.Ok) + return False + except ValueError: + QMessageBox.critical(self, 'Missing recipient amount', \ + 'You did not specify an amount to send!', QMessageBox.Ok) + return False + except: + QMessageBox.critical(self, 'Invalid Value String', \ + 'The amount you specified ' + 'to send to address %d is invalid (%s).' % (i + 1, valueStr), QMessageBox.Ok) + LOGERROR('Invalid amount specified: "%s"', valueStr) + return False + + totalSend += value + scraddr = addrStr_to_scrAddr(recipStr) + + scraddrValuePairs.append((scraddr, value)) + self.comments.append((str(self.widgetTable[i][COLS.Comm].text()), value)) + + try: + feeStr = str(self.edtFeeAmt.text()) + fee = str2coin(feeStr, negAllowed=False) + except NegativeValueError: + QMessageBox.critical(self, tr('Negative Value'), tr(""" + You must enter a positive value for the fee."""), QMessageBox.Ok) + return False + except TooMuchPrecisionError: + QMessageBox.critical(self, tr('Too much precision'), tr(""" + Bitcoins can only be specified down to 8 decimal places. + The smallest unit of a Bitcoin is 0.0000 0001 BTC. + Please enter a fee of at least 0.0000 0001"""), QMessageBox.Ok) + return False + except: + QMessageBox.critical(self, tr('Invalid Fee String'), tr(""" + The fee you specified is invalid. A standard fee is + 0.0001 BTC, though some transactions may succeed with + zero fee."""), QMessageBox.Ok) + LOGERROR(tr('Invalid fee specified: "%s"') % feeStr) + return False + + + bal = self.getUsableBalance() + if totalSend + fee > bal: + valTry = coin2str(totalSend + fee, maxZeros=2).strip() + valMax = coin2str(bal, maxZeros=2).strip() + if self.altBalance == None: + QMessageBox.critical(self, tr('Insufficient Funds'), tr(""" + You just tried to send %s BTC, including fee, but you only + have %s BTC (spendable) in this wallet!""") % \ + (valTry, valMax), QMessageBox.Ok) + else: + QMessageBox.critical(self, tr('Insufficient Funds'), tr(""" + You just tried to send %s BTC, including fee, but you only + have %s BTC with this coin control selection!""") % \ + (valTry, valMax), QMessageBox.Ok) + return False + + # Iteratively calculate the minimum fee by first trying the user selected + # fee then on each iteration set the feeTry to the minFee, and see if the + # new feeTry can cover the original amount plus the new minfee. This loop + # will rarely iterate. It will only iterate when there is enough dust in + # utxoList so that each fee increase causes enough dust to be used to + # increase the fee yet again. Also, for the loop to iterate, the + # totalSend + fee must be close to the bal, but not go over when the min + # fee is increased If it does go over, it will exit the loop on the + # last condition,and give the user an insufficient balance warning. + minFee = None + utxoSelect = [] + feeTry = fee + while minFee is None or (feeTry < minFee and totalSend + minFee <= bal): + if minFee: + feeTry = minFee + utxoList = self.getUsableTxOutList() + utxoSelect = PySelectCoins(utxoList, totalSend, feeTry) + minFee = calcMinSuggestedFees(utxoSelect, totalSend, feeTry, len(scraddrValuePairs))[1] + + + # We now have a min-fee that we know we can match if the user agrees + if fee < minFee: + + usrFeeStr = coin2strNZS(fee) + minFeeStr = coin2strNZS(minFee) + newBalStr = coin2strNZS(bal - minFee) + + if totalSend + minFee > bal: + # Need to adjust this based on overrideMin flag + self.edtFeeAmt.setText(coin2str(minFee, maxZeros=1).strip()) + QMessageBox.warning(self, tr('Insufficient Balance'), tr(""" + The required transaction fee causes this transaction to exceed + your balance. In order to send this transaction, you will be + required to pay a fee of %s BTC. +

    + Please go back and adjust the value of your transaction, not + to exceed a total of %s BTC (the necessary fee has + been entered into the form, so you can use the "MAX" button + to enter the remaining balance for a recipient).""") % \ + (minFeeStr, newBalStr), QMessageBox.Ok) + return + + reply = QMessageBox.warning(self, tr('Insufficient Fee'), tr(""" + The fee you have specified (%s BTC) is insufficient for the + size and priority of your transaction. You must include at + least %s BTC to send this transaction. +

    + Do you agree to the fee of %s BTC?""") % \ + (usrFeeStr, minFeeStr, minFeeStr), \ + QMessageBox.Yes | QMessageBox.Cancel) + + if reply == QMessageBox.Cancel: + return False + if reply == QMessageBox.No: + pass + elif reply == QMessageBox.Yes: + fee = long(minFee) + + + # Warn user of excessive fee specified + if fee > 100*MIN_RELAY_TX_FEE or (minFee > 0 and fee > 10*minFee): + reply = QMessageBox.warning(self, tr('Excessive Fee'), tr(""" + You have specified a fee of %s BTC which is much higher + than the minimum fee required for this transaction: %s BTC. + Are you absolutely sure that you want to send with this + fee? +

    + If you do not want this fee, click "No" and then change the fee + at the bottom of the "Send Bitcoins" window before trying + again.""") % (fee, minFee), QMessageBox.Yes | QMessageBox.No) + + if not reply==QMessageBox.Yes: + return False + + + if len(utxoSelect) == 0: + QMessageBox.critical(self, tr('Coin Selection Error'), tr(""" + There was an error constructing your transaction, due to a + quirk in the way Bitcoin transactions work. If you see this + error more than once, try sending your BTC in two or more + separate transactions."""), QMessageBox.Ok) + return False + + # ## IF we got here, everything is good to go... + # Just need to get a change address and then construct the tx + totalTxSelect = sum([u.getValue() for u in utxoSelect]) + totalChange = totalTxSelect - (totalSend + fee) + + self.origSVPairs = list(scraddrValuePairs) # copy + self.changeScrAddr = '' + self.selectedBehavior = '' + if totalChange > 0: + self.changeScrAddr = self.determineChangeAddr(utxoSelect) + LOGINFO('Change address behavior: %s', self.selectedBehavior) + if not self.changeScrAddr: + return False + scraddrValuePairs.append([self.changeScrAddr, totalChange]) + else: + if self.main.usermode == USERMODE.Expert and \ + self.chkDefaultChangeAddr.isChecked(): + self.selectedBehavior = NO_CHANGE + + changePair = None + if len(self.selectedBehavior) > 0: + changePair = (self.changeScrAddr, self.selectedBehavior) + + # Anonymize the outputs + random.shuffle(scraddrValuePairs) + + # Convert all scrAddrs to scripts for creation + recipPairs = [[scrAddr_to_script(s),v] for s,v in scraddrValuePairs] + + # Now create the unsigned TxDP + txdp = PyTxDistProposal().createFromTxOutSelection(utxoSelect, recipPairs) + + txValues = [totalSend, fee, totalChange] + if not self.unsignedCheckbox.isChecked(): + dlg = DlgConfirmSend(self.wlt, self.origSVPairs, txValues[1], self, \ + self.main, True, changePair) + + if not dlg.exec_(): + return False + + return txdp + + + def createTxAndBroadcast(self): + # The Send! button is clicked validate and broadcast tx + txdp = self.validateInputsGetTxDP() + if txdp: + if self.createUnsignedTxCallback and self.unsignedCheckbox.isChecked(): + self.createUnsignedTxCallback(txdp) + else: + try: + if self.wlt.isLocked: + Passphrase = None + + unlockdlg = DlgUnlockWallet(self.wlt, self, self.main, 'Send Transaction', returnPassphrase=True) + if unlockdlg.exec_(): + if unlockdlg.Accepted == 1: + Passphrase = unlockdlg.securePassphrase.copy() + unlockdlg.securePassphrase.destroy() + + if Passphrase is None or self.wlt.kdf is None: + QMessageBox.critical(self.parent(), 'Wallet is Locked', \ + 'Cannot sign transaction while your wallet is locked. ', \ + QMessageBox.Ok) + return + else: + self.wlt.kdfKey = self.wlt.kdf.DeriveKey(Passphrase) + Passphrase.destroy() + + self.wlt.mainWnd = self.main + self.wlt.parent = self + + commentStr = '' + if len(self.comments) == 1: + commentStr = self.comments[0][0] + else: + for i in range(len(self.comments)): + amt = self.comments[i][1] + if len(self.comments[i][0].strip()) > 0: + commentStr += '%s (%s); ' % (self.comments[i][0], coin2str_approx(amt).strip()) + + + tx = self.wlt.signTxDistProposal(txdp) + finalTx = tx.prepareFinalTx() + if len(commentStr) > 0: + self.wlt.setComment(finalTx.getHash(), commentStr) + self.main.broadcastTransaction(finalTx) + TheBDM.saveScrAddrHistories() + except: + LOGEXCEPT('Problem sending transaction!') + # TODO: not sure what errors to catch here, yet... + raise + if self.sendCallback: + self.sendCallback() + + ############################################################################# + def getUsableBalance(self): + if self.altBalance == None: + return self.wlt.getBalance('Spendable') + else: + return self.altBalance + + + ############################################################################# + def getUsableTxOutList(self): + if self.altBalance == None: + return list(self.wlt.getTxOutList('Spendable')) + else: + utxoList = [] + for a160 in self.sourceAddrList: + # Trying to avoid a swig bug involving iteration over vector<> types + utxos = self.wlt.getAddrTxOutList(a160) + for i in range(len(utxos)): + utxos[i].pprintOneLine(290000) + utxoList.append(PyUnspentTxOut().createFromCppUtxo(utxos[i])) + return utxoList + + + ############################################################################# + def determineChangeAddr(self, utxoList): + changeAddrStr = '' + changeAddr160 = '' + changeScrAddr = '' + self.selectedBehavior = 'NewAddr' + addrStr = '' + if not self.main.usermode == USERMODE.Expert: + changeAddrStr = self.wlt.getNextUnusedAddress().getAddrStr() + changeAddr160 = addrStr_to_hash160(changeAddrStr)[1] + changeScrAddr = addrStr_to_scrAddr(changeAddrStr) + self.wlt.setComment(changeAddr160, CHANGE_ADDR_DESCR_STRING) + else: + if not self.chkDefaultChangeAddr.isChecked(): + changeAddrStr = self.wlt.getNextUnusedAddress().getAddrStr() + changeAddr160 = addrStr_to_hash160(changeAddrStr)[1] + changeScrAddr = addrStr_to_scrAddr(changeAddrStr) + self.wlt.setComment(changeAddr160, CHANGE_ADDR_DESCR_STRING) + # If generate new address, remove previously-remembered behavior + self.main.setWltSetting(self.wltID, 'ChangeBehavior', self.selectedBehavior) + else: + if self.radioFeedback.isChecked(): + changeScrAddr = utxoList[0].getRecipientScrAddr() + self.selectedBehavior = 'Feedback' + elif self.radioSpecify.isChecked(): + addrStr = str(self.edtChangeAddr.text()).strip() + if not checkAddrStrValid(addrStr): + QMessageBox.warning(self, tr('Invalid Address'), tr(""" + You specified an invalid change address for this + transcation."""), QMessageBox.Ok) + return '', False + changeScrAddr = addrStr_to_scrAddr(addrStr) + if addrStr_to_hash160(addrStr)[0]==P2SHBYTE: + LOGWARN('P2SH address used in change output') + self.selectedBehavior = 'Specify' + + if self.main.usermode == USERMODE.Expert and self.chkRememberChng.isChecked(): + self.main.setWltSetting(self.wltID, 'ChangeBehavior', self.selectedBehavior) + if self.selectedBehavior == 'Specify' and len(addrStr) > 0: + self.main.setWltSetting(self.wltID, 'ChangeAddr', addrStr) + else: + self.main.setWltSetting(self.wltID, 'ChangeBehavior', 'NewAddr') + + return changeScrAddr + + ##################################################################### + def setMaximum(self, targWidget): + nRecip = len(self.widgetTable) + totalOther = 0 + r = 0 + try: + bal = self.getUsableBalance() + txFee = str2coin(str(self.edtFeeAmt.text())) + while r < nRecip: + # Use while loop so 'r' is still in scope in the except-clause + if targWidget == self.widgetTable[r][self.COLS.Btc]: + r += 1 + continue + + amtStr = str(self.widgetTable[r][self.COLS.Btc].text()).strip() + if len(amtStr) > 0: + totalOther += str2coin(amtStr) + r += 1 + + except: + QMessageBox.warning(self, 'Invalid Input', \ + 'Cannot compute the maximum amount ' + 'because there is an error in the amount ' + 'for recipient %d.' % (r + 1,), QMessageBox.Ok) + return + + + maxStr = coin2str((bal - (txFee + totalOther)), maxZeros=0) + if bal < txFee + totalOther: + QMessageBox.warning(self, 'Insufficient funds', \ + 'You have specified more than your spendable balance to ' + 'the other recipients and the transaction fee. Therefore, the ' + 'maximum amount for this recipient would actually be negative.', \ + QMessageBox.Ok) + return + + targWidget.setText(maxStr.strip()) + + + ##################################################################### + def createSetMaxButton(self, targWidget): + newBtn = QPushButton('MAX') + newBtn.setMaximumWidth(relaxedSizeStr(self, 'MAX')[0]) + newBtn.setToolTip('Fills in the maximum spendable amount minus ' + 'the amounts specified for other recipients ' + 'and the transaction fee ') + funcSetMax = lambda: self.setMaximum(targWidget) + self.connect(newBtn, SIGNAL(CLICKED), funcSetMax) + return newBtn + ##################################################################### + def makeRecipFrame(self, nRecip): + prevNRecip = len(self.widgetTable) + nRecip = max(nRecip, 1) + inputs = [] + for i in range(nRecip): + if i < prevNRecip and i < nRecip: + inputs.append([]) + for j in (self.COLS.Addr, self.COLS.Btc, self.COLS.Comm): + inputs[-1].append(str(self.widgetTable[i][j].text())) + + + frmRecip = QFrame() + frmRecip.setFrameStyle(QFrame.NoFrame) + frmRecipLayout = QVBoxLayout() + + COLS = self.COLS + + self.widgetTable = [] + for r in range(nRecip): + self.widgetTable.append([]) + + self.widgetTable[r].append(QLabel('Address %d:' % (r + 1,))) + + self.widgetTable[r].append(QLineEdit()) + self.widgetTable[r][-1].setMinimumWidth(relaxedSizeNChar(GETFONT('var'), 38)[0]) + self.widgetTable[r][-1].setMaximumHeight(self.maxHeight) + self.widgetTable[r][-1].setFont(GETFONT('var', 9)) + + # This is the hack of all hacks -- but I have no other way to make this work. + # For some reason, the references on variable r are carrying over between loops + # and all widgets are getting connected to the last one. The only way I could + # work around this was just ultra explicit garbage. I'll pay 0.1 BTC to anyone + # who figures out why my original code was failing... + # idx = r+0 + # chgColor = lambda x: self.updateAddrField(idx, COLS.Addr, QColor(255,255,255)) + # self.connect(self.widgetTable[idx][-1], SIGNAL('textChanged(QString)'), chgColor) + if r == 0: + chgColor = lambda x: self.updateAddrField(0, COLS.Addr, QColor(255, 255, 255)) + self.connect(self.widgetTable[0][-1], SIGNAL('textChanged(QString)'), chgColor) + elif r == 1: + chgColor = lambda x: self.updateAddrField(1, COLS.Addr, QColor(255, 255, 255)) + self.connect(self.widgetTable[1][-1], SIGNAL('textChanged(QString)'), chgColor) + elif r == 2: + chgColor = lambda x: self.updateAddrField(2, COLS.Addr, QColor(255, 255, 255)) + self.connect(self.widgetTable[2][-1], SIGNAL('textChanged(QString)'), chgColor) + elif r == 3: + chgColor = lambda x: self.updateAddrField(3, COLS.Addr, QColor(255, 255, 255)) + self.connect(self.widgetTable[3][-1], SIGNAL('textChanged(QString)'), chgColor) + elif r == 4: + chgColor = lambda x: self.updateAddrField(4, COLS.Addr, QColor(255, 255, 255)) + self.connect(self.widgetTable[4][-1], SIGNAL('textChanged(QString)'), chgColor) + + + addrEntryBox = self.widgetTable[r][-1] + self.widgetTable[r].append(createAddrBookButton(self.parent(), addrEntryBox, \ + None, 'Send to')) + + + self.widgetTable[r].append(QRichLabel('')) + self.widgetTable[r][-1].setVisible(False) + + + self.widgetTable[r].append(QLabel('Amount:')) + + self.widgetTable[r].append(QLineEdit()) + self.widgetTable[r][-1].setFont(GETFONT('Fixed')) + self.widgetTable[r][-1].setMinimumWidth(tightSizeNChar(GETFONT('Fixed'), 14)[0]) + self.widgetTable[r][-1].setMaximumHeight(self.maxHeight) + self.widgetTable[r][-1].setAlignment(Qt.AlignLeft) + + self.widgetTable[r].append(QLabel('BTC')) + self.widgetTable[r][-1].setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + + # self.widgetTable[r].append( QPushButton('MAX') ) + # self.widgetTable[r][-1].setMaximumWidth( relaxedSizeStr(self, 'MAX')[0]) + # self.widgetTable[r][-1].setToolTip( \ + # 'Fills in the maximum spendable amount minus amounts ' + # 'specified for other recipients and the transaction fee ') + # self.connect(self.widgetTable[r][-1], SIGNAL(CLICKED), setMaxFunc) + self.widgetTable[r].append(self.createSetMaxButton(self.widgetTable[r][COLS.Btc])) + + self.widgetTable[r].append(QLabel('Comment:')) + self.widgetTable[r].append(QLineEdit()) + self.widgetTable[r][-1].setFont(GETFONT('var', 9)) + self.widgetTable[r][-1].setMaximumHeight(self.maxHeight) + + if r < nRecip and r < prevNRecip: + self.widgetTable[r][COLS.Addr].setText(inputs[r][0]) + self.widgetTable[r][COLS.Btc ].setText(inputs[r][1]) + self.widgetTable[r][COLS.Comm].setText(inputs[r][2]) + + subfrm = QFrame() + subfrm.setFrameStyle(STYLE_RAISED) + subLayout = QGridLayout() + subLayout.addWidget(self.widgetTable[r][COLS.LblAddr], 0, 0, 1, 1) + subLayout.addWidget(self.widgetTable[r][COLS.Addr], 0, 1, 1, 5) + subLayout.addWidget(self.widgetTable[r][COLS.AddrBook], 0, 6, 1, 1) + + subLayout.addWidget(self.widgetTable[r][COLS.LblWltID], 1, 1, 1, 5) + + subLayout.addWidget(self.widgetTable[r][COLS.LblAmt], 2, 0, 1, 1) + subLayout.addWidget(self.widgetTable[r][COLS.Btc], 2, 1, 1, 2) + subLayout.addWidget(self.widgetTable[r][COLS.LblUnit], 2, 3, 1, 1) + subLayout.addWidget(self.widgetTable[r][COLS.BtnMax], 2, 4, 1, 1) + subLayout.addWidget(QLabel(''), 2, 5, 1, 2) + + subLayout.addWidget(self.widgetTable[r][COLS.LblComm], 3, 0, 1, 1) + subLayout.addWidget(self.widgetTable[r][COLS.Comm], 3, 1, 1, 6) + subLayout.setContentsMargins(15, 15, 15, 15) + subLayout.setSpacing(3) + subfrm.setLayout(subLayout) + + frmRecipLayout.addWidget(subfrm) + + + btnFrm = QFrame() + btnFrm.setFrameStyle(QFrame.NoFrame) + btnLayout = QHBoxLayout() + lbtnAddRecip = QLabelButton('+ Recipient') + lbtnAddRecip.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) + lbtnRmRecip = QLabelButton('- Recipient') + lbtnRmRecip.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) + self.connect(lbtnAddRecip, SIGNAL(CLICKED), lambda: self.makeRecipFrame(nRecip + 1)) + self.connect(lbtnRmRecip, SIGNAL(CLICKED), lambda: self.makeRecipFrame(nRecip - 1)) + btnLayout.addStretch() + btnLayout.addWidget(lbtnAddRecip) + btnLayout.addWidget(lbtnRmRecip) + btnFrm.setLayout(btnLayout) + + # widgetsForWidth = [COLS.LblAddr, COLS.Addr, COLS.LblAmt, COLS.Btc] + # minScrollWidth = sum([self.widgetTable[0][col].width() for col in widgetsForWidth]) + + frmRecipLayout.addWidget(btnFrm) + frmRecipLayout.addStretch() + frmRecip.setLayout(frmRecipLayout) + # return frmRecip + self.scrollRecipArea.setWidget(frmRecip) + + + ############################################################################# + def addDonation(self, amt=DONATION): + COLS = self.COLS + lastIsEmpty = True + for col in (COLS.Addr, COLS.Btc, COLS.Comm): + if len(str(self.widgetTable[-1][col].text())) > 0: + lastIsEmpty = False + + if not lastIsEmpty: + self.makeRecipFrame(len(self.widgetTable) + 1) + + self.widgetTable[-1][self.COLS.Addr].setText(ARMORY_DONATION_ADDR) + self.widgetTable[-1][self.COLS.Btc].setText(coin2str(amt, maxZeros=2).strip()) + self.widgetTable[-1][self.COLS.Comm].setText(\ + 'Donation to Armory developers. Thank you for your generosity!') + + ############################################################################# + def clickEnterURI(self): + dlg = DlgUriCopyAndPaste(self.parent(), self.main) + dlg.exec_() + + if len(dlg.uriDict) > 0: + COLS = self.COLS + lastIsEmpty = True + for col in (COLS.Addr, COLS.Btc, COLS.Comm): + if len(str(self.widgetTable[-1][col].text())) > 0: + lastIsEmpty = False + + if not lastIsEmpty: + self.makeRecipFrame(len(self.widgetTable) + 1) + + self.widgetTable[-1][self.COLS.Addr].setText(dlg.uriDict['address']) + if dlg.uriDict.has_key('amount'): + amtStr = coin2str(dlg.uriDict['amount'], maxZeros=1).strip() + self.widgetTable[-1][self.COLS.Btc].setText(amtStr) + + + haveLbl = dlg.uriDict.has_key('label') + haveMsg = dlg.uriDict.has_key('message') + + dispComment = '' + if haveLbl and haveMsg: + dispComment = dlg.uriDict['label'] + ': ' + dlg.uriDict['message'] + elif not haveLbl and haveMsg: + dispComment = dlg.uriDict['message'] + elif haveLbl and not haveMsg: + dispComment = dlg.uriDict['label'] + + self.widgetTable[-1][self.COLS.Comm].setText(dispComment) + + + ############################################################################# + def toggleSpecify(self, b): + self.lblChangeAddr.setVisible(b) + self.edtChangeAddr.setVisible(b) + self.btnChangeAddr.setVisible(b) + + ############################################################################# + def toggleChngAddr(self, b): + self.radioFeedback.setVisible(b) + self.radioSpecify.setVisible(b) + self.ttipFeedback.setVisible(b) + self.ttipSpecify.setVisible(b) + self.chkRememberChng.setVisible(b) + self.vertLine.setVisible(b) + if not self.radioFeedback.isChecked() and not self.radioSpecify.isChecked(): + self.radioFeedback.setChecked(True) + self.toggleSpecify(b and self.radioSpecify.isChecked()) + + + ############################################################################# + def updateAddrField(self, idx, col, color): + palette = QPalette() + palette.setColor(QPalette.Base, color) + self.widgetTable[idx][col].setPalette(palette); + self.widgetTable[idx][col].setAutoFillBackground(True); + try: + addrtext = str(self.widgetTable[idx][self.COLS.Addr].text()) + wid = self.main.getWalletForAddr160(addrStr_to_hash160(addrtext)[1]) + if wid: + wlt = self.main.walletMap[wid] + dispStr = '%s (%s)' % (wlt.labelName, wlt.uniqueIDB58) + self.widgetTable[idx][self.COLS.LblWltID].setVisible(True) + self.widgetTable[idx][self.COLS.LblWltID].setText(dispStr, color='TextBlue') + else: + self.widgetTable[idx][self.COLS.LblWltID].setVisible(False) + except: + self.widgetTable[idx][self.COLS.LblWltID].setVisible(False) + + +class ReviewOfflineTxFrame(ArmoryDialog): + def __init__(self, parent=None, main=None, initLabel=''): + super(ReviewOfflineTxFrame, self).__init__(parent, main) + + self.txdp = None + self.wlt = None + self.lblDescr = QRichLabel('') + + ttipDataIsSafe = self.main.createToolTipWidget(\ + 'There is no security-sensitive information in this data below, so ' + 'it is perfectly safe to copy-and-paste it into an ' + 'email message, or save it to a borrowed USB key.') + + btnSave = QPushButton('Save as file...') + self.connect(btnSave, SIGNAL(CLICKED), self.doSaveFile) + ttipSave = self.main.createToolTipWidget(\ + 'Save this data to a USB key or other device, to be transferred to ' + 'a computer that contains the private keys for this wallet.') + + btnCopy = QPushButton('Copy to clipboard') + self.connect(btnCopy, SIGNAL(CLICKED), self.copyAsciiTxDP) + self.lblCopied = QRichLabel(' ') + self.lblCopied.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) + + ttipCopy = self.main.createToolTipWidget(\ + 'Copy the transaction data to the clipboard, so that it can be ' + 'pasted into an email or a text document.') + + lblInstruct = QRichLabel('Instructions for completing this transaction:') + self.lblUTX = QRichLabel('') + + frmUTX = makeLayoutFrame(HORIZONTAL, [ttipDataIsSafe, self.lblUTX]) + frmUpper = makeLayoutFrame(HORIZONTAL, [self.lblDescr], STYLE_SUNKEN) + + # Wow, I just cannot get the txtEdits to be the right size without + # forcing them very explicitly + w, h = tightSizeStr(GETFONT('Fixed', 8), '0' * 93)[0], int(12 * 8.2) + self.txtTxDP = QTextEdit() + self.txtTxDP.setFont(GETFONT('Fixed', 8)) + self.txtTxDP.setMinimumWidth(w) + self.txtTxDP.setMinimumHeight(h) + self.txtTxDP.setReadOnly(True) + + + + frmLower = QFrame() + frmLower.setFrameStyle(STYLE_RAISED) + frmLowerLayout = QGridLayout() + + frmLowerLayout.addWidget(frmUTX, 0, 0, 1, 3) + frmLowerLayout.addWidget(self.txtTxDP, 1, 0, 3, 1) + frmLowerLayout.addWidget(btnSave, 1, 1, 1, 1) + frmLowerLayout.addWidget(ttipSave, 1, 2, 1, 1) + frmLowerLayout.addWidget(btnCopy, 2, 1, 1, 1) + frmLowerLayout.addWidget(ttipCopy, 2, 2, 1, 1) + frmLowerLayout.addWidget(self.lblCopied, 3, 1, 1, 2) + frmLowerLayout.setColumnStretch(0, 1) + frmLowerLayout.setColumnStretch(1, 0) + frmLowerLayout.setColumnStretch(2, 0) + frmLowerLayout.setColumnStretch(3, 0) + frmLowerLayout.setRowStretch(0, 0) + frmLowerLayout.setRowStretch(1, 1) + frmLowerLayout.setRowStretch(2, 1) + frmLowerLayout.setRowStretch(3, 1) + + frmLower.setLayout(frmLowerLayout) + + + frmAll = makeLayoutFrame(VERTICAL, [lblInstruct, \ + frmUpper, \ + 'Space(5)', \ + frmLower]) + frmAll.layout().setStretch(0, 0) + frmAll.layout().setStretch(1, 0) + frmAll.layout().setStretch(2, 0) + frmAll.layout().setStretch(3, 2) + frmAll.layout().setStretch(4, 1) + frmAll.layout().setStretch(5, 0) + + dlgLayout = QVBoxLayout() + dlgLayout.addWidget(frmAll) + + self.setLayout(dlgLayout) + + def setTxDp(self, txdp): + self.txdp = txdp + self.lblUTX.setText('Transaction Data \t (Unsigned ID: %s)' % txdp.uniqueB58) + self.txtTxDP.setText(txdp.serializeAscii()) + + def setWallet(self, wlt): + self.wlt = wlt + if determineWalletType(wlt, self.main)[0] in \ + [ WLTTYPES.Offline, WLTTYPES.WatchOnly ]: + self.lblDescr.setText(tr(""" + The block of data shown below is the complete transaction you + just requested, but is invalid because it does not contain any + signatures. You must take this data to the computer with the + full wallet to get it signed, then bring it back here to be + broadcast to the Bitcoin network. +

    + Use "Save as file..." to save an *.unsigned.tx + file to USB drive or other removable media. + On the offline computer, click "Offline Transactions" on the main + window. Load the transaction, review it, then sign it + (the filename now end with *.signed.tx). Click "Continue" + below when you have the signed transaction on this computer. +

    + NOTE: The USB drive only ever holds public transaction + data that will be broadcast to the network. This data may be + considered privacy-sensitive, but does not compromise + the security of your wallet.""")) + else: + self.lblDescr.setText(tr(""" + You have chosen to create the previous transaction but not sign + it or broadcast it, yet. You can save the unsigned + transaction to file, or copy&paste from the text box. + You can use the following window (after clicking "Continue") to + sign and broadcast the transaction when you are ready""")) + + + def copyAsciiTxDP(self): + clipb = QApplication.clipboard() + clipb.clear() + clipb.setText(self.txtTxDP.toPlainText()) + self.lblCopied.setText('Copied!') + + def doSaveFile(self): + """ Save the Unsigned-Tx block of data """ + dpid = self.txdp.uniqueB58 + suffix = ('' if OS_WINDOWS else '.unsigned.tx') + toSave = self.main.getFileSave(\ + 'Save Unsigned Transaction', \ + ['Armory Transactions (*.unsigned.tx)'], \ + 'armory_%s_%s' % (dpid, suffix)) + # In windows, we get all these superfluous file suffixes + toSave = toSave.replace('unsigned.tx.unsigned.tx', 'unsigned.tx') + toSave = toSave.replace('unsigned.tx.unsigned.tx', 'unsigned.tx') + LOGINFO('Saving unsigned tx file: %s', toSave) + try: + theFile = open(toSave, 'w') + theFile.write(self.txtTxDP.toPlainText()) + theFile.close() + except IOError: + LOGEXCEPT('Failed to save file: %s', toSave) + pass + +################################################################################ +class SignBroadcastOfflineTxFrame(ArmoryFrame): + """ + We will make the assumption that this Frame is used ONLY for outgoing + transactions from your wallet. This simplifies the logic if we don't + have to identify input senders/values, and handle the cases where those + may not be specified + """ + def __init__(self, parent=None, main=None, initLabel=''): + super(SignBroadcastOfflineTxFrame, self).__init__(parent, main) + + self.wlt = None + self.sentToSelfWarn = False + self.fileLoaded = None + + lblDescr = QRichLabel(\ + 'Copy or load a transaction from file into the text box below. ' + 'If the transaction is unsigned and you have the correct wallet, ' + 'you will have the opportunity to sign it. If it is already signed ' + 'you will have the opportunity to broadcast it to ' + 'the Bitcoin network to make it final.') + + self.txtTxDP = QTextEdit() + self.txtTxDP.setFont(GETFONT('Fixed', 8)) + w, h = relaxedSizeNChar(self.txtTxDP, 80)[0], int(12 * 8.2) + self.txtTxDP.sizeHint = lambda: QSize(w, h) + self.txtTxDP.setMinimumWidth(w) + self.txtTxDP.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + self.btnSign = QPushButton('Sign') + self.btnBroadcast = QPushButton('Broadcast') + self.btnSave = QPushButton('Save file...') + self.btnLoad = QPushButton('Load file...') + self.btnCopy = QPushButton('Copy Text') + self.btnCopyHex = QPushButton('Copy Final Tx (Hex)') + self.lblCopied = QRichLabel('') + self.lblCopied.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) + + self.btnSign.setEnabled(False) + self.btnBroadcast.setEnabled(False) + + self.connect(self.txtTxDP, SIGNAL('textChanged()'), self.processTxDP) + + + self.connect(self.btnSign, SIGNAL(CLICKED), self.signTx) + self.connect(self.btnBroadcast, SIGNAL(CLICKED), self.broadTx) + self.connect(self.btnSave, SIGNAL(CLICKED), self.saveTx) + self.connect(self.btnLoad, SIGNAL(CLICKED), self.loadTx) + self.connect(self.btnCopy, SIGNAL(CLICKED), self.copyTx) + self.connect(self.btnCopyHex, SIGNAL(CLICKED), self.copyTxHex) + + self.lblStatus = QRichLabel('') + self.lblStatus.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) + wStat, hStat = relaxedSizeStr(self.lblStatus, 'Signature is Invalid!') + self.lblStatus.setMinimumWidth(int(wStat * 1.2)) + self.lblStatus.setMinimumHeight(int(hStat * 1.2)) + + + frmDescr = makeLayoutFrame(HORIZONTAL, [lblDescr], STYLE_RAISED) + + self.infoLbls = [] + + # ## + self.infoLbls.append([]) + self.infoLbls[-1].append(self.main.createToolTipWidget(\ + 'This is wallet from which the offline transaction spends bitcoins')) + self.infoLbls[-1].append(QRichLabel('Wallet:')) + self.infoLbls[-1].append(QRichLabel('')) + + # ## + self.infoLbls.append([]) + self.infoLbls[-1].append(self.main.createToolTipWidget('The name of the wallet')) + self.infoLbls[-1].append(QRichLabel('Wallet Label:')) + self.infoLbls[-1].append(QRichLabel('')) + + # ## + self.infoLbls.append([]) + self.infoLbls[-1].append(self.main.createToolTipWidget(\ + 'A unique string that identifies an unsigned transaction. ' + 'This is different than the ID that the transaction will have when ' + 'it is finally broadcast, because the broadcast ID cannot be ' + 'calculated without all the signatures')) + self.infoLbls[-1].append(QRichLabel('Pre-Broadcast ID:')) + self.infoLbls[-1].append(QRichLabel('')) + + # ## + self.infoLbls.append([]) + self.infoLbls[-1].append(self.main.createToolTipWidget(\ + 'Net effect on this wallet\'s balance')) + self.infoLbls[-1].append(QRichLabel('Transaction Amount:')) + self.infoLbls[-1].append(QRichLabel('')) + + self.moreInfo = QLabelButton('Click here for more
    information about
    this transaction') + self.connect(self.moreInfo, SIGNAL(CLICKED), self.execMoreTxInfo) + frmMoreInfo = makeLayoutFrame(HORIZONTAL, [self.moreInfo], STYLE_SUNKEN) + frmMoreInfo.setMinimumHeight(tightSizeStr(self.moreInfo, 'Any String')[1] * 5) + + expert = (self.main.usermode == USERMODE.Expert) + frmBtn = makeLayoutFrame(VERTICAL, [ self.btnSign, \ + self.btnBroadcast, \ + self.btnSave, \ + self.btnLoad, \ + self.btnCopy, \ + self.btnCopyHex if expert else QRichLabel(''), \ + self.lblCopied, \ + HLINE(), \ + self.lblStatus, \ + HLINE(), \ + STRETCH, \ + frmMoreInfo]) + + frmBtn.setMaximumWidth(tightSizeNChar(QPushButton(''), 30)[0]) + + frmInfoLayout = QGridLayout() + for r in range(len(self.infoLbls)): + for c in range(len(self.infoLbls[r])): + frmInfoLayout.addWidget(self.infoLbls[r][c], r, c, 1, 1) + + frmInfo = QFrame() + frmInfo.setFrameStyle(STYLE_SUNKEN) + frmInfo.setLayout(frmInfoLayout) + + frmBottom = QFrame() + frmBottom.setFrameStyle(STYLE_SUNKEN) + frmBottomLayout = QGridLayout() + frmBottomLayout.addWidget(self.txtTxDP, 0, 0, 1, 1) + frmBottomLayout.addWidget(frmBtn, 0, 1, 2, 1) + frmBottomLayout.addWidget(frmInfo, 1, 0, 1, 1) + # frmBottomLayout.addWidget(frmMoreInfo, 1,1, 1,1) + frmBottom.setLayout(frmBottomLayout) + + layout = QVBoxLayout() + layout.addWidget(frmDescr) + layout.addWidget(frmBottom) + + self.setLayout(layout) + self.processTxDP() + + def processTxDP(self): + # TODO: it wouldn't be TOO hard to modify this dialog to take + # arbitrary hex-serialized transactions for broadcast... + # but it's not trivial either (for instance, I assume + # that we have inputs values, etc) + self.wlt = None + self.leValue = None + self.txdpObj = None + self.idxSelf = [] + self.idxOther = [] + self.lblStatus.setText('') + self.lblCopied.setText('') + self.enoughSigs = False + self.sigsValid = False + self.txdpReadable = False + + txdpStr = str(self.txtTxDP.toPlainText()) + try: + self.txdpObj = PyTxDistProposal().unserializeAscii(txdpStr) + self.enoughSigs = self.txdpObj.checkTxHasEnoughSignatures() + self.sigsValid = self.txdpObj.checkTxHasEnoughSignatures(alsoVerify=True) + self.txdpReadable = True + except BadAddressError: + QMessageBox.critical(self, 'Inconsistent Data!', \ + 'This transaction contains inconsistent information. This ' + 'is probably not your fault...', QMessageBox.Ok) + self.txdpObj = None + self.txdpReadable = False + except NetworkIDError: + QMessageBox.critical(self, 'Wrong Network!', \ + 'This transaction is actually for a different network! ' + 'Did you load the correct transaction?', QMessageBox.Ok) + self.txdpObj = None + self.txdpReadable = False + except (UnserializeError, IndexError, ValueError): + self.txdpObj = None + self.txdpReadable = False + + if not self.enoughSigs or not self.sigsValid or not self.txdpReadable: + self.btnBroadcast.setEnabled(False) + else: + if self.main.netMode == NETWORKMODE.Full: + self.btnBroadcast.setEnabled(True) + else: + self.btnBroadcast.setEnabled(False) + self.btnBroadcast.setToolTip('No connection to Bitcoin network!') + + self.btnSave.setEnabled(True) + self.btnCopyHex.setEnabled(False) + if not self.txdpReadable: + if len(txdpStr) > 0: + self.lblStatus.setText('Unrecognized!') + else: + self.lblStatus.setText('') + self.btnSign.setEnabled(False) + self.btnBroadcast.setEnabled(False) + self.btnSave.setEnabled(False) + self.makeReviewFrame() + return + elif not self.enoughSigs: + if not self.main.getSettingOrSetDefault('DNAA_ReviewOfflineTx', False): + result = MsgBoxWithDNAA(MSGBOX.Warning, title='Offline Warning', \ + msg='Please review your transaction carefully before ' + 'signing and broadcasting it! The extra security of ' + 'using offline wallets is lost if you do ' + 'not confirm the transaction is correct!', dnaaMsg=None) + self.main.writeSetting('DNAA_ReviewOfflineTx', result[1]) + self.lblStatus.setText('Unsigned') + self.btnSign.setEnabled(True) + self.btnBroadcast.setEnabled(False) + elif not self.sigsValid: + self.lblStatus.setText('Bad Signature!') + self.btnSign.setEnabled(True) + self.btnBroadcast.setEnabled(False) + else: + self.lblStatus.setText('All Signatures Valid!') + self.btnSign.setEnabled(False) + self.btnCopyHex.setEnabled(True) + + + # NOTE: We assume this is an OUTGOING transaction. When I pull in the + # multi-sig code, I will have to either make a different dialog, + # or add some logic to this one + FIELDS = enum('Hash', 'OutList', 'SumOut', 'InList', 'SumIn', 'Time', 'Blk', 'Idx') + data = extractTxInfo(self.txdpObj, -1) + + # Collect the input wallets (hopefully just one of them) + fromWlts = set() + for scrAddr, amt, a, b, c in data[FIELDS.InList]: + wltID = self.main.getWalletForAddr160(scrAddr[1:]) + if not wltID == '': + fromWlts.add(wltID) + + if len(fromWlts) > 1: + QMessageBox.warning(self, 'Multiple Input Wallets', \ + 'Somehow, you have obtained a transaction that actually pulls from more ' + 'than one wallet. The support for handling multi-wallet signatures is ' + 'not currently implemented (this also could have happened if you imported ' + 'the same private key into two different wallets).' , QMessageBox.Ok) + self.makeReviewFrame() + return + elif len(fromWlts) == 0: + QMessageBox.warning(self, 'Unrelated Transaction', \ + 'This transaction appears to have no relationship to any of the wallets ' + 'stored on this computer. Did you load the correct transaction?', \ + QMessageBox.Ok) + self.makeReviewFrame() + return + + spendWltID = fromWlts.pop() + self.wlt = self.main.walletMap[spendWltID] + + toWlts = set() + myOutSum = 0 + theirOutSum = 0 + rvPairs = [] + idx = 0 + for scrType, amt, binScript, multiSigList in data[FIELDS.OutList]: + recip = script_to_scrAddr(binScript) + try: + wltID = self.main.getWalletForAddr160(CheckHash160(recip)) + except BadAddressError: + wltID = '' + + if wltID == spendWltID: + toWlts.add(wltID) + myOutSum += amt + self.idxSelf.append(idx) + else: + rvPairs.append([recip, amt]) + theirOutSum += amt + self.idxOther.append(idx) + idx += 1 + + myInSum = data[FIELDS.SumIn] # because we assume all are ours + + if myInSum == None: + fee = None + else: + fee = myInSum - data[FIELDS.SumOut] + + self.leValue = theirOutSum + self.makeReviewFrame() + + + ############################################################################ + def makeReviewFrame(self): + # ## + if self.txdpObj == None: + self.infoLbls[0][2].setText('') + self.infoLbls[1][2].setText('') + self.infoLbls[2][2].setText('') + self.infoLbls[3][2].setText('') + else: + ##### 0 + + ##### 1 + if self.wlt: + self.infoLbls[0][2].setText(self.wlt.uniqueIDB58) + self.infoLbls[1][2].setText(self.wlt.labelName) + else: + self.infoLbls[0][2].setText('[[ Unrelated ]]') + self.infoLbls[1][2].setText('') + + ##### 2 + self.infoLbls[2][2].setText(self.txdpObj.uniqueB58) + + ##### 3 + if self.leValue: + self.infoLbls[3][2].setText(coin2strNZS(self.leValue) + ' BTC') + else: + self.infoLbls[3][2].setText('') + + self.moreInfo.setVisible(True) + + def execMoreTxInfo(self): + + if not self.txdpObj: + self.processTxDP() + + if not self.txdpObj: + QMessageBox.warning(self, 'Invalid Transaction', \ + 'Transaction data is invalid and cannot be shown!', QMessageBox.Ok) + return + + dlgTxInfo = DlgDispTxInfo(self.txdpObj, self.wlt, self.parent(), self.main, \ + precomputeIdxGray=self.idxSelf, precomputeAmt=-self.leValue, txtime=-1) + dlgTxInfo.exec_() + + + + def signTx(self): + if not self.txdpObj: + QMessageBox.critical(self, 'Cannot Sign', \ + 'This transaction is not relevant to any of your wallets.' + 'Did you load the correct transaction?', QMessageBox.Ok) + return + + if self.txdpObj == None: + QMessageBox.warning(self, 'Not Signable', \ + 'This is not a valid transaction, and thus it cannot ' + 'be signed. ', QMessageBox.Ok) + return + elif self.enoughSigs and self.sigsValid: + QMessageBox.warning(self, 'Already Signed', \ + 'This transaction has already been signed!', QMessageBox.Ok) + return + + + if self.wlt and self.wlt.watchingOnly: + QMessageBox.warning(self, 'No Private Keys!', \ + 'This transaction refers one of your wallets, but that wallet ' + 'is a watching-only wallet. Therefore, private keys are ' + 'not available to sign this transaction.', \ + QMessageBox.Ok) + return + + + # We should provide the same confirmation dialog here, as we do when + # sending a regular (online) transaction. But the DlgConfirmSend was + # not really designed + txdp = self.txdpObj + rvpairs = [] + rvpairsMine = [] + outInfo = txdp.pytxObj.makeRecipientsList() + theFee = sum(txdp.inputValues) - sum([info[1] for info in outInfo]) + for info in outInfo: + if not info[0] in CPP_TXOUT_HAS_ADDRSTR: + rvpairs.append(['Non-Standard Output', info[1]]) + continue + + addrStr = script_to_addrStr(info[2]) + addr160 = addrStr_to_hash160(addrStr)[1] + scrAddr = script_to_scrAddr(info[2]) + rvpairs.append([scrAddr, info[1]]) + if self.wlt.hasAddr(addr160): + rvpairsMine.append([scrAddr, info[1]]) + + if len(rvpairsMine) == 0 and len(rvpairs) > 1: + QMessageBox.warning(self, 'Missing Change', \ + 'This transaction has %d recipients, and none of them ' + 'are addresses in this wallet (for receiving change). ' + 'This can happen if you specified a custom change address ' + 'for this transaction, or sometimes happens solely by ' + 'chance with a multi-recipient transaction. It could also ' + 'be the result of someone tampering with the transaction. ' + '

    The transaction is valid and ready to be signed. Please ' + 'verify the recipient and amounts carefully before ' + 'confirming the transaction on the next screen.' % len(rvpairs), \ + QMessageBox.Ok) + dlg = DlgConfirmSend(self.wlt, rvpairs, theFee, self, self.main) + if not dlg.exec_(): + return + + + + if self.wlt.useEncryption and self.wlt.isLocked: + Passphrase = None + + unlockdlg = DlgUnlockWallet(self.wlt, self, self.main, 'Send Transaction', returnPassphrase=True) + if unlockdlg.exec_(): + if unlockdlg.Accepted == 1: + Passphrase = unlockdlg.securePassphrase.copy() + unlockdlg.securePassphrase.destroy() + + if Passphrase is None or self.wlt.kdf is None: + QMessageBox.critical(self.parent(), 'Wallet is Locked', \ + 'Cannot sign transaction while your wallet is locked. ', \ + QMessageBox.Ok) + return + else: + self.wlt.kdfKey = self.wlt.kdf.DeriveKey(Passphrase) + Passphrase.destroy() + + newTxdp = self.wlt.signTxDistProposal(self.txdpObj) + self.wlt.advanceHighestIndex() + self.txtTxDP.setText(newTxdp.serializeAscii()) + self.txdpObj = newTxdp + + if not self.fileLoaded == None: + self.saveTxAuto() + + + def broadTx(self): + if self.main.netMode == NETWORKMODE.Disconnected: + QMessageBox.warning(self, 'No Internet!', \ + 'Armory lost its connection to Bitcoin-Qt, and cannot ' + 'broadcast any transactions until it is reconnected. ' + 'Please verify that Bitcoin-Qt (or bitcoind) is open ' + 'and synchronized with the network.', QMessageBox.Ok) + return + elif self.main.netMode == NETWORKMODE.Offline: + QMessageBox.warning(self, 'No Internet!', \ + 'You do not currently have a connection to the Bitcoin network. ' + 'If this does not seem correct, verify that Bitcoin-Qt is open ' + 'and synchronized with the network.', QMessageBox.Ok) + return + + + + try: + finalTx = self.txdpObj.prepareFinalTx() + except: + QMessageBox.warning(self, 'Error', \ + 'There was an error processing this transaction, for reasons ' + 'that are probably not your fault...', QMessageBox.Ok) + return + + # We should provide the same confirmation dialog here, as we do when + # sending a regular (online) transaction. But the DlgConfirmSend was + # not really designed + txdp = self.txdpObj + rvpairs = [] + rvpairsMine = [] + outInfo = txdp.pytxObj.makeRecipientsList() + theFee = sum(txdp.inputValues) - sum([info[1] for info in outInfo]) + for info in outInfo: + if not info[0] in CPP_TXOUT_HAS_ADDRSTR: + rvpairs.append(['Non-Standard Output', info[1]]) + continue + + addrStr = script_to_addrStr(info[2]) + addr160 = addrStr_to_hash160(addrStr)[1] + scrAddr = script_to_scrAddr(info[2]) + rvpairs.append([scrAddr, info[1]]) + + dlg = DlgConfirmSend(self.wlt, rvpairs, theFee, self, self.main) + + if dlg.exec_(): + self.main.broadcastTransaction(finalTx) + if self.fileLoaded and os.path.exists(self.fileLoaded): + try: + # pcs = self.fileLoaded.split('.') + # newFileName = '.'.join(pcs[:-2]) + '.DONE.' + '.'.join(pcs[-2:]) + shutil.move(self.fileLoaded, self.fileLoaded.replace('signed', 'SENT')) + except: + QMessageBox.critical(self, 'File Remove Error', \ + 'The file could not be deleted. If you want to delete ' + 'it, please do so manually. The file was loaded from: ' + '

    %s: ' % self.fileLoaded, QMessageBox.Ok) + if self.parent() is ArmoryDialog: + self.parent().accept() + + + def saveTxAuto(self): + if not self.txdpReadable: + QMessageBox.warning(self, 'Formatting Error', \ + 'The transaction data was not in a format recognized by ' + 'Armory.') + return + + + if not self.fileLoaded == None and self.enoughSigs and self.sigsValid: + newSaveFile = self.fileLoaded.replace('unsigned', 'signed') + print newSaveFile + f = open(newSaveFile, 'w') + f.write(str(self.txtTxDP.toPlainText())) + f.close() + if not newSaveFile == self.fileLoaded: + os.remove(self.fileLoaded) + self.fileLoaded = newSaveFile + QMessageBox.information(self, 'Transaction Saved!', \ + 'Your transaction has been saved to the following location:' + '\n\n%s\n\nIt can now be broadcast from any computer running ' + 'Armory in online mode.' % newSaveFile, QMessageBox.Ok) + return + + def saveTx(self): + if not self.txdpReadable: + QMessageBox.warning(self, 'Formatting Error', \ + 'The transaction data was not in a format recognized by ' + 'Armory.') + return + + + # The strange windows branching is because PyQt in Windows automatically + # adds the ffilter suffix to the default filename, where as it needs to + # be explicitly added in PyQt in Linux. Not sure why this behavior exists. + defaultFilename = '' + if not self.txdpObj == None: + if self.enoughSigs and self.sigsValid: + suffix = '' if OS_WINDOWS else '.signed.tx' + defaultFilename = 'armory_%s_%s' % (self.txdpObj.uniqueB58, suffix) + ffilt = 'Transactions (*.signed.tx *.unsigned.tx)' + else: + suffix = '' if OS_WINDOWS else '.unsigned.tx' + defaultFilename = 'armory_%s_%s' % (self.txdpObj.uniqueB58, suffix) + ffilt = 'Transactions (*.unsigned.tx *.signed.tx)' + filename = self.main.getFileSave('Save Transaction', \ + [ffilt], \ + defaultFilename) + + filename = filename.replace('unsigned.tx.unsigned.tx', 'unsigned.tx') + filename = filename.replace('unsigned.tx.unsigned.tx', 'unsigned.tx') + filename = filename.replace('signed.tx.signed.tx', 'signed.tx') + filename = filename.replace('signed.tx.signed.tx', 'signed.tx') + filename = filename.replace('unsigned.tx.signed.tx', 'signed.tx') + if len(str(filename)) > 0: + LOGINFO('Saving transaction file: %s', filename) + f = open(filename, 'w') + f.write(str(self.txtTxDP.toPlainText())) + f.close() + + + def loadTx(self): + filename = self.main.getFileLoad(tr('Load Transaction'), \ + ['Transactions (*.signed.tx *.unsigned.tx *.SENT.tx)']) + + if len(str(filename)) > 0: + LOGINFO('Selected transaction file to load: %s', filename) + print filename + f = open(filename, 'r') + self.txtTxDP.setText(f.read()) + f.close() + self.fileLoaded = filename + print self.fileLoaded + + + def copyTx(self): + clipb = QApplication.clipboard() + clipb.clear() + clipb.setText(str(self.txtTxDP.toPlainText())) + self.lblCopied.setText('Copied!') + + + def copyTxHex(self): + clipb = QApplication.clipboard() + clipb.clear() + clipb.setText(binary_to_hex(self.txdpObj.prepareFinalTx().serialize())) + self.lblCopied.setText('Copied!') + +# Need to put circular imports at the end of the script to avoid an import deadlock +from qtdialogs import CLICKED, STRETCH, createAddrBookButton,\ + DlgConfirmSend, DlgUriCopyAndPaste, DlgUnlockWallet,\ + extractTxInfo, DlgDispTxInfo, NO_CHANGE diff --git a/ui/UpgradeDownloader.py b/ui/UpgradeDownloader.py new file mode 100644 index 000000000..73008f746 --- /dev/null +++ b/ui/UpgradeDownloader.py @@ -0,0 +1,738 @@ +from PyQt4.Qt import * +from PyQt4.QtGui import * +from PyQt4.QtNetwork import * +from qtdefines import * +from armoryengine.parseAnnounce import * + +class UpgradeDownloader: + def __init__(self, parent, main): + self.finishedCB = lambda : None + self.startedCB = lambda : None + self.url = None + self.filesha = None + self.downloadFile = None + self.progressBar = None + self.frame = None + self.parent = parent + self.main = main + self.packageName = '' + + self.networkAccess = QNetworkAccessManager() + + # downloadLinkFile + def setFile(self, url, filehash): + self.url = url + self.filesha = filehash + + if self.downloadButton: + if url and not self.downloadFile: + self.downloadButton.setEnabled(True) + else: + self.downloadButton.setEnabled(False) + + def useDownloadLinkFileAndSignature(self, linkfile): + self.downloadLinkFile = linkfile + + def setFinishedCallback(self, callback): + self.finishedCB = callback + + def setStartedCallback(self, callback): + self.startedCB = callback + + def setPackageName(self, pkgName): + self.packageName = pkgName + + def createDownloaderWidget(self, parent): + if self.frame: + raise RuntimeError("already created a downloader widget") + + self.frame = QWidget(parent) + + bottomRowLayout = QHBoxLayout(self.frame) + + self.progressBar = QProgressBar(self.frame) + bottomRowLayout.addWidget(self.progressBar, +1) + + self.downloadButton = QPushButton(tr("Download"), self.frame) + self.frame.connect(self.downloadButton, SIGNAL('clicked()'), \ + self.startOrStopDownload) + bottomRowLayout.addWidget(self.downloadButton) + + return self.frame + + def startOrStopDownload(self): + if self.downloadFile: + o = self.downloadFile + self.downloadFile = None + o.close() + else: + self.startDownload() + + def startDownload(self): + req = QNetworkRequest(QUrl.fromEncoded(self.url)) + self.receivedData = "" + self.downloadFile = self.networkAccess.get(req) + QObject.connect(self.downloadFile, SIGNAL('readyRead()'), self.readMoreDownloadData) + QObject.connect(self.downloadFile, SIGNAL('finished()'), self.downloadFinished) + + if not self.downloadButton is None: + self.downloadButton.setText(tr("Cancel")) + + self.progressTimer() + self.startedCB() + + ############################################################################# + def downloadFinished(self): + if not self.downloadButton is None: + self.downloadButton.setText(tr("Download")) + + # We will ask the user if they want us to unpack it for them + # Only if linux, only if satoshi, only if not offline-pkg-signed + linuxUnpackFile = None + + # downloadFile will be removed on cancel + if not self.downloadFile is None: + status = self.downloadFile.attribute(QNetworkRequest.HttpStatusCodeAttribute).toInt()[0] + if len(self.receivedData)==0: + if status == 404: + status = tr("File not found") + QMessageBox.warning(self.frame, tr("Download failed"), \ + tr("There was a failure downloading this file: {}").format(str(status))) + else: + res = binary_to_hex(sha256(self.receivedData)) + LOGINFO("Downloaded package has hash " + res) + if res != self.filesha: + QMessageBox.warning(self.frame, tr("Verification failed"), tr(""" + The download completed but its cryptographic signature is invalid. + Please try the download again. If you get another error, please + report the problem to support@bitcoinarmory.com. +

    + The downloaded data has been discarded. """)) + else: + defaultFN = os.path.basename(self.url) + if self.downloadLinkFile: + defaultFN += ".signed" + + dest = self.main.getFileSave(tr("Save File"), + [tr('Installers (*.exe, *.app, *.deb, *.tar.gz)')], + defaultFilename=defaultFN) + + if len(dest)!=0: + df = open(dest, "wb") + if self.downloadLinkFile: + df.write("START_OF_SIGNATURE_SECTION") + df.write(self.downloadLinkFile) + df.write("END_OF_SIGNATURE_SECTION") + df.write(self.receivedData) + df.close() + + if self.downloadLinkFile: + QMessageBox.warning(self.frame, tr("Download complete"), tr(""" + The package has the + signature from Armory Technologies, + Inc. bundled with it, so it can be verified + by an offline computer before installation. To use this + feature, the offline system must be running Armory + 0.91-beta or higher. Go to + "Help"\xe2\x86\x92"Verify Signed Package" + and load the *.signed file. The file was saved + to: +

    + %s +

    + There is no special procedure to update a previous + installation. The installer will update existing + versions without touching your wallets or settings.""") % \ + (htmlColor("TextGreen"), dest), \ + QMessageBox.Ok) + else: + if OS_LINUX and \ + self.packageName=='Satoshi' and \ + dest.endswith('tar.gz'): + linuxUnpackFile = dest + else: + QMessageBox.warning(self.frame, tr("Download complete"), tr(""" + The file downloaded successfully, and carries a valid + signature from Armory Technologies, + Inc. You can now use it to install the + software. The file was saved to: +

    %s

    + There is no special procedure to update a previous + installation. The installer will update existing + versions without touching your wallets or settings.""") % \ + (htmlColor("TextGreen"), dest), QMessageBox.Ok) + + + + if linuxUnpackFile is not None: + reply = QMessageBox.warning(self.frame, tr('Unpack Download'), tr(""" + You just downloaded the Bitcoin Core software for Linux. + Would you like Armory to extract it for you and adjust your + settings to use it automatically? +

    + If you modified your settings to run Bitcoin Core manually, + click "No" then extract the downloaded file and manually start + bitcoin-qt or bitcoind in from the extracted "bin/%d" + directory.""") % (64 if SystemSpecs.IsX64 else 32), \ + QMessageBox.Yes | QMessageBox.No) + + if reply==QMessageBox.Yes: + finalDir = self.main.unpackLinuxTarGz(dest, changeSettings=True) + if finalDir is None: + QMessageBox.critical(self.frame, tr('Error Unpacking'), tr(""" + There was an error unpacking the Bitcoin Core file. To use + it, you need to go to where the file was saved, right-click + on it and select "Extract Here", then adjust your settings + ("File"\xe2\x86\x92"Settings" from the main + window) to point "Bitcoin Install Dir" to the extracted + directory. +

    + You saved the installer to: +

    + %s""") % dest, QMessageBox.Ok) + else: + QMessageBox.warning(self.frame, tr('Finished!'), tr(""" + The operation was successful. Restart Armory to use the + newly-downloaded Bitcoin Core software"""), QMessageBox.Ok) + + self.receivedData = None + self.downloadFile = None + self.downloadButton.setEnabled(True) + self.progressBar.setFormat("") + + if self.finishedCB: + self.finishedCB() + + def readMoreDownloadData(self): + if not self.receivedData is None: + self.receivedData = self.receivedData + self.downloadFile.readAll() + + def progressTimer(self): + if not self.downloadFile: + self.progressBar.reset() + self.progressBar.setRange(0, 100) + self.progressBar.setValue(0) + return + + size = self.downloadFile.header(QNetworkRequest.ContentLengthHeader).toInt()[0] + self.progressBar.setRange(0, size) + self.progressBar.setValue(len(self.receivedData)) + + totalStr = bytesToHumanSize(size) + sofarStr = bytesToHumanSize(len(self.receivedData)) + s = tr("{0} / {1} downloaded").format(sofarStr, totalStr) + self.progressBar.setFormat(s) + + QTimer.singleShot(250, self.progressTimer) + + +class UpgradeDownloaderDialog(ArmoryDialog): + # parent: QWidget + # showPackage: automatically select this package name, if available, for the current OS + # downloadText: the text *WITH SIGNATURE* of the downloaded text data + # changeLog: the text of the downloaded changelogs + def __init__(self, parent, main, showPackage, downloadText, changeLog): + super(UpgradeDownloaderDialog, self).__init__(parent, main) + + self.downloader = UpgradeDownloader(parent, main) + self.bitsColor = htmlColor('Foreground') + + def enableOrDisable(e): + self.os.setEnabled(e) + self.osver.setEnabled(e) + self.osarch.setEnabled(e) + self.packages.setEnabled(e) + self.closeButton.setEnabled(e) + + self.downloader.setFinishedCallback(lambda : enableOrDisable(True)) + + def onStart(): + enableOrDisable(False) + downloadText=None + if self.saveAsOfflinePackage.isChecked(): + downloadText = self.downloadText + self.downloader.useDownloadLinkFileAndSignature(downloadText) + + self.downloader.setStartedCallback(onStart) + + self.downloadText = downloadText + self.nestedDownloadMap = downloadLinkParser(filetext=downloadText).downloadMap + self.changelog = changelogParser().parseChangelogText(changeLog) + + self.localizedData = { \ + "Ubuntu" : tr("Ubuntu/Debian"), \ + "Windows" : tr("Windows"), \ + "MacOSX" : tr("MacOSX"), \ + "32" : tr("32-bit"), \ + "64" : tr("64-bit"), \ + "Satoshi" : tr("Bitcoin Core"), \ + "ArmoryTesting" : tr("Armory Testing (unstable)"), \ + "ArmoryOffline" : tr("Offline Armory Wallet") \ + } + + + oslabel = QLabel(tr("OS:"), self) + self.os = QComboBox(self) + self.osver = QComboBox(self) + self.osarch = QComboBox(self) + + packages = QTreeWidget(self) + self.packages = packages + packages.setRootIsDecorated(False) + packages.sortByColumn(0, Qt.AscendingOrder) + packages.setSortingEnabled(True) + + headerItem = QTreeWidgetItem() + headerItem.setText(0,tr("Package")) + headerItem.setText(1,tr("Version")) + packages.setHeaderItem(headerItem) + packages.setMaximumHeight(int(7*tightSizeStr(packages, "Abcdefg")[1])) + packages.header().setResizeMode(0, QHeaderView.Stretch) + packages.header().setResizeMode(1, QHeaderView.Stretch) + + self.connect(self.os, SIGNAL("activated(int)"), self.cascadeOsVer) + self.connect(self.osver, SIGNAL("activated(int)"), self.cascadeOsArch) + self.connect(self.osarch, SIGNAL("activated(int)"), self.displayPackages) + + self.connect(packages, \ + SIGNAL('currentItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)'), \ + self.useSelectedPackage) + + self.changelogView = QTextBrowser(self) + self.changelogView.setOpenExternalLinks(True) + + self.saveAsOfflinePackage = QCheckBox(tr("Save with offline-verifiable signature")) + self.closeButton = QPushButton(tr("Close"), self) + self.connect(self.closeButton, SIGNAL('clicked()'), self.accept) + + self.btnDLInfo = QLabelButton('Download Info') + self.btnDLInfo.setVisible(False) + self.connect(self.btnDLInfo, SIGNAL('clicked()'), self.popupPackageInfo) + + + self.lblSelectedSimple = QRichLabel(tr('No download selected'), + doWrap=False, hAlign=Qt.AlignHCenter) + self.lblSelectedSimpleMore = QRichLabel(tr(''), doWrap=False) + self.lblSelectedComplex = QRichLabel(tr('No download selected')) + self.lblCurrentVersion = QRichLabel('', hAlign=Qt.AlignHCenter) + + # At the moment, not sure we actually need this label + self.lblSelectedComplex.setVisible(False) + + + self.btnShowComplex = QLabelButton(tr('Show all downloads for all OS')) + self.connect(self.btnShowComplex, SIGNAL('clicked()'), self.showComplex) + + frmDisp = makeHorizFrame(['Stretch', self.lblSelectedSimpleMore, 'Stretch']) + frmBtnShowComplex = makeHorizFrame(['Stretch', self.btnShowComplex]) + layoutSimple = QVBoxLayout() + layoutSimple.addWidget(self.lblSelectedSimple) + layoutSimple.addWidget(frmDisp) + layoutSimple.addWidget(self.lblCurrentVersion) + layoutSimple.addWidget(frmBtnShowComplex) + frmTopSimple = QFrame() + frmTopSimple.setLayout(layoutSimple) + + + layoutComplex = QGridLayout() + layoutComplex.addWidget(oslabel, 0,0) + layoutComplex.addWidget(self.os, 0,1) + layoutComplex.addWidget(self.osver, 0,2) + layoutComplex.addWidget(self.osarch,0,3) + layoutComplex.addWidget(packages, 1,0, 1,4) + layoutComplex.addWidget(self.lblSelectedComplex, 2,0, 1,4) + layoutComplex.setColumnStretch(0,0) + layoutComplex.setColumnStretch(1,2) + layoutComplex.setColumnStretch(2,2) + layoutComplex.setColumnStretch(3,1) + frmTopComplex = QFrame() + frmTopComplex.setLayout(layoutComplex) + + + frmTopComplex.setFrameStyle(STYLE_SUNKEN) + frmTopSimple.setFrameStyle(STYLE_SUNKEN) + + self.stackedDisplay = QStackedWidget() + self.stackedDisplay.addWidget(frmTopSimple) + self.stackedDisplay.addWidget(frmTopComplex) + + layout = QGridLayout() + layout.addWidget(self.stackedDisplay, 0,0, 1,3) + layout.addWidget(self.changelogView, 1,0, 1,3) + layout.addWidget(self.saveAsOfflinePackage, 2,0) + layout.addWidget(self.btnDLInfo, 2,2) + layout.addWidget(self.downloader.createDownloaderWidget(self), \ + 3,0, 1,3) + layout.addWidget(self.closeButton, 4,2) + layout.setRowStretch(0, 1) + layout.setColumnStretch(1, 1) + + + self.cascadeOs() + self.selectMyOs() + self.useSelectedPackage() + self.cascadeOs() + # I have no clue why we have to call these things so many times! + self.selectMyOs() + self.cascadeOs() + + + # Above we had to select *something*, we should check that the + # architecture actually matches our system. If not, warn + #trueBits = '64' if SystemSpecs.IsX64 else '32' + #selectBits = self.itemData(self.osarch)[:2] + #if showPackage and not trueBits==selectBits: + #QMessageBox.warning(self, tr("Wrong Architecture"), tr(""" + #You appear to be on a %s-bit architecture, but the only + #available download is for %s-bit systems. It is unlikely + #that this download will work on this operating system. + #

    + #Please make sure that the correct operating system is + #selected before attempting to download and install any + #packages.""") % (trueBits, selectBits), QMessageBox.Ok) + #self.bitsColor = htmlColor('TextRed') + + + if showPackage == 'Armory': + expectVer = self.main.armoryVersions[1] + elif showPackage == 'Satoshi': + expectVer = self.main.satoshiVersions[1] + + # This is currently broken... will have to fix later + #if showPackage: + #for n in range(0, packages.topLevelItemCount()): + #row = packages.topLevelItem(n) + #if str(row.data(0, 32).toString())==showPackage: + #packages.setCurrentItem(row) + #if not expectVer or str(row.data(1, 32).toString())==expectVer: + #break + #self.useSelectedPackage() + #else: + #foundPackage = False + + self.stackedDisplay.setCurrentIndex(1) + QMessageBox.warning(self, tr("Not Found"), tr(""" + Armory could not determine an appropriate download for + your operating system. You will have to manually select + the correct download on the next window."""), QMessageBox.Ok) + #else: + #self.stackedDisplay.setCurrentIndex(1) + + + self.setLayout(layout) + self.setMinimumWidth(600) + self.setWindowTitle(tr('Secure Downloader')) + + + def showSimple(self): + self.stackedDisplay.setCurrentIndex(0) + + def showComplex(self): + self.stackedDisplay.setCurrentIndex(1) + + + def findCmbData(self, cmb, findStr, last=False): + """ + So I ran into some issues with finding python strings in comboboxes + full of QStrings. I confirmed that + self.os.itemText(i)==tr("Ubuntu/Debian") + but not + self.os.findData(tr("Ubuntu/Debian")) + I'm probably being stupid, I thought I saw this work before... + + Return zero if failed so we just select the first item in the list + if we don't find it. + """ + for i in range(cmb.count()): + if cmb.itemText(i)==findStr: + return i + + return 0 if not last else cmb.count()-1 + + + def selectMyOs(self): + osVar = OS_VARIANT + if isinstance(osVar, (list,tuple)): + osVar = osVar[0] + + osIndex = 0 + if OS_WINDOWS: + osIndex = self.findCmbData(self.os, tr("Windows")) + elif OS_LINUX: + if osVar.lower() in ['debian', 'linuxmint']: + d1 = self.findCmbData(self.os, tr('Debian')) + d2 = self.findCmbData(self.os, tr('Ubuntu')) + osIndex = max(d1,d2) + elif osVar.lower() == "ubuntu": + osIndex = self.findCmbData(self.os, tr('Ubuntu')) + else: + osIndex = self.findCmbData(self.os, tr('Ubuntu')) + elif OS_MACOSX: + osIndex = self.findCmbData(self.osver, tr('MacOSX')) + + self.os.setCurrentIndex(osIndex) + self.cascadeOsVer() # signals don't go through for some reason + + osverIndex = 0 + if OS_WINDOWS: + osverIndex = self.findCmbData(self.osver, platform.win32_ver(), True) + elif OS_LINUX: + osverIndex = self.findCmbData(self.osver, OS_VARIANT[1], True) + elif OS_MACOSX: + osverIndex = self.findCmbData(self.osver, platform.mac_ver()[0], True) + self.osver.setCurrentIndex(osverIndex) + self.cascadeOsArch() + + archIndex = 0 + if platform.machine() == "x86_64": + archIndex = self.findCmbData(self.osarch, tr('64')) + else: + archIndex = self.findCmbData(self.osarch, tr('32')) + + self.osarch.setCurrentIndex(archIndex) + + + def useSelectedPackage(self): + if self.packages.currentItem() is None: + self.changelogView.setHtml("" + tr(""" + There is no version information to be shown here.""") +"") + self.downloader.setFile(None, None) + else: + packagename = str(self.packages.currentItem().data(0, 32).toString()) + packagever = str(self.packages.currentItem().data(1, 32).toString()) + packageurl = str(self.packages.currentItem().data(2, 32).toString()) + packagehash = str(self.packages.currentItem().data(3, 32).toString()) + + self.downloader.setFile(packageurl, packagehash) + self.selectedDLInfo = [packagename,packagever,packageurl,packagehash] + self.btnDLInfo.setVisible(True) + + self.downloader.setPackageName(packagename) + + # Figure out where to bound the changelog information + startIndex = -1 + if self.changelog is not None: + for i,triplet in enumerate(self.changelog): + if triplet[0]==packagever: + startIndex = i + break + + stopIndex = len(self.changelog) + if len(self.main.armoryVersions[0])>0: + for i,triplet in enumerate(self.changelog): + currVer = getVersionInt(readVersionString(self.main.armoryVersions[0])) + thisVer = getVersionInt(readVersionString(triplet[0])) + if thisVer <= currVer: + stopIndex = i + break + + + + if startIndex > -1: + + logHtml = "" + if startIndex >= stopIndex: + logHtml = tr("Release notes are not available for this package") + else: + for i in range(startIndex, stopIndex): + block = self.changelog[i] + logHtml += "

    " + tr("Version {0}").format(block[0]) + "

    \n" + logHtml += "" + tr("Released on {0}").format(block[1]) + "\n" + + features = block[2] + logHtml += "
      " + for f in features: + logHtml += "
    • " + tr("{0}: {1}").format(f[0], f[1]) + "
    • \n" + logHtml += "
    \n\n" + else: + if packagename == "Satoshi": + logHtml = tr(""" + No version information is available here for any of the + core Bitcoin software downloads. You can find the + information at: + https://bitcoin.org/en/version-history""") + else: + logHtml = tr("Release notes are not available for this package") + + + #logHtml += tr(""" + #

    + #----- + #

    + #Package: %s version %s
    + #Download URL: %s
    + #Verified sha256sum: %s""") % \ + #(packagename, packagever, packageurl, packagehash) + + self.changelogView.setHtml(logHtml) + self.updateLabels(packagename, packagever, + self.itemData(self.os), + self.itemData(self.osver), + self.itemData(self.osarch)) + + + + def popupPackageInfo(self): + pkgname,pkgver,pkgurl,pkghash = self.selectedDLInfo + pkgname= tr(pkgname) + pkgver = tr(pkgver) + osname = tr(self.itemData(self.os)) + osver = tr(self.itemData(self.osver)) + osarch = self.itemData(self.osarch) + inst = os.path.basename(pkgurl) + QMessageBox.information(self, tr('Package Information'), tr(""" + Download information for %(pkgname)s version %(pkgver)s: +
    +
      +
    • Operating System:
    • +
        +
      • %(osname)s %(osver)s %(osarch)s-bit
      • +
      +
    • Installer Filename:
    • +
        +
      • %(inst)s
      • +
      +
    • Download URL:
    • +
        +
      • %(pkgurl)s
      • +
      +
    • Verified sha256sum:
    • +
        +
      • %(pkghash)s
      • +
      +
    """) % locals(), QMessageBox.Ok) + + + + + + def cascade(self, combobox, valuesfrom, nextToCascade): + combobox.blockSignals(True) + current = combobox.currentText() + combobox.clear() + for v in valuesfrom: + combobox.addItem(self.localized(v), QVariant(v)) + at = combobox.findText(current) + if at != -1: + combobox.setCurrentIndex(at) + + nextToCascade() + combobox.blockSignals(False) + + + # pass the combobox (self.os, osver, ...) and the part of nestedDownloadMap + # to look into + def cascadeOs(self): + allOSes = set() + for pack in self.nestedDownloadMap.itervalues(): + for packver in pack.itervalues(): + for os in packver.iterkeys(): + allOSes.add(os) + self.cascade(self.os, allOSes, self.cascadeOsVer) + + + def cascadeOsVer(self): + chosenos = str(self.os.itemData(self.os.currentIndex()).toString()) + if len(chosenos)==0: + return + + allVers = set() + for pack in self.nestedDownloadMap.itervalues(): + for packver in pack.itervalues(): + if chosenos in packver: + for osver in packver[chosenos].iterkeys(): + allVers.add(osver) + + # We use a list here because we need to sort the subvers + allVers = sorted([x for x in allVers]) + self.cascade(self.osver, allVers, self.cascadeOsArch) + + def cascadeOsArch(self): + chosenos = str(self.os.itemData(self.os.currentIndex()).toString()) + chosenosver = str(self.osver.itemData(self.osver.currentIndex()).toString()) + if len(chosenosver)==0: + return + + allArchs = set() + for pack in self.nestedDownloadMap.itervalues(): + for packver in pack.itervalues(): + if chosenos in packver and chosenosver in packver[chosenos]: + for osarch in packver[chosenos][chosenosver].iterkeys(): + allArchs.add(osarch) + self.cascade(self.osarch, allArchs, self.displayPackages) + + def displayPackages(self): + packages = self.packages + packages.clear() + chosenos = str(self.os.itemData(self.os.currentIndex()).toString()) + chosenosver = str(self.osver.itemData(self.osver.currentIndex()).toString()) + chosenosarch = str(self.osarch.itemData(self.osarch.currentIndex()).toString()) + if len(chosenosarch)==0: + return + + for packname,pack in self.nestedDownloadMap.iteritems(): + for packvername,packver in pack.iteritems(): + if chosenos in packver \ + and chosenosver in packver[chosenos] \ + and chosenosarch in packver[chosenos][chosenosver]: + + row = QTreeWidgetItem() + row.setText(0, self.localized(packname)) + row.setData(0, 32, packname) # not localized + row.setText(1, self.localized(packvername)) + row.setData(1, 32, packvername) + row.setData(2, 32, packver[chosenos][chosenosver][chosenosarch][0]) + row.setData(3, 32, packver[chosenos][chosenosver][chosenosarch][1]) + packages.addTopLevelItem(row) + + + self.updateLabels(packname, packvername, + self.itemData(self.os), + self.itemData(self.osver), + self.itemData(self.osarch)) + + + def updateLabels(self, pkgName, pkgVer, osName, osVer, osArch): + if not pkgName: + self.lblSelectedComplex.setText(tr("""No package currently selected""")) + self.lblSelectedSimple.setText(tr("""No package currently selected""")) + self.lblSelectedSimpleMore.setText(tr("")) + else: + self.lblSelectedComplex.setText(tr(""" + Selected Package: {} {} for {} {} {}"""). \ + format(tr(pkgName), tr(pkgVer), tr(osName), tr(osVer), tr(osArch))) + + self.lblSelectedSimple.setText(tr(""" Securely + download latest version of %s""") % pkgName) + + self.lblCurrentVersion.setText('') + currVerStr = '' + if pkgName=='Satoshi': + if self.main.satoshiVersions[0]: + self.lblCurrentVersion.setText(tr(""" + You are currently using Bitcoin Core version %s""") % \ + self.main.satoshiVersions[0]) + elif pkgName=='Armory': + if self.main.armoryVersions[0]: + self.lblCurrentVersion.setText(tr(""" + You are currently using Armory version %s""") % \ + self.main.armoryVersions[0]) + + self.lblSelectedSimpleMore.setText(tr(""" + Software Download: %s version %s
    + Operating System: %s %s
    + System Architecture: %s """) % \ + (tr(pkgName), tr(pkgVer), tr(osName), tr(osVer), self.bitsColor, tr(osArch))) + + # get the untranslated name from the combobox specified + def itemData(self, combobox): + return str(combobox.itemData(combobox.currentIndex()).toString()) + + def localized(self, v): + if v in self.localizedData: + return str(self.localizedData[v]) + else: + return str(v) + + +# kate: indent-width 3; replace-tabs on; diff --git a/ui/VerifyOfflinePackage.py b/ui/VerifyOfflinePackage.py new file mode 100644 index 000000000..e0e54314a --- /dev/null +++ b/ui/VerifyOfflinePackage.py @@ -0,0 +1,138 @@ +from PyQt4.Qt import * #@UnusedWildImport +from PyQt4.QtGui import * #@UnusedWildImport +from qtdefines import tr, QRichLabel, ArmoryDialog +from armoryengine.parseAnnounce import * +from armorycolors import htmlColor + + +class VerifyOfflinePackageDialog(QDialog): + def __init__(self, parent, main): + super(VerifyOfflinePackageDialog, self).__init__(parent) + self.main = main + + layout = QVBoxLayout(self) + + load = QGroupBox(tr("Load Signed Package"), self) + layout.addWidget(load) + + layoutload = QVBoxLayout() + load.setLayout(layoutload) + self.loadFileButton = QPushButton(tr("Select file to verify..."), load); + layoutload.addWidget(self.loadFileButton) + self.connect(self.loadFileButton, SIGNAL('clicked()'), self.load) + + self.lblVerified = QRichLabel('', hAlign=Qt.AlignHCenter, doWrap=False) + layout.addWidget(self.lblVerified) + + + save = QGroupBox(tr("Save Verified Package"), self) + layout.addItem(QSpacerItem(10,10)) + layout.addWidget(save) + layoutsave = QVBoxLayout() + save.setLayout(layoutsave) + self.saveFileButton = QPushButton(tr("Select file to save to..."), load); + self.saveFileButton.setEnabled(False) + layoutsave.addWidget(self.saveFileButton) + self.connect(self.saveFileButton, SIGNAL('clicked()'), self.save) + + + def load(self): + self.fileData = None + #self.fromfile = QFileDialog.getOpenFileName(self, tr("Load file to verify"), "", tr("Armory Signed Packages (*.signed)")) + self.fromfile = self.main.getFileLoad(tr('Load file to Verify'),\ + ['Armory Signed Packages (*.signed)']) + if len(self.fromfile)==0: + return + + df = open(self.fromfile, "rb") + allfile = df.read() + df.close() + magicstart="START_OF_SIGNATURE_SECTION" + magicend="END_OF_SIGNATURE_SECTION" + if 0 != allfile.find(magicstart, 0, 1024*1024*4): # don't search past 4MiB + QMessageBox.warning(self, tr("Invalid File"), tr("This file is not a signed package")) + return + + end = allfile.find(magicend, 0, 1024*1024*4) # don't search past 4MiB + if -1 == end: # don't search past 4MiB + QMessageBox.warning(self, tr("Invalid File"), tr("The end of the signature in the file could not be found")) + + signatureData = allfile[len(magicstart):end] + fileData = allfile[end+len(magicend):] + + print "All:",end, end+len(magicend), len(fileData), len(allfile) + + allsigs = downloadLinkParser(filetext=signatureData).downloadMap + + res = binary_to_hex(sha256(fileData)) + + good=False + url=None + print "Hash of package file: ", res + + # simply check if any of the hashes match + for pack in allsigs.itervalues(): + for packver in pack.itervalues(): + for packos in packver.itervalues(): + for packosver in packos.itervalues(): + for packosarch in packosver.itervalues(): + okhash = packosarch[1] + if okhash == res: + url = packosarch[0] + good=True + + if good: + self.saveFileButton.setEnabled(True) + self.fileData = fileData + self.fileName = os.path.basename(url) + self.lblVerified.setText(tr("""Signature is + Valid!""") % htmlColor('TextGreen')) + reply = QMessageBox.warning(self, tr("Signature Valid"), tr(""" + The downloaded file has a valid signature from + Armory Technologies, Inc., and is + safe to install. +

    + Would you like to overwrite the original file with the extracted + installer? If you would like to save it to a new location, click + "No" and then use the "Save Verified Package" button to select + a new save location.""") % htmlColor('TextGreen'), \ + QMessageBox.Yes | QMessageBox.No) + + if reply==QMessageBox.Yes: + newFile = self.fromfile + if newFile.endswith('.signed'): + newFile = self.fromfile[:-7] + + LOGINFO('Saving installer to: ' + newFile) + + with open(newFile, 'wb') as df: + df.write(self.fileData) + + if os.path.exists(newFile): + LOGINFO('Removing original file: ' + self.fromfile) + os.remove(self.fromfile) + + QMessageBox.warning(self, tr('Saved!'), tr(""" + The installer was successfully extracted and saved to the + following location: +

    + %s""") % newFile, QMessageBox.Ok) + + + else: + self.saveFileButton.setEnabled(False) + self.lblVerified.setText(tr("""Invalid signature + on loaded file!""") % htmlColor('TextRed')) + QMessageBox.warning(self, tr("Signature failure"), \ + tr("This file has an invalid signature")) + + def save(self): + tofile = QFileDialog.getSaveFileName(self, tr("Save confirmed package"), \ + QDir.homePath() + "/" + self.fileName) + if len(tofile)==0: + return + df = open(tofile, "wb") + df.write(self.fileData) + df.close() + +# kate: indent-width 3; replace-tabs on; diff --git a/ui/WalletFrames.py b/ui/WalletFrames.py new file mode 100644 index 000000000..b5fee44d8 --- /dev/null +++ b/ui/WalletFrames.py @@ -0,0 +1,973 @@ +################################################################################ +# # +# Copyright (C) 2011-2014, Armory Technologies, Inc. # +# Distributed under the GNU Affero General Public License (AGPL v3) # +# See LICENSE or http://www.gnu.org/licenses/agpl.html # +# # +################################################################################ + +from PyQt4.Qt import * #@UnusedWildImport +from PyQt4.QtGui import * #@UnusedWildImport + +from armoryengine.BDM import TheBDM +from qtdefines import * #@UnusedWildImport + +WALLET_DATA_ENTRY_FIELD_WIDTH = 60 + +# This class has all of the select wallet display and control functionality for +# selecting a wallet, and doing coin control. It can be dropped into any dialog +# and will interface with the dialog with select wlt and coin control callbacks. +class SelectWalletFrame(ArmoryFrame): + def __init__(self, parent, main, layoutDir=VERTICAL, + firstSelect=None, + onlyMyWallets=False, + wltIDList=None, + atLeast=0, + selectWltCallback=None, + coinControlCallback=None, + onlyOfflineWallets=False): + + super(SelectWalletFrame, self).__init__(parent, main) + self.coinControlCallback = coinControlCallback + + self.walletComboBox = QComboBox() + self.walletListBox = QListWidget() + self.balAtLeast = atLeast + self.selectWltCallback = selectWltCallback + self.doVerticalLayout = layoutDir==VERTICAL + + if self.main and len(self.main.walletMap) == 0: + QMessageBox.critical(self, 'No Wallets!', \ + 'There are no wallets to select from. Please create or import ' + 'a wallet first.', QMessageBox.Ok) + self.accept() + return + + self.wltIDList = wltIDList if wltIDList else self.getWalletIdList(onlyOfflineWallets) + + selectedWltIndex = 0 + self.selectedID = None + wltItems = 0 + self.displayIDs = [] + if len(self.wltIDList) > 0: + self.selectedID = self.wltIDList[0] + for wltID in self.wltIDList: + wlt = self.main.walletMap[wltID] + wlttype = determineWalletType(wlt, self.main)[0] + if onlyMyWallets and wlttype == WLTTYPES.WatchOnly: + continue + + self.displayIDs.append(wltID) + if self.doVerticalLayout: + self.walletComboBox.addItem(wlt.labelName) + else: + self.walletListBox.addItem(QListWidgetItem(wlt.labelName)) + + if wltID == firstSelect: + selectedWltIndex = wltItems + self.selectedID = wltID + wltItems += 1 + + if self.doVerticalLayout: + self.walletComboBox.setCurrentIndex(selectedWltIndex) + else: + self.walletListBox.setCurrentRow(selectedWltIndex) + + + self.connect(self.walletComboBox, SIGNAL('currentIndexChanged(int)'), self.updateOnWalletChange) + self.connect(self.walletListBox, SIGNAL('currentRowChanged(int)'), self.updateOnWalletChange) + + # Start the layout + layout = QVBoxLayout() + + lbls = [] + lbls.append(QRichLabel("Wallet ID:", doWrap=False)) + lbls.append(QRichLabel("Name:", doWrap=False)) + lbls.append(QRichLabel("Description:", doWrap=False)) + lbls.append(QRichLabel("Spendable BTC:", doWrap=False)) + + for i in range(len(lbls)): + lbls[i].setAlignment(Qt.AlignLeft | Qt.AlignTop) + lbls[i].setText('' + str(lbls[i].text()) + '') + + self.dispID = QRichLabel('') + self.dispName = QRichLabel('') + self.dispName.setWordWrap(True) + # This line fixes squished text when word wrapping + self.dispName.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) + self.dispDescr = QRichLabel('') + self.dispDescr.setWordWrap(True) + # This line fixes squished text when word wrapping + self.dispDescr.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) + self.dispBal = QMoneyLabel(0) + + self.dispBal.setTextFormat(Qt.RichText) + + wltInfoFrame = QFrame() + wltInfoFrame.setFrameStyle(STYLE_SUNKEN) + wltInfoFrame.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) + frmLayout = QGridLayout() + for i in range(len(lbls)): + frmLayout.addWidget(lbls[i], i, 0, 1, 1) + + self.dispID.setAlignment(Qt.AlignLeft | Qt.AlignTop) + self.dispName.setAlignment(Qt.AlignLeft | Qt.AlignTop) + self.dispDescr.setAlignment(Qt.AlignLeft | Qt.AlignTop) + self.dispBal.setAlignment(Qt.AlignLeft | Qt.AlignTop) + self.dispDescr.setMinimumWidth(tightSizeNChar(self.dispDescr, 30)[0]) + frmLayout.addWidget(self.dispID, 0, 2, 1, 1) + frmLayout.addWidget(self.dispName, 1, 2, 1, 1) + frmLayout.addWidget(self.dispDescr, 2, 2, 1, 1) + frmLayout.addWidget(self.dispBal, 3, 2, 1, 1) + if coinControlCallback: + self.lblCoinCtrl = QRichLabel('Source: All addresses', doWrap=False) + frmLayout.addWidget(self.lblCoinCtrl, 4, 2, 1, 1) + self.btnCoinCtrl = QPushButton('Coin Control') + self.connect(self.btnCoinCtrl, SIGNAL(CLICKED), self.doCoinCtrl) + frmLayout.addWidget(self.btnCoinCtrl, 4, 0, 1, 2) + frmLayout.setColumnStretch(0, 1) + frmLayout.setColumnStretch(1, 1) + frmLayout.setColumnStretch(2, 1) + + frmLayout.addItem(QSpacerItem(20, 10, QSizePolicy.Fixed, QSizePolicy.Expanding), 0, 1, 4, 1) + wltInfoFrame.setLayout(frmLayout) + + if self.doVerticalLayout: + layout.addWidget(makeLayoutFrame(VERTICAL, [self.walletComboBox, wltInfoFrame]) ) + else: + layout.addWidget(makeLayoutFrame(HORIZONTAL, [self.walletListBox, wltInfoFrame]) ) + + self.setLayout(layout) + # Make sure this is called once so that the default selection is displayed + #self.updateOnWalletChange() + + + def getWalletIdList(self, onlyOfflineWallets): + result = [] + if onlyOfflineWallets: + result = self.main.getWatchingOnlyWallets() + else: + result = list(self.main.walletIDList) + return result + + + def getSelectedWltID(self): + idx = -1 + if self.doVerticalLayout: + idx = self.walletComboBox.currentIndex() + else: + idx = self.walletListBox.currentRow() + + return '' if idx<0 else self.displayIDs[idx] + + def doCoinCtrl(self): + wlt = self.main.walletMap[self.getSelectedWltID()] + dlgcc = DlgCoinControl(self, self.main, wlt, self.sourceAddrList) + if dlgcc.exec_(): + self.sourceAddrList = [x[0] for x in dlgcc.coinControlList] + self.altBalance = sum([x[1] for x in dlgcc.coinControlList]) + + nAddr = len(self.sourceAddrList) + if self.altBalance == wlt.getBalance('Spendable'): + self.lblCoinCtrl.setText('Source: All addresses') + self.sourceAddrList = None + self.altBalance = None + elif nAddr == 0: + self.lblCoinCtrl.setText('Source: None selected') + elif nAddr == 1: + aStr = hash160_to_addrStr(self.sourceAddrList[0]) + self.lblCoinCtrl.setText('Source: %s...' % aStr[:12]) + elif nAddr > 1: + self.lblCoinCtrl.setText('Source: %d addresses' % nAddr) + self.updateOnCoinControl() + + def updateOnWalletChange(self, ignoredInt=None): + """ + "ignoredInt" is because the signals should call this function with the + selected index of the relevant container, but we grab it again anyway + using getSelectedWltID() + """ + + wltID = self.getSelectedWltID() + + if len(wltID) > 0: + wlt = self.main.walletMap[wltID] + + self.dispID.setText(wltID) + self.dispName.setText(wlt.labelName) + self.dispDescr.setText(wlt.labelDescr) + self.selectedID = wltID + + if not TheBDM.getBDMState() == 'BlockchainReady': + self.dispBal.setText('-' * 12) + else: + bal = wlt.getBalance('Spendable') + balStr = coin2str(wlt.getBalance('Spendable'), maxZeros=1) + if bal <= self.balAtLeast: + self.dispBal.setText('%s' % balStr) + else: + self.dispBal.setText('' + balStr + '') + if self.selectWltCallback: + self.selectWltCallback(wlt) + self.repaint() + # Reset the coin control variables after a new wallet is selected + if self.coinControlCallback: + self.altBalance = None + self.sourceAddrList = None + self.btnCoinCtrl.setEnabled(wlt.getBalance('Spendable')>0) + self.lblCoinCtrl.setText('Source: All addresses' if wlt.getBalance('Spendable')>0 else\ + 'Source: 0 addresses' ) + self.updateOnCoinControl() + + def updateOnCoinControl(self): + useAllAddr = (self.altBalance == None) + wlt = self.main.walletMap[self.getSelectedWltID()] + fullBal = wlt.getBalance('Spendable') + if useAllAddr: + self.dispID.setText(wlt.uniqueIDB58) + self.dispName.setText(wlt.labelName) + self.dispDescr.setText(wlt.labelDescr) + if fullBal == 0: + self.dispBal.setText('0.0', color='TextRed', bold=True) + else: + self.dispBal.setValueText(fullBal, wBold=True) + else: + self.dispID.setText(wlt.uniqueIDB58 + '*') + self.dispName.setText(wlt.labelName + '*') + self.dispDescr.setText('*Coin Control Subset*', color='TextBlue', bold=True) + self.dispBal.setText(coin2str(self.altBalance, maxZeros=0), color='TextBlue') + rawValTxt = str(self.dispBal.text()) + self.dispBal.setText(rawValTxt + ' (of %s)' % \ + (htmlColor('DisableFG'), coin2str(fullBal, maxZeros=0))) + + if not TheBDM.getBDMState() == 'BlockchainReady': + self.dispBal.setText('(available when online)', color='DisableFG') + self.repaint() + if self.coinControlCallback: + self.coinControlCallback(self.sourceAddrList, self.altBalance) + +# Container for controls used in configuring a wallet to be added to any +# dialog or wizard. Currently it is only used the create wallet wizard. +# Just has Name and Description +# Advanced options have just been moved to their own frame to be used in +# the restore wallet dialog as well. +class NewWalletFrame(ArmoryFrame): + + def __init__(self, parent, main, initLabel=''): + super(NewWalletFrame, self).__init__(parent, main) + self.editName = QLineEdit() + self.editName.setMinimumWidth(tightSizeNChar(self.editName,\ + WALLET_DATA_ENTRY_FIELD_WIDTH)[0]) + self.editName.setText(initLabel) + lblName = QLabel("Wallet &name:") + lblName.setBuddy(self.editName) + + self.editDescription = QTextEdit() + self.editDescription.setMaximumHeight(75) + self.editDescription.setMinimumWidth(tightSizeNChar(self.editDescription,\ + WALLET_DATA_ENTRY_FIELD_WIDTH)[0]) + lblDescription = QLabel("Wallet &description:") + lblDescription.setAlignment(Qt.AlignVCenter) + lblDescription.setBuddy(self.editDescription) + + # breaking this up into tabs + frameLayout = QVBoxLayout() + newWalletTabs = QTabWidget() + + #### Basic Tab + nameFrame = makeHorizFrame([lblName, STRETCH, self.editName]) + descriptionFrame = makeHorizFrame([lblDescription, + STRETCH, self.editDescription]) + basicQTab = makeVertFrame([nameFrame, descriptionFrame, STRETCH]) + newWalletTabs.addTab(basicQTab, "Configure") + + # Fork watching-only wallet + self.advancedOptionsTab = AdvancedOptionsFrame(parent, main) + newWalletTabs.addTab(self.advancedOptionsTab, "Advanced Options") + + frameLayout.addWidget(newWalletTabs) + self.setLayout(frameLayout) + + # These help us collect entropy as the user goes through the wizard + # to be used for wallet creation + self.main.registerWidgetActivateTime(self) + + + def getKdfSec(self): + return self.advancedOptionsTab.getKdfSec() + + def getKdfBytes(self): + return self.advancedOptionsTab.getKdfBytes() + + def getName(self): + return str(self.editName.text()) + + def getDescription(self): + return str(self.editDescription.toPlainText()) + +class AdvancedOptionsFrame(ArmoryFrame): + def __init__(self, parent, main, initLabel=''): + super(AdvancedOptionsFrame, self).__init__(parent, main) + lblComputeDescription = QRichLabel( \ + 'Armory will test your system\'s speed to determine the most ' + 'challenging encryption settings that can be performed ' + 'in a given amount of time. High settings make it much harder ' + 'for someone to guess your passphrase. This is used for all ' + 'encrypted wallets, but the default parameters can be changed below.\n') + lblComputeDescription.setWordWrap(True) + timeDescriptionTip = main.createToolTipWidget( \ + 'This is the amount of time it will take for your computer ' + 'to unlock your wallet after you enter your passphrase. ' + '(the actual time used will be less than the specified ' + 'time, but more than one half of it). ') + + # Set maximum compute time + self.editComputeTime = QLineEdit() + self.editComputeTime.setText('250 ms') + self.editComputeTime.setMaxLength(12) + lblComputeTime = QLabel('Target compute &time (s, ms):') + memDescriptionTip = main.createToolTipWidget( \ + 'This is the maximum memory that will be ' + 'used as part of the encryption process. The actual value used ' + 'may be lower, depending on your system\'s speed. If a ' + 'low value is chosen, Armory will compensate by chaining ' + 'together more calculations to meet the target time. High ' + 'memory target will make GPU-acceleration useless for ' + 'guessing your passphrase.') + lblComputeTime.setBuddy(self.editComputeTime) + + # Set maximum memory usage + self.editComputeMem = QLineEdit() + self.editComputeMem.setText('32.0 MB') + self.editComputeMem.setMaxLength(12) + lblComputeMem = QLabel('Max &memory usage (kB, MB):') + lblComputeMem.setBuddy(self.editComputeMem) + + self.editComputeTime.setMaximumWidth( tightSizeNChar(self, 20)[0] ) + self.editComputeMem.setMaximumWidth( tightSizeNChar(self, 20)[0] ) + + entryFrame = QFrame() + entryLayout = QGridLayout() + entryLayout.addWidget(timeDescriptionTip, 0, 0, 1, 1) + entryLayout.addWidget(lblComputeTime, 0, 1, 1, 1) + entryLayout.addWidget(self.editComputeTime, 0, 2, 1, 1) + entryLayout.addWidget(memDescriptionTip, 1, 0, 1, 1) + entryLayout.addWidget(lblComputeMem, 1, 1, 1, 1) + entryLayout.addWidget(self.editComputeMem, 1, 2, 1, 1) + entryFrame.setLayout(entryLayout) + layout = QVBoxLayout() + layout.addWidget(lblComputeDescription) + layout.addWidget(entryFrame) + layout.addStretch() + self.setLayout(layout) + + def getKdfSec(self): + # return -1 if the input is invalid + kdfSec = -1 + try: + kdfT, kdfUnit = str(self.editComputeTime.text()).strip().split(' ') + if kdfUnit.lower() == 'ms': + kdfSec = float(kdfT) / 1000. + elif kdfUnit.lower() in ('s', 'sec', 'seconds'): + kdfSec = float(kdfT) + except: + pass + return kdfSec + + def getKdfBytes(self): + # return -1 if the input is invalid + kdfBytes = -1 + try: + kdfM, kdfUnit = str(self.editComputeMem.text()).split(' ') + if kdfUnit.lower() == 'mb': + kdfBytes = round(float(kdfM) * (1024.0 ** 2)) + elif kdfUnit.lower() == 'kb': + kdfBytes = round(float(kdfM) * (1024.0)) + except: + pass + return kdfBytes + +class SetPassphraseFrame(ArmoryFrame): + def __init__(self, parent, main, initLabel='', passphraseCallback=None): + super(SetPassphraseFrame, self).__init__(parent, main) + self.passphraseCallback = passphraseCallback + layout = QGridLayout() + lblDlgDescr = QLabel('Please enter a passphrase for wallet encryption.\n\n' + 'A good passphrase consists of at least 10 or more\n' + 'random letters, or 6 or more random words.\n') + lblDlgDescr.setWordWrap(True) + layout.addWidget(lblDlgDescr, 0, 0, 1, 2) + lblPwd1 = QLabel("New Passphrase:") + self.editPasswd1 = QLineEdit() + self.editPasswd1.setEchoMode(QLineEdit.Password) + self.editPasswd1.setMinimumWidth(MIN_PASSWD_WIDTH(self)) + + lblPwd2 = QLabel("Again:") + self.editPasswd2 = QLineEdit() + self.editPasswd2.setEchoMode(QLineEdit.Password) + self.editPasswd2.setMinimumWidth(MIN_PASSWD_WIDTH(self)) + + layout.addWidget(lblPwd1, 1, 0) + layout.addWidget(lblPwd2, 2, 0) + layout.addWidget(self.editPasswd1, 1, 1) + layout.addWidget(self.editPasswd2, 2, 1) + + self.lblMatches = QLabel(' ' * 20) + self.lblMatches.setTextFormat(Qt.RichText) + layout.addWidget(self.lblMatches, 3, 1) + self.setLayout(layout) + self.connect(self.editPasswd1, SIGNAL('textChanged(QString)'), \ + self.checkPassphrase) + self.connect(self.editPasswd2, SIGNAL('textChanged(QString)'), \ + self.checkPassphrase) + + + # These help us collect entropy as the user goes through the wizard + # to be used for wallet creation + self.main.registerWidgetActivateTime(self) + + + # This function is multi purpose. It updates the screen and validates the passphrase + def checkPassphrase(self, sideEffects=True): + result = True + p1 = self.editPasswd1.text() + p2 = self.editPasswd2.text() + goodColor = htmlColor('TextGreen') + badColor = htmlColor('TextRed') + if not isASCII(unicode(p1)) or \ + not isASCII(unicode(p2)): + if sideEffects: + self.lblMatches.setText('Passphrase is non-ASCII!' % badColor) + result = False + elif not p1 == p2: + if sideEffects: + self.lblMatches.setText('Passphrases do not match!' % badColor) + result = False + elif len(p1) < 5: + if sideEffects: + self.lblMatches.setText('Passphrase is too short!' % badColor) + result = False + if sideEffects: + if result: + self.lblMatches.setText('Passphrases match!' % goodColor) + if self.passphraseCallback: + self.passphraseCallback() + return result + + def getPassphrase(self): + return str(self.editPasswd1.text()) + +class VerifyPassphraseFrame(ArmoryFrame): + def __init__(self, parent, main, initLabel=''): + super(VerifyPassphraseFrame, self).__init__(parent, main) + lblWarnImgL = QLabel() + lblWarnImgL.setPixmap(QPixmap(':/MsgBox_warning48.png')) + lblWarnImgL.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) + + lblWarnTxt1 = QRichLabel(\ + '!!! DO NOT FORGET YOUR PASSPHRASE !!!', size=4) + lblWarnTxt1.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) + lblWarnTxt2 = QRichLabel(\ + 'No one can help you recover you bitcoins if you forget the ' + 'passphrase and don\'t have a paper backup! Your wallet and ' + 'any digital backups are useless if you forget it. ' + '

    ' + 'A paper backup protects your wallet forever, against ' + 'hard-drive loss and losing your passphrase. It also protects you ' + 'from theft, if the wallet was encrypted and the paper backup ' + 'was not stolen with it. Please make a paper backup and keep it in ' + 'a safe place.' + '

    ' + 'Please enter your passphrase a third time to indicate that you ' + 'are aware of the risks of losing your passphrase!', doWrap=True) + + + self.edtPasswd3 = QLineEdit() + self.edtPasswd3.setEchoMode(QLineEdit.Password) + self.edtPasswd3.setMinimumWidth(MIN_PASSWD_WIDTH(self)) + + layout = QGridLayout() + layout.addWidget(lblWarnImgL, 0, 0, 4, 1) + layout.addWidget(lblWarnTxt1, 0, 1, 1, 1) + layout.addWidget(lblWarnTxt2, 2, 1, 1, 1) + layout.addWidget(self.edtPasswd3, 5, 1, 1, 1) + self.setLayout(layout) + + # These help us collect entropy as the user goes through the wizard + # to be used for wallet creation + self.main.registerWidgetActivateTime(self) + + +class WalletBackupFrame(ArmoryFrame): + # Some static enums, and a QRadioButton with mouse-enter/mouse-leave events + FEATURES = enum('ProtGen', 'ProtImport', 'LostPass', 'Durable', \ + 'Visual', 'Physical', 'Count') + OPTIONS = enum('Paper1', 'PaperN', 'DigPlain', 'DigCrypt', 'Export', 'Count') + def __init__(self, parent, main, initLabel=''): + super(WalletBackupFrame, self).__init__(parent, main) + # Don't have a wallet yet so assume false. + self.hasImportedAddr = False + self.isBackupCreated = False + self.passphrase = None + self.lblTitle = QRichLabel(tr("Backup Options")) + lblTitleDescr = QRichLabel(tr(""" + Armory wallets only need to be backed up one time, ever. + The backup is good no matter how many addresses you use. """)) + lblTitleDescr.setOpenExternalLinks(True) + + + self.optPaperBackupTop = QRadioButtonBackupCtr(self, \ + tr('Printable Paper Backup'), self.OPTIONS.Paper1) + self.optPaperBackupOne = QRadioButtonBackupCtr(self, \ + tr('Single-Sheet (Recommended)'), self.OPTIONS.Paper1) + self.optPaperBackupFrag = QRadioButtonBackupCtr(self, \ + tr('Fragmented Backup (M-of-N)'), self.OPTIONS.PaperN) + + self.optDigitalBackupTop = QRadioButtonBackupCtr(self, \ + tr('Digital Backup'), self.OPTIONS.DigPlain) + self.optDigitalBackupPlain = QRadioButtonBackupCtr(self, \ + tr('Unencrypted'), self.OPTIONS.DigPlain) + self.optDigitalBackupCrypt = QRadioButtonBackupCtr(self, \ + tr('Encrypted'), self.OPTIONS.DigCrypt) + + self.optIndivKeyListTop = QRadioButtonBackupCtr(self, \ + tr('Export Key Lists'), self.OPTIONS.Export) + + + self.optPaperBackupTop.setFont(GETFONT('Var', bold=True)) + self.optDigitalBackupTop.setFont(GETFONT('Var', bold=True)) + self.optIndivKeyListTop.setFont(GETFONT('Var', bold=True)) + + # I need to be able to unset the sub-options when they become disabled + self.optPaperBackupNONE = QRadioButton('') + self.optDigitalBackupNONE = QRadioButton('') + + btngrpTop = QButtonGroup(self) + btngrpTop.addButton(self.optPaperBackupTop) + btngrpTop.addButton(self.optDigitalBackupTop) + btngrpTop.addButton(self.optIndivKeyListTop) + btngrpTop.setExclusive(True) + + btngrpPaper = QButtonGroup(self) + btngrpPaper.addButton(self.optPaperBackupNONE) + btngrpPaper.addButton(self.optPaperBackupOne) + btngrpPaper.addButton(self.optPaperBackupFrag) + btngrpPaper.setExclusive(True) + + btngrpDig = QButtonGroup(self) + btngrpDig.addButton(self.optDigitalBackupNONE) + btngrpDig.addButton(self.optDigitalBackupPlain) + btngrpDig.addButton(self.optDigitalBackupCrypt) + btngrpDig.setExclusive(True) + + self.connect(self.optPaperBackupTop, SIGNAL(CLICKED), self.optionClicked) + self.connect(self.optPaperBackupOne, SIGNAL(CLICKED), self.optionClicked) + self.connect(self.optPaperBackupFrag, SIGNAL(CLICKED), self.optionClicked) + self.connect(self.optDigitalBackupTop, SIGNAL(CLICKED), self.optionClicked) + self.connect(self.optDigitalBackupPlain, SIGNAL(CLICKED), self.optionClicked) + self.connect(self.optDigitalBackupCrypt, SIGNAL(CLICKED), self.optionClicked) + self.connect(self.optIndivKeyListTop, SIGNAL(CLICKED), self.optionClicked) + + + spacer = lambda: QSpacerItem(20, 1, QSizePolicy.Fixed, QSizePolicy.Expanding) + layoutOpts = QGridLayout() + layoutOpts.addWidget(self.optPaperBackupTop, 0, 0, 1, 2) + layoutOpts.addItem(spacer(), 1, 0) + layoutOpts.addItem(spacer(), 2, 0) + layoutOpts.addWidget(self.optDigitalBackupTop, 3, 0, 1, 2) + layoutOpts.addItem(spacer(), 4, 0) + layoutOpts.addItem(spacer(), 5, 0) + layoutOpts.addWidget(self.optIndivKeyListTop, 6, 0, 1, 2) + + layoutOpts.addWidget(self.optPaperBackupOne, 1, 1) + layoutOpts.addWidget(self.optPaperBackupFrag, 2, 1) + layoutOpts.addWidget(self.optDigitalBackupPlain, 4, 1) + layoutOpts.addWidget(self.optDigitalBackupCrypt, 5, 1) + layoutOpts.setColumnStretch(0, 0) + layoutOpts.setColumnStretch(1, 1) + + frmOpts = QFrame() + frmOpts.setLayout(layoutOpts) + frmOpts.setFrameStyle(STYLE_SUNKEN) + + + self.featuresTips = [None] * self.FEATURES.Count + self.featuresLbls = [None] * self.FEATURES.Count + self.featuresImgs = [None] * self.FEATURES.Count + + + F = self.FEATURES + self.featuresTips[F.ProtGen] = self.main.createToolTipWidget(tr(""" + Every time you click "Receive Bitcoins," a new address is generated. + All of these addresses are generated from a single seed value, which + is included in all backups. Therefore, all addresses that you have + generated so far and will ever generate with this wallet, are + protected by this backup! """)) + if not self.hasImportedAddr: + self.featuresTips[F.ProtImport] = self.main.createToolTipWidget(tr(""" + This wallet does not currently have any imported + addresses, so you can safely ignore this feature!. + When imported addresses are present, backups only protects those + imported before the backup was made! You must replace that + backup if you import more addresses! """)) + else: + self.featuresTips[F.ProtImport] = self.main.createToolTipWidget(tr(""" + When imported addresses are present, backups only protects those + imported before the backup was made! You must replace that + backup if you import more addresses! + Your wallet does contain imported addresses.""")) + self.featuresTips[F.LostPass] = self.main.createToolTipWidget(tr(""" + Lost/forgotten passphrases are, by far, the most common + reason for users losing bitcoins. It is critical you have + at least one backup that works if you forget your wallet + passphrase. """)) + self.featuresTips[F.Durable] = self.main.createToolTipWidget(tr(""" + USB drives and CD/DVD disks are not intended for long-term storage. + They will probably last many years, but not guaranteed + even for 3-5 years. On the other hand, printed text on paper will + last many decades, and useful even when thoroughly faded. """)) + self.featuresTips[F.Visual] = self.main.createToolTipWidget(tr(""" + The ability to look at a backup and determine if + it is still usable. If a digital backup is stored in a safe + deposit box, you have no way to verify its integrity unless + you take a secure computer/device with you. A simple glance at + a paper backup is enough to verify that it is still intact. """)) + self.featuresTips[F.Physical] = self.main.createToolTipWidget(tr(""" + If multiple pieces/fragments are required to restore this wallet. + For instance, encrypted backups require the backup + and the passphrase. This feature is only needed for those + concerned about physical security, not just online security.""")) + + + MkFeatLabel = lambda x: QRichLabel(tr(x), doWrap=False) + self.featuresLbls[F.ProtGen] = MkFeatLabel('Protects All Future Addresses') + self.featuresLbls[F.ProtImport] = MkFeatLabel('Protects Imported Addresses') + self.featuresLbls[F.LostPass] = MkFeatLabel('Forgotten Passphrase') + self.featuresLbls[F.Durable] = MkFeatLabel('Long-term Durability') + self.featuresLbls[F.Visual] = MkFeatLabel('Visual Integrity') + self.featuresLbls[F.Physical] = MkFeatLabel('Multi-Point Protection') + + if not self.hasImportedAddr: + self.featuresLbls[F.ProtImport].setEnabled(False) + + self.lblSelFeat = QRichLabel('', doWrap=False, hAlign=Qt.AlignHCenter) + + layoutFeat = QGridLayout() + layoutFeat.addWidget(self.lblSelFeat, 0, 0, 1, 3) + layoutFeat.addWidget(HLINE(), 1, 0, 1, 3) + for i in range(self.FEATURES.Count): + self.featuresImgs[i] = QLabel('') + layoutFeat.addWidget(self.featuresTips[i], i + 2, 0) + layoutFeat.addWidget(self.featuresLbls[i], i + 2, 1) + layoutFeat.addWidget(self.featuresImgs[i], i + 2, 2) + layoutFeat.setColumnStretch(0, 0) + layoutFeat.setColumnStretch(1, 1) + layoutFeat.setColumnStretch(2, 0) + + frmFeat = QFrame() + frmFeat.setLayout(layoutFeat) + frmFeat.setFrameStyle(STYLE_SUNKEN) + + + self.lblDescrSelected = QRichLabel('') + frmFeatDescr = makeVertFrame([self.lblDescrSelected]) + self.lblDescrSelected.setMinimumHeight(tightSizeNChar(self, 10)[1] * 8) + + self.btnDoIt = QPushButton('Create Backup') + self.connect(self.btnDoIt, SIGNAL(CLICKED), self.clickedDoIt) + + layout = QGridLayout() + layout.addWidget(self.lblTitle, 0, 0, 1, 2) + layout.addWidget(lblTitleDescr, 1, 0, 1, 2) + layout.addWidget(frmOpts, 2, 0) + layout.addWidget(frmFeat, 2, 1) + layout.addWidget(frmFeatDescr, 3, 0, 1, 2) + layout.addWidget(self.btnDoIt, 4, 0, 1, 2) + layout.setRowStretch(0, 0) + layout.setRowStretch(1, 0) + layout.setRowStretch(2, 0) + layout.setRowStretch(3, 1) + layout.setRowStretch(4, 0) + self.setLayout(layout) + self.setMinimumSize(640, 350) + + self.optPaperBackupTop.setChecked(True) + self.optPaperBackupOne.setChecked(True) + self.setDispFrame(-1) + self.optionClicked() + + ############################################################################# + def setWallet(self, wlt): + self.wlt = wlt + wltID = wlt.uniqueIDB58 + wltName = wlt.labelName + self.hasImportedAddr = self.wlt.hasAnyImported() + # Highlight imported-addr feature if their wallet contains them + pcolor = 'TextWarn' if self.hasImportedAddr else 'DisableFG' + self.featuresLbls[self.FEATURES.ProtImport].setText(tr(\ + 'Protects Imported Addresses'), color=pcolor) + if not self.hasImportedAddr: + self.featuresTips[self.FEATURES.ProtImport] = self.main.createToolTipWidget(tr(""" + This wallet does not currently have any imported + addresses, so you can safely ignore this feature!. + When imported addresses are present, backups only protects those + imported before the backup was made! You must replace that + backup if you import more addresses! """)) + else: + self.featuresTips[self.FEATURES.ProtImport] = self.main.createToolTipWidget(tr(""" + When imported addresses are present, backups only protects those + imported before the backup was made! You must replace that + backup if you import more addresses! + Your wallet does contain imported addresses.""")) + self.lblTitle.setText(tr(""" + Backup Options for Wallet "%s" (%s)""" % (wltName, wltID))) + + ############################################################################# + def setDispFrame(self, index): + if index < 0: + self.setDispFrame(self.getIndexChecked()) + else: + # Highlight imported-addr feature if their wallet contains them + pcolor = 'TextWarn' if self.hasImportedAddr else 'DisableFG' + self.featuresLbls[self.FEATURES.ProtImport].setText(tr(\ + 'Protects Imported Addresses'), color=pcolor) + + txtPaper = tr(""" + Paper backups protect every address ever generated by your + wallet. It is unencrypted, which means it needs to be stored + in a secure place, but it will help you recover your wallet + if you forget your encryption passphrase! +

    + You don't need a printer to make a paper backup! + The data can be copied by hand with pen and paper. + Paper backups are preferred to digital backups, because you + know the paper backup will work no matter how many years (or + decades) it sits in storage. """) + txtDigital = tr(""" + Digital backups can be saved to an external hard-drive or + USB removable media. It is recommended you make a few + copies to protect against "bit rot" (degradation).

    """) + txtDigPlain = tr(""" + IMPORTANT: Do not save an unencrypted digital + backup to your primary hard drive! + Please save it directly to the backup device. + Deleting the file does not guarantee the data is actually + gone! """) + txtDigCrypt = tr(""" + IMPORTANT: It is critical that you have at least + one unencrypted backup! Without it, your bitcoins will + be lost forever if you forget your passphrase! This is + by far the most common reason users lose coins! Having + at least one paper backup is recommended.""") + txtIndivKeys = tr(""" + View and export invidivual addresses strings, + public keys and/or private keys contained in your wallet. + This is useful for exporting your private keys to be imported into + another wallet app or service. +

    + You can view/backup imported keys, as well as unused keys in your + keypool (pregenerated addresses protected by your backup that + have not yet been used). """) + + + chk = lambda: QPixmap(':/checkmark32.png').scaled(20, 20) + _X_ = lambda: QPixmap(':/red_X.png').scaled(16, 16) + if index == self.OPTIONS.Paper1: + self.lblSelFeat.setText(tr('Single-Sheet Paper Backup'), bold=True) + self.featuresImgs[self.FEATURES.ProtGen ].setPixmap(chk()) + self.featuresImgs[self.FEATURES.ProtImport].setPixmap(chk()) + self.featuresImgs[self.FEATURES.LostPass ].setPixmap(chk()) + self.featuresImgs[self.FEATURES.Durable ].setPixmap(chk()) + self.featuresImgs[self.FEATURES.Visual ].setPixmap(chk()) + self.featuresImgs[self.FEATURES.Physical ].setPixmap(_X_()) + self.lblDescrSelected.setText(txtPaper) + elif index == self.OPTIONS.PaperN: + self.lblSelFeat.setText(tr('Fragmented Paper Backup'), bold=True) + self.featuresImgs[self.FEATURES.ProtGen ].setPixmap(chk()) + self.featuresImgs[self.FEATURES.ProtImport].setPixmap(_X_()) + self.featuresImgs[self.FEATURES.LostPass ].setPixmap(chk()) + self.featuresImgs[self.FEATURES.Durable ].setPixmap(chk()) + self.featuresImgs[self.FEATURES.Visual ].setPixmap(chk()) + self.featuresImgs[self.FEATURES.Physical ].setPixmap(chk()) + self.lblDescrSelected.setText(txtPaper) + elif index == self.OPTIONS.DigPlain: + self.lblSelFeat.setText(tr('Unencrypted Digital Backup'), bold=True) + self.featuresImgs[self.FEATURES.ProtGen ].setPixmap(chk()) + self.featuresImgs[self.FEATURES.ProtImport].setPixmap(chk()) + self.featuresImgs[self.FEATURES.LostPass ].setPixmap(chk()) + self.featuresImgs[self.FEATURES.Durable ].setPixmap(_X_()) + self.featuresImgs[self.FEATURES.Visual ].setPixmap(_X_()) + self.featuresImgs[self.FEATURES.Physical ].setPixmap(_X_()) + self.lblDescrSelected.setText(txtDigital + txtDigPlain) + elif index == self.OPTIONS.DigCrypt: + self.lblSelFeat.setText(tr('Encrypted Digital Backup'), bold=True) + self.featuresImgs[self.FEATURES.ProtGen ].setPixmap(chk()) + self.featuresImgs[self.FEATURES.ProtImport].setPixmap(chk()) + self.featuresImgs[self.FEATURES.LostPass ].setPixmap(_X_()) + self.featuresImgs[self.FEATURES.Durable ].setPixmap(_X_()) + self.featuresImgs[self.FEATURES.Visual ].setPixmap(_X_()) + self.featuresImgs[self.FEATURES.Physical ].setPixmap(chk()) + self.lblDescrSelected.setText(txtDigital + txtDigCrypt) + elif index == self.OPTIONS.Export: + self.lblSelFeat.setText(tr('Export Key Lists'), bold=True) + self.featuresImgs[self.FEATURES.ProtGen ].setPixmap(chk()) + self.featuresImgs[self.FEATURES.ProtImport].setPixmap(chk()) + self.featuresImgs[self.FEATURES.LostPass ].setPixmap(chk()) + self.featuresImgs[self.FEATURES.Durable ].setPixmap(_X_()) + self.featuresImgs[self.FEATURES.Visual ].setPixmap(_X_()) + self.featuresImgs[self.FEATURES.Physical ].setPixmap(_X_()) + self.lblDescrSelected.setText(txtIndivKeys) + else: + LOGERROR('What index was sent to setDispFrame? %d', index) + + ############################################################################# + def getIndexChecked(self): + if self.optPaperBackupOne.isChecked(): + return self.OPTIONS.Paper1 + elif self.optPaperBackupFrag.isChecked(): + return self.OPTIONS.PaperN + elif self.optPaperBackupTop.isChecked(): + return self.OPTIONS.Paper1 + elif self.optDigitalBackupPlain.isChecked(): + return self.OPTIONS.DigPlain + elif self.optDigitalBackupCrypt.isChecked(): + return self.OPTIONS.DigCrypt + elif self.optDigitalBackupTop.isChecked(): + return self.OPTIONS.DigPlain + elif self.optIndivKeyListTop.isChecked(): + return self.OPTIONS.Export + else: + return 0 + + ############################################################################# + def optionClicked(self): + if self.optPaperBackupTop.isChecked(): + self.optPaperBackupOne.setEnabled(True) + self.optPaperBackupFrag.setEnabled(True) + self.optDigitalBackupPlain.setEnabled(False) + self.optDigitalBackupCrypt.setEnabled(False) + self.optDigitalBackupPlain.setChecked(False) + self.optDigitalBackupCrypt.setChecked(False) + self.optDigitalBackupNONE.setChecked(True) + self.btnDoIt.setText(tr('Create Paper Backup')) + elif self.optDigitalBackupTop.isChecked(): + self.optDigitalBackupPlain.setEnabled(True) + self.optDigitalBackupCrypt.setEnabled(True) + self.optPaperBackupOne.setEnabled(False) + self.optPaperBackupFrag.setEnabled(False) + self.optPaperBackupOne.setChecked(False) + self.optPaperBackupFrag.setChecked(False) + self.optPaperBackupNONE.setChecked(True) + self.btnDoIt.setText(tr('Create Digital Backup')) + elif self.optIndivKeyListTop.isChecked(): + self.optPaperBackupOne.setEnabled(False) + self.optPaperBackupFrag.setEnabled(False) + self.optPaperBackupOne.setChecked(False) + self.optPaperBackupFrag.setChecked(False) + self.optDigitalBackupPlain.setEnabled(False) + self.optDigitalBackupCrypt.setEnabled(False) + self.optDigitalBackupPlain.setChecked(False) + self.optDigitalBackupCrypt.setChecked(False) + self.optDigitalBackupNONE.setChecked(True) + self.optPaperBackupNONE.setChecked(True) + self.btnDoIt.setText(tr('Export Key Lists')) + self.setDispFrame(-1) + + def setPassphrase(self, passphrase): + self.passphrase = passphrase + + def clickedDoIt(self): + isBackupCreated = False + + if self.passphrase: + from qtdialogs import DlgProgress + unlockProgress = DlgProgress(self, self.main, HBar=1, + Title="Unlocking Wallet") + unlockProgress.exec_(self.wlt.unlock, + securePassphrase=SecureBinaryData( \ + self.passphrase), + Progress=unlockProgress.UpdateHBar) + + if self.optPaperBackupOne.isChecked(): + isBackupCreated = OpenPaperBackupWindow('Single', self.parent(), self.main, self.wlt) + elif self.optPaperBackupFrag.isChecked(): + isBackupCreated = OpenPaperBackupWindow('Frag', self.parent(), self.main, self.wlt) + elif self.optDigitalBackupPlain.isChecked(): + if self.main.digitalBackupWarning(): + isBackupCreated = self.main.makeWalletCopy(self, self.wlt, 'Decrypt', 'decrypt') + elif self.optDigitalBackupCrypt.isChecked(): + isBackupCreated = self.main.makeWalletCopy(self, self.wlt, 'Encrypt', 'encrypt') + elif self.optIndivKeyListTop.isChecked(): + if self.wlt.useEncryption and self.wlt.isLocked: + dlg = DlgUnlockWallet(self.wlt, self, self.main, 'Unlock Private Keys') + if not dlg.exec_(): + if self.main.usermode == USERMODE.Expert: + QMessageBox.warning(self, tr('Unlock Failed'), tr(""" + Wallet was not be unlocked. The public keys and addresses + will still be shown, but private keys will not be available + unless you reopen the dialog with the correct passphrase."""), \ + QMessageBox.Ok) + else: + QMessageBox.warning(self, tr('Unlock Failed'), tr(""" + 'Wallet could not be unlocked to display individual keys."""), \ + QMessageBox.Ok) + if self.main.usermode == USERMODE.Standard: + return + DlgShowKeyList(self.wlt, self.parent(), self.main).exec_() + isBackupCreated = True + if isBackupCreated: + self.isBackupCreated = True + + + +class WizardCreateWatchingOnlyWalletFrame(ArmoryFrame): + + def __init__(self, parent, main, initLabel='', backupCreatedCallback=None): + super(WizardCreateWatchingOnlyWalletFrame, self).__init__(parent, main) + + + summaryText = QRichLabel(tr(""" + Your wallet has been created and is ready to be used. It will + appear in the "Available Wallets" list in the main window. + You may click "Finish" if you do not plan to use this + wallet on any other computer. +

    + A watching-only wallet behaves exactly like a a regular + wallet, but does not contain any signing keys. You can generate + addresses and confirm receipt of payments, but not spend or move + the funds in the wallet. To move the funds, + use the "Offline Transactions" button on the main + window for directions (which involves bringing the transaction + to this computer for a signature). Or you can give the + watching-only wallet to someone who needs to monitor the wallet + but should not be able to move the money. +

    + Click the button to save a watching-only copy of this wallet. + Use the "Import or Restore Wallet" button in the + upper-right corner""")) + lbtnForkWlt = QPushButton('Create Watching-Only Copy') + self.connect(lbtnForkWlt, SIGNAL(CLICKED), self.forkOnlineWallet) + layout = QVBoxLayout() + layout.addWidget(summaryText) + layout.addWidget(lbtnForkWlt) + self.setLayout(layout) + + + def forkOnlineWallet(self): + currPath = self.wlt.walletPath + pieces = os.path.splitext(currPath) + currPath = pieces[0] + '.watchonly' + pieces[1] + + saveLoc = self.main.getFileSave('Save Watching-Only Copy', \ + defaultFilename=currPath) + if not saveLoc.endswith('.wallet'): + saveLoc += '.wallet' + self.wlt.forkOnlineWallet(saveLoc, self.wlt.labelName, \ + '(Watching-Only) ' + self.wlt.labelDescr) + + def setWallet(self, wlt): + self.wlt = wlt + +# Need to put circular imports at the end of the script to avoid an import deadlock +from qtdialogs import CLICKED, DlgCoinControl, STRETCH, MIN_PASSWD_WIDTH, \ + QRadioButtonBackupCtr, OpenPaperBackupWindow, DlgUnlockWallet, DlgShowKeyList diff --git a/ui/Wizards.py b/ui/Wizards.py new file mode 100644 index 000000000..c192c887f --- /dev/null +++ b/ui/Wizards.py @@ -0,0 +1,360 @@ +################################################################################ +# # +# Copyright (C) 2011-2014, Armory Technologies, Inc. # +# Distributed under the GNU Affero General Public License (AGPL v3) # +# See LICENSE or http://www.gnu.org/licenses/agpl.html # +# # +################################################################################ + +from PyQt4.Qt import * #@UnusedWildImport +from PyQt4.QtGui import * #@UnusedWildImport +from armoryengine.ArmoryUtils import USE_TESTNET +from ui.WalletFrames import NewWalletFrame, SetPassphraseFrame, VerifyPassphraseFrame,\ + WalletBackupFrame, WizardCreateWatchingOnlyWalletFrame +from ui.TxFrames import SendBitcoinsFrame, SignBroadcastOfflineTxFrame,\ + ReviewOfflineTxFrame +from qtdefines import USERMODE, GETFONT, tr, AddToRunningDialogsList +from armoryengine.PyBtcWallet import PyBtcWallet +from CppBlockUtils import SecureBinaryData +from armoryengine.BDM import TheBDM +from qtdialogs import DlgProgress, DlgConfirmSend + +# This class is intended to be an abstract Wizard class that +# will hold all of the functionality that is common to all +# Wizards in Armory. +class ArmoryWizard(QWizard): + def __init__(self, parent, main): + super(QWizard, self).__init__(parent) + self.setWizardStyle(QWizard.ClassicStyle) + self.parent = parent + self.main = main + self.setFont(GETFONT('var')) + self.setWindowFlags(Qt.Window) + # Need to adjust the wizard frame size whenever the page changes. + self.connect(self, SIGNAL('currentIdChanged(int)'), self.fitContents) + if USE_TESTNET: + self.setWindowTitle('Armory - Bitcoin Wallet Management [TESTNET]') + self.setWindowIcon(QIcon(':/armory_icon_green_32x32.png')) + else: + self.setWindowTitle('Armory - Bitcoin Wallet Management') + self.setWindowIcon(QIcon(':/armory_icon_32x32.png')) + + def fitContents(self): + self.adjustSize() + + @AddToRunningDialogsList + def exec_(self): + return super(ArmoryWizard, self).exec_() + +# This class is intended to be an abstract Wizard Page class that +# will hold all of the functionality that is common to all +# Wizard pages in Armory. +# The layout is QVBoxLayout and holds a single QFrame (self.pageFrame) +class ArmoryWizardPage(QWizardPage): + def __init__(self, wizard, pageFrame): + super(ArmoryWizardPage, self).__init__(wizard) + self.pageFrame = pageFrame + self.pageLayout = QVBoxLayout() + self.pageLayout.addWidget(self.pageFrame) + self.setLayout(self.pageLayout) + + # override this method to implement validators + def validatePage(self): + return True + +################################ Wallet Wizard ################################ +# Wallet Wizard has these pages: +# 1. Create Wallet +# 2. Set Passphrase +# 3. Verify Passphrase +# 4. Create Paper Backup +# 5. Create Watcing Only Wallet +class WalletWizard(ArmoryWizard): + def __init__(self, parent, main): + super(WalletWizard,self).__init__(parent, main) + self.newWallet = None + self.isBackupCreated = False + self.setWindowTitle(tr("Wallet Creation Wizard")) + self.setOption(QWizard.HaveFinishButtonOnEarlyPages, on=True) + self.setOption(QWizard.IgnoreSubTitles, on=True) + + # Page 1: Create Wallet + self.walletCreationPage = WalletCreationPage(self) + self.addPage(self.walletCreationPage) + + # Page 2: Set Passphrase + self.setPassphrasePage = SetPassphrasePage(self) + self.addPage(self.setPassphrasePage) + + # Page 3: Verify Passphrase + self.verifyPassphrasePage = VerifyPassphrasePage(self) + self.addPage(self.verifyPassphrasePage) + + # Page 4: Create Paper Backup + self.walletBackupPage = WalletBackupPage(self) + self.addPage(self.walletBackupPage) + + # Page 5: Create Watching Only Wallet -- but only if expert, or offline + self.hasCWOWPage = False + if self.main.usermode==USERMODE.Expert or not self.main.internetAvail: + self.hasCWOWPage = True + self.createWOWPage = CreateWatchingOnlyWalletPage(self) + self.addPage(self.createWOWPage) + + self.setButtonLayout([QWizard.BackButton, + QWizard.Stretch, + QWizard.NextButton, + QWizard.FinishButton]) + + def initializePage(self, *args, **kwargs): + + if self.currentPage() == self.verifyPassphrasePage: + self.verifyPassphrasePage.setPassphrase( + self.setPassphrasePage.pageFrame.getPassphrase()) + elif self.hasCWOWPage and self.currentPage() == self.createWOWPage: + self.createWOWPage.pageFrame.setWallet(self.newWallet) + + if self.currentPage() == self.walletBackupPage: + self.createNewWalletFromWizard() + self.walletBackupPage.pageFrame.setPassphrase( + self.setPassphrasePage.pageFrame.getPassphrase()) + self.walletBackupPage.pageFrame.setWallet(self.newWallet) + + # Only hide the back button on wallet backup page + self.setButtonLayout([QWizard.Stretch, + QWizard.NextButton, + QWizard.FinishButton]) + else: + self.setButtonLayout([QWizard.BackButton, + QWizard.Stretch, + QWizard.NextButton, + QWizard.FinishButton]) + def done(self, event): + if self.newWallet and not self.walletBackupPage.pageFrame.isBackupCreated: + reply = QMessageBox.question(self, tr('Wallet Backup Warning'), tr(""" + You have not made a backup for your new wallet. You only have + to make a backup of your wallet one time to protect + all the funds held by this wallet any time in the future + (it is a backup of the signing keys, not the coins themselves). +

    + If you do not make a backup, you will permanently lose + the money in this wallet if you ever forget your password, or + suffer from hardware failure. +

    + Are you sure that you want to leave this wizard without backing + up your wallet?"""), \ + QMessageBox.Yes | QMessageBox.No) + if reply == QMessageBox.No: + # Stay in the wizard + return None + return super(WalletWizard, self).done(event) + + def createNewWalletFromWizard(self): + self.newWallet = PyBtcWallet().createNewWallet( \ + securePassphrase=self.setPassphrasePage.pageFrame.getPassphrase(), \ + kdfTargSec=self.walletCreationPage.pageFrame.getKdfSec(), \ + kdfMaxMem=self.walletCreationPage.pageFrame.getKdfBytes(), \ + shortLabel=self.walletCreationPage.pageFrame.getName(), \ + longLabel=self.walletCreationPage.pageFrame.getDescription(), \ + doRegisterWithBDM=False, \ + extraEntropy=self.main.getExtraEntropyForKeyGen()) + + self.newWallet.unlock(securePassphrase= + SecureBinaryData(self.setPassphrasePage.pageFrame.getPassphrase())) + # We always want to fill the address pool, right away. + fillPoolProgress = DlgProgress(self, self.main, HBar=1, \ + Title="Creating Wallet") + fillPoolProgress.exec_(self.newWallet.fillAddressPool, doRegister=False, + Progress=fillPoolProgress.UpdateHBar) + + # Reopening from file helps make sure everything is correct -- don't + # let the user use a wallet that triggers errors on reading it + wltpath = self.newWallet.walletPath + walletFromDisk = PyBtcWallet().readWalletFile(wltpath) + self.main.addWalletToApplication(walletFromDisk, walletIsNew=True) + if TheBDM.getBDMState() in ('Uninitialized', 'Offline'): + TheBDM.registerWallet(walletFromDisk, isFresh=True, wait=False) + else: + self.main.newWalletList.append([walletFromDisk, True]) + + def cleanupPage(self, *args, **kwargs): + if self.hasCWOWPage and self.currentPage() == self.createWOWPage: + self.setButtonLayout([QWizard.Stretch, + QWizard.NextButton, + QWizard.FinishButton]) + else: + self.setButtonLayout([QWizard.BackButton, + QWizard.Stretch, + QWizard.NextButton, + QWizard.FinishButton]) + +class WalletCreationPage(ArmoryWizardPage): + def __init__(self, wizard): + super(WalletCreationPage, self).__init__(wizard, + NewWalletFrame(wizard, wizard.main, "Primary Wallet")) + self.setTitle(tr("Step 1: Create Wallet")) + self.setSubTitle(tr(""" + Create a new wallet for managing your funds. + The name and description can be changed at any time.""")) + + # override this method to implement validators + def validatePage(self): + result = True + if self.pageFrame.getKdfSec() == -1: + QMessageBox.critical(self, 'Invalid Target Compute Time', \ + 'You entered Target Compute Time incorrectly.\n\nEnter: (ms, s)', QMessageBox.Ok) + result = False + elif self.pageFrame.getKdfBytes() == -1: + QMessageBox.critical(self, 'Invalid Max Memory Usage', \ + 'You entered Max Memory Usag incorrectly.\n\nnter: (kb, mb)', QMessageBox.Ok) + result = False + return result + +class SetPassphrasePage(ArmoryWizardPage): + def __init__(self, wizard): + super(SetPassphrasePage, self).__init__(wizard, + SetPassphraseFrame(wizard, wizard.main, "Set Passphrase", self.updateNextButton)) + self.setTitle(tr("Step 2: Set Passphrase")) + self.updateNextButton() + + def updateNextButton(self): + self.emit(SIGNAL("completeChanged()")) + + def isComplete(self): + return self.pageFrame.checkPassphrase(False) + +class VerifyPassphrasePage(ArmoryWizardPage): + def __init__(self, wizard): + super(VerifyPassphrasePage, self).__init__(wizard, + VerifyPassphraseFrame(wizard, wizard.main, "Verify Passphrase")) + self.passphrase = None + self.setTitle(tr("Step 3: Verify Passphrase")) + + def setPassphrase(self, passphrase): + self.passphrase = passphrase + + def validatePage(self): + result = self.passphrase == str(self.pageFrame.edtPasswd3.text()) + if not result: + QMessageBox.critical(self, 'Invalid Passphrase', \ + 'You entered your confirmation passphrase incorrectly!', QMessageBox.Ok) + return result + +class WalletBackupPage(ArmoryWizardPage): + def __init__(self, wizard): + super(WalletBackupPage, self).__init__(wizard, + WalletBackupFrame(wizard, wizard.main, "Backup Wallet")) + self.myWizard = wizard + self.setTitle(tr("Step 4: Backup Wallet")) + self.setFinalPage(True) + +class CreateWatchingOnlyWalletPage(ArmoryWizardPage): + def __init__(self, wizard): + super(CreateWatchingOnlyWalletPage, self).__init__(wizard, + WizardCreateWatchingOnlyWalletFrame(wizard, wizard.main, "Create Watching Only Wallet")) + self.setTitle(tr("Step 5: Create Watching Only Wallet")) + +############################### Offline TX Wizard ############################## +# Offline TX Wizard has these pages: +# 1. Create Transaction +# 2. Sign Transaction on Offline Computer +# 3. Broadcast Transaction +class TxWizard(ArmoryWizard): + def __init__(self, parent, main, wlt, prefill=None, onlyOfflineWallets=False): + super(TxWizard,self).__init__(parent, main) + self.setWindowTitle(tr("Offline Transaction Wizard")) + self.setOption(QWizard.IgnoreSubTitles, on=True) + self.setOption(QWizard.HaveCustomButton1, on=True) + self.setOption(QWizard.HaveFinishButtonOnEarlyPages, on=True) + + # Page 1: Create Offline TX + self.createTxPage = CreateTxPage(self, wlt, prefill, onlyOfflineWallets=onlyOfflineWallets) + self.addPage(self.createTxPage) + + # Page 2: Sign Offline TX + self.reviewOfflineTxPage = ReviewOfflineTxPage(self) + self.addPage(self.reviewOfflineTxPage) + + # Page 3: Broadcast Offline TX + self.signBroadcastOfflineTxPage = SignBroadcastOfflineTxPage(self) + self.addPage(self.signBroadcastOfflineTxPage) + + self.setButtonText(QWizard.NextButton, tr('Create Unsigned Transaction')) + self.setButtonText(QWizard.CustomButton1, tr('Send!')) + self.connect(self, SIGNAL('customButtonClicked(int)'), self.sendClicked) + self.setButtonLayout([QWizard.CancelButton, + QWizard.BackButton, + QWizard.Stretch, + QWizard.NextButton, + QWizard.CustomButton1]) + + + + def initializePage(self, *args, **kwargs): + if self.currentPage() == self.createTxPage: + self.createTxPage.pageFrame.fireWalletChange() + elif self.currentPage() == self.reviewOfflineTxPage: + self.setButtonText(QWizard.NextButton, tr('Next')) + self.setButtonLayout([QWizard.BackButton, + QWizard.Stretch, + QWizard.NextButton, + QWizard.FinishButton]) + self.reviewOfflineTxPage.pageFrame.setTxDp(self.createTxPage.txdp) + self.reviewOfflineTxPage.pageFrame.setWallet( + self.createTxPage.pageFrame.wlt) + + def cleanupPage(self, *args, **kwargs): + if self.currentPage() == self.reviewOfflineTxPage: + self.updateOnSelectWallet(self.createTxPage.pageFrame.wlt) + self.setButtonText(QWizard.NextButton, tr('Create Unsigned Transaction')) + + def sendClicked(self, customButtonIndex): + self.createTxPage.pageFrame.createTxAndBroadcast() + self.accept() + + def updateOnSelectWallet(self, wlt): + if wlt.watchingOnly: + self.setButtonLayout([QWizard.CancelButton, + QWizard.BackButton, + QWizard.Stretch, + QWizard.NextButton]) + else: + self.setButtonLayout([QWizard.CancelButton, + QWizard.BackButton, + QWizard.Stretch, + QWizard.NextButton, + QWizard.CustomButton1]) + +class CreateTxPage(ArmoryWizardPage): + def __init__(self, wizard, wlt, prefill=None, onlyOfflineWallets=False): + super(CreateTxPage, self).__init__(wizard, + SendBitcoinsFrame(wizard, wizard.main, + "Create Transaction", wlt, prefill, + selectWltCallback=self.updateOnSelectWallet, + onlyOfflineWallets=onlyOfflineWallets)) + self.setTitle(tr("Step 1: Create Transaction")) + self.txdp = None + + def validatePage(self): + result = self.pageFrame.validateInputsGetTxDP() + # the validator also computes the transaction and returns it or False if not valid + if result: + self.txdp = result + result = True + return result + + def updateOnSelectWallet(self, wlt): + self.wizard().updateOnSelectWallet(wlt) + +class ReviewOfflineTxPage(ArmoryWizardPage): + def __init__(self, wizard): + super(ReviewOfflineTxPage, self).__init__(wizard, + ReviewOfflineTxFrame(wizard, wizard.main, "Review Offline Transaction")) + self.setTitle(tr("Step 2: Review Offline Transaction")) + self.setFinalPage(True) + +class SignBroadcastOfflineTxPage(ArmoryWizardPage): + def __init__(self, wizard): + super(SignBroadcastOfflineTxPage, self).__init__(wizard, + SignBroadcastOfflineTxFrame(wizard, wizard.main, "Sign/Broadcast Offline Transaction")) + self.setTitle(tr("Step 3: Sign/Broadcast Offline Transaction")) diff --git a/ui/__init__.py b/ui/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dialogs/toolsDialogs.py b/ui/toolsDialogs.py similarity index 87% rename from dialogs/toolsDialogs.py rename to ui/toolsDialogs.py index 2b3755def..25d7bf68a 100644 --- a/dialogs/toolsDialogs.py +++ b/ui/toolsDialogs.py @@ -1,6 +1,6 @@ ################################################################################ # # -# Copyright (C) 2011-2013, Armory Technologies, Inc. # +# Copyright (C) 2011-2014, Armory Technologies, Inc. # # Distributed under the GNU Affero General Public License (AGPL v3) # # See LICENSE or http://www.gnu.org/licenses/agpl.html # # # @@ -14,7 +14,8 @@ from qtdefines import * from qtdialogs import MIN_PASSWD_WIDTH, DlgPasswd3, createAddrBookButton,\ DlgUnlockWallet -from armoryengine import isASCII +from armoryengine.ArmoryUtils import isASCII +from announcefetch import ANNOUNCE_SIGN_PUBKEY class MessageSigningVerificationDialog(ArmoryDialog): @@ -115,7 +116,9 @@ def __init__(self, parent=None, main=None): self.clearFields) def getPrivateKeyFromAddrInput(self): - addr160 = addrStr_to_hash160(str(self.addressLineEdit.text())) + atype, addr160 = addrStr_to_hash160(str(self.addressLineEdit.text())) + if atype==P2SHBYTE: + LOGWARN('P2SH address requested') walletId = self.main.getWalletForAddr160(addr160) wallet = self.main.walletMap[walletId] if wallet.useEncryption and wallet.isLocked: @@ -198,7 +201,7 @@ def __init__(self, parent=None, main=None): self.verifySignatureButton = QPushButton("Verify Signature") self.clearFieldsButton = QPushButton("Clear All") - self.lblSigResult = QRichLabel('') + self.lblSigResult = QRichLabel('', doWrap=False) buttonFrame = makeHorizFrame([self.verifySignatureButton, self.clearFieldsButton,\ 'Stretch', self.lblSigResult]) self.signMessageLayout.addWidget(buttonFrame, 3, 1, 1, 2) @@ -217,28 +220,51 @@ def clearFields(self): self.lblSigResult.setText('') def displayVerifiedBox(self, addrB58, messageString): - # TODO: Fix some hardcoded colors using armorycolors.py instead + atihash160 = hash160(hex_to_binary(ANNOUNCE_SIGN_PUBKEY)) + addrDisp = addrB58 + if addrB58==hash160_to_addrStr(atihash160): + addrDisp = 'Armory Technologies, Inc.' + if CLI_OPTIONS.testAnnounceCode: + ownerStr = tr(""" + Armory Technologies, Inc. + (testing key) has signed the following + block of text:
    """) % htmlColor('TextGreen') + else: + ownerStr = tr(""" + Armory Technologies, Inc. + has signed the following block of text:
    """) % \ + htmlColor('TextGreen') + else: + ownerStr = tr(""" + 'The owner of the following Bitcoin address... +
    +
    + %s +
    +
    + ... has produced a valid signature for + the following message:
    + """) % addrB58 + if addrB58: msg = messageString.replace('\r\n','\n') msg = ' ' + '
    '.join(msg.split('\n')) + # The user will be able to see the entire message + # in the Message Signing/Verification dialog + msg = '
    '.join([line[:60]+ '...'*(len(line)>60) for line in msg.split('
    ')][:12]) MsgBoxCustom(MSGBOX.Good, tr('Verified!'), tr(""" - The owner of the following Bitcoin address... -
    -
    - %s -
    -
    - ...has produced a valid signature for - the following message:
    + %s
    %s


    - Please make sure that the address above (%s...) matches the exact address - you were expecting. A valid signature is meaningless unless it is made - from a recognized address!""") % (addrB58, msg, addrB58[:10])) - self.lblSigResult.setText('Valid Signature by %s!' % addrB58) + Please make sure that the address above (%s...) matches the + exact address you were expecting. A valid signature is meaningless + unless it is made + from a recognized address!""") % (ownerStr, msg, addrB58[:10])) + self.lblSigResult.setText(\ + 'Valid Signature by %s' % addrDisp) else: self.displayInvalidSignatureMessage() @@ -302,6 +328,7 @@ def __init__(self, parent=None, main=None): signatureLabel = QLabel('Signed Message Block:') self.signedMessageBlockTextEdit = QTextEdit() self.signedMessageBlockTextEdit.setStyleSheet("font: 9pt \"Courier\";") + self.signedMessageBlockTextEdit.setAcceptRichText(False) self.signMessageLayout.addWidget(signatureLabel, 0, 0) self.signMessageLayout.addWidget(self.signedMessageBlockTextEdit, 0, 1) @@ -329,3 +356,6 @@ def clearFields(self): super(SignedMessageBlockVerificationWidget, self).clearFields() self.signedMessageBlockTextEdit.setPlainText('') self.messageTextEdit.setPlainText('') + + + diff --git a/unittestold.py b/unittestold.py new file mode 100644 index 000000000..22759df1d --- /dev/null +++ b/unittestold.py @@ -0,0 +1,2479 @@ +################################################################################ +# +# Copyright (C) 2011-2014, Armory Technologies, Inc. +# Distributed under the GNU Affero General Public License (AGPL v3) +# See LICENSE or http://www.gnu.org/licenses/agpl.html +# +################################################################################ +import os +import shutil +import time + +from CppBlockUtils import SecureBinaryData, CryptoECDSA, CryptoAES +import CppBlockUtils as Cpp +from armoryengine import * +from armoryengine.ArmoryUtils import LITTLEENDIAN, BIGENDIAN, hex_to_binary, \ + hash256, coin2str, coin2str_approx, str2coin, binary_to_hex, USE_TESTNET, \ + hash160_to_addrStr, checkAddrStrValid, int_to_binary, BITCOIN_PORT, hex_to_int, \ + ONE_BTC, ARMORY_HOME_DIR, convertKeyDataToAddress, InterruptTestError, \ + addrStr_to_hash160, pprintHex, SettingsFile, uriReservedToPercent, \ + uriPercentToReserved, parseBitcoinURI, RightNow, PyBackgroundThread, \ + BTC_HOME_DIR, FiniteField, SplitSecret, ReconstructSecret, \ + EstimateCumulativeBlockchainSize, bytesToHumanSize, unixTimeToFormatStr +from armoryengine.BDM import TheBDM +from armoryengine.Block import PyBlock +from armoryengine.CoinSelection import pprintUnspentTxOutList, PySelectCoins, \ + calcMinSuggestedFees, sumTxOutList, PyUnspentTxOut +from armoryengine.Networking import quad_to_str, quad_to_binary, binary_to_quad, \ + str_to_quad, PyMessage, ArmoryClientFactory +from armoryengine.PyBtcWallet import BLOCKCHAIN_READONLY +from SDM import satoshiIsAvailable, SatoshiDaemonManager +from armoryengine.Script import PyScriptProcessor +from armoryengine.Transaction import PyTx, pprintLedgerEntry, PyTxIn, PyOutPoint, \ + PyTxOut, PyCreateAndSignTx, getTxOutMultiSigInfo, PyTxDistProposal +import armoryengine.ArmoryUtils + + +LE = LITTLEENDIAN +BE = BIGENDIAN + + +Test_BasicUtils = False +Test_PyBlockUtils = False +Test_CppBlockUtils = True +Test_SimpleAddress = False +Test_MultiSigTx = False +Test_TxSimpleCreate = False +Test_EncryptedAddress = False +Test_EncryptedWallet = False +Test_TxDistProposals = False +Test_SelectCoins = False +Test_CryptoTiming = False +Test_FiniteField = False +Test_PyBkgdThread = False + +Test_NetworkObjects = False +Test_ReactorLoop = False +Test_SettingsFile = False +Test_WalletMigrate = False +Test_AddressBooks = False +Test_URIParse = False + +Test_BkgdThread = False +Test_AsyncBDM = False +Test_Timers = False +Test_EstBlockchain = True + +Test_SatoshiManager = False + +''' +import optparse +parser = optparse.OptionParser(usage="%prog [options]\n"+ + "Connects to a running bitcoin node and "+ + "prints all or part of the best-block-chain.") +parser.add_option("--testnet", dest="testnet", action="store_true", default=False, + help="Speak testnet protocol") + +(options, args) = parser.parse_args() +''' + + + +def testFunction( fnName, expectedOutput, *args, **kwargs): + """ + Provide a function name, inputs and some known outputs + Prints a pass/fail string if the outputs match + """ + fn = getattr(armoryengine.ArmoryUtils, fnName) + actualOutput = fn(*args,**kwargs) + testPassed = (expectedOutput == actualOutput) + passStr = '____PASS____' if testPassed else '***FAIL***' + print '\t', passStr, '( function:', fnName, ')' + if not testPassed: + print '\t','___Inputs___:', args + print '\t','___ExpOut___:', expectedOutput + print '\t','___ActOut___:', actualOutput + + +def printpassorfail(abool): + """ + Print a simple, formatted pass/fail string + """ + w = 60 + if abool: + print '\n' + ' '*w + '*** PASSED ***', + else: + print '\n' + ' '*w + '___ FAILED ___', + + + + + +################################################################################ +################################################################################ +if Test_BasicUtils: + print '' + print '' + print '*** Running Bitcoin engine unit tests ***' + + addr = '1Ncui8YjT7JJD91tkf42dijPnqywbupf7w' # Sam Rushing's BTC address + i = 4093 + hstr = 'fd0f' + bstr = '\xfd\x0f' + + testFunction('int_to_hex', hstr, i ) + testFunction('hex_to_int', i, hstr) + testFunction('int_to_binary', bstr, i ) + testFunction('binary_to_int', i, bstr) + testFunction('hex_to_binary', bstr, hstr) + testFunction('binary_to_hex', hstr, bstr) + testFunction('hex_switchEndian', '67452301', '01234567') + + hstr = '0ffd' + bstr = '\x0f\xfd' + + testFunction('int_to_hex', hstr, i , 2, BIGENDIAN) + testFunction('hex_to_int', i, hstr, BIGENDIAN) + testFunction('int_to_binary', bstr, i , 2, BIGENDIAN) + testFunction('binary_to_int', i, bstr, BIGENDIAN) + + #h = '00000123456789abcdef000000' + #ans = 'aaaaabcdeghjknrsuwxyaaaaaa' + #testFunction('binary_to_typingBase16', ans, h ) + #testFunction('typingBase16_to_binary', h, ans) + + blockhead = '010000001d8f4ec0443e1f19f305e488c1085c95de7cc3fd25e0d2c5bb5d0000000000009762547903d36881a86751f3f5049e23050113f779735ef82734ebf0b4450081d8c8c84db3936a1a334b035b' + blockhash = '1195e67a7a6d0674bbd28ae096d602e1f038c8254b49dfe79d47000000000000' + blockhashBE = '000000000000479de7df494b25c838f0e102d696e08ad2bb74066d7a7ae69511' + + testFunction('ubtc_to_floatStr', '12.05600000', 1205600000) + testFunction('floatStr_to_ubtc', 1205600000, '12.056') + testFunction('float_to_btc', 1205600000, 12.056) + + testFunction('packVarInt', ['A',1], 65) + testFunction('packVarInt', ['\xfd\xff\x00', 3], 255) + testFunction('packVarInt', ['\xfe\x00\x00\x01\x00', 5], 65536) + testFunction('packVarInt', ['\xff\x00\x10\xa5\xd4\xe8\x00\x00\x00', 9], 10**12) + + testFunction('unpackVarInt', [65,1], 'A') + testFunction('unpackVarInt', [255, 3], '\xfd\xff\x00') + testFunction('unpackVarInt', [65536, 5], '\xfe\x00\x00\x01\x00') + testFunction('unpackVarInt', [10**12, 9], '\xff\x00\x10\xa5\xd4\xe8\x00\x00\x00') + + + data = hex_to_binary('11' + 'aa'*31) + dataBE = hex_to_binary('11' + 'aa'*31, endIn=LITTLEENDIAN, endOut=BIGENDIAN) + dataE1 = hex_to_binary('11' + 'aa'*30 + 'ab') + dataE2 = hex_to_binary('11' + 'aa'*29 + 'abab') + dchk = hash256(data)[:4] + testFunction('verifyChecksum', data, data, dchk) + testFunction('verifyChecksum', data, dataBE, dchk, beQuiet=True) + testFunction('verifyChecksum', '', dataE1, dchk, hash256, False, True) # don't fix + testFunction('verifyChecksum', data, dataE1, dchk, hash256, True, True) # try fix + testFunction('verifyChecksum', '', dataE2, dchk, hash256, False, True) # don't fix + testFunction('verifyChecksum', '', dataE2, dchk, hash256, True, True) # try fix + + + verTuple = (0,50,0,0) + verInt = 5000000 + verStr = '0.50' + testFunction('getVersionString', verStr, verTuple) + testFunction('getVersionInt', verInt, verTuple) + testFunction('readVersionString', verTuple, verStr) + testFunction('readVersionInt', verTuple, verInt) + + verTuple = (1,0,12,0) + verInt = 10012000 + verStr = '1.00.12' + testFunction('getVersionString', verStr, verTuple) + testFunction('getVersionInt', verInt, verTuple) + testFunction('readVersionString', verTuple, verStr) + testFunction('readVersionInt', verTuple, verInt) + + verTuple = (0,20,0,108) + verInt = 2000108 + verStr = '0.20.0.108' + testFunction('getVersionString', verStr, verTuple) + testFunction('getVersionInt', verInt, verTuple) + testFunction('readVersionString', verTuple, verStr) + testFunction('readVersionInt', verTuple, verInt) + + miniKey = 'S4b3N3oGqDqR5jNuxEvDwf' + miniPriv = hex_to_binary('0c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d') + testFunction('decodeMiniPrivateKey', miniPriv, miniKey) + + print 'Testing coin2str method' + def printC2S(c): + print str(c).rjust(16), + print coin2str(c).rjust(16), + print coin2str(c,4).rjust(16), + print coin2str(c,2).rjust(16), + print coin2str(c,0).rjust(16), + print coin2str(c,8, maxZeros=6).rjust(16), + print coin2str(c,8, maxZeros=2).rjust(16), + print coin2str(c,6, maxZeros=4).rjust(16), + print coin2str(c,6, maxZeros=4, rJust=False), + print coin2str_approx(c,3) + printC2S(0) + printC2S(1) + printC2S(100) + printC2S(10000) + printC2S(10111) + printC2S(10000000) + printC2S(100000000) + printC2S(1241110000) + printC2S(10000099080) + printC2S(10000099000) + printC2S(10000909001) + printC2S(12345678900) + printC2S(98753178900) + printC2S(-1) + printC2S(-100) + printC2S(-10000) + printC2S(-10000000) + printC2S(-10000090000) + printC2S(-10000990000) + printC2S(-10009090001) + printC2S(-10001090000) + printC2S(100000001090000) + + + print '' + print 'Testing str2coin method' + def printS2C(s): + print ('"'+s+'"').ljust(18) , str2coin(s, roundHighPrec=True) + + printS2C('0.00000000') + printS2C('0.0000') + printS2C('0.0') + printS2C('-0') + printS2C('0.00000001') + printS2C('0.0001') + printS2C('.0001') + printS2C('-.0001') + printS2C('-0.2') + printS2C('-1') + printS2C('-1.0 ') + printS2C(' -1.0 ') + printS2C('-1.') + printS2C('10000000') + printS2C('100000.00000001') + printS2C('0.00000001') + printS2C('0.000000014') + printS2C('0.000000015') + printS2C('0.000000019') + printS2C('0.000000019') + printS2C('0.9999') + printS2C('0.99999999') + printS2C('0.999999999') + + +# Unserialize an reserialize +tx1raw = hex_to_binary( \ + '01000000016290dce984203b6a5032e543e9e272d8bce934c7de4d15fa0fe44d' + 'd49ae4ece9010000008b48304502204f2fa458d439f957308bca264689aa175e' + '3b7c5f78a901cb450ebd20936b2c500221008ea3883a5b80128e55c9c6070aa6' + '264e1e0ce3d18b7cd7e85108ce3d18b7419a0141044202550a5a6d3bb81549c4' + 'a7803b1ad59cdbba4770439a4923624a8acfc7d34900beb54a24188f7f0a4068' + '9d905d4847cc7d6c8d808a457d833c2d44ef83f76bffffffff0242582c0a0000' + '00001976a914c1b4695d53b6ee57a28647ce63e45665df6762c288ac80d1f008' + '000000001976a9140e0aec36fe2545fb31a41164fb6954adcd96b34288ac00000000') +tx2raw = hex_to_binary( \ + '0100000001f658dbc28e703d86ee17c9a2d3b167a8508b082fa0745f55be5144' + 'a4369873aa010000008c49304602210041e1186ca9a41fdfe1569d5d807ca7ff' + '6c5ffd19d2ad1be42f7f2a20cdc8f1cc0221003366b5d64fe81e53910e156914' + '091d12646bc0d1d662b7a65ead3ebe4ab8f6c40141048d103d81ac9691cf13f3' + 'fc94e44968ef67b27f58b27372c13108552d24a6ee04785838f34624b294afee' + '83749b64478bb8480c20b242c376e77eea2b3dc48b4bffffffff0200e1f50500' + '0000001976a9141b00a2f6899335366f04b277e19d777559c35bc888ac40aeeb' + '02000000001976a9140e0aec36fe2545fb31a41164fb6954adcd96b34288ac00000000') + +tx1 = PyTx().unserialize(tx1raw) +tx2 = PyTx().unserialize(tx2raw) + +tx1again = tx1.serialize() +tx2again = tx2.serialize() + + +################################################################################ +################################################################################ +if Test_PyBlockUtils: + + print '' + print 'Testing transaction serialization round trip:' + print '\t Tx1 == PyTx().unserialize( Tx1.serialize() ) ? ', + printpassorfail(tx1raw == tx1again) + print '' + print '\t Tx2 == PyTx().unserialize( Tx2.serialize() ) ? ', + printpassorfail(tx2raw == tx2again) + print '' + + # Here's a full block, which we should be able to parse and process + hexBlock = ( + '01000000eb10c9a996a2340a4d74eaab41421ed8664aa49d18538bab59010000000000005a2f06efa9f2bd804f17877537f2080030cadbfa1eb50e02338117cc' + '604d91b9b7541a4ecfbb0a1a64f1ade70301000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0804cfbb0a1a' + '02360affffffff0100f2052a01000000434104c2239c4eedb3beb26785753463be3ec62b82f6acd62efb65f452f8806f2ede0b338e31d1f69b1ce449558d7061' + 'aa1648ddc2bf680834d3986624006a272dc21cac000000000100000003e8caa12bcb2e7e86499c9de49c45c5a1c6167ea4b894c8c83aebba1b6100f343010000' + '008c493046022100e2f5af5329d1244807f8347a2c8d9acc55a21a5db769e9274e7e7ba0bb605b26022100c34ca3350df5089f3415d8af82364d7f567a6a297f' + 'cc2c1d2034865633238b8c014104129e422ac490ddfcb7b1c405ab9fb42441246c4bca578de4f27b230de08408c64cad03af71ee8a3140b40408a7058a1984a9' + 'f246492386113764c1ac132990d1ffffffff5b55c18864e16c08ef9989d31c7a343e34c27c30cd7caa759651b0e08cae0106000000008c4930460221009ec9aa' + '3e0caf7caa321723dea561e232603e00686d4bfadf46c5c7352b07eb00022100a4f18d937d1e2354b2e69e02b18d11620a6a9332d563e9e2bbcb01cee559680a' + '014104411b35dd963028300e36e82ee8cf1b0c8d5bf1fc4273e970469f5cb931ee07759a2de5fef638961726d04bd5eb4e5072330b9b371e479733c942964bb8' + '6e2b22ffffffff3de0c1e913e6271769d8c0172cea2f00d6d3240afc3a20f9fa247ce58af30d2a010000008c493046022100b610e169fd15ac9f60fe2b507529' + '281cf2267673f4690ba428cbb2ba3c3811fd022100ffbe9e3d71b21977a8e97fde4c3ba47b896d08bc09ecb9d086bb59175b5b9f03014104ff07a1833fd8098b' + '25f48c66dcf8fde34cbdbcc0f5f21a8c2005b160406cbf34cc432842c6b37b2590d16b165b36a3efc9908d65fb0e605314c9b278f40f3e1affffffff0240420f' + '00000000001976a914adfa66f57ded1b655eb4ccd96ee07ca62bc1ddfd88ac007d6a7d040000001976a914981a0c9ae61fa8f8c96ae6f8e383d6e07e77133e88' + 'ac00000000010000000138e7586e0784280df58bd3dc5e3d350c9036b1ec4107951378f45881799c92a4000000008a47304402207c945ae0bbdaf9dadba07bdf' + '23faa676485a53817af975ddf85a104f764fb93b02201ac6af32ddf597e610b4002e41f2de46664587a379a0161323a85389b4f82dda014104ec8883d3e4f7a3' + '9d75c9f5bb9fd581dc9fb1b7cdf7d6b5a665e4db1fdb09281a74ab138a2dba25248b5be38bf80249601ae688c90c6e0ac8811cdb740fcec31dffffffff022f66' + 'ac61050000001976a914964642290c194e3bfab661c1085e47d67786d2d388ac2f77e200000000001976a9141486a7046affd935919a3cb4b50a8a0c233c286c' + '88ac00000000') + + blk = PyBlock().unserialize( hex_to_binary(hexBlock) ) + blockReHex = binary_to_hex(blk.serialize()) + print '' + print 'Testing block serialization round trip:' + print '\t theBlock == Block().unserialize( theBlock.serialize() ) ? ', + printpassorfail(hexBlock == blockReHex) + print '' + + + binRoot = blk.blockData.getMerkleRoot() + print '' + print 'Testing merkle tree calculation:' + print '\tMerkleRoot in block header:', binary_to_hex(blk.blockHeader.merkleRoot) + print '\tMerkleRoot calculated: ', binary_to_hex(binRoot) + print '\tRoot calculation verified? ', + printpassorfail(blk.blockHeader.merkleRoot == blk.blockData.merkleRoot) + print '' + print '' + + +################################################################################ +################################################################################ +if Test_CppBlockUtils: + + print '\n\nLoading Blockchain from:', BLKFILE_FIRSTFILE + BDM_LoadBlockchainFile(BLKFILE_FIRSTFILE) + print 'Done!' + + + print '\n\nCurrent Top Block is:', TheBDM.getTopBlockHeader().getBlockHeight() + TheBDM.getTopBlockHeader().pprint() + + + #print '\n\nChecking integrity of blockchain:' + #result = TheBDM.verifyBlkFileIntegrity() + #print 'Done!', + #if result==True: + #print 'No errors detected in the blk0001.dat file' + #else: + #print 'Integrity check failed! Something is wrong with your blk0001.dat file.' + + cppWlt = Cpp.BtcWallet() + + if not USE_TESTNET: + cppWlt.addAddress_1_(hex_to_binary("604875c897a079f4db88e5d71145be2093cae194")) + cppWlt.addAddress_1_(hex_to_binary("8996182392d6f05e732410de4fc3fa273bac7ee6")) + cppWlt.addAddress_1_(hex_to_binary("b5e2331304bc6c541ffe81a66ab664159979125b")) + cppWlt.addAddress_1_(hex_to_binary("ebbfaaeedd97bc30df0d6887fd62021d768f5cb8")) + cppWlt.addAddress_1_(hex_to_binary("11b366edfc0a8b66feebae5c2e25a7b6a5d1cf31")) + else: + # Test-network addresses + cppWlt.addAddress_1_(hex_to_binary("5aa2b7e93537198ef969ad5fb63bea5e098ab0cc")) + cppWlt.addAddress_1_(hex_to_binary("28b2eb2dc53cd15ab3dc6abf6c8ea3978523f948")) + cppWlt.addAddress_1_(hex_to_binary("720fbde315f371f62c158b7353b3629e7fb071a8")) + cppWlt.addAddress_1_(hex_to_binary("0cc51a562976a075b984c7215968d41af43be98f")) + cppWlt.addAddress_1_(hex_to_binary("57ac7bfb77b1f678043ac6ea0fa67b4686c271e5")) + cppWlt.addAddress_1_(hex_to_binary("b11bdcd6371e5b567b439cd95d928e869d1f546a")) + cppWlt.addAddress_1_(hex_to_binary("2bb0974f6d43e3baa03d82610aac2b6ed017967d")) + cppWlt.addAddress_1_(hex_to_binary("61d62799e52bc8ee514976a19d67478f25df2bb1")) + + # We do the scan three times to make sure that there are no problems + # with rescanning the same tx's multiple times (it's bound to happen + # so might as well make sure it's robust) + TheBDM.scanBlockchainForTx(cppWlt) + TheBDM.scanBlockchainForTx(cppWlt) + TheBDM.scanBlockchainForTx(cppWlt) + + nAddr = cppWlt.getNumAddr() + print 'Address Balances:' + for i in range(nAddr): + cppAddr = cppWlt.getAddrByIndex(i) + bal = cppAddr.getBalance() + print ' %s %s' % (hash160_to_addrStr(cppAddr.getAddrStr20())[:12], coin2str(bal)) + + leVect = cppWlt.getTxLedger() + print '\n\nLedger for all Addr:' + for le in leVect: + pprintLedgerEntry(le, ' '*3) + + + + #TestNonStd + # Not sure what happened to this test... + #bdm.findAllNonStdTx(); + + +################################################################################ +################################################################################ +if Test_SimpleAddress: + + # Execute the tests with Satoshi's public key from the Bitcoin specification page + satoshiPubKeyHex = '04fc9702847840aaf195de8442ebecedf5b095cdbb9bc716bda9110971b28a49e0ead8564ff0db22209e0374782c093bb899692d524e9d6a6956e7c5ecbcd68284' + satoshiAddrStr = '1AGRxqDa5WjUKBwHB9XYEjmkv1ucoUUy1s' + addrPiece1Hex = '65a4358f4691660849d9f235eb05f11fabbd69fa' + addrPiece2Hex = 'd8b2307a' + addrPiece1Bin = hex_to_binary(addrPiece1Hex) + addrPiece2Bin = hex_to_binary(addrPiece2Hex) + + print '\nTesting ECDSA key/address methods:' + print "\tSatoshi's PubKey: ", satoshiPubKeyHex[:32], '...' + print "\tSatoshi's Address: ", satoshiAddrStr + saddr = PyBtcAddress().createFromPublicKey( hex_to_binary(satoshiPubKeyHex) ) + print '' + print '\tAddr calc from pubkey: ', saddr.calculateAddrStr() + print '\tAddress is valid: ', checkAddrStrValid(satoshiAddrStr) + + + ################################################################################ + addr = PyBtcAddress().createNewRandomAddress() + msg = int_to_binary(39029348428) + theHash = hash256(msg) + derSig = addr.generateDERSignature(theHash) + print 'Testing ECDSA signing & verification -- arbitrary binary strings:', + printpassorfail( addr.verifyDERSignature( theHash, derSig)) + print '' + + + ################################################################################ + # From tx tests before, we have tx1 and tx2, where tx2 uses and output from tx1 + sp = PyScriptProcessor() + sp.setTxObjects(tx1, tx2, 0) + print 'Testing ECDSA signing & verification -- two linked transactions: ', + printpassorfail( sp.verifyTransactionValid() ) + print '' + + + + + + +################################################################################ +################################################################################ +if Test_NetworkObjects: + print '\n' + print '*********************************************************************' + print 'Testing networking object ser/unser tests' + print '*********************************************************************' + print '' + + print 'Testing standard IPv4 address conversions' + addrQuad = (192, 168, 1, 125) + print addrQuad, '-->', quad_to_str(addrQuad) + addrBin = quad_to_binary( addrQuad) + print addrQuad, '-->', binary_to_hex(addrBin) + print binary_to_hex(addrBin), '-->', binary_to_quad(addrBin) + addrStr = '192.168.1.125' + print addrStr, '-->', str_to_quad(addrStr) + + + netAddrHex = ('f9beb4d9 61646472 00000000 00000000' + '1f000000 689dcea8 01d6c7db 4e010000' + '00000000 00000000 00000000 000000ff' + 'ff0233b6 ec208d' ).replace(' ','') + + invHex = ('f9beb4d9 696e7600 00000000 00000000' + '25000000 fef89552 01010000 0021eca1' + '50d3f7cd 5eca5ada 7ad02f8f 3bf38420' + '0cb53e8d d51b153d e92bac7a 1b' ).replace(' ','') + + getDataHex = ('f9beb4d9 67657464 61746100 00000000' + '25000000 f51e33f8 01010000 0018c643' + '1b6200ec 361a9e80 31c174ad 5e4fc5f9' + '26b2f2df d3acdb62 7cbf87b8 20' ).replace(' ','') + + msgtxHex = ( + 'f9beb4d9 74780000 00000000 00000000 02010000 18c6431b 01000000 01bc9ea8' + '21256fb0 eb081274 bc7afdde 6d5a4b63 6c55cfbe 2befa8f0 0a1c79e5 fc000000' + '008b4830 45022009 4e0a68c5 5d515b23 310cc0e2 227bbfb8 cd775bb7 f9bedff1' + '01ba06a0 637bee02 2100f81a 11389610 ab92d592 de1cc283 5f0804a0 49baae8b' + 'd20b4aeb e29cbb82 6aba0141 04fc5c28 d283c217 a857ae2a bfebcf11 33dec9d5' + 'd51bb918 c5d75326 2b3cc90a 48504bde 41993614 be6ea62e e531ce4a 4723b550' + 'b3e50492 f320c65d 10d021a2 45ffffff ff02002f 5f1c0000 00001976 a914835b' + '78efa362 ad78474c 14c2043b 35adc697 706a88ac 807f3d36 00000000 1976a914' + '188f9581 3b59ca6b 8e9eadc6 9fecd33e c48d65de 88ac0000 0000' + ).replace(' ','') + + + msgVerHex = ( + 'f9beb4d9 76657273 696f6e00 00000000 55000000 409c0000 01000000 00000000' + 'ff4edc4e 00000000 01000000 00000000 00000000 00000000 0000ffff 7f000001' + '208d0100 00000000 00000000 00000000 00000000 ffff7f00 0001d447 61d0a76a' + '8ad8e4c7 00ffffff ff' ).replace(' ','') + + + msgVerack = ('f9beb4d9 76657261 636b0000 00000000 00000000').replace(' ','') + + + msgblk = '' + if os.path.exists('msgblock.bin'): + with open('msgblock.bin') as f: + msgblk = f.read() + + msgTest = PyMessage().unserialize(hex_to_binary(msgVerHex)) + msgTest.pprint() + ser = msgTest.serialize() + msgTest = PyMessage().unserialize(ser) + msgTest.pprint() + printpassorfail(ser==msgTest.serialize()) + + msgTest = PyMessage().unserialize(hex_to_binary(msgVerack)) + msgTest.pprint() + ser = msgTest.serialize() + msgTest = PyMessage().unserialize(ser) + msgTest.pprint() + printpassorfail(ser==msgTest.serialize()) + + msgTest = PyMessage().unserialize(hex_to_binary(netAddrHex)) + msgTest.pprint() + ser = msgTest.serialize() + msgTest = PyMessage().unserialize(ser) + msgTest.pprint() + printpassorfail(ser==msgTest.serialize()) + + + msgTest = PyMessage().unserialize(hex_to_binary(invHex)) + msgTest.pprint() + ser = msgTest.serialize() + msgTest = PyMessage().unserialize(ser) + msgTest.pprint() + printpassorfail(ser==msgTest.serialize()) + + + msgTest = PyMessage().unserialize(hex_to_binary(getDataHex)) + msgTest.pprint() + ser = msgTest.serialize() + msgTest = PyMessage().unserialize(ser) + msgTest.pprint() + printpassorfail(ser==msgTest.serialize()) + + + msgTest = PyMessage().unserialize(hex_to_binary(msgtxHex)) + msgTest.pprint() + ser = msgTest.serialize() + msgTest = PyMessage().unserialize(ser) + msgTest.pprint() + printpassorfail(ser==msgTest.serialize()) + + + # 36 kB of data on the screen is unnecessary under most circumstances... + print '\n\nTesting blk data reading:' + msgTest = PyMessage().unserialize(msgblk) + #msgTest.pprint() + msgTest.payload.header.pprint(nIndent=1) + print ' NumTx: ', len(msgTest.payload.txList) + print ' ...\n' + ser = msgTest.serialize() + msgTest = PyMessage().unserialize(ser) + msgTest.payload.header.pprint(nIndent=1) + print ' NumTx: ', len(msgTest.payload.txList) + print ' ...\n' + #msgTest.pprint() + printpassorfail(ser==msgTest.serialize()) + + + if Test_ReactorLoop: + ################################################################################ + # Now test the networking: must have Satoshi client open + print '\n\n' + print 'Running python-twisted networking/reactor tests' + print 'If this test works, it will connect to the localhost' + print 'Bitcoin client, display all incoming messages, and' + print 'request new transactions that we see from inv messages.' + print 'You will have to manually stop this test with ctrl-C' + from twisted.internet.protocol import Protocol, ClientFactory + from twisted.internet.defer import Deferred + from twisted.internet import reactor + + # Load blockchain so that we can test ALL the code + BDM_LoadBlockchainFile() + btcNetFactory = None + + def restartConnection(protoObj, failReason): + print '!Trying to restart connection' + from twisted.internet import reactor + reactor.connectTCP(protoObj.peer[0], protoObj.peer[1], btcNetFactory) + + # On handshake complete, do nothing special, but we do want to tell it to + # restart the connection + btcNetFactory = ArmoryClientFactory( \ + TheBDM, + def_handshake=None, \ + func_loseConnect=restartConnection) + + from twisted.internet import reactor + reactor.connectTCP('127.0.0.1', BITCOIN_PORT, btcNetFactory) + reactor.run() + + +################################################################################ +################################################################################ +if Test_TxSimpleCreate: + + print 'Testing PyCreateAndSignTx' + AddrA = PyBtcAddress().createFromPrivateKey(hex_to_int('aa'*32)) + AddrB = PyBtcAddress().createFromPrivateKey(hex_to_int('bb'*32)) + print ' Address A:', AddrA.getAddrStr() + print ' Address B:', AddrB.getAddrStr() + # This TxIn will be completely ignored, so it can contain garbage + txinA = PyTxIn() + txinA.outpoint = PyOutPoint().unserialize(hex_to_binary('00'*36)) + txinA.binScript = hex_to_binary('99'*4) + txinA.sequence = hex_to_binary('ff'*4) + + txoutA = PyTxOut() + txoutA.value = 50 * ONE_BTC + txoutA.binScript = '\x76\xa9\x14' + AddrA.getAddr160() + '\x88\xac' + + tx1 = PyTx() + tx1.version = 1 + tx1.numInputs = 1 + tx1.inputs = [txinA] + tx1.numOutputs = 1 + tx1.outputs = [txoutA] + tx1.locktime = 0 + + tx1hash = tx1.getHash() + print 'Creating transaction to send coins from A to B' + tx2 = PyCreateAndSignTx( [[ AddrA, tx1, 0 ]], [[AddrB, 50*(10**8)]]) + + print 'Verifying the transaction we just created', + psp = PyScriptProcessor() + psp.setTxObjects(tx1, tx2, 0) + verifResult = psp.verifyTransactionValid() + printpassorfail( verifResult) + + + # I made these two tx in a fake blockchain... but they should still work + tx1 = PyTx().unserialize(hex_to_binary( ( + '01000000 0163451d 1002611c 1388d5ba 4ddfdf99 196a86b5 990fb5b0 dc786207' + '4fdcb8ee d2000000 004a4930 46022100 cb02fb5a 910e7554 85e3578e 6e9be315' + 'a161540a 73f84ee6 f5d68641 925c59ac 0221007e 530a1826 30b50e2c 12dd09cd' + 'ebfd809f 038be982 bdc2c7e9 d4cbf634 9e088d01 ffffffff 0200ca9a 3b000000' + '001976a9 14cb2abd e8bccacc 32e893df 3a054b9e f7f227a4 ce88ac00 286bee00' + '00000019 76a914ee 26c56fc1 d942be8d 7a24b2a1 001dd894 69398088 ac000000' + '00' ).replace(' ',''))) + + tx2 = PyTx().unserialize(hex_to_binary( ( + '01000000 01a5b837 da38b64a 6297862c ba8210d0 21ac59e1 2b7c6d7e 70c355f6' + '972ee7a8 6e010000 008c4930 46022100 89e47100 d88d5f8c 8f62a796 dac3afb8' + 'f090c6fc 2eb0c4af ac7b7567 3a364c01 0221002b f40e554d ae51264b 0a86df17' + '3e45756a 89bbd302 4f166cc4 2cfd1874 13636901 41046868 0737c76d abb801cb' + '2204f57d be4e4579 e4f710cd 67dc1b42 27592c81 e9b5cf02 b5ac9e8b 4c9f49be' + '5251056b 6a6d011e 4c37f6b6 d17ede6b 55faa235 19e2ffff ffff0100 286bee00' + '00000019 76a914c5 22664fb0 e55cdc5c 0cea73b4 aad97ec8 34323288 ac000000' + '00' ).replace(' ',''))) + + print '\nVerify tx from fake blockchain :', + psp = PyScriptProcessor() + psp.setTxObjects(tx1, tx2, 0) + verifResult = psp.verifyTransactionValid() + printpassorfail( verifResult) + +################################################################################ +################################################################################ +if Test_MultiSigTx: + print '\n' + print '*********************************************************************' + print 'Testing Multi-signature transaction verification' + print '*********************************************************************' + print '' + # 2-of-2 transaction + tx1 = PyTx().unserialize(hex_to_binary('010000000189a0022c8291b4328338ec95179612b8ebf72067051de019a6084fb97eae0ebe000000004a4930460221009627882154854e3de066943ba96faba02bb8b80c1670a0a30d0408caa49f03df022100b625414510a2a66ebb43fffa3f4023744695380847ee1073117ec90cb60f2c8301ffffffff0210c18d0000000000434104a701496f10db6aa8acbb6a7aa14d62f4925f8da03de7f0262010025945f6ebcc3efd55b6aa4bc6f811a0dc1bbdd2644bdd81c8a63766aa11f650cd7736bbcaf8ac001bb7000000000043526b006b7dac7ca914fc1243972b59c1726735d3c5cca40e415039dce9879a6c936b7dac7ca914375dd72e03e7b5dbb49f7e843b7bef4a2cc2ce9e879a6c936b6c6ca200000000')) + tx2 = PyTx().unserialize(hex_to_binary('01000000011c9608650a912be7fa88eecec664e6fbfa4b676708697fa99c28b3370005f32d01000000fd1701483045022017462c29efc9158cf26f2070d444bb2b087b8a0e6287a9274fa36fad30c46485022100c6d4cc6cd504f768389637df71c1ccd452e0691348d0f418130c31da8cc2a6e8014104e83c1d4079a1b36417f0544063eadbc44833a992b9667ab29b4ff252d8287687bad7581581ae385854d4e5f1fcedce7de12b1aec1cb004cabb2ec1f3de9b2e60493046022100fdc7beb27de0c3a53fbf96df7ccf9518c5fe7873eeed413ce17e4c0e8bf9c06e022100cc15103b3c2e1f49d066897fe681a12e397e87ed7ee39f1c8c4a5fef30f4c2c60141047cf315904fcc2e3e2465153d39019e0d66a8aaec1cec1178feb10d46537427239fd64b81e41651e89b89fefe6a23561d25dddc835395dd3542f83b32a1906aebffffffff01c0d8a700000000001976a914fc1243972b59c1726735d3c5cca40e415039dce988ac00000000')) + + print '\nVerify 2-of-2 tx from Testnet :', + psp = PyScriptProcessor() + psp.setTxObjects(tx1, tx2, 0) + verifResult = psp.verifyTransactionValid() + printpassorfail( verifResult) + + # 2-of-3 transaction + tx1 = PyTx().unserialize(hex_to_binary('010000000371c06e0639dbe6bc35e6f948da4874ae69d9d91934ec7c5366292d0cbd5f97b0010000008a47304402200117cdd3ec6259af29acea44db354a6f57ac10d8496782033f5fe0febfd77f1b02202ceb02d60dbb43e6d4e03e5b5fbadc031f8bbb3c6c34ad307939947987f600bf01410452d63c092209529ca2c75e056e947bc95f9daffb371e601b46d24377aaa3d004ab3c6be2d6d262b34d736b95f3b0ef6876826c93c4077d619c02ebd974c7facdffffffffa65aa866aa7743ec05ba61418015fc32ecabd99886732056f1d4454c8f762bf8000000008c493046022100ea0a9b41c9372837e52898205c7bebf86b28936a3ee725672d0ca8f434f876f0022100beb7243a51fbc0997e55cb519d3b9cbd59f7aba68d80ba1e8adbb53443cda3c00141043efd1ca3cffc50638031281d227ff347a3a27bc145e2f846891d29f87bc068c27710559c4d9cd71f7e9e763d6e2753172406eb1ed1fadcaf9a8972b4270f05b4ffffffffd866d14151ee1b733a2a7273f155ecb25c18303c31b2c4de5aa6080aef2e0006000000008b483045022052210f95f6b413c74ce12cfc1b14a36cb267f9fa3919fa6e20dade1cd570439f022100b9e5b325f312904804f043d06c6ebc8e4b1c6cd272856c48ab1736b9d562e10c01410423fdddfe7e4d70d762dd6596771e035f4b43d54d28c2231be1102056f81f067914fe4fb6fd6e3381228ee5587ddd2028c846025741e963d9b1d6cf2c2dea0dbcffffffff0210ef3200000000004341048a33e9fd2de28137574cc69fe5620199abe37b7d08a51c528876fe6c5fa7fc28535f5a667244445e79fffc9df85ec3d79d77693b1f37af0e2d7c1fa2e7113a48acc0d454070000000061526b006b7dac7ca9143cd1def404e12a85ead2b4d3f5f9f817fb0d46ef879a6c936b7dac7ca9146a4e7d5f798e90e84db9244d4805459f87275943879a6c936b7dac7ca914486efdd300987a054510b4ce1148d4ad290d911e879a6c936b6c6ca200000000')) + tx2 = PyTx().unserialize(hex_to_binary('01000000012f654d4d1d7246d1a824c5b6c5177c0b5a1983864579aabb88cabd5d05e032e201000000fda0014730440220151ad44e7f78f9e0c4a3f2135c19ca3de8dbbb7c58893db096c0c5f1573d5dec02200724a78c3fa5f153103cb46816df46eb6cfac3718038607ddec344310066161e01410459fd82189b81772258a3fc723fdda900eb8193057d4a573ee5ad39e26b58b5c12c4a51b0edd01769f96ed1998221daf0df89634a7137a8fa312d5ccc95ed8925483045022100ca34834ece5925cff6c3d63e2bda6b0ce0685b18f481c32e70de9a971e85f12f0220572d0b5de0cf7b8d4e28f4914a955e301faaaa42f05feaa1cc63b45f938d75d9014104ce6242d72ee67e867e6f8ec434b95fcb1889c5b485ec3414df407e11194a7ce012eda021b68f1dd124598a9b677d6e7d7c95b1b7347f5c5a08efa628ef0204e1483045022074e01e8225e8c4f9d0b3f86908d42a61e611f406e13817d16240f94f52f49359022100f4c768dd89c6435afd3834ae2c882465ade92d7e1cc5c2c2c3d8d25c41b3ea61014104ce66c9f5068b715b62cc1622572cd98a08812d8ca01563045263c3e7af6b997e603e8e62041c4eb82dfd386a3412c34c334c34eb3c76fb0e37483fc72323f807ffffffff01b0ad5407000000001976a9146a4e7d5f798e90e84db9244d4805459f8727594388ac00000000')) + + print '\nVerify 2-of-3 tx from Testnet :', + psp = PyScriptProcessor() + psp.setTxObjects(tx1, tx2, 0) + verifResult = psp.verifyTransactionValid() + printpassorfail( verifResult) + + + # Check Multisig + tx1 = PyTx().unserialize(hex_to_binary('0100000001845ad165bdc0f9b5829cf5a594c4148dfd89e24756303f3a8dabeb597afa589b010000008b483045022063c233df8efa3d1885e069e375a8eabf16b23475ef21bdc9628a513ee4caceb702210090a102c7b602043e72b34a154d495ac19b3b9e42acb962c399451f2baead8f4c014104b38f79037ad25b84a564eaf53ede93dec70b35216e6682aa71a47cefa2996ec49acfbb0a8730577c62ef9a7cc20c740aaaaee75419bef9640a4216c2b49c42d3ffffffff02000c022900000000434104c08c0a71ccbe838403e3870aa1ab871b0ab3a6014b0ba41f6df2b9aefea73134ecaa0b27797620e402a33799e9047f86519d9e43bbd504cf753c293752933f4fac406f40010000000062537a7652a269537a829178a91480677c5392220db736455533477d0bc2fba65502879b69537a829178a91402d7aa2e76d9066fb2b3c41ff8839a5c81bdca19879b69537a829178a91410039ce4fdb5d4ee56148fe3935b9bfbbe4ecc89879b6953ae00000000')) + tx2 = PyTx().unserialize(hex_to_binary('0100000001bb664ff716b9dfc831bcc666c1767f362ad467fcfbaf4961de92e45547daab8701000000fd190100493046022100d73f633f114e0e0b324d87d38d34f22966a03b072803afa99c9408201f6d6dc6022100900e85be52ad2278d24e7edbb7269367f5f2d6f1bd338d017ca460008776614401473044022071fef8ac0aa6318817dbd242bf51fb5b75be312aa31ecb44a0afe7b49fcf840302204c223179a383bb6fcb80312ac66e473345065f7d9136f9662d867acf96c12a42015241048c006ff0d2cfde86455086af5a25b88c2b81858aab67f6a3132c885a2cb9ec38e700576fd46c7d72d7d22555eee3a14e2876c643cd70b1b0a77fbf46e62331ac4104b68ef7d8f24d45e1771101e269c0aacf8d3ed7ebe12b65521712bba768ef53e1e84fff3afbee360acea0d1f461c013557f71d426ac17a293c5eebf06e468253e00ffffffff0280969800000000001976a9140817482d2e97e4be877efe59f4bae108564549f188ac7015a7000000000062537a7652a269537a829178a91480677c5392220db736455533477d0bc2fba65502879b69537a829178a91402d7aa2e76d9066fb2b3c41ff8839a5c81bdca19879b69537a829178a91410039ce4fdb5d4ee56148fe3935b9bfbbe4ecc89879b6953ae00000000')) + + + print '\nOP_CHECKMULTISIG from Testnet :', + psp = PyScriptProcessor() + psp.setTxObjects(tx1, tx2, 0) + verifResult = psp.verifyTransactionValid() + printpassorfail( verifResult) + + + print '\nTest multisig addr extraction :', + scripts = [] + scripts.append(hex_to_binary('4104b54b5fc1917945fff64785d4baaca66a9704e9ed26002f51f53763499643321fbc047683a62be16e114e25404ce6ffdcf625a928002403402bf9f01e5cbd5f3dad4104f576e534f9bbf6d7c5f186ff4c6e0c5442c2755314bdee62fbc656f94d6cbf32c5eb3522da21cf9f954133000ffccb20dbfec030737640cc3315ce09619210d0ac')) + scripts.append(hex_to_binary('537a7652a269537a829178a91480677c5392220db736455533477d0bc2fba65502879b69537a829178a91402d7aa2e76d9066fb2b3c41ff8839a5c81bdca19879b69537a829178a91410039ce4fdb5d4ee56148fe3935b9bfbbe4ecc89879b6953ae')) + scripts.append(hex_to_binary('527a7651a269527a829178a914731cdb75c88a01cbb96729888f726b3b9f29277a879b69527a829178a914e9b4261c6122f8957683636548923acc069e8141879b6952ae')) + + + for scr in scripts: + mstype, addrList, pubList = getTxOutMultiSigInfo(scr) + print '\nNum addresses: ', len(addrList), '\n ', + for a in addrList: + print PyBtcAddress().createFromPublicKeyHash160(a).getAddrStr(), + + + + # TODO: Add some tests for the OP_CHECKMULTISIG support in TxDP + + + + + +################################################################################ +################################################################################ +if Test_NetworkObjects: + print '\n' + print '*********************************************************************' + print 'Testing secure address/wallet features' + print '*********************************************************************' + print '' + + netAddrHex = ('f9beb4d9 61646472 00000000 00000000' + '1f000000 689dcea8 01d6c7db 4e010000' + '00000000 00000000 00000000 000000ff' + 'ff0233b6 ec208d' ).replace(' ','') + + invHex = ('f9beb4d9 696e7600 00000000 00000000' + '25000000 fef89552 01010000 0021eca1' + '50d3f7cd 5eca5ada 7ad02f8f 3bf38420' + '0cb53e8d d51b153d e92bac7a 1b' ).replace(' ','') + + + + +################################################################################ +################################################################################ +if Test_EncryptedAddress: + print '\n' + print '*********************************************************************' + print 'Testing secure address/wallet features' + print '*********************************************************************' + print '' + + # Enable this flag to get a TON of debugging output! + debugPrint = False + + # Create an address to use for all subsequent tests + privKey = SecureBinaryData(hex_to_binary('aa'*32)) + privChk = privKey.getHash256()[:4] + pubKey = CryptoECDSA().ComputePublicKey(privKey) + addr20 = pubKey.getHash160() + + # We pretend that we plugged some passphrases through a KDF + FAKE_KDF_OUTPUT1 = SecureBinaryData( hex_to_binary('11'*32) ) + FAKE_KDF_OUTPUT2 = SecureBinaryData( hex_to_binary('22'*32) ) + + # Test serializing an empty address object: we'll be using this + # in other methods to determine the length of an address, which + # will be the same for all PyBtcAddress objects, empty or not + print '\nTest serializing empty address' + serializedAddr = PyBtcAddress().serialize() + print 'PyBtcAddress serializations are', len(serializedAddr), 'bytes' + printpassorfail(True) # if we didn't crash, we win! + + ############################################################################# + # Try to create addresses without crashing + print '\n\nTesting PyBtcAddress with plaintext private key (try not to crash)' + testAddr = PyBtcAddress().createFromPlainKeyData(privKey, addr20) + testAddr = PyBtcAddress().createFromPlainKeyData(privKey, addr20, chksum=privChk) + testAddr = PyBtcAddress().createFromPlainKeyData(privKey, addr20, publicKey65=pubKey) + testAddr = PyBtcAddress().createFromPlainKeyData(privKey, addr20, publicKey65=pubKey, skipCheck=True) + testAddr = PyBtcAddress().createFromPlainKeyData(privKey, addr20, skipPubCompute=True) + if debugPrint: testAddr.pprint(indent=' '*3) + + + testAddr = PyBtcAddress().createFromPlainKeyData(privKey, addr20, publicKey65=pubKey) + print '\nTest serializing unencrypted wallet', + serializedAddr = testAddr.serialize() + retestAddr = PyBtcAddress().unserialize(serializedAddr) + serializedRetest = retestAddr.serialize() + printpassorfail(serializedAddr == serializedRetest) + + theIV = SecureBinaryData(hex_to_binary('77'*16)) + # Now try locking and unlock addresses + print '\nTesting address locking' + testAddr.enableKeyEncryption(theIV) + testAddr.lock(FAKE_KDF_OUTPUT1) + if debugPrint: testAddr.pprint(indent=' '*3) + + print '\nTest serializing locked address', + serializedAddr = testAddr.serialize() + retestAddr = PyBtcAddress().unserialize(serializedAddr) + serializedRetest = retestAddr.serialize() + printpassorfail(serializedAddr == serializedRetest) + + print '\nTesting address unlocking' + testAddr.unlock(FAKE_KDF_OUTPUT1) + if debugPrint: testAddr.pprint(indent=' '*3) + + print '\nTest serializing encrypted-but-unlocked address', + serializedAddr = testAddr.serialize() + retestAddr = PyBtcAddress().unserialize(serializedAddr) + serializedRetest = retestAddr.serialize() + printpassorfail(serializedAddr == serializedRetest) + + ############################################################################# + print '\n\nTest changing passphrases' + print ' OP(None --> Key1)' + testAddr = PyBtcAddress().createFromPlainKeyData(privKey, addr20, publicKey65=pubKey) + testAddr.enableKeyEncryption(theIV) + testAddr.changeEncryptionKey(None, FAKE_KDF_OUTPUT1) + if debugPrint: testAddr.pprint(indent=' '*3) + + # Save off this data for a later test + addr20_1 = testAddr.getAddr160() + encryptedKey1 = testAddr.binPrivKey32_Encr + encryptionIV1 = testAddr.binInitVect16 + plainPubKey1 = testAddr.binPublicKey65 + + print '\n OP(Key1 --> Unencrypted)' + testAddr.changeEncryptionKey(FAKE_KDF_OUTPUT1, None) + if debugPrint: testAddr.pprint(indent=' '*3) + + print '\n OP(Unencrypted --> Key2)' + if not testAddr.isKeyEncryptionEnabled(): + testAddr.enableKeyEncryption(theIV) + testAddr.changeEncryptionKey(None, FAKE_KDF_OUTPUT2) + if debugPrint: testAddr.pprint(indent=' '*3) + + # Save off this data for a later test + addr20_2 = testAddr.getAddr160() + encryptedKey2 = testAddr.binPrivKey32_Encr + encryptionIV2 = testAddr.binInitVect16 + plainPubKey2 = testAddr.binPublicKey65 + + print '\n OP(Key2 --> Key1)' + testAddr.changeEncryptionKey(FAKE_KDF_OUTPUT2, FAKE_KDF_OUTPUT1) + if debugPrint: testAddr.pprint(indent=' '*3) + + print '\n OP(Key1 --> Lock --> Key2)' + testAddr.lock(FAKE_KDF_OUTPUT1) + testAddr.changeEncryptionKey(FAKE_KDF_OUTPUT1, FAKE_KDF_OUTPUT2) + if debugPrint: testAddr.pprint(indent=' '*3) + + print '\n OP(Key2 --> Lock --> Unencrypted)' + testAddr.changeEncryptionKey(FAKE_KDF_OUTPUT2, None) + if debugPrint: testAddr.pprint(indent=' '*3) + + print '\nEncryption Key Tests: ' + printpassorfail(testAddr.serializePlainPrivateKey() == privKey.toBinStr()) + + + ############################################################################# + # TODO: Gotta test pre-encrypted key handling + print '\n\nTest loading pre-encrypted key data' + testAddr = PyBtcAddress().createFromEncryptedKeyData(addr20_1, \ + encryptedKey1, \ + encryptionIV1) + if debugPrint: testAddr.pprint(indent=' '*3) + + print '\n OP(EncrAddr --> Unlock1)' + testAddr.unlock(FAKE_KDF_OUTPUT1) + if debugPrint: testAddr.pprint(indent=' '*3) + + print '\n OP(Unlock1 --> Lock1)' + testAddr.lock() + if debugPrint: testAddr.pprint(indent=' '*3) + + print '\n OP(Lock1 --> Lock2)' + testAddr.changeEncryptionKey(FAKE_KDF_OUTPUT1, FAKE_KDF_OUTPUT2) + if debugPrint: testAddr.pprint(indent=' '*3) + + print '\nTest serializing locked wallet from pre-encrypted data', + serializedAddr = testAddr.serialize() + retestAddr = PyBtcAddress().unserialize(serializedAddr) + serializedRetest = retestAddr.serialize() + printpassorfail(serializedAddr == serializedRetest) + + ############################################################################# + # Now testing chained-key (deterministic) address generation + print '\n\nTest chained priv key generation' + print 'Starting with plain key data' + chaincode = SecureBinaryData(hex_to_binary('ee'*32)) + addr0 = PyBtcAddress().createFromPlainKeyData(privKey, addr20) + addr0.markAsRootAddr(chaincode) + pub0 = addr0.binPublicKey65 + if debugPrint: addr0.pprint(indent=' '*3) + + print '\nTest serializing address-chain-root', + serializedAddr = addr0.serialize() + retestAddr = PyBtcAddress().unserialize(serializedAddr) + serializedRetest = retestAddr.serialize() + printpassorfail(serializedAddr == serializedRetest) + + print '\nGenerate chained PRIVATE key address' + print ' OP(addr[0] --> addr[1])' + addr1 = addr0.extendAddressChain() + if debugPrint: addr1.pprint(indent=' '*3) + + print '\n OP(addr[0] --> addr[1]) [again]' + addr1a = addr0.extendAddressChain() + if debugPrint: addr1a.pprint(indent=' '*3) + + print '\n OP(addr[1] --> addr[2])' + addr2 = addr1.extendAddressChain() + pub2 = addr2.binPublicKey65.copy() + priv2 = addr2.binPrivKey32_Plain.copy() + if debugPrint: addr2.pprint(indent=' '*3) + + print '\nAddr1.privKey == Addr1a.privKey:', + printpassorfail(addr1.binPublicKey65 == addr1a.binPublicKey65) + + print '\nTest serializing priv-key-chained', + serializedAddr = addr2.serialize() + retestAddr = PyBtcAddress().unserialize(serializedAddr) + serializedRetest = retestAddr.serialize() + printpassorfail(serializedAddr == serializedRetest) + + ############################################################################# + print '\n\nGenerate chained PUBLIC key address' + print ' addr[0]' + addr0 = PyBtcAddress().createFromPublicKeyData(pub0) + addr0.markAsRootAddr(chaincode) + if debugPrint: addr0.pprint(indent=' '*3) + + print '\nTest serializing pub-key-only-root', + serializedAddr = addr0.serialize() + retestAddr = PyBtcAddress().unserialize(serializedAddr) + serializedRetest = retestAddr.serialize() + printpassorfail(serializedAddr == serializedRetest) + + print '\n OP(addr[0] --> addr[1])' + addr1 = addr0.extendAddressChain() + if debugPrint: addr1.pprint(indent=' '*3) + + print '\n OP(addr[1] --> addr[2])' + addr2 = addr1.extendAddressChain() + pub2a = addr2.binPublicKey65.copy() + if debugPrint: addr2.pprint(indent=' '*3) + + print '\nAddr2.PublicKey == Addr2a.PublicKey:', + printpassorfail(pub2 == pub2a) + + print '\nTest serializing pub-key-from-chain', + serializedAddr = addr2.serialize() + retestAddr = PyBtcAddress().unserialize(serializedAddr) + serializedRetest = retestAddr.serialize() + printpassorfail(serializedAddr == serializedRetest) + + ############################################################################# + print '\n\nGenerate chained keys from locked addresses' + addr0 = PyBtcAddress().createFromPlainKeyData( privKey, \ + willBeEncr=True, IV16=theIV) + addr0.markAsRootAddr(chaincode) + print '\n OP(addr[0] plain)' + if debugPrint: addr0.pprint(indent=' '*3) + + print '\nTest serializing unlocked addr-chain-root', + serializedAddr = addr0.serialize() + retestAddr = PyBtcAddress().unserialize(serializedAddr) + serializedRetest = retestAddr.serialize() + printpassorfail(serializedAddr == serializedRetest) + + print '\n OP(addr[0] locked)' + addr0.lock(FAKE_KDF_OUTPUT1) + if debugPrint: addr0.pprint(indent=' '*3) + + print '\n OP(addr[0] w/Key --> addr[1])' + addr1 = addr0.extendAddressChain(FAKE_KDF_OUTPUT1, newIV=theIV) + if debugPrint: addr1.pprint(indent=' '*3) + + print '\n OP(addr[1] w/Key --> addr[2])' + addr2 = addr1.extendAddressChain(FAKE_KDF_OUTPUT1, newIV=theIV) + addr2.unlock(FAKE_KDF_OUTPUT1) + priv2a = addr2.binPrivKey32_Plain.copy() + addr2.lock() + if debugPrint: addr2.pprint(indent=' '*3) + + print '\nAddr2.priv == Addr2a.priv:', + printpassorfail(priv2 == priv2a) + + print '\nTest serializing chained address from locked root', + serializedAddr = addr2.serialize() + retestAddr = PyBtcAddress().unserialize(serializedAddr) + serializedRetest = retestAddr.serialize() + printpassorfail(serializedAddr == serializedRetest) + + + ############################################################################# + print '\n\nGenerate chained keys from locked addresses, no unlocking' + addr0 = PyBtcAddress().createFromPlainKeyData( privKey, \ + willBeEncr=True, IV16=theIV) + addr0.markAsRootAddr(chaincode) + print '\n OP(addr[0] locked)' + addr0.lock(FAKE_KDF_OUTPUT1) + if debugPrint: addr0.pprint(indent=' '*3) + + print '\n OP(addr[0] locked --> addr[1] locked)' + addr1 = addr0.extendAddressChain(newIV=theIV) + if debugPrint: addr1.pprint(indent=' '*3) + + print '\n OP(addr[1] locked --> addr[2] locked)' + addr2 = addr1.extendAddressChain(newIV=theIV) + pub2b = addr2.binPublicKey65.copy() + if debugPrint: addr2.pprint(indent=' '*3) + + print '\nAddr2.Pub == Addr2b.pub:', + printpassorfail(pub2 == pub2b) + + print '\nTest serializing priv-key-bearing address marked for unlock', + serializedAddr = addr2.serialize() + retestAddr = PyBtcAddress().unserialize(serializedAddr) + serializedRetest = retestAddr.serialize() + printpassorfail(serializedAddr == serializedRetest) + + addr2.unlock(FAKE_KDF_OUTPUT1) + priv2b = addr2.binPrivKey32_Plain.copy() + print '\n OP(addr[2] locked --> unlocked)' + if debugPrint: addr2.pprint(indent=' '*3) + + + addr2.lock() + print '\n OP(addr[2] unlocked --> locked)' + if debugPrint: addr2.pprint(indent=' '*3) + + + print '\nAddr2.priv == Addr2b.priv:', + printpassorfail(priv2 == priv2b) + + +################################################################################ +################################################################################ +if Test_EncryptedWallet: + print '\n' + print '*********************************************************************' + print 'Testing deterministic, encrypted wallet features' + print '*********************************************************************' + print '' + + debugPrint = True + debugPrintAlot = False + + # Remove wallet files, need fresh dir for this test + + shortlabel = 'TestWallet1' + wltID = '6Q168oJ7' + if USE_TESTNET: + wltID = '3VB8XSoY' + + fileA = os.path.join(ARMORY_HOME_DIR, 'armory_%s_.wallet' % wltID) + fileB = os.path.join(ARMORY_HOME_DIR, 'armory_%s_backup.wallet' % wltID) + fileAupd = os.path.join(ARMORY_HOME_DIR, 'armory_%s_backup_unsuccessful.wallet' % wltID) + fileBupd = os.path.join(ARMORY_HOME_DIR, 'armory_%s_update_unsuccessful.wallet' % wltID) + + for f in (fileA, fileB, fileAupd, fileBupd): + print 'Removing file:', f, + if os.path.exists(f): + os.remove(f) + print '...removed!' + else: + print '(DNE, do nothing)' + + # We need a controlled test, so we script the all the normally-random stuff + privKey = SecureBinaryData('\xaa'*32) + privKey2 = SecureBinaryData('\x33'*32) + chainstr = SecureBinaryData('\xee'*32) + theIV = SecureBinaryData(hex_to_binary('77'*16)) + passphrase = SecureBinaryData('A passphrase') + passphrase2 = SecureBinaryData('A new passphrase') + + wlt = PyBtcWallet().createNewWallet(withEncrypt=False, \ + plainRootKey=privKey, \ + chaincode=chainstr, \ + IV=theIV, \ + shortLabel=shortlabel) + wlt.addrPoolSize = 5 + wlt.detectHighestUsedIndex(True) + + print 'New wallet is at:', wlt.getWalletPath() + wlt.pprint(indent=' '*5, allAddrInfo=debugPrint) + + + + ############################################################################# + print '\n(1) Getting a new address:' + newAddr = wlt.getNextUnusedAddress() + wlt.pprint(indent=' '*5, allAddrInfo=debugPrint) + + print '\n(1) Re-reading wallet from file, compare the two wallets' + wlt2 = PyBtcWallet().readWalletFile(wlt.walletPath) + wlt2.pprint(indent=' '*5, allAddrInfo=debugPrint) + printpassorfail(wlt.isEqualTo(wlt2, debug=debugPrintAlot)) + + ############################################################################# + print '\n(2)Testing unencrypted wallet import-address' + wlt.importExternalAddressData(privKey=privKey2) + wlt.pprint(indent=' '*5, allAddrInfo=debugPrint) + + print '\n(2) Re-reading wallet from file, compare the two wallets' + wlt2 = PyBtcWallet().readWalletFile(wlt.walletPath) + wlt2.pprint(indent=' '*5, allAddrInfo=debugPrint) + printpassorfail(wlt.isEqualTo(wlt2, debug=debugPrintAlot)) + + print '\n(2a)Testing deleteImportedAddress' + print '\nWallet size before delete:', os.path.getsize(wlt.walletPath) + print '\n#Addresses before delete:', len(wlt.linearAddr160List) + toDelete160 = convertKeyDataToAddress(privKey2) + wlt.deleteImportedAddress(toDelete160) + print '\nWallet size after delete:', os.path.getsize(wlt.walletPath) + print '\n(2a) #Addresses after delete:', len(wlt.linearAddr160List) + wlt.pprint(indent=' '*5, allAddrInfo=debugPrint) + + print '\n(2a) Reimporting address for remaining tests' + print '\nWallet size before reimport:', os.path.getsize(wlt.walletPath) + wlt.importExternalAddressData(privKey=privKey2) + print '\nWallet size after reimport:', os.path.getsize(wlt.walletPath) + wlt.pprint(indent=' '*5, allAddrInfo=debugPrint) + + + print '\n(2b)Testing ENCRYPTED wallet import-address' + privKey3 = SecureBinaryData('\xbb'*32) + privKey4 = SecureBinaryData('\x44'*32) + chainstr2 = SecureBinaryData('\xdd'*32) + theIV2 = SecureBinaryData(hex_to_binary('66'*16)) + passphrase2= SecureBinaryData('hello') + wltE = PyBtcWallet().createNewWallet(withEncrypt=True, \ + plainRootKey=privKey3, \ + securePassphrase=passphrase2, \ + chaincode=chainstr2, \ + IV=theIV2, \ + shortLabel=shortlabel) + + try: + wltE.importExternalAddressData(privKey=privKey2) + wltE.pprint(indent=' '*5, allAddrInfo=debugPrint) + printpassorfail(False) + print 'FAILED! We should have thrown an error about importing into a ' + print ' locked wallet...' + except: + printpassorfail(True) + + + wltE.unlock(securePassphrase=passphrase2) + wltE.importExternalAddressData(privKey=privKey2) + wltE.pprint(indent=' '*5, allAddrInfo=debugPrint) + + print '\n(2b) Re-reading wallet from file, compare the two wallets' + wlt2 = PyBtcWallet().readWalletFile(wltE.walletPath) + wlt2.pprint(indent=' '*5, allAddrInfo=debugPrint) + printpassorfail(wltE.isEqualTo(wlt2, debug=debugPrintAlot)) + + + print '\n(2b) Unlocking wlt2 after re-reading locked-import-wallet' + wlt2.unlock(securePassphrase=passphrase2) + + + + ############################################################################# + # Now play with encrypted wallets + print '\n\n' + print '*********************************************************************' + print '\n(3)Testing conversion to encrypted wallet' + + kdfParams = wlt.computeSystemSpecificKdfParams(0.1) + wlt.changeKdfParams(*kdfParams) + + print '\n(3)New KDF takes', wlt.testKdfComputeTime(), 'seconds to compute' + wlt.kdf.printKdfParams() + wlt.changeWalletEncryption( securePassphrase=passphrase ) + wlt.pprint(indent=' '*5, allAddrInfo=debugPrint) + + print '\n(3) Re-reading wallet from file, compare the two wallets' + wlt2 = PyBtcWallet().readWalletFile(wlt.getWalletPath()) + wlt2.pprint(indent=' '*5, allAddrInfo=debugPrint) + printpassorfail(wlt.isEqualTo(wlt2, debug=debugPrintAlot)) + # NOTE: this isEqual operation compares the serializations + # of the wallet addresses, which only contains the + # encrypted versions of the private keys. However, + # wlt is unlocked and contains the plaintext keys, too + # while wlt2 does not. + + print '\n(3)Look at wlt again, before we lock it' + wlt.pprint(indent=' '*5, allAddrInfo=debugPrint) + wlt.lock() + print '\n(3)And now it should be locked...' + wlt.pprint(indent=' '*5, allAddrInfo=debugPrint) + + + ############################################################################# + print '\n(4)Testing changing passphrase on encrypted wallet', + + wlt.unlock( securePassphrase=passphrase ) + print '...to same passphrase' + wlt.changeWalletEncryption( securePassphrase=passphrase ) + + print '\n(4)And now testing new passphrase...' + wlt.changeWalletEncryption( securePassphrase=passphrase2 ) + wlt.pprint(indent=' '*5, allAddrInfo=debugPrint) + + print '\n(4) Re-reading wallet from file, compare the two wallets' + wlt2 = PyBtcWallet().readWalletFile(wlt.getWalletPath()) + wlt2.pprint(indent=' '*5, allAddrInfo=debugPrint) + printpassorfail(wlt.isEqualTo(wlt2, debug=debugPrintAlot)) + + ############################################################################# + print '\n(5)Testing changing KDF on encrypted wallet' + + wlt.unlock( securePassphrase=passphrase2 ) + print '\n(5)Before kdf change:' + wlt.kdf.printKdfParams() + wlt.pprint(indent=' '*5, allAddrInfo=debugPrint) + + wlt.changeKdfParams(1024, 999, hex_to_binary('00'*32), passphrase2) + print '\n(5)After kdf change:' + wlt.kdf.printKdfParams() + wlt.pprint(indent=' '*5, allAddrInfo=debugPrint) + + print '\n(5) And now changing the encryption to the same, again' + wlt.changeWalletEncryption( securePassphrase=passphrase2 ) + wlt.pprint(indent=' '*5, allAddrInfo=debugPrint) + + print '\n(5) Get new address from locked wallet' + print 'Locking wallet' + wlt.lock() + for i in range(10): + wlt.getNextUnusedAddress() + wlt.pprint(indent=' '*5, allAddrInfo=debugPrint) + + print '\n(5) Re-reading wallet from file, compare the two wallets' + wlt2 = PyBtcWallet().readWalletFile(wlt.getWalletPath()) + wlt2.pprint(indent=' '*5, allAddrInfo=debugPrint) + printpassorfail(wlt.isEqualTo(wlt2, debug=debugPrintAlot)) + + ############################################################################# + # !!! #forkOnlineWallet() + print '\n(6)Testing forking encrypted wallet for online mode' + wlt.forkOnlineWallet('OnlineVersionOfEncryptedWallet.bin') + wlt2.readWalletFile('OnlineVersionOfEncryptedWallet.bin') + wlt2.pprint(indent=' '*5, allAddrInfo=debugPrint) + + print '\n(6)Getting a new addresses from both wallets' + for i in range(wlt.addrPoolSize*2): + wlt.getNextUnusedAddress() + wlt2.getNextUnusedAddress() + + newaddr1 = wlt.getNextUnusedAddress() + print 'New address (reg): ', newaddr1.getAddrStr() + newaddr2 = wlt2.getNextUnusedAddress() + print 'New address (online):', newaddr2.getAddrStr() + + printpassorfail(newaddr1.getAddr160() == newaddr2.getAddr160()) + + wlt2.pprint(indent=' '*5, allAddrInfo=debugPrint) + + print '\n(6) Re-reading wallet from file, compare the two wallets' + wlt3 = PyBtcWallet().readWalletFile('OnlineVersionOfEncryptedWallet.bin') + wlt3.pprint(indent=' '*5, allAddrInfo=debugPrint) + + + ############################################################################# + print '\n(7)Testing removing wallet encryption' + print 'Wallet is locked? ', wlt.isLocked + wlt.unlock(securePassphrase=passphrase2) + wlt.changeWalletEncryption( None ) + wlt.pprint(indent=' '*5, allAddrInfo=debugPrint) + + print '\n(7) Re-reading wallet from file, compare the two wallets' + wlt2 = PyBtcWallet().readWalletFile(wlt.getWalletPath()) + wlt2.pprint(indent=' '*5, allAddrInfo=debugPrint) + printpassorfail(wlt.isEqualTo(wlt2, debug=debugPrintAlot)) + + ############################################################################# + print '\n\n' + print '*********************************************************************' + print '\n(8)Doing interrupt tests to test wallet-file-update recovery' + def hashfile(fn): + f = open(fn,'r') + d = hash256(f.read()) + f.close() + return binary_to_hex(d[:8]) + + def printfilestatus(fn): + if os.path.exists(fn): + print ' ', hashfile(fn), ' ', fn.split('/')[-1] + else: + print ' ', 'No file:'.ljust(16), ' ', fn.split('/')[-1] + + def printstat(): + printfilestatus(fileA) + printfilestatus(fileB) + printfilestatus(fileAupd) + printfilestatus(fileBupd) + + print '\n(8a)Starting test with the unencrypted wallet from part (6)' + printstat() + correctMainHash = hashfile(fileA) + + try: + wlt.interruptTest1 = True + wlt.getNextUnusedAddress() + except InterruptTestError: + print 'Interrupted!' + pass + wlt.interruptTest1 = False + + print '\n(8a)Interrupted getNextUnusedAddress on primary file update' + printstat() + print '\n(8a)Do consistency check on the wallet' + wlt.doWalletFileConsistencyCheck() + printstat() + printpassorfail(correctMainHash==hashfile(fileA)) + + print '\n(8b) Try interrupting at state 2' + printstat() + + try: + wlt.interruptTest2 = True + wlt.getNextUnusedAddress() + except InterruptTestError: + print 'Interrupted!' + pass + wlt.interruptTest2 = False + + print '\n(8b)Interrupted getNextUnusedAddress on between primary/backup update' + printstat() + print '\n(8b)Do consistency check on the wallet' + wlt.doWalletFileConsistencyCheck() + printstat() + printpassorfail(hashfile(fileA)==hashfile(fileB)) + + + + print '\n(8c) Try interrupting at state 3' + printstat() + + try: + wlt.interruptTest3 = True + wlt.getNextUnusedAddress() + except InterruptTestError: + print 'Interrupted!' + pass + wlt.interruptTest3 = False + + print '\n(8c)Interrupted getNextUnusedAddress on backup file update' + printstat() + print '\n(8c)Do consistency check on the wallet' + wlt.doWalletFileConsistencyCheck() + printstat() + printpassorfail(hashfile(fileA)==hashfile(fileB)) + + + ############################################################################# + print '\n\n' + print '*********************************************************************' + print '\n(9)Checksum-based byte-error correction tests!' + print '\n(9)Start with a good primary and backup file...' + printstat() + + print '\n(9a)Open primary wallet, change second byte in KDF' + wltfile = open(wlt.walletPath,'r+b') + wltfile.seek(326) + wltfile.write('\xff') + wltfile.close() + print '\n(9a)Byte changed, file hashes:' + printstat() + + print '\n(9a)Try to read wallet from file, should correct KDF error, write fix' + wlt2 = PyBtcWallet().readWalletFile(wlt.walletPath) + printstat() + printpassorfail(hashfile(fileA)==hashfile(fileB)) + + print '\n\n' + print '*********************************************************************' + print '\n(9b)Change a byte in each checksummed field in root addr' + wltfile = open(wlt.walletPath,'r+b') + wltfile.seek(838); wltfile.write('\xff') + wltfile.seek(885); wltfile.write('\xff') + wltfile.seek(929); wltfile.write('\xff') + wltfile.seek(954); wltfile.write('\xff') + wltfile.seek(1000); wltfile.write('\xff') + wltfile.close() + print '\n(9b) New file hashes...' + printstat() + + print '\n(9b)Try to read wallet from file, should correct address errors' + wlt2 = PyBtcWallet().readWalletFile(wlt.walletPath) + printstat() + printpassorfail(hashfile(fileA)==hashfile(fileB)) + + print '\n\n' + print '*********************************************************************' + print '\n(9c)Change a byte in each checksummed field, of first non-root addr' + wltfile = open(wlt.walletPath,'r+b') + wltfile.seek(1261+21+838); wltfile.write('\xff') + wltfile.seek(1261+21+885); wltfile.write('\xff') + wltfile.seek(1261+21+929); wltfile.write('\xff') + wltfile.seek(1261+21+954); wltfile.write('\xff') + wltfile.seek(1261+21+1000); wltfile.write('\xff') + wltfile.close() + print '\n(9c) New file hashes...' + printstat() + + print '\n(9c)Try to read wallet from file, should correct address errors' + wlt2 = PyBtcWallet().readWalletFile(wlt.walletPath) + printstat() + printpassorfail(hashfile(fileA)==hashfile(fileB)) + + print '\n\n' + print '*********************************************************************' + print '\n(9d)Now butcher the CHECKSUM, see if correction works' + wltfile = open(wlt.walletPath,'r+b') + wltfile.seek(977); wltfile.write('\xff') + wltfile.close() + print '\n(9d) New file hashes...' + printstat() + + print '\n(9d)Try to read wallet from file, should correct address errors' + wlt2 = PyBtcWallet().readWalletFile(wlt.walletPath) + printstat() + printpassorfail(hashfile(fileA)==hashfile(fileB)) + + + print '*******' + print '\n(9z) Test comment I/O' + comment1 = 'This is my normal unit-testing address.' + comment2 = 'This is fake tx... no tx has this hash.' + comment3 = comment1 + ' Corrected!' + hash1 = '\x1f'*20 # address160 + hash2 = '\x2f'*32 # tx hash + wlt.setComment(hash1, comment1) + wlt.setComment(hash2, comment2) + wlt.setComment(hash1, comment3) + + wlt2 = PyBtcWallet().readWalletFile(wlt.walletPath) + c3 = wlt2.getComment(hash1) + c2 = wlt2.getComment(hash2) + print c3 + print c2 + printpassorfail(c3==comment3) + printpassorfail(c2==comment2) + + + + ############################################################################# + print '\n\n' + print '*********************************************************************' + print '\n(10) Finally! Start the wallet tests involving the blockchain!' + + print '\n(10) Add an address with some money to this wallet' + binPrivKey = hex_to_binary('a47a7e263f9ec17d7fbb4a649541001e8bb1266917aa77f5773810d7d81f00a5') + newAddr20 = wlt.importExternalAddressData( privKey=binPrivKey ) + if debugPrint: wlt.pprint(indent=' '*5, allAddrInfo=debugPrint) + + + + print '\n(10) Make sure C++ has all the addresses:' + cppwlt = wlt.cppWallet + naddr = cppwlt.getNumAddr() + for i in range(naddr): + print ' Address:', hash160_to_addrStr(cppwlt.getAddrByIndex(i).getAddrStr20()) + + + print '\n(10) Loading blockchain from blk0001.dat' + BDM_LoadBlockchainFile() # looks for blk0001.dat in satoshi client location + + print '\n(10) Now syncing this wallet with the blockchain' + # While using the blk0001.dat maintained by satoshi client, never write data + wlt.setBlockchainSyncFlag(BLOCKCHAIN_READONLY) + wlt.syncWithBlockchain() + + utxoList = wlt.getUnspentTxOutList() + pprintUnspentTxOutList(utxoList, 'Unspent TxOuts for your wallet: ') + + nBTC = 1.4*ONE_BTC + print '\n(10) Select inputs for a', coin2str(nBTC), 'BTC tx to myself' + prelimSelection = PySelectCoins(utxoList, nBTC, minFee=0) + feeRecommended = calcMinSuggestedFees(prelimSelection, nBTC, 0) + pprintUnspentTxOutList(prelimSelection, 'Selected TxOuts for (tgt,fee)=(%s,%s)' % \ + (coin2str(nBTC), coin2str(0))) + print '*Recommended fees: AbsMin=%s, Suggest=%s' % tuple([coin2str(f) for f in feeRecommended]) + recip = addrStr_to_hash160('1F7G4aq9fbAhqGb9jcnsVn6CRm6dqJf3sD') + + theSum = sumTxOutList(prelimSelection) + recipPairs = [ \ + [recip, nBTC], \ + [newAddr20, theSum-nBTC] ] + + if theSum==0: + print 'Not enough funds. Skipping TxDP construction' + else: + print '\n\n(10)Creating TxDistProposal:' + txdp = PyTxDistProposal().createFromTxOutSelection(prelimSelection, recipPairs) + if debugPrint: txdp.pprint(' ') + print '\n\n(10)Signing the TxDP:' + wlt.signTxDistProposal(txdp) + if debugPrint: txdp.pprint(' ') + + txToBroadcast = txdp.prepareFinalTx() + print '' + txToBroadcast.pprint() + print '' + + print binary_to_hex(txToBroadcast.serialize()) + pprintHex(binary_to_hex(txToBroadcast.serialize())) + + + + + ############################################################################# + print '\n\n' + print '*********************************************************************' + print '\n(11) One more blockchain test, this time with online/watching-only' + wlt2.readWalletFile('OnlineVersionOfEncryptedWallet.bin') + wlt2.doBlockchainSync=BLOCKCHAIN_READONLY + wlt2.syncWithBlockchain() + wlt2.pprint(indent=' '*5, allAddrInfo=debugPrint) + + print '\n(11) Search for unspent TxOuts for this online wallet' + utxoList = wlt2.getUnspentTxOutList() + pprintUnspentTxOutList(utxoList, 'Unspent TxOuts for your wallet: ') + + nBTC = 1.4*ONE_BTC + print '\n(11) Select inputs for a', coin2str(nBTC), 'BTC tx to myself' + prelimSelection = PySelectCoins(utxoList, nBTC, minFee=0) + feeRecommended = calcMinSuggestedFees(prelimSelection, nBTC, 0) + pprintUnspentTxOutList(prelimSelection, 'Selected TxOuts for (tgt,fee)=(%s,%s)' % \ + (coin2str(nBTC), coin2str(0))) + print '*Recommended fees: AbsMin=%s, Suggest=%s' % tuple([coin2str(f) for f in feeRecommended]) + recip = addrStr_to_hash160('1F7G4aq9fbAhqGb9jcnsVn6CRm6dqJf3sD') + + theSum = sumTxOutList(prelimSelection) + recipPairs = [ \ + [recip, nBTC], \ + [recip, theSum-nBTC] ] + + if theSum == 0: + print 'Not enough funds.... skipping tx construction' + else: + print '\n\n(11)Creating TxDistProposal:' + txdp = PyTxDistProposal().createFromTxOutSelection(prelimSelection, recipPairs) + if debugPrint: txdp.pytxObj.pprint() + print '\n\n(11) Attempting to sign TxDP with online wallet' + wlt2.signTxDistProposal(txdp) + + os.remove('OnlineVersionOfEncryptedWallet.bin') + os.remove('OnlineVersionOfEncryptedWalletbackup.bin') + + + + +################################################################################ +################################################################################ +if Test_TxDistProposals: + print '' + print '*********************************************************************' + print 'Testing Tx Distribution Proposals for offline signatures' + print '*********************************************************************' + print '' + print 'Create a valid tx, serialize it, unserialize it, sign it' + + debugPrint = True + + print '\n(1) Create a wallet, add our address' + privKey = SecureBinaryData(hex_to_binary('aa'*32)) + pubKey = CryptoECDSA().ComputePublicKey(privKey) + addr20 = pubKey.getHash160() + wlt = PyBtcWallet().createNewWallet(withEncrypt=False) + + + binPrivKey = hex_to_binary('a47a7e263f9ec17d7fbb4a649541001e8bb1266917aa77f5773810d7d81f00a5') + myOwnAddr160 = wlt.importExternalAddressData( privKey=binPrivKey ) + wlt.pprint(indent=' '*5, allAddrInfo=False) + + BDM_LoadBlockchainFile() # looks for blk0001.dat in satoshi client location + wlt.setBlockchainSyncFlag(BLOCKCHAIN_READONLY) + + # Get all the unspent TxOuts for this addr + wlt.syncWithBlockchain() + utxoList = wlt.getTxOutList('Spendable') + pprintUnspentTxOutList(utxoList, 'Unspent TxOuts for your wallet: ') + + nBTC = 0.05*ONE_BTC + print '\n(1) Select inputs for a', coin2str(nBTC), 'BTC tx to myself' + prelimSelection = PySelectCoins(utxoList, nBTC, minFee=0) + pprintUnspentTxOutList(prelimSelection, 'Selected TxOuts for (tgt,fee)=(%s,%s)' % \ + (coin2str(nBTC), coin2str(0))) + recip160 = addrStr_to_hash160('1F7G4aq9fbAhqGb9jcnsVn6CRm6dqJf3sD') + + theSum = sumTxOutList(prelimSelection) + recipPairs = [ [recip160, nBTC], \ + [myOwnAddr160, theSum-nBTC] ] + + print '\n(1)Creating TxDistProposal:' + txdp = PyTxDistProposal().createFromTxOutSelection(prelimSelection, recipPairs) + + print '\n(1)Serializing:' + asciiBlock = txdp.serializeAscii() + for l in asciiBlock.split('\n'): + print ' ', l + + print '\n(1)Unserializing' + txdp2 = PyTxDistProposal().unserializeAscii(asciiBlock) + print '\n(1) TxDP has enough signatures?', txdp.checkTxHasEnoughSignatures() + + txdp2.pprint() + print '\n(1)Sign it, now' + txdpSigned = wlt.signTxDistProposal(txdp2) + print '\n(1) Signed enough inputs?', txdpSigned.checkTxHasEnoughSignatures() + print '\n(1) Verified?', txdpSigned.checkTxHasEnoughSignatures(alsoVerify=True) + + print '\n(1) Re-serialized signed txdp' + asciiBlock = txdpSigned.serializeAscii() + for l in asciiBlock.split('\n'): + print ' ', l + + print '\n(1) Preparing TxDP for broadcast' + txdp3 = PyTxDistProposal().unserializeAscii(asciiBlock) + txToBroadcast = txdpSigned.prepareFinalTx() + print '\n(1) Final tx to broadcast!' + print binary_to_hex(txToBroadcast.serialize()) + print '' + pprintHex(binary_to_hex(txToBroadcast.serialize())) + + # TODO: test a multisig TxDP + + +################################################################################ +################################################################################ +if Test_SelectCoins: + print '' + print '*********************************************************************' + print 'Testing SelectCoins' + print '*********************************************************************' + print '' + + addrs = [ch*20 for ch in ['\xaa','\xbb','\xcc','\xdd','\xee']] + utxo3s = [ [addrs[0], ONE_BTC*1.0, 5 ], \ + [addrs[0], ONE_BTC*1.5, 130], \ + [addrs[0], ONE_BTC*0.0005, 200], \ + [addrs[0], ONE_BTC*2.1, 130], \ + [addrs[0], ONE_BTC*0.0001, 130], \ + [addrs[0], ONE_BTC*5.5, 0], \ + [addrs[0], ONE_BTC*0.3, 0], \ + [addrs[0], ONE_BTC*10.1, 130], \ + #[addrs[1], ONE_BTC*22.3331, 130], \ + [addrs[1], ONE_BTC*0.0004, 1000], \ + [addrs[1], ONE_BTC*1.1, 100], \ + [addrs[1], ONE_BTC*3.3, 130], \ + [addrs[2], ONE_BTC*5.2, 130], \ + [addrs[3], ONE_BTC*5.3, 130], \ + [addrs[4], ONE_BTC*5.4, 130] ] + utxolist = [] + for trip in utxo3s: + utxo = PyUnspentTxOut() + utxo.addr = trip[0] + utxo.val = trip[1] + utxo.conf = trip[2] + utxo.binScript = '\x76\xa9\x14' + utxo.addr + '\x88\xac' + utxolist.append(utxo) + + + targetOutVal = 10*ONE_BTC + minFee = 0 + + + pprintUnspentTxOutList(utxolist, 'Test set of UTXOs') + + testTargs = [t*ONE_BTC for t in [0.001, 0.01, 0.0005, 0.1, 1.0, 2.5, 5.0, 1.3928, 10.0, 22.32221, 50, 60, 90]] + testFees = [f*ONE_BTC for f in [0, 0.0001, 0.0005, 0.01, 0.1]] + + for targ in testTargs: + for fee in testFees: + selected = PySelectCoins(utxolist, targ, fee) + pprintUnspentTxOutList(selected, '(Targ,Fee) = (%s,%s)' % (coin2str(targ).strip(), coin2str(fee).strip())) + + + + + +################################################################################ +################################################################################ +if Test_CryptoTiming: + print '' + print '*********************************************************************' + print 'Testing Crypto++ Methods via SWIG' + print '*********************************************************************' + print '' + print 'Testing key-derivation function - timings and memory usage:' + + testPass1 = SecureBinaryData('This is my password ') + testPass2 = SecureBinaryData('This is my password.') + + # Key-derivation function 1 -- default time/mem + print ' ***KDF 1: Default params***' + kdf1 = Cpp.KdfRomix() + kdf1.computeKdfParams() + + # Key-derivation function 2 + print ' ***KDF 2: 0.5s-1.0s timing, default mem***' + kdf2 = Cpp.KdfRomix() + kdf2.computeKdfParams(1.0) + + # Key-derivation function 3 + print ' ***KDF 3: 0.25s-0.5s timing, 256kB max***' + kdf3 = Cpp.KdfRomix() + kdf3.computeKdfParams(0.5, 256*1024) + + for i,kdf in enumerate([kdf1, kdf2, kdf3]): + memStr = kdf.getMemoryReqtBytes() + if memStr>1024*1024: + memStr = '%0.1f'%(memStr/(1024.*1024.)) + ' MB' + elif memStr>1024: + memStr = '%0.1f'%(memStr/1024.) + ' kB' + else: + memStr = '%0.1f'%(memStr) + ' bytes' + print ' Testing KDF(' + str(i+1) + ')' + print ' Hash Function:'.ljust(24), kdf.getHashFunctionName() + print ' Mem Required :'.ljust(24), memStr + print ' Num Iteration:'.ljust(24), kdf.getNumIterations(); + print ' Hex Salt Used:'.ljust(24), kdf.getSalt().toHexStr()[:29] + '...' + for pswd in [testPass1, testPass2, testPass1]: + start=time.time() + key = kdf.DeriveKey(pswd) + + print ' Pass: "%s" --> Key: %s (%0.6f sec)' % \ + (pswd.toBinStr().ljust(20), \ + key.toHexStr()[:32], \ + time.time()-start) + + print '' + print '' + print 'Testing Crypto++::AES timings' + keyAES = SecureBinaryData( hex_to_binary('aa'*32) ) + secret = SecureBinaryData( hex_to_binary('aa'*32) ) + withIV = SecureBinaryData( hex_to_binary('bb'*16) ) + noIV = SecureBinaryData('') + cipher = SecureBinaryData() + plain = SecureBinaryData() + + nTest = 10000 + + """ + # Test with no initialization vector + start = time.time() + for i in range(nTest): + cipher = CryptoAES().EncryptCFB(secret, keyAES, noIV) + end = time.time() + print ' AES Encryption with IV generation: %0.1f/sec' % (nTest/(end-start)) + """ + + # Now using an IV + start = time.time() + for i in range(nTest): + cipher = CryptoAES().EncryptCFB(secret, keyAES, withIV) + end = time.time() + print ' AES Encryption with supplied IV : %0.1f/sec' % (nTest/(end-start)) + + + # Test decryption speed + start = time.time() + for i in range(nTest): + plain = CryptoAES().DecryptCFB(cipher, keyAES, withIV) + end = time.time() + print ' AES Decryption with supplied IV : %0.1f/sec' % (nTest/(end-start)) + + print ' AES roundtrip, compare results:' + print ' Secret : ', secret.toHexStr() + print ' Cipher : ', cipher.toHexStr() + print ' Decrypt: ', plain.toHexStr() + print ' Result : ', + printpassorfail(plain==secret) + + + print '\n' + print 'Testing Crypto++::ECDSA timings' + privKey = SecureBinaryData(hex_to_binary('aa'*32)) + pubKey = SecureBinaryData() + nTest = 100 + + # Test Conversion from PrivKey to PubKey + start = time.time() + for i in range(nTest): + pubKey = CryptoECDSA().ComputePublicKey(privKey) + end = time.time() + print ' PrivateKey --> PublicKey'.ljust(36), + print ': %0.1f/sec' % (nTest/(end-start)) + + # Check keypair match + start = time.time() + for i in range(nTest): + match = CryptoECDSA().CheckPubPrivKeyMatch(privKey, pubKey) + end = time.time() + print ' PubPrivPair--> CheckMatch'.ljust(36), + print ': %0.1f/sec' % (nTest/(end-start)) + + # Test signing speed + msg = SecureBinaryData( hex_to_binary('ff'*32) ) + sig = SecureBinaryData() + start = time.time() + for i in range(nTest): + sig = CryptoECDSA().SignData(msg, privKey) + end = time.time() + print ' PrivateKey --> Signature'.ljust(36), + print ': %0.1f/sec' % (nTest/(end-start)) + + # Test ECDSA verification speed + start = time.time() + for i in range(nTest): + isValid = CryptoECDSA().VerifyData(msg, sig, pubKey) + end = time.time() + print ' PublicKey --> VerifySig'.ljust(36), + print ': %0.1f/sec' % (nTest/(end-start)) + + + # Deterministic wallet chain computation + chainedPrivKey = SecureBinaryData() + chainedPubKey = SecureBinaryData() + chaincode = SecureBinaryData( hex_to_binary('45'*32)) + + start = time.time() + for i in range(nTest): + chainedPubKey = CryptoECDSA().ComputeChainedPrivateKey(privKey, chaincode) + end = time.time() + print ' PrivateKey --> NextInChain'.ljust(36), + print ': %0.1f/sec' % (nTest/(end-start)) + + start = time.time() + for i in range(nTest): + chainedPubKey = CryptoECDSA().ComputeChainedPublicKey(pubKey, chaincode) + end = time.time() + print ' PublicKey --> NextInChain'.ljust(36), + print ': %0.1f/sec' % (nTest/(end-start)) + + + + +if Test_SettingsFile: + print '' + print '*********************************************************************' + print 'Testing Settings-file operations' + print '*********************************************************************' + print '' + + testFile1 = 'settingsFile1.txt' + testFile2 = 'settingsFile2.txt' + settings = SettingsFile(testFile1) + settings.set('TestKey1', 32) + settings.set('TestKey2', 12.3) + settings.set('TestKey3', 'hello settings file') + settings.set('TestKey4', (1,2,3)) + settings.set('TestKey5', [1,2,3]) + settings.set('Test Key 6', 12) + settings.set('Test Key 7', ['str1', 'str2']) + settings.set('TestKey8', False) + settings.set('TestKey9', True) + settings.set('TestKey10', [True, True, False]) + + + settings.pprint() + + settings.extend('TestKey2', 1.1) + settings.extend('TestKey4', 6) + settings.extend('TestKey11', 'astring') + settings.extend('TestKey12', 83) + + print 'Reading in' + newSettings = SettingsFile(testFile1) + newSettings.pprint() + + print 'Expect list:' + print ' ',settings.get('Test Key 6') + print ' ',settings.get('Test Key 6', expectList=True) + print ' ',settings.get('Test Key 7', expectList=False) + print ' ',settings.get('Test Key 7', expectList=True) + + print 'Writing new settings file' + newSettings.writeSettingsFile(testFile2) + + with open(testFile1, 'r') as f: + f1 = f.read() + with open(testFile2, 'r') as f: + f2 = f.read() + + os.remove(testFile1) + 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]) + + + + + + +if Test_AddressBooks: + + + cppWlt = Cpp.BtcWallet() + cppWlt.addAddress_1_(hex_to_binary("0c6b92101c7025643c346d9c3e23034a8a843e21")) + cppWlt.addAddress_1_(hex_to_binary("11b366edfc0a8b66feebae5c2e25a7b6a5d1cf31")) + cppWlt.addAddress_1_(hex_to_binary("34c9f8dc91dfe1ae1c59e76cbe1aa39d0b7fc041")) + cppWlt.addAddress_1_(hex_to_binary("d77561813ca968270d5f63794ddb6aab3493605e")) + cppWlt.addAddress_1_(hex_to_binary("0e0aec36fe2545fb31a41164fb6954adcd96b342")) + cppWlt.addAddress_1_(hex_to_binary("6c27c8e67b7376f3ab63553fe37a4481c4f951cf")) + + TheBDM.registerWallet(cppWlt) + + print '\n\nLoading Blockchain from:', BLKFILE_FIRSTFILE + BDM_LoadBlockchainFile(BLKFILE_FIRSTFILE) + print 'Done!' + + TheBDM.scanBlockchainForTx(cppWlt) + cppWlt.pprintLedger() + + + AddrBook = cppWlt.createAddressBook() + print "AB ", len(AddrBook) + print "Lst ", len(list(AddrBook)) + print "[:] ", len(AddrBook[:]) + + for abe in AddrBook: + print binary_to_hex(abe.getAddr160()), + print len(abe.getTxList()) + for rtx in abe.getTxList(): + print '\t', binary_to_hex(rtx.getTxHash()), rtx.getBlkNum(), rtx.getTxIndex() + + +if Test_URIParse: + + print 'Testing percent-encoding:' + test = [] + test.append('regularmessage') + test.append('regular_message') + test.append('regular%message') + test.append('regular&message') + test.append('regular message~') + + for t in test: + t1 = uriReservedToPercent(t) + t2 = uriPercentToReserved(t1) + passStr = 'PASS' if t==t2 else '***FAIL***' + print '\t', + print ('"%s"'%t).ljust(20), + print ('"%s"'%t1).ljust(20), + print ('"%s"'%t2).ljust(20), + print ('(%s)'%passStr) + + + + print 'Testing URI parsing' + test = [] + test.append('notavaliduristring') + test.append('bitcoin:1NS17iag9jJgTHD1VXjvLCEnZuQ3rJED9L') + test.append('bitcoin:1NS17iag9jJgTHD1VXjvLCEnZuQ3rJED9L;version=1.0') + test.append('bitcoin:1NS17iag9jJgTHD1VXjvLCEnZuQ3rJED9L?amount=20.3') + test.append('bitcoin:1NS17iag9jJgTHD1VXjvLCEnZuQ3rJED9L;version=1.0?amount=20.3') + test.append('bitcoin:1NS17iag9jJgTHD1VXjvLCEnZuQ3rJED9L?amount=0.00003&label=Luke-Jr') + test.append('bitcoin:1NS17iag9jJgTHD1VXjvLCEnZuQ3rJED9L;version=1.0?amount=20.3&label=Luke-Jr') + test.append('bitcoin:1NS17iag9jJgTHD1VXjvLCEnZuQ3rJED9L;version=1.0?amount=203&label=Luke-Jr') + test.append('bitcoin:1NS17iag9jJgTHD1VXjvLCEnZuQ3rJED9L?amount=203&message=Donation%20for%20proj') + test.append('bitcoin:1NS17iag9jJgTHD1VXjvLCEnZuQ3rJED9L?amount=203&label=Alan%27s%20key&message=Donation%20for%20proj') + test.append('bitcoin:1NS17iag9jJgTHD1VXjvLCEnZuQ3rJED9L?amount=2f03&label=invalid') + + for t in test: + print 'URI:', t + outputdict = parseBitcoinURI(t) + if len(outputdict)==0: + print '\t' + continue + + for key,val in outputdict.iteritems(): + print '\t', key.ljust(12), '=', val + + +if Test_BkgdThread: + import math + + def longFuncA(a,b,c): + print 'FuncA', a,b,c + nIter = 10**7 + start = RightNow() + for i in xrange(nIter): + math.log(float(i)+1) + print 'Finished %d log() calculations'%nIter, + print '...in', RightNow() - start, 'seconds' + + + def longFuncB(a,b,c): + print 'FuncB', a,b,c + nIter = 10**7 + start = RightNow() + for i in xrange(nIter): + math.sqrt(float(i)+1) + print 'Finished %d sqrt() calculations'%nIter, + print '...in', RightNow() - start, 'seconds' + + + def longFuncC(a,b,c): + print 'FuncC', a,b,c + nIter = 10**7 + start = RightNow() + for i in xrange(nIter): + math.sin(float(i)+1) + print 'Finished %d sin() calculations'%nIter, + print '...in', RightNow() - start, 'seconds' + + + print 'Creating Thread' + thr = PyBackgroundThread() + thr.setPreThreadFunction(longFuncA, 1, '2', 3.0) + thr.setThreadFunction(longFuncB, *(5, '6', 8.0)) + thr.setPostThreadFunction(longFuncC, a=50, b='90', c=12.0) + print 'Starting thread...' + thr.start() + print 'Print statement right after thread.start()... waiting' + print 'Run longFuncC again just for fun, while we wait...' + longFuncC(0,0,0) + + + + +if Test_AsyncBDM: + + print '***********************************************************************' + print 'Testing asynchronous BlockDataManager' + print '***********************************************************************' + + def printBDMStuff(): + print 'BlkMode: ', TheBDM.getBDMState() + print 'IsScanning:', TheBDM.isScanning() + print 'IsInit: ', TheBDM.isInitialized() + print 'doBlock: ', TheBDM.alwaysBlock + print 'ScanAllow: ', TheBDM.allowRescan + print 'isDirty: ', TheBDM.isDirty + print 'NumAddr: ', TheBDM.masterCppWallet.getNumAddr() + print 'NumPyWlt: ', len(TheBDM.pyWltList) + print 'NumCppWlt: ', len(TheBDM.cppWltList) + print 'NumInputs: ', TheBDM.inputQueue.qsize() + print 'NumOutput: ', TheBDM.outputQueue.qsize() + + try: + print 'Starting AsyncBDM Test' + printBDMStuff() + + + print '\n\n(Async) Loading Blockchain from:', BTC_HOME_DIR + TheBDM.setSatoshiDir(BTC_HOME_DIR) + + start = RightNow() + TheBDM.loadBlockchain(wait=False) + print RightNow()-start, 'seconds' + + print TheBDM.getBDMState() + while TheBDM.isScanning(): + print 'Still waiting for scan to finish...' + time.sleep(1) + printBDMStuff() + print 'Thread done!' + + print '\n\nGetting top block information' + head = TheBDM.getTopBlockHeader() + head.pprint() + + print '\n\nGetting genesis block information' + head = TheBDM.getHeaderByHeight(0) + head.pprint() + + # Do the same thing, but with blocking + print '\n\n(Async) Resetting' + TheBDM.Reset(wait=True) + print 'Done resetting BDM.' + printBDMStuff() + + print '\n\n(Blocking) Loading Blockchain from:', BTC_HOME_DIR + TheBDM.setBlocking(True) + TheBDM.setSatoshiDir(BTC_HOME_DIR) + + start = RightNow() + TheBDM.loadBlockchain() + print RightNow()-start, 'seconds' + + printBDMStuff() + print 'Done!' + + + + print 'Start testing blockchain with wallets, now' + print 'Resetting BDM' + TheBDM.Reset(wait=True) + + print 'Setting blocking=False' + TheBDM.setBlocking(False) + TheBDM.setSatoshiDir(BTC_HOME_DIR) + + cppWlt = Cpp.BtcWallet() + + if not USE_TESTNET: + cppWlt.addAddress_1_(hex_to_binary("604875c897a079f4db88e5d71145be2093cae194")) + cppWlt.addAddress_1_(hex_to_binary("8996182392d6f05e732410de4fc3fa273bac7ee6")) + cppWlt.addAddress_1_(hex_to_binary("b5e2331304bc6c541ffe81a66ab664159979125b")) + cppWlt.addAddress_1_(hex_to_binary("ebbfaaeedd97bc30df0d6887fd62021d768f5cb8")) + else: + # Test-network addresses + cppWlt.addAddress_1_(hex_to_binary("5aa2b7e93537198ef969ad5fb63bea5e098ab0cc")) + cppWlt.addAddress_1_(hex_to_binary("28b2eb2dc53cd15ab3dc6abf6c8ea3978523f948")) + cppWlt.addAddress_1_(hex_to_binary("720fbde315f371f62c158b7353b3629e7fb071a8")) + cppWlt.addAddress_1_(hex_to_binary("0cc51a562976a075b984c7215968d41af43be98f")) + + cppWltEmpty = Cpp.BtcWallet() + + print 'Registering cppWallet:' + TheBDM.registerWallet(cppWlt) + TheBDM.registerWallet(cppWltEmpty) + + print 'Loading blockchain with wallet already registered', + start = RightNow() + TheBDM.loadBlockchain() + while TheBDM.getBDMState()=='Scanning': + time.sleep(0.1) + print '.', + print (RightNow() - start), ' seconds' + + print '\n\nUpdating registered wallets with blockchain info' + start = RightNow() + TheBDM.updateWalletsAfterScan() + while TheBDM.getBDMState()=='Scanning': + time.sleep(0.1) + print '.', + print (RightNow() - start), ' seconds' + + #nAddr = cppWlt.getNumAddr() + #print 'Address Balances:' + #for i in range(nAddr): + #cppAddr = cppWlt.getAddrByIndex(i) + #bal = cppAddr.getSpendableBalance() + #print ' %s %s' % (hash160_to_addrStr(cppAddr.getAddrStr20())[:12], coin2str(bal)) + + printBDMStuff() + nAddr = cppWlt.getNumAddr() + print 'Address Balances:' + for i in range(nAddr): + cppAddr = cppWlt.getAddrByIndex(i) + bal = cppAddr.getSpendableBalance() + print ' %s %s' % (hash160_to_addrStr(cppAddr.getAddrStr20())[:12], coin2str(bal)) + + + if not USE_TESTNET: + cppWltEmpty.addAddress_1_(hex_to_binary("11b366edfc0a8b66feebae5c2e25a7b6a5d1cf31")) + else: + cppWltEmpty.addAddress_1_(hex_to_binary("57ac7bfb77b1f678043ac6ea0fa67b4686c271e5")) + cppWltEmpty.addAddress_1_(hex_to_binary("b11bdcd6371e5b567b439cd95d928e869d1f546a")) + cppWltEmpty.addAddress_1_(hex_to_binary("2bb0974f6d43e3baa03d82610aac2b6ed017967d")) + cppWltEmpty.addAddress_1_(hex_to_binary("61d62799e52bc8ee514976a19d67478f25df2bb1")) + + # In practice, we won't be adding the addresses directly to the C++ wallets + # We will add them to the python wallets, which will absorb them into the + # python code AND register them with the BDM + # But working with the C++ wallets directly, need to re-register them + print 'Re-registering cppWallets:' + TheBDM.registerWallet(cppWlt) + TheBDM.registerWallet(cppWltEmpty) + + print 'Need to rescan %d blocks (Wlt1)' % TheBDM.numBlocksToRescan(cppWlt) + print 'Need to rescan %d blocks (Wlt2)' % TheBDM.numBlocksToRescan(cppWltEmpty) + TheBDM.rescanBlockchain() + start = RightNow() + while TheBDM.getBDMState()=='Scanning': + time.sleep(0.1) + print '.', + print (RightNow() - start), ' seconds' + + start = RightNow() + print 'Update wallets after scan' + TheBDM.updateWalletsAfterScan() + print (RightNow() - start), ' seconds' + + printBDMStuff() + nAddr = cppWltEmpty.getNumAddr() + print 'Address Balances:' + for i in range(nAddr): + cppAddr = cppWltEmpty.getAddrByIndex(i) + bal = cppAddr.getSpendableBalance() + print ' %s %s' % (hash160_to_addrStr(cppAddr.getAddrStr20())[:12], coin2str(bal)) + + + + # Test pybtcwallets: + # Include a test wallet with a tiny amount of BTC + + # Since the BDM is already loaded, want to skip the scan until we're ready + if USE_TESTNET: + pywlt = PyBtcWallet().readWalletFile('armory.testnet.watchonly.wallet') + else: + pywlt = PyBtcWallet().readWalletFile('armory.mainnet.watchonly.wallet') + + TheBDM.registerWallet(pywlt, isFresh=False, wait=True) + print 'NumToRescan: ', TheBDM.numBlocksToRescan(pywlt.cppWallet, wait=True) + TheBDM.rescanBlockchain(wait=False) + start = RightNow() + while TheBDM.getBDMState()=='Scanning': + time.sleep(0.1) + print '.', + print (RightNow() - start), ' seconds' + + except: + print 'CRASH' + raise + finally: + TheBDM.execCleanShutdown() + + + +################################################################################ +################################################################################ +if Test_Timers: + print '***********************************************************************' + print 'Testing Timer Objects' + print '***********************************************************************' + + n=100000 + +if Test_FiniteField: + print '***********************************************************************' + print 'Testing Finite Field' + print '***********************************************************************' + from random import uniform + + print '\nTesting finite-field matrix operations' + ff = FiniteField(8) + m = [[-3,2,-5],[-1,0,-2],[3,-4,1]] + v = [1,5,3] + print 'mtrx:', m, 'vect:', v + print 'm*v:' ,ff.mtrxmultvect(m, v) + print 'm*m:' ,ff.mtrxmult(m, m) + print 'det:', ff.mtrxdet(m) + print 'adj:', ff.mtrxadjoint(m) + minv = ff.mtrxinv(m) + print 'minv:', minv + print 'm*minv:', ff.mtrxmult(m, minv) + + + ff = FiniteField(8) + m = [[1,-3,2,-5],[-1,1,0,-2],[3,-4,1,1],[3,6,-2,-2]] + v = [1,5,3,3] + print 'mtrx:', m, 'vect:', v + print 'm*v:' ,ff.mtrxmultvect(m, v) + print 'm*m:' ,ff.mtrxmult(m, m) + print 'det:', ff.mtrxdet(m) + print 'adj:', ff.mtrxadjoint(m) + minv = ff.mtrxinv(m) + print 'minv:', minv + print 'm*minv:', ff.mtrxmult(m, minv) + + + + from random import shuffle + + def testSecret(secretHex, M, N, nbytes=1): + + secret = hex_to_binary(secretHex) + print '\nSplitting secret into %d-of-%d: secret=%s' % (M,N,secretHex) + tstart = RightNow() + out = SplitSecret(secret, M, N) + tsplit = RightNow() - tstart + + print 'Fragments:' + for i in range(len(out)): + x = binary_to_hex(out[i][0]) + y = binary_to_hex(out[i][1]) + print ' Fragment %d: [%s, %s]' % (i+1,x,y) + + trecon = 0 + print 'Reconstructing secret from various subsets of fragments...' + for i in range(10): + shuffle(out) + tstart = RightNow() + reconstruct = ReconstructSecret(out, M, nbytes) + trecon += RightNow() - tstart + + print ' The reconstructed secret is:', binary_to_hex(reconstruct) + + print 'Splitting secret took: %0.5f sec' % tsplit + print 'Reconstructing takes: %0.5f sec' % (trecon/10) + + + testSecret('9f', 2,3) + testSecret('9f', 3,5) + testSecret('9f', 4,7) + testSecret('9f', 5,9) + testSecret('9f', 6,7) + + testSecret('9f'*16, 3,5, 16) + testSecret('9f'*16, 7,10, 16) + + + +################################################################################ +################################################################################ +if Test_PyBkgdThread: + print '***********************************************************************' + print 'Testing Background Threading' + print '***********************************************************************' + from random import uniform + + # Will run the ComputePublicKey function a bunch of times in the background + def compute(N, threadID): + s = RightNow() + for i in range(N): + key = int_to_binary(long(uniform(0,2**32)), widthBytes=32) + k = CryptoECDSA().ComputePublicKey(SecureBinaryData(key)) + ns = (RightNow() - s) + #print 'Thread %d: %d keys in %0.2f sec, %0.2f key/sec' % (threadID, N, ns, N/ns) + + + # Figure out how many aggregate keys/sec we get with threading + def test_N_threads(NThr): + NPer = 1000 + + # Test All + thr = [] + for i in range(NThr): + thr.append( PyBackgroundThread(compute, NPer, i)) + + startTime = RightNow() + for i in range(NThr): + thr[i].start() + + for i in range(NThr): + thr[i].join() + + total = (RightNow() - startTime) + NC = NThr*NPer + return NC, total + + for i in range(1,10): + n,s = test_N_threads(i) + print 'NThreads: %02d, %0.2f keys/sec' % (i, n/s) + + + +if Test_EstBlockchain: + print '***********************************************************************' + print 'Testing blockchain size estimation algorithm' + print '***********************************************************************' + for blk in [0,1000,1001,1002, 10000, 125000, 175000, 225000, 230000, 231000, 250000,275000, 300000, 1000000]: + sz = EstimateCumulativeBlockchainSize(blk) + print blk, bytesToHumanSize(sz), '(%d)'%sz + + +if Test_SatoshiManager: + print '***********************************************************************' + print 'Testing Satoshi Manager ' + print '***********************************************************************' + + # This is not a proper "unittest", it's more of a case-study ... it's going + # to create a new directory and start a fresh download of the blockchain. + # Or I may set it up to have an existing set of block files, and it just + # needs to update. + + + + + + alreadyPort = satoshiIsAvailable() + if alreadyPort>0: + print 'Bitcoind is open already on port %d! ' % alreadyPort + print 'Please close it and try again.' + exit(0) + + + if not os.path.exists('sdmtest'): + os.mkdir('sdmtest') + + if not os.path.exists('sdmtest/bitcoind'): + shutil.copy('/usr/lib/bitcoin/bitcoind','sdmtest') + + print 'Creating SatoshiDaemonManager...' + sdm = SatoshiDaemonManager() + sdm.setupSDM(satoshiHome='sdmtest') + + print 'Reading bitcoin.conf file... (should create it if DNE)' + sdm.readBitcoinConf(makeIfDNE=True) + sdm.printSDMInfo() + + var = raw_input("Continue? [Y/n]: ") + + fout = open('record_dl_times.txt','w') + + startTime = RightNow() + try: + print 'Starting bitcoind...' + sdm.startBitcoind() + + for i in range(1000000): + if i%30==0: + sdm.printSDMInfo() + state = sdm.getSDMState() + print 'Current SDM state:', state, + time.sleep(5) + + if state in ('BitcoindReady', 'BitcoindSynchronizing'): + info = sdm.getTopBlockInfo() + print ': TopBlock: %d (%s)' % (info['numblks'], unixTimeToFormatStr(info['toptime'])) + nb = int(info['numblks']) + dt = int(RightNow() - startTime) + fout.write('%d %d\n' % (nb,dt)) + else: + print '' + + + sdm.stopBitcoind() + while(sdm.isRunningBitcoind()): + time.sleep(0.1) + t+=0.1 + print 'Waiting for bitcoind to shutdown, %0.2f seconds' % t + + print 'Stopping again, just for fun' + sdm.stopBitcoind() + finally: + # Gotta shutdown bitcoind no matter what + print 'Attempting to shutdown, no matter what!' + sdm.stopBitcoind() + + + + + diff --git a/urllib3/__init__.py b/urllib3/__init__.py new file mode 100644 index 000000000..086387f31 --- /dev/null +++ b/urllib3/__init__.py @@ -0,0 +1,58 @@ +# urllib3/__init__.py +# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt) +# +# This module is part of urllib3 and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +""" +urllib3 - Thread-safe connection pooling and re-using. +""" + +__author__ = 'Andrey Petrov (andrey.petrov@shazow.net)' +__license__ = 'MIT' +__version__ = '1.8' + + +from .connectionpool import ( + HTTPConnectionPool, + HTTPSConnectionPool, + connection_from_url +) + +from . import exceptions +from .filepost import encode_multipart_formdata +from .poolmanager import PoolManager, ProxyManager, proxy_from_url +from .response import HTTPResponse +from .util import make_headers, get_host, Timeout + + +# Set default logging handler to avoid "No handler found" warnings. +import logging +try: # Python 2.7+ + from logging import NullHandler +except ImportError: + class NullHandler(logging.Handler): + def emit(self, record): + pass + +logging.getLogger(__name__).addHandler(NullHandler()) + +def add_stderr_logger(level=logging.DEBUG): + """ + Helper for quickly adding a StreamHandler to the logger. Useful for + debugging. + + Returns the handler after adding it. + """ + # This method needs to be in this __init__.py to get the __name__ correct + # even if urllib3 is vendored within another package. + logger = logging.getLogger(__name__) + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s')) + logger.addHandler(handler) + logger.setLevel(level) + logger.debug('Added an stderr logging handler to logger: %s' % __name__) + return handler + +# ... Clean up. +del NullHandler diff --git a/urllib3/_collections.py b/urllib3/_collections.py new file mode 100644 index 000000000..9cea3a44c --- /dev/null +++ b/urllib3/_collections.py @@ -0,0 +1,205 @@ +# urllib3/_collections.py +# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt) +# +# This module is part of urllib3 and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +from collections import Mapping, MutableMapping +try: + from threading import RLock +except ImportError: # Platform-specific: No threads available + class RLock: + def __enter__(self): + pass + + def __exit__(self, exc_type, exc_value, traceback): + pass + + +try: # Python 2.7+ + from collections import OrderedDict +except ImportError: + from .packages.ordered_dict import OrderedDict +from .packages.six import itervalues + + +__all__ = ['RecentlyUsedContainer', 'HTTPHeaderDict'] + + +_Null = object() + + +class RecentlyUsedContainer(MutableMapping): + """ + Provides a thread-safe dict-like container which maintains up to + ``maxsize`` keys while throwing away the least-recently-used keys beyond + ``maxsize``. + + :param maxsize: + Maximum number of recent elements to retain. + + :param dispose_func: + Every time an item is evicted from the container, + ``dispose_func(value)`` is called. Callback which will get called + """ + + ContainerCls = OrderedDict + + def __init__(self, maxsize=10, dispose_func=None): + self._maxsize = maxsize + self.dispose_func = dispose_func + + self._container = self.ContainerCls() + self.lock = RLock() + + def __getitem__(self, key): + # Re-insert the item, moving it to the end of the eviction line. + with self.lock: + item = self._container.pop(key) + self._container[key] = item + return item + + def __setitem__(self, key, value): + evicted_value = _Null + with self.lock: + # Possibly evict the existing value of 'key' + evicted_value = self._container.get(key, _Null) + self._container[key] = value + + # If we didn't evict an existing value, we might have to evict the + # least recently used item from the beginning of the container. + if len(self._container) > self._maxsize: + _key, evicted_value = self._container.popitem(last=False) + + if self.dispose_func and evicted_value is not _Null: + self.dispose_func(evicted_value) + + def __delitem__(self, key): + with self.lock: + value = self._container.pop(key) + + if self.dispose_func: + self.dispose_func(value) + + def __len__(self): + with self.lock: + return len(self._container) + + def __iter__(self): + raise NotImplementedError('Iteration over this class is unlikely to be threadsafe.') + + def clear(self): + with self.lock: + # Copy pointers to all values, then wipe the mapping + # under Python 2, this copies the list of values twice :-| + values = list(self._container.values()) + self._container.clear() + + if self.dispose_func: + for value in values: + self.dispose_func(value) + + def keys(self): + with self.lock: + return self._container.keys() + + +class HTTPHeaderDict(MutableMapping): + """ + :param headers: + An iterable of field-value pairs. Must not contain multiple field names + when compared case-insensitively. + + :param kwargs: + Additional field-value pairs to pass in to ``dict.update``. + + A ``dict`` like container for storing HTTP Headers. + + Field names are stored and compared case-insensitively in compliance with + RFC 2616. Iteration provides the first case-sensitive key seen for each + case-insensitive pair. + + Using ``__setitem__`` syntax overwrites fields that compare equal + case-insensitively in order to maintain ``dict``'s api. For fields that + compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add`` + in a loop. + + If multiple fields that are equal case-insensitively are passed to the + constructor or ``.update``, the behavior is undefined and some will be + lost. + + >>> headers = HTTPHeaderDict() + >>> headers.add('Set-Cookie', 'foo=bar') + >>> headers.add('set-cookie', 'baz=quxx') + >>> headers['content-length'] = '7' + >>> headers['SET-cookie'] + 'foo=bar, baz=quxx' + >>> headers['Content-Length'] + '7' + + If you want to access the raw headers with their original casing + for debugging purposes you can access the private ``._data`` attribute + which is a normal python ``dict`` that maps the case-insensitive key to a + list of tuples stored as (case-sensitive-original-name, value). Using the + structure from above as our example: + + >>> headers._data + {'set-cookie': [('Set-Cookie', 'foo=bar'), ('set-cookie', 'baz=quxx')], + 'content-length': [('content-length', '7')]} + """ + + def __init__(self, headers=None, **kwargs): + self._data = {} + if headers is None: + headers = {} + self.update(headers, **kwargs) + + def add(self, key, value): + """Adds a (name, value) pair, doesn't overwrite the value if it already + exists. + + >>> headers = HTTPHeaderDict(foo='bar') + >>> headers.add('Foo', 'baz') + >>> headers['foo'] + 'bar, baz' + """ + self._data.setdefault(key.lower(), []).append((key, value)) + + def getlist(self, key): + """Returns a list of all the values for the named field. Returns an + empty list if the key doesn't exist.""" + return self[key].split(', ') if key in self else [] + + def copy(self): + h = HTTPHeaderDict() + for key in self._data: + for rawkey, value in self._data[key]: + h.add(rawkey, value) + return h + + def __eq__(self, other): + if not isinstance(other, Mapping): + return False + other = HTTPHeaderDict(other) + return dict((k1, self[k1]) for k1 in self._data) == \ + dict((k2, other[k2]) for k2 in other._data) + + def __getitem__(self, key): + values = self._data[key.lower()] + return ', '.join(value[1] for value in values) + + def __setitem__(self, key, value): + self._data[key.lower()] = [(key, value)] + + def __delitem__(self, key): + del self._data[key.lower()] + + def __len__(self): + return len(self._data) + + def __iter__(self): + for headers in itervalues(self._data): + yield headers[0][0] + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, dict(self.items())) diff --git a/urllib3/connection.py b/urllib3/connection.py new file mode 100644 index 000000000..97e53e964 --- /dev/null +++ b/urllib3/connection.py @@ -0,0 +1,214 @@ +# urllib3/connection.py +# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt) +# +# This module is part of urllib3 and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +import sys +import socket +from socket import timeout as SocketTimeout + +try: # Python 3 + from http.client import HTTPConnection as _HTTPConnection, HTTPException +except ImportError: + from httplib import HTTPConnection as _HTTPConnection, HTTPException + +class DummyConnection(object): + "Used to detect a failed ConnectionCls import." + pass + +try: # Compiled with SSL? + ssl = None + HTTPSConnection = DummyConnection + + class BaseSSLError(BaseException): + pass + + try: # Python 3 + from http.client import HTTPSConnection as _HTTPSConnection + except ImportError: + from httplib import HTTPSConnection as _HTTPSConnection + + import ssl + BaseSSLError = ssl.SSLError + +except (ImportError, AttributeError): # Platform-specific: No SSL. + pass + +from .exceptions import ( + ConnectTimeoutError, +) +from .packages.ssl_match_hostname import match_hostname +from .packages import six +from .util import ( + assert_fingerprint, + resolve_cert_reqs, + resolve_ssl_version, + ssl_wrap_socket, +) + + +port_by_scheme = { + 'http': 80, + 'https': 443, +} + + +class HTTPConnection(_HTTPConnection, object): + """ + Based on httplib.HTTPConnection but provides an extra constructor + backwards-compatibility layer between older and newer Pythons. + """ + + default_port = port_by_scheme['http'] + + # By default, disable Nagle's Algorithm. + tcp_nodelay = 1 + + def __init__(self, *args, **kw): + if six.PY3: # Python 3 + kw.pop('strict', None) + + if sys.version_info < (2, 7): # Python 2.6 and earlier + kw.pop('source_address', None) + self.source_address = None + + _HTTPConnection.__init__(self, *args, **kw) + + def _new_conn(self): + """ Establish a socket connection and set nodelay settings on it + + :return: a new socket connection + """ + extra_args = [] + if self.source_address: # Python 2.7+ + extra_args.append(self.source_address) + + conn = socket.create_connection( + (self.host, self.port), + self.timeout, + *extra_args + ) + conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, + self.tcp_nodelay) + return conn + + def _prepare_conn(self, conn): + self.sock = conn + if self._tunnel_host: + # TODO: Fix tunnel so it doesn't depend on self.sock state. + self._tunnel() + + def connect(self): + conn = self._new_conn() + self._prepare_conn(conn) + + +class HTTPSConnection(HTTPConnection): + default_port = port_by_scheme['https'] + + def __init__(self, host, port=None, key_file=None, cert_file=None, + strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, + source_address=None): + + HTTPConnection.__init__(self, host, port, + strict=strict, + timeout=timeout, + source_address=source_address) + + self.key_file = key_file + self.cert_file = cert_file + + def connect(self): + conn = self._new_conn() + self._prepare_conn(conn) + self.sock = ssl.wrap_socket(conn, self.key_file, self.cert_file) + + +class VerifiedHTTPSConnection(HTTPSConnection): + """ + Based on httplib.HTTPSConnection but wraps the socket with + SSL certification. + """ + cert_reqs = None + ca_certs = None + ssl_version = None + + def set_cert(self, key_file=None, cert_file=None, + cert_reqs=None, ca_certs=None, + assert_hostname=None, assert_fingerprint=None): + + self.key_file = key_file + self.cert_file = cert_file + self.cert_reqs = cert_reqs + self.ca_certs = ca_certs + self.assert_hostname = assert_hostname + self.assert_fingerprint = assert_fingerprint + + def connect(self): + # Add certificate verification + try: + sock = socket.create_connection( + address=(self.host, self.port), + timeout=self.timeout, + ) + except SocketTimeout: + raise ConnectTimeoutError( + self, "Connection to %s timed out. (connect timeout=%s)" % + (self.host, self.timeout)) + + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, + self.tcp_nodelay) + + resolved_cert_reqs = resolve_cert_reqs(self.cert_reqs) + resolved_ssl_version = resolve_ssl_version(self.ssl_version) + + # the _tunnel_host attribute was added in python 2.6.3 (via + # http://hg.python.org/cpython/rev/0f57b30a152f) so pythons 2.6(0-2) do + # not have them. + if getattr(self, '_tunnel_host', None): + self.sock = sock + # Calls self._set_hostport(), so self.host is + # self._tunnel_host below. + self._tunnel() + + # Wrap socket using verification with the root certs in + # trusted_root_certs + self.sock = ssl_wrap_socket(sock, self.key_file, self.cert_file, + cert_reqs=resolved_cert_reqs, + ca_certs=self.ca_certs, + server_hostname=self.host, + ssl_version=resolved_ssl_version) + + + if resolved_cert_reqs != ssl.CERT_NONE: + if self.assert_fingerprint: + assert_fingerprint(self.sock.getpeercert(binary_form=True), + self.assert_fingerprint) + elif self.assert_hostname is not False: + #match_hostname(self.sock.getpeercert(), + #self.assert_hostname or self.host) + # TODO: At the very least, we should not totally nerf this + # function for all hosts. But it does work for this + # very isolated usecase, so I'm leaving it in here. + # Can fix later if we need to + print "***** FIXME *****" + print " ** Had issues with bitcoinarmory.com validation;" + print " ** SSLError: hostname 'scripts.bitcoinarmory.com'" + print " ** doesn't match either of 'bitcoinarmory.com', " + print " ** 'www.bitcoinarmory.com'. I am hardcoding the" + print " ** certname into the bundled requests/urllib3" + print " ** which breaks compatibility using it with any " + print " ** HTTPS hosts other than bitcoinarmory.com." + validDNS = ['bitcoinarmory.com', 'scripts.bitcoinarmory.com'] + for key,val in self.sock.getpeercert().get('subjectAltName', []): + if key=='DNS' and val in validDNS: + break + else: + raise ssl.SSLError('Cert does not match bitcoinarmory.com') + + +if ssl: + # Make a copy for testing. + UnverifiedHTTPSConnection = HTTPSConnection + HTTPSConnection = VerifiedHTTPSConnection diff --git a/urllib3/connectionpool.py b/urllib3/connectionpool.py new file mode 100644 index 000000000..6d0dbb184 --- /dev/null +++ b/urllib3/connectionpool.py @@ -0,0 +1,695 @@ +# urllib3/connectionpool.py +# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt) +# +# This module is part of urllib3 and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +import errno +import logging + +from socket import error as SocketError, timeout as SocketTimeout +import socket + +try: # Python 3 + from queue import LifoQueue, Empty, Full +except ImportError: + from Queue import LifoQueue, Empty, Full + import Queue as _ # Platform-specific: Windows + + +from .exceptions import ( + ClosedPoolError, + ConnectionError, + ConnectTimeoutError, + EmptyPoolError, + HostChangedError, + MaxRetryError, + SSLError, + TimeoutError, + ReadTimeoutError, + ProxyError, +) +from .packages.ssl_match_hostname import CertificateError +from .packages import six +from .connection import ( + port_by_scheme, + DummyConnection, + HTTPConnection, HTTPSConnection, VerifiedHTTPSConnection, + HTTPException, BaseSSLError, +) +from .request import RequestMethods +from .response import HTTPResponse +from .util import ( + assert_fingerprint, + get_host, + is_connection_dropped, + Timeout, +) + + +xrange = six.moves.xrange + +log = logging.getLogger(__name__) + +_Default = object() + +## Pool objects + +class ConnectionPool(object): + """ + Base class for all connection pools, such as + :class:`.HTTPConnectionPool` and :class:`.HTTPSConnectionPool`. + """ + + scheme = None + QueueCls = LifoQueue + + def __init__(self, host, port=None): + # httplib doesn't like it when we include brackets in ipv6 addresses + host = host.strip('[]') + + self.host = host + self.port = port + + def __str__(self): + return '%s(host=%r, port=%r)' % (type(self).__name__, + self.host, self.port) + +# This is taken from http://hg.python.org/cpython/file/7aaba721ebc0/Lib/socket.py#l252 +_blocking_errnos = set([errno.EAGAIN, errno.EWOULDBLOCK]) + +class HTTPConnectionPool(ConnectionPool, RequestMethods): + """ + Thread-safe connection pool for one host. + + :param host: + Host used for this HTTP Connection (e.g. "localhost"), passed into + :class:`httplib.HTTPConnection`. + + :param port: + Port used for this HTTP Connection (None is equivalent to 80), passed + into :class:`httplib.HTTPConnection`. + + :param strict: + Causes BadStatusLine to be raised if the status line can't be parsed + as a valid HTTP/1.0 or 1.1 status line, passed into + :class:`httplib.HTTPConnection`. + + .. note:: + Only works in Python 2. This parameter is ignored in Python 3. + + :param timeout: + Socket timeout in seconds for each individual connection. This can + be a float or integer, which sets the timeout for the HTTP request, + or an instance of :class:`urllib3.util.Timeout` which gives you more + fine-grained control over request timeouts. After the constructor has + been parsed, this is always a `urllib3.util.Timeout` object. + + :param maxsize: + Number of connections to save that can be reused. More than 1 is useful + in multithreaded situations. If ``block`` is set to false, more + connections will be created but they will not be saved once they've + been used. + + :param block: + If set to True, no more than ``maxsize`` connections will be used at + a time. When no free connections are available, the call will block + until a connection has been released. This is a useful side effect for + particular multithreaded situations where one does not want to use more + than maxsize connections per host to prevent flooding. + + :param headers: + Headers to include with all requests, unless other headers are given + explicitly. + + :param _proxy: + Parsed proxy URL, should not be used directly, instead, see + :class:`urllib3.connectionpool.ProxyManager`" + + :param _proxy_headers: + A dictionary with proxy headers, should not be used directly, + instead, see :class:`urllib3.connectionpool.ProxyManager`" + """ + + scheme = 'http' + ConnectionCls = HTTPConnection + + def __init__(self, host, port=None, strict=False, + timeout=Timeout.DEFAULT_TIMEOUT, maxsize=1, block=False, + headers=None, _proxy=None, _proxy_headers=None): + ConnectionPool.__init__(self, host, port) + RequestMethods.__init__(self, headers) + + self.strict = strict + + # This is for backwards compatibility and can be removed once a timeout + # can only be set to a Timeout object + if not isinstance(timeout, Timeout): + timeout = Timeout.from_float(timeout) + + self.timeout = timeout + + self.pool = self.QueueCls(maxsize) + self.block = block + + self.proxy = _proxy + self.proxy_headers = _proxy_headers or {} + + # Fill the queue up so that doing get() on it will block properly + for _ in xrange(maxsize): + self.pool.put(None) + + # These are mostly for testing and debugging purposes. + self.num_connections = 0 + self.num_requests = 0 + + def _new_conn(self): + """ + Return a fresh :class:`HTTPConnection`. + """ + self.num_connections += 1 + log.info("Starting new HTTP connection (%d): %s" % + (self.num_connections, self.host)) + + conn = self.ConnectionCls(host=self.host, port=self.port, + timeout=self.timeout.connect_timeout, + strict=self.strict) + if self.proxy is not None: + # Enable Nagle's algorithm for proxies, to avoid packet + # fragmentation. + conn.tcp_nodelay = 0 + return conn + + def _get_conn(self, timeout=None): + """ + Get a connection. Will return a pooled connection if one is available. + + If no connections are available and :prop:`.block` is ``False``, then a + fresh connection is returned. + + :param timeout: + Seconds to wait before giving up and raising + :class:`urllib3.exceptions.EmptyPoolError` if the pool is empty and + :prop:`.block` is ``True``. + """ + conn = None + try: + conn = self.pool.get(block=self.block, timeout=timeout) + + except AttributeError: # self.pool is None + raise ClosedPoolError(self, "Pool is closed.") + + except Empty: + if self.block: + raise EmptyPoolError(self, + "Pool reached maximum size and no more " + "connections are allowed.") + pass # Oh well, we'll create a new connection then + + # If this is a persistent connection, check if it got disconnected + if conn and is_connection_dropped(conn): + log.info("Resetting dropped connection: %s" % self.host) + conn.close() + + return conn or self._new_conn() + + def _put_conn(self, conn): + """ + Put a connection back into the pool. + + :param conn: + Connection object for the current host and port as returned by + :meth:`._new_conn` or :meth:`._get_conn`. + + If the pool is already full, the connection is closed and discarded + because we exceeded maxsize. If connections are discarded frequently, + then maxsize should be increased. + + If the pool is closed, then the connection will be closed and discarded. + """ + try: + self.pool.put(conn, block=False) + return # Everything is dandy, done. + except AttributeError: + # self.pool is None. + pass + except Full: + # This should never happen if self.block == True + log.warning( + "Connection pool is full, discarding connection: %s" % + self.host) + + # Connection never got put back into the pool, close it. + if conn: + conn.close() + + def _get_timeout(self, timeout): + """ Helper that always returns a :class:`urllib3.util.Timeout` """ + if timeout is _Default: + return self.timeout.clone() + + if isinstance(timeout, Timeout): + return timeout.clone() + else: + # User passed us an int/float. This is for backwards compatibility, + # can be removed later + return Timeout.from_float(timeout) + + def _make_request(self, conn, method, url, timeout=_Default, + **httplib_request_kw): + """ + Perform a request on a given urllib connection object taken from our + pool. + + :param conn: + a connection from one of our connection pools + + :param timeout: + Socket timeout in seconds for the request. This can be a + float or integer, which will set the same timeout value for + the socket connect and the socket read, or an instance of + :class:`urllib3.util.Timeout`, which gives you more fine-grained + control over your timeouts. + """ + self.num_requests += 1 + + timeout_obj = self._get_timeout(timeout) + + try: + timeout_obj.start_connect() + conn.timeout = timeout_obj.connect_timeout + # conn.request() calls httplib.*.request, not the method in + # urllib3.request. It also calls makefile (recv) on the socket. + conn.request(method, url, **httplib_request_kw) + except SocketTimeout: + raise ConnectTimeoutError( + self, "Connection to %s timed out. (connect timeout=%s)" % + (self.host, timeout_obj.connect_timeout)) + + # Reset the timeout for the recv() on the socket + read_timeout = timeout_obj.read_timeout + + # App Engine doesn't have a sock attr + if hasattr(conn, 'sock'): + # In Python 3 socket.py will catch EAGAIN and return None when you + # try and read into the file pointer created by http.client, which + # instead raises a BadStatusLine exception. Instead of catching + # the exception and assuming all BadStatusLine exceptions are read + # timeouts, check for a zero timeout before making the request. + if read_timeout == 0: + raise ReadTimeoutError( + self, url, + "Read timed out. (read timeout=%s)" % read_timeout) + if read_timeout is Timeout.DEFAULT_TIMEOUT: + conn.sock.settimeout(socket.getdefaulttimeout()) + else: # None or a value + conn.sock.settimeout(read_timeout) + + # Receive the response from the server + try: + try: # Python 2.7+, use buffering of HTTP responses + httplib_response = conn.getresponse(buffering=True) + except TypeError: # Python 2.6 and older + httplib_response = conn.getresponse() + except SocketTimeout: + raise ReadTimeoutError( + self, url, "Read timed out. (read timeout=%s)" % read_timeout) + + except BaseSSLError as e: + # Catch possible read timeouts thrown as SSL errors. If not the + # case, rethrow the original. We need to do this because of: + # http://bugs.python.org/issue10272 + if 'timed out' in str(e) or \ + 'did not complete (read)' in str(e): # Python 2.6 + raise ReadTimeoutError(self, url, "Read timed out.") + + raise + + except SocketError as e: # Platform-specific: Python 2 + # See the above comment about EAGAIN in Python 3. In Python 2 we + # have to specifically catch it and throw the timeout error + if e.errno in _blocking_errnos: + raise ReadTimeoutError( + self, url, + "Read timed out. (read timeout=%s)" % read_timeout) + + raise + + # AppEngine doesn't have a version attr. + http_version = getattr(conn, '_http_vsn_str', 'HTTP/?') + log.debug("\"%s %s %s\" %s %s" % (method, url, http_version, + httplib_response.status, + httplib_response.length)) + return httplib_response + + def close(self): + """ + Close all pooled connections and disable the pool. + """ + # Disable access to the pool + old_pool, self.pool = self.pool, None + + try: + while True: + conn = old_pool.get(block=False) + if conn: + conn.close() + + except Empty: + pass # Done. + + def is_same_host(self, url): + """ + Check if the given ``url`` is a member of the same host as this + connection pool. + """ + if url.startswith('/'): + return True + + # TODO: Add optional support for socket.gethostbyname checking. + scheme, host, port = get_host(url) + + # Use explicit default port for comparison when none is given + if self.port and not port: + port = port_by_scheme.get(scheme) + elif not self.port and port == port_by_scheme.get(scheme): + port = None + + return (scheme, host, port) == (self.scheme, self.host, self.port) + + def urlopen(self, method, url, body=None, headers=None, retries=3, + redirect=True, assert_same_host=True, timeout=_Default, + pool_timeout=None, release_conn=None, **response_kw): + """ + Get a connection from the pool and perform an HTTP request. This is the + lowest level call for making a request, so you'll need to specify all + the raw details. + + .. note:: + + More commonly, it's appropriate to use a convenience method provided + by :class:`.RequestMethods`, such as :meth:`request`. + + .. note:: + + `release_conn` will only behave as expected if + `preload_content=False` because we want to make + `preload_content=False` the default behaviour someday soon without + breaking backwards compatibility. + + :param method: + HTTP request method (such as GET, POST, PUT, etc.) + + :param body: + Data to send in the request body (useful for creating + POST requests, see HTTPConnectionPool.post_url for + more convenience). + + :param headers: + Dictionary of custom headers to send, such as User-Agent, + If-None-Match, etc. If None, pool headers are used. If provided, + these headers completely replace any pool-specific headers. + + :param retries: + Number of retries to allow before raising a MaxRetryError exception. + If `False`, then retries are disabled and any exception is raised + immediately. + + :param redirect: + If True, automatically handle redirects (status codes 301, 302, + 303, 307, 308). Each redirect counts as a retry. Disabling retries + will disable redirect, too. + + :param assert_same_host: + If ``True``, will make sure that the host of the pool requests is + consistent else will raise HostChangedError. When False, you can + use the pool on an HTTP proxy and request foreign hosts. + + :param timeout: + If specified, overrides the default timeout for this one + request. It may be a float (in seconds) or an instance of + :class:`urllib3.util.Timeout`. + + :param pool_timeout: + If set and the pool is set to block=True, then this method will + block for ``pool_timeout`` seconds and raise EmptyPoolError if no + connection is available within the time period. + + :param release_conn: + If False, then the urlopen call will not release the connection + back into the pool once a response is received (but will release if + you read the entire contents of the response such as when + `preload_content=True`). This is useful if you're not preloading + the response's content immediately. You will need to call + ``r.release_conn()`` on the response ``r`` to return the connection + back into the pool. If None, it takes the value of + ``response_kw.get('preload_content', True)``. + + :param \**response_kw: + Additional parameters are passed to + :meth:`urllib3.response.HTTPResponse.from_httplib` + """ + if headers is None: + headers = self.headers + + if retries < 0 and retries is not False: + raise MaxRetryError(self, url) + + if release_conn is None: + release_conn = response_kw.get('preload_content', True) + + # Check host + if assert_same_host and not self.is_same_host(url): + raise HostChangedError(self, url, retries - 1) + + conn = None + + # Merge the proxy headers. Only do this in HTTP. We have to copy the + # headers dict so we can safely change it without those changes being + # reflected in anyone else's copy. + if self.scheme == 'http': + headers = headers.copy() + headers.update(self.proxy_headers) + + # Must keep the exception bound to a separate variable or else Python 3 + # complains about UnboundLocalError. + err = None + + try: + # Request a connection from the queue + conn = self._get_conn(timeout=pool_timeout) + + # Make the request on the httplib connection object + httplib_response = self._make_request(conn, method, url, + timeout=timeout, + body=body, headers=headers) + + # If we're going to release the connection in ``finally:``, then + # the request doesn't need to know about the connection. Otherwise + # it will also try to release it and we'll have a double-release + # mess. + response_conn = not release_conn and conn + + # Import httplib's response into our own wrapper object + response = HTTPResponse.from_httplib(httplib_response, + pool=self, + connection=response_conn, + **response_kw) + + # else: + # The connection will be put back into the pool when + # ``response.release_conn()`` is called (implicitly by + # ``response.read()``) + + except Empty: + # Timed out by queue. + raise EmptyPoolError(self, "No pool connections are available.") + + except (BaseSSLError, CertificateError) as e: + # Release connection unconditionally because there is no way to + # close it externally in case of exception. + release_conn = True + raise SSLError(e) + + except (TimeoutError, HTTPException, SocketError) as e: + if conn: + # Discard the connection for these exceptions. It will be + # be replaced during the next _get_conn() call. + conn.close() + conn = None + + if not retries: + if isinstance(e, TimeoutError): + # TimeoutError is exempt from MaxRetryError-wrapping. + # FIXME: ... Not sure why. Add a reason here. + raise + + # Wrap unexpected exceptions with the most appropriate + # module-level exception and re-raise. + if isinstance(e, SocketError) and self.proxy: + raise ProxyError('Cannot connect to proxy.', e) + + if retries is False: + raise ConnectionError('Connection failed.', e) + + raise MaxRetryError(self, url, e) + + # Keep track of the error for the retry warning. + err = e + + finally: + if release_conn: + # Put the connection back to be reused. If the connection is + # expired then it will be None, which will get replaced with a + # fresh connection during _get_conn. + self._put_conn(conn) + + if not conn: + # Try again + log.warning("Retrying (%d attempts remain) after connection " + "broken by '%r': %s" % (retries, err, url)) + return self.urlopen(method, url, body, headers, retries - 1, + redirect, assert_same_host, + timeout=timeout, pool_timeout=pool_timeout, + release_conn=release_conn, **response_kw) + + # Handle redirect? + redirect_location = redirect and response.get_redirect_location() + if redirect_location and retries is not False: + if response.status == 303: + method = 'GET' + log.info("Redirecting %s -> %s" % (url, redirect_location)) + return self.urlopen(method, redirect_location, body, headers, + retries - 1, redirect, assert_same_host, + timeout=timeout, pool_timeout=pool_timeout, + release_conn=release_conn, **response_kw) + + return response + + +class HTTPSConnectionPool(HTTPConnectionPool): + """ + Same as :class:`.HTTPConnectionPool`, but HTTPS. + + When Python is compiled with the :mod:`ssl` module, then + :class:`.VerifiedHTTPSConnection` is used, which *can* verify certificates, + instead of :class:`.HTTPSConnection`. + + :class:`.VerifiedHTTPSConnection` uses one of ``assert_fingerprint``, + ``assert_hostname`` and ``host`` in this order to verify connections. + If ``assert_hostname`` is False, no verification is done. + + The ``key_file``, ``cert_file``, ``cert_reqs``, ``ca_certs`` and + ``ssl_version`` are only used if :mod:`ssl` is available and are fed into + :meth:`urllib3.util.ssl_wrap_socket` to upgrade the connection socket + into an SSL socket. + """ + + scheme = 'https' + ConnectionCls = HTTPSConnection + + def __init__(self, host, port=None, + strict=False, timeout=None, maxsize=1, + block=False, headers=None, + _proxy=None, _proxy_headers=None, + key_file=None, cert_file=None, cert_reqs=None, + ca_certs=None, ssl_version=None, + assert_hostname=None, assert_fingerprint=None): + + HTTPConnectionPool.__init__(self, host, port, strict, timeout, maxsize, + block, headers, _proxy, _proxy_headers) + self.key_file = key_file + self.cert_file = cert_file + self.cert_reqs = cert_reqs + self.ca_certs = ca_certs + self.ssl_version = ssl_version + self.assert_hostname = assert_hostname + self.assert_fingerprint = assert_fingerprint + + def _prepare_conn(self, conn): + """ + Prepare the ``connection`` for :meth:`urllib3.util.ssl_wrap_socket` + and establish the tunnel if proxy is used. + """ + + if isinstance(conn, VerifiedHTTPSConnection): + conn.set_cert(key_file=self.key_file, + cert_file=self.cert_file, + cert_reqs=self.cert_reqs, + ca_certs=self.ca_certs, + assert_hostname=self.assert_hostname, + assert_fingerprint=self.assert_fingerprint) + conn.ssl_version = self.ssl_version + + if self.proxy is not None: + # Python 2.7+ + try: + set_tunnel = conn.set_tunnel + except AttributeError: # Platform-specific: Python 2.6 + set_tunnel = conn._set_tunnel + set_tunnel(self.host, self.port, self.proxy_headers) + # Establish tunnel connection early, because otherwise httplib + # would improperly set Host: header to proxy's IP:port. + conn.connect() + + return conn + + def _new_conn(self): + """ + Return a fresh :class:`httplib.HTTPSConnection`. + """ + self.num_connections += 1 + log.info("Starting new HTTPS connection (%d): %s" + % (self.num_connections, self.host)) + + if not self.ConnectionCls or self.ConnectionCls is DummyConnection: + # Platform-specific: Python without ssl + raise SSLError("Can't connect to HTTPS URL because the SSL " + "module is not available.") + + actual_host = self.host + actual_port = self.port + if self.proxy is not None: + actual_host = self.proxy.host + actual_port = self.proxy.port + + extra_params = {} + if not six.PY3: # Python 2 + extra_params['strict'] = self.strict + + conn = self.ConnectionCls(host=actual_host, port=actual_port, + timeout=self.timeout.connect_timeout, + **extra_params) + if self.proxy is not None: + # Enable Nagle's algorithm for proxies, to avoid packet + # fragmentation. + conn.tcp_nodelay = 0 + + return self._prepare_conn(conn) + + +def connection_from_url(url, **kw): + """ + Given a url, return an :class:`.ConnectionPool` instance of its host. + + This is a shortcut for not having to parse out the scheme, host, and port + of the url before creating an :class:`.ConnectionPool` instance. + + :param url: + Absolute URL string that must include the scheme. Port is optional. + + :param \**kw: + Passes additional parameters to the constructor of the appropriate + :class:`.ConnectionPool`. Useful for specifying things like + timeout, maxsize, headers, etc. + + Example: :: + + >>> conn = connection_from_url('http://google.com/') + >>> r = conn.request('GET', '/') + """ + scheme, host, port = get_host(url) + if scheme == 'https': + return HTTPSConnectionPool(host, port=port, **kw) + else: + return HTTPConnectionPool(host, port=port, **kw) diff --git a/urllib3/contrib/__init__.py b/urllib3/contrib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/urllib3/contrib/ntlmpool.py b/urllib3/contrib/ntlmpool.py new file mode 100644 index 000000000..b8cd93303 --- /dev/null +++ b/urllib3/contrib/ntlmpool.py @@ -0,0 +1,120 @@ +# urllib3/contrib/ntlmpool.py +# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt) +# +# This module is part of urllib3 and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +""" +NTLM authenticating pool, contributed by erikcederstran + +Issue #10, see: http://code.google.com/p/urllib3/issues/detail?id=10 +""" + +try: + from http.client import HTTPSConnection +except ImportError: + from httplib import HTTPSConnection +from logging import getLogger +from ntlm import ntlm + +from urllib3 import HTTPSConnectionPool + + +log = getLogger(__name__) + + +class NTLMConnectionPool(HTTPSConnectionPool): + """ + Implements an NTLM authentication version of an urllib3 connection pool + """ + + scheme = 'https' + + def __init__(self, user, pw, authurl, *args, **kwargs): + """ + authurl is a random URL on the server that is protected by NTLM. + user is the Windows user, probably in the DOMAIN\\username format. + pw is the password for the user. + """ + super(NTLMConnectionPool, self).__init__(*args, **kwargs) + self.authurl = authurl + self.rawuser = user + user_parts = user.split('\\', 1) + self.domain = user_parts[0].upper() + self.user = user_parts[1] + self.pw = pw + + def _new_conn(self): + # Performs the NTLM handshake that secures the connection. The socket + # must be kept open while requests are performed. + self.num_connections += 1 + log.debug('Starting NTLM HTTPS connection no. %d: https://%s%s' % + (self.num_connections, self.host, self.authurl)) + + headers = {} + headers['Connection'] = 'Keep-Alive' + req_header = 'Authorization' + resp_header = 'www-authenticate' + + conn = HTTPSConnection(host=self.host, port=self.port) + + # Send negotiation message + headers[req_header] = ( + 'NTLM %s' % ntlm.create_NTLM_NEGOTIATE_MESSAGE(self.rawuser)) + log.debug('Request headers: %s' % headers) + conn.request('GET', self.authurl, None, headers) + res = conn.getresponse() + reshdr = dict(res.getheaders()) + log.debug('Response status: %s %s' % (res.status, res.reason)) + log.debug('Response headers: %s' % reshdr) + log.debug('Response data: %s [...]' % res.read(100)) + + # Remove the reference to the socket, so that it can not be closed by + # the response object (we want to keep the socket open) + res.fp = None + + # Server should respond with a challenge message + auth_header_values = reshdr[resp_header].split(', ') + auth_header_value = None + for s in auth_header_values: + if s[:5] == 'NTLM ': + auth_header_value = s[5:] + if auth_header_value is None: + raise Exception('Unexpected %s response header: %s' % + (resp_header, reshdr[resp_header])) + + # Send authentication message + ServerChallenge, NegotiateFlags = \ + ntlm.parse_NTLM_CHALLENGE_MESSAGE(auth_header_value) + auth_msg = ntlm.create_NTLM_AUTHENTICATE_MESSAGE(ServerChallenge, + self.user, + self.domain, + self.pw, + NegotiateFlags) + headers[req_header] = 'NTLM %s' % auth_msg + log.debug('Request headers: %s' % headers) + conn.request('GET', self.authurl, None, headers) + res = conn.getresponse() + log.debug('Response status: %s %s' % (res.status, res.reason)) + log.debug('Response headers: %s' % dict(res.getheaders())) + log.debug('Response data: %s [...]' % res.read()[:100]) + if res.status != 200: + if res.status == 401: + raise Exception('Server rejected request: wrong ' + 'username or password') + raise Exception('Wrong server response: %s %s' % + (res.status, res.reason)) + + res.fp = None + log.debug('Connection established') + return conn + + def urlopen(self, method, url, body=None, headers=None, retries=3, + redirect=True, assert_same_host=True): + if headers is None: + headers = {} + headers['Connection'] = 'Keep-Alive' + return super(NTLMConnectionPool, self).urlopen(method, url, body, + headers, retries, + redirect, + assert_same_host) diff --git a/urllib3/contrib/pyopenssl.py b/urllib3/contrib/pyopenssl.py new file mode 100644 index 000000000..7c513f3ae --- /dev/null +++ b/urllib3/contrib/pyopenssl.py @@ -0,0 +1,413 @@ +'''SSL with SNI_-support for Python 2. + +This needs the following packages installed: + +* pyOpenSSL (tested with 0.13) +* ndg-httpsclient (tested with 0.3.2) +* pyasn1 (tested with 0.1.6) + +To activate it call :func:`~urllib3.contrib.pyopenssl.inject_into_urllib3`. +This can be done in a ``sitecustomize`` module, or at any other time before +your application begins using ``urllib3``, like this:: + + try: + import urllib3.contrib.pyopenssl + urllib3.contrib.pyopenssl.inject_into_urllib3() + except ImportError: + pass + +Now you can use :mod:`urllib3` as you normally would, and it will support SNI +when the required modules are installed. + +Activating this module also has the positive side effect of disabling SSL/TLS +encryption in Python 2 (see `CRIME attack`_). + +If you want to configure the default list of supported cipher suites, you can +set the ``urllib3.contrib.pyopenssl.DEFAULT_SSL_CIPHER_LIST`` variable. + +Module Variables +---------------- + +:var DEFAULT_SSL_CIPHER_LIST: The list of supported SSL/TLS cipher suites. + Default: ``ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES: + ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS`` + +.. _sni: https://en.wikipedia.org/wiki/Server_Name_Indication +.. _crime attack: https://en.wikipedia.org/wiki/CRIME_(security_exploit) + +''' + +from ndg.httpsclient.ssl_peer_verification import SUBJ_ALT_NAME_SUPPORT +from ndg.httpsclient.subj_alt_name import SubjectAltName as BaseSubjectAltName +import OpenSSL.SSL +from pyasn1.codec.der import decoder as der_decoder +from pyasn1.type import univ, constraint +from socket import _fileobject, timeout +import ssl +import select +from cStringIO import StringIO + +from .. import connection +from .. import util + +__all__ = ['inject_into_urllib3', 'extract_from_urllib3'] + +# SNI only *really* works if we can read the subjectAltName of certificates. +HAS_SNI = SUBJ_ALT_NAME_SUPPORT + +# Map from urllib3 to PyOpenSSL compatible parameter-values. +_openssl_versions = { + ssl.PROTOCOL_SSLv23: OpenSSL.SSL.SSLv23_METHOD, + ssl.PROTOCOL_SSLv3: OpenSSL.SSL.SSLv3_METHOD, + ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD, +} +_openssl_verify = { + ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE, + ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER, + ssl.CERT_REQUIRED: OpenSSL.SSL.VERIFY_PEER + + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, +} + +# A secure default. +# Sources for more information on TLS ciphers: +# +# - https://wiki.mozilla.org/Security/Server_Side_TLS +# - https://www.ssllabs.com/projects/best-practices/index.html +# - https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/ +# +# The general intent is: +# - Prefer cipher suites that offer perfect forward secrecy (DHE/ECDHE), +# - prefer ECDHE over DHE for better performance, +# - prefer any AES-GCM over any AES-CBC for better performance and security, +# - use 3DES as fallback which is secure but slow, +# - disable NULL authentication, MD5 MACs and DSS for security reasons. +DEFAULT_SSL_CIPHER_LIST = "ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:" + \ + "ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:" + \ + "!aNULL:!MD5:!DSS" + + +orig_util_HAS_SNI = util.HAS_SNI +orig_connection_ssl_wrap_socket = connection.ssl_wrap_socket + + +def inject_into_urllib3(): + 'Monkey-patch urllib3 with PyOpenSSL-backed SSL-support.' + + connection.ssl_wrap_socket = ssl_wrap_socket + util.HAS_SNI = HAS_SNI + + +def extract_from_urllib3(): + 'Undo monkey-patching by :func:`inject_into_urllib3`.' + + connection.ssl_wrap_socket = orig_connection_ssl_wrap_socket + util.HAS_SNI = orig_util_HAS_SNI + + +### Note: This is a slightly bug-fixed version of same from ndg-httpsclient. +class SubjectAltName(BaseSubjectAltName): + '''ASN.1 implementation for subjectAltNames support''' + + # There is no limit to how many SAN certificates a certificate may have, + # however this needs to have some limit so we'll set an arbitrarily high + # limit. + sizeSpec = univ.SequenceOf.sizeSpec + \ + constraint.ValueSizeConstraint(1, 1024) + + +### Note: This is a slightly bug-fixed version of same from ndg-httpsclient. +def get_subj_alt_name(peer_cert): + # Search through extensions + dns_name = [] + if not SUBJ_ALT_NAME_SUPPORT: + return dns_name + + general_names = SubjectAltName() + for i in range(peer_cert.get_extension_count()): + ext = peer_cert.get_extension(i) + ext_name = ext.get_short_name() + if ext_name != 'subjectAltName': + continue + + # PyOpenSSL returns extension data in ASN.1 encoded form + ext_dat = ext.get_data() + decoded_dat = der_decoder.decode(ext_dat, + asn1Spec=general_names) + + for name in decoded_dat: + if not isinstance(name, SubjectAltName): + continue + for entry in range(len(name)): + component = name.getComponentByPosition(entry) + if component.getName() != 'dNSName': + continue + dns_name.append(str(component.getComponent())) + + return dns_name + + +class fileobject(_fileobject): + + def _wait_for_sock(self): + rd, wd, ed = select.select([self._sock], [], [], + self._sock.gettimeout()) + if not rd: + raise timeout() + + + def read(self, size=-1): + # Use max, disallow tiny reads in a loop as they are very inefficient. + # We never leave read() with any leftover data from a new recv() call + # in our internal buffer. + rbufsize = max(self._rbufsize, self.default_bufsize) + # Our use of StringIO rather than lists of string objects returned by + # recv() minimizes memory usage and fragmentation that occurs when + # rbufsize is large compared to the typical return value of recv(). + buf = self._rbuf + buf.seek(0, 2) # seek end + if size < 0: + # Read until EOF + self._rbuf = StringIO() # reset _rbuf. we consume it via buf. + while True: + try: + data = self._sock.recv(rbufsize) + except OpenSSL.SSL.WantReadError: + self._wait_for_sock() + continue + if not data: + break + buf.write(data) + return buf.getvalue() + else: + # Read until size bytes or EOF seen, whichever comes first + buf_len = buf.tell() + if buf_len >= size: + # Already have size bytes in our buffer? Extract and return. + buf.seek(0) + rv = buf.read(size) + self._rbuf = StringIO() + self._rbuf.write(buf.read()) + return rv + + self._rbuf = StringIO() # reset _rbuf. we consume it via buf. + while True: + left = size - buf_len + # recv() will malloc the amount of memory given as its + # parameter even though it often returns much less data + # than that. The returned data string is short lived + # as we copy it into a StringIO and free it. This avoids + # fragmentation issues on many platforms. + try: + data = self._sock.recv(left) + except OpenSSL.SSL.WantReadError: + self._wait_for_sock() + continue + if not data: + break + n = len(data) + if n == size and not buf_len: + # Shortcut. Avoid buffer data copies when: + # - We have no data in our buffer. + # AND + # - Our call to recv returned exactly the + # number of bytes we were asked to read. + return data + if n == left: + buf.write(data) + del data # explicit free + break + assert n <= left, "recv(%d) returned %d bytes" % (left, n) + buf.write(data) + buf_len += n + del data # explicit free + #assert buf_len == buf.tell() + return buf.getvalue() + + def readline(self, size=-1): + buf = self._rbuf + buf.seek(0, 2) # seek end + if buf.tell() > 0: + # check if we already have it in our buffer + buf.seek(0) + bline = buf.readline(size) + if bline.endswith('\n') or len(bline) == size: + self._rbuf = StringIO() + self._rbuf.write(buf.read()) + return bline + del bline + if size < 0: + # Read until \n or EOF, whichever comes first + if self._rbufsize <= 1: + # Speed up unbuffered case + buf.seek(0) + buffers = [buf.read()] + self._rbuf = StringIO() # reset _rbuf. we consume it via buf. + data = None + recv = self._sock.recv + while True: + try: + while data != "\n": + data = recv(1) + if not data: + break + buffers.append(data) + except OpenSSL.SSL.WantReadError: + self._wait_for_sock() + continue + break + return "".join(buffers) + + buf.seek(0, 2) # seek end + self._rbuf = StringIO() # reset _rbuf. we consume it via buf. + while True: + try: + data = self._sock.recv(self._rbufsize) + except OpenSSL.SSL.WantReadError: + self._wait_for_sock() + continue + if not data: + break + nl = data.find('\n') + if nl >= 0: + nl += 1 + buf.write(data[:nl]) + self._rbuf.write(data[nl:]) + del data + break + buf.write(data) + return buf.getvalue() + else: + # Read until size bytes or \n or EOF seen, whichever comes first + buf.seek(0, 2) # seek end + buf_len = buf.tell() + if buf_len >= size: + buf.seek(0) + rv = buf.read(size) + self._rbuf = StringIO() + self._rbuf.write(buf.read()) + return rv + self._rbuf = StringIO() # reset _rbuf. we consume it via buf. + while True: + try: + data = self._sock.recv(self._rbufsize) + except OpenSSL.SSL.WantReadError: + self._wait_for_sock() + continue + if not data: + break + left = size - buf_len + # did we just receive a newline? + nl = data.find('\n', 0, left) + if nl >= 0: + nl += 1 + # save the excess data to _rbuf + self._rbuf.write(data[nl:]) + if buf_len: + buf.write(data[:nl]) + break + else: + # Shortcut. Avoid data copy through buf when returning + # a substring of our first recv(). + return data[:nl] + n = len(data) + if n == size and not buf_len: + # Shortcut. Avoid data copy through buf when + # returning exactly all of our first recv(). + return data + if n >= left: + buf.write(data[:left]) + self._rbuf.write(data[left:]) + break + buf.write(data) + buf_len += n + #assert buf_len == buf.tell() + return buf.getvalue() + + +class WrappedSocket(object): + '''API-compatibility wrapper for Python OpenSSL's Connection-class.''' + + def __init__(self, connection, socket): + self.connection = connection + self.socket = socket + + def fileno(self): + return self.socket.fileno() + + def makefile(self, mode, bufsize=-1): + return fileobject(self.connection, mode, bufsize) + + def settimeout(self, timeout): + return self.socket.settimeout(timeout) + + def sendall(self, data): + return self.connection.sendall(data) + + def close(self): + return self.connection.shutdown() + + def getpeercert(self, binary_form=False): + x509 = self.connection.get_peer_certificate() + + if not x509: + return x509 + + if binary_form: + return OpenSSL.crypto.dump_certificate( + OpenSSL.crypto.FILETYPE_ASN1, + x509) + + return { + 'subject': ( + (('commonName', x509.get_subject().CN),), + ), + 'subjectAltName': [ + ('DNS', value) + for value in get_subj_alt_name(x509) + ] + } + + +def _verify_callback(cnx, x509, err_no, err_depth, return_code): + return err_no == 0 + + +def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None, + ca_certs=None, server_hostname=None, + ssl_version=None): + ctx = OpenSSL.SSL.Context(_openssl_versions[ssl_version]) + if certfile: + ctx.use_certificate_file(certfile) + if keyfile: + ctx.use_privatekey_file(keyfile) + if cert_reqs != ssl.CERT_NONE: + ctx.set_verify(_openssl_verify[cert_reqs], _verify_callback) + if ca_certs: + try: + ctx.load_verify_locations(ca_certs, None) + except OpenSSL.SSL.Error as e: + raise ssl.SSLError('bad ca_certs: %r' % ca_certs, e) + else: + ctx.set_default_verify_paths() + + # Disable TLS compression to migitate CRIME attack (issue #309) + OP_NO_COMPRESSION = 0x20000 + ctx.set_options(OP_NO_COMPRESSION) + + # Set list of supported ciphersuites. + ctx.set_cipher_list(DEFAULT_SSL_CIPHER_LIST) + + cnx = OpenSSL.SSL.Connection(ctx, sock) + cnx.set_tlsext_host_name(server_hostname) + cnx.set_connect_state() + while True: + try: + cnx.do_handshake() + except OpenSSL.SSL.WantReadError: + select.select([sock], [], []) + continue + except OpenSSL.SSL.Error as e: + raise ssl.SSLError('bad handshake', e) + break + + return WrappedSocket(cnx, sock) diff --git a/urllib3/exceptions.py b/urllib3/exceptions.py new file mode 100644 index 000000000..b4df831fe --- /dev/null +++ b/urllib3/exceptions.py @@ -0,0 +1,126 @@ +# urllib3/exceptions.py +# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt) +# +# This module is part of urllib3 and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + + +## Base Exceptions + +class HTTPError(Exception): + "Base exception used by this module." + pass + + +class PoolError(HTTPError): + "Base exception for errors caused within a pool." + def __init__(self, pool, message): + self.pool = pool + HTTPError.__init__(self, "%s: %s" % (pool, message)) + + def __reduce__(self): + # For pickling purposes. + return self.__class__, (None, None) + + +class RequestError(PoolError): + "Base exception for PoolErrors that have associated URLs." + def __init__(self, pool, url, message): + self.url = url + PoolError.__init__(self, pool, message) + + def __reduce__(self): + # For pickling purposes. + return self.__class__, (None, self.url, None) + + +class SSLError(HTTPError): + "Raised when SSL certificate fails in an HTTPS connection." + pass + + +class ProxyError(HTTPError): + "Raised when the connection to a proxy fails." + pass + + +class ConnectionError(HTTPError): + "Raised when a normal connection fails." + pass + + +class DecodeError(HTTPError): + "Raised when automatic decoding based on Content-Type fails." + pass + + +## Leaf Exceptions + +class MaxRetryError(RequestError): + "Raised when the maximum number of retries is exceeded." + + def __init__(self, pool, url, reason=None): + self.reason = reason + + message = "Max retries exceeded with url: %s" % url + if reason: + message += " (Caused by %s: %s)" % (type(reason), reason) + else: + message += " (Caused by redirect)" + + RequestError.__init__(self, pool, url, message) + + +class HostChangedError(RequestError): + "Raised when an existing pool gets a request for a foreign host." + + def __init__(self, pool, url, retries=3): + message = "Tried to open a foreign host with url: %s" % url + RequestError.__init__(self, pool, url, message) + self.retries = retries + + +class TimeoutStateError(HTTPError): + """ Raised when passing an invalid state to a timeout """ + pass + + +class TimeoutError(HTTPError): + """ Raised when a socket timeout error occurs. + + Catching this error will catch both :exc:`ReadTimeoutErrors + ` and :exc:`ConnectTimeoutErrors `. + """ + pass + + +class ReadTimeoutError(TimeoutError, RequestError): + "Raised when a socket timeout occurs while receiving data from a server" + pass + + +# This timeout error does not have a URL attached and needs to inherit from the +# base HTTPError +class ConnectTimeoutError(TimeoutError): + "Raised when a socket timeout occurs while connecting to a server" + pass + + +class EmptyPoolError(PoolError): + "Raised when a pool runs out of connections and no more are allowed." + pass + + +class ClosedPoolError(PoolError): + "Raised when a request enters a pool after the pool has been closed." + pass + + +class LocationParseError(ValueError, HTTPError): + "Raised when get_host or similar fails to parse the URL input." + + def __init__(self, location): + message = "Failed to parse: %s" % location + HTTPError.__init__(self, message) + + self.location = location diff --git a/urllib3/fields.py b/urllib3/fields.py new file mode 100644 index 000000000..ed017657a --- /dev/null +++ b/urllib3/fields.py @@ -0,0 +1,177 @@ +# urllib3/fields.py +# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt) +# +# This module is part of urllib3 and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +import email.utils +import mimetypes + +from .packages import six + + +def guess_content_type(filename, default='application/octet-stream'): + """ + Guess the "Content-Type" of a file. + + :param filename: + The filename to guess the "Content-Type" of using :mod:`mimetimes`. + :param default: + If no "Content-Type" can be guessed, default to `default`. + """ + if filename: + return mimetypes.guess_type(filename)[0] or default + return default + + +def format_header_param(name, value): + """ + Helper function to format and quote a single header parameter. + + Particularly useful for header parameters which might contain + non-ASCII values, like file names. This follows RFC 2231, as + suggested by RFC 2388 Section 4.4. + + :param name: + The name of the parameter, a string expected to be ASCII only. + :param value: + The value of the parameter, provided as a unicode string. + """ + if not any(ch in value for ch in '"\\\r\n'): + result = '%s="%s"' % (name, value) + try: + result.encode('ascii') + except UnicodeEncodeError: + pass + else: + return result + if not six.PY3: # Python 2: + value = value.encode('utf-8') + value = email.utils.encode_rfc2231(value, 'utf-8') + value = '%s*=%s' % (name, value) + return value + + +class RequestField(object): + """ + A data container for request body parameters. + + :param name: + The name of this request field. + :param data: + The data/value body. + :param filename: + An optional filename of the request field. + :param headers: + An optional dict-like object of headers to initially use for the field. + """ + def __init__(self, name, data, filename=None, headers=None): + self._name = name + self._filename = filename + self.data = data + self.headers = {} + if headers: + self.headers = dict(headers) + + @classmethod + def from_tuples(cls, fieldname, value): + """ + A :class:`~urllib3.fields.RequestField` factory from old-style tuple parameters. + + Supports constructing :class:`~urllib3.fields.RequestField` from parameter + of key/value strings AND key/filetuple. A filetuple is a (filename, data, MIME type) + tuple where the MIME type is optional. For example: :: + + 'foo': 'bar', + 'fakefile': ('foofile.txt', 'contents of foofile'), + 'realfile': ('barfile.txt', open('realfile').read()), + 'typedfile': ('bazfile.bin', open('bazfile').read(), 'image/jpeg'), + 'nonamefile': 'contents of nonamefile field', + + Field names and filenames must be unicode. + """ + if isinstance(value, tuple): + if len(value) == 3: + filename, data, content_type = value + else: + filename, data = value + content_type = guess_content_type(filename) + else: + filename = None + content_type = None + data = value + + request_param = cls(fieldname, data, filename=filename) + request_param.make_multipart(content_type=content_type) + + return request_param + + def _render_part(self, name, value): + """ + Overridable helper function to format a single header parameter. + + :param name: + The name of the parameter, a string expected to be ASCII only. + :param value: + The value of the parameter, provided as a unicode string. + """ + return format_header_param(name, value) + + def _render_parts(self, header_parts): + """ + Helper function to format and quote a single header. + + Useful for single headers that are composed of multiple items. E.g., + 'Content-Disposition' fields. + + :param header_parts: + A sequence of (k, v) typles or a :class:`dict` of (k, v) to format as + `k1="v1"; k2="v2"; ...`. + """ + parts = [] + iterable = header_parts + if isinstance(header_parts, dict): + iterable = header_parts.items() + + for name, value in iterable: + if value: + parts.append(self._render_part(name, value)) + + return '; '.join(parts) + + def render_headers(self): + """ + Renders the headers for this request field. + """ + lines = [] + + sort_keys = ['Content-Disposition', 'Content-Type', 'Content-Location'] + for sort_key in sort_keys: + if self.headers.get(sort_key, False): + lines.append('%s: %s' % (sort_key, self.headers[sort_key])) + + for header_name, header_value in self.headers.items(): + if header_name not in sort_keys: + if header_value: + lines.append('%s: %s' % (header_name, header_value)) + + lines.append('\r\n') + return '\r\n'.join(lines) + + def make_multipart(self, content_disposition=None, content_type=None, content_location=None): + """ + Makes this request field into a multipart request field. + + This method overrides "Content-Disposition", "Content-Type" and + "Content-Location" headers to the request parameter. + + :param content_type: + The 'Content-Type' of the request body. + :param content_location: + The 'Content-Location' of the request body. + + """ + self.headers['Content-Disposition'] = content_disposition or 'form-data' + self.headers['Content-Disposition'] += '; '.join(['', self._render_parts((('name', self._name), ('filename', self._filename)))]) + self.headers['Content-Type'] = content_type + self.headers['Content-Location'] = content_location diff --git a/urllib3/filepost.py b/urllib3/filepost.py new file mode 100644 index 000000000..e8b30bddf --- /dev/null +++ b/urllib3/filepost.py @@ -0,0 +1,100 @@ +# urllib3/filepost.py +# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt) +# +# This module is part of urllib3 and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +import codecs +import mimetypes + +from uuid import uuid4 +from io import BytesIO + +from .packages import six +from .packages.six import b +from .fields import RequestField + +writer = codecs.lookup('utf-8')[3] + + +def choose_boundary(): + """ + Our embarassingly-simple replacement for mimetools.choose_boundary. + """ + return uuid4().hex + + +def iter_field_objects(fields): + """ + Iterate over fields. + + Supports list of (k, v) tuples and dicts, and lists of + :class:`~urllib3.fields.RequestField`. + + """ + if isinstance(fields, dict): + i = six.iteritems(fields) + else: + i = iter(fields) + + for field in i: + if isinstance(field, RequestField): + yield field + else: + yield RequestField.from_tuples(*field) + + +def iter_fields(fields): + """ + .. deprecated:: 1.6 + + Iterate over fields. + + The addition of :class:`~urllib3.fields.RequestField` makes this function + obsolete. Instead, use :func:`iter_field_objects`, which returns + :class:`~urllib3.fields.RequestField` objects. + + Supports list of (k, v) tuples and dicts. + """ + if isinstance(fields, dict): + return ((k, v) for k, v in six.iteritems(fields)) + + return ((k, v) for k, v in fields) + + +def encode_multipart_formdata(fields, boundary=None): + """ + Encode a dictionary of ``fields`` using the multipart/form-data MIME format. + + :param fields: + Dictionary of fields or list of (key, :class:`~urllib3.fields.RequestField`). + + :param boundary: + If not specified, then a random boundary will be generated using + :func:`mimetools.choose_boundary`. + """ + body = BytesIO() + if boundary is None: + boundary = choose_boundary() + + for field in iter_field_objects(fields): + body.write(b('--%s\r\n' % (boundary))) + + writer(body).write(field.render_headers()) + data = field.data + + if isinstance(data, int): + data = str(data) # Backwards compatibility + + if isinstance(data, six.text_type): + writer(body).write(data) + else: + body.write(data) + + body.write(b'\r\n') + + body.write(b('--%s--\r\n' % (boundary))) + + content_type = str('multipart/form-data; boundary=%s' % boundary) + + return body.getvalue(), content_type diff --git a/urllib3/packages/__init__.py b/urllib3/packages/__init__.py new file mode 100644 index 000000000..37e835157 --- /dev/null +++ b/urllib3/packages/__init__.py @@ -0,0 +1,4 @@ +from __future__ import absolute_import + +from . import ssl_match_hostname + diff --git a/urllib3/packages/ordered_dict.py b/urllib3/packages/ordered_dict.py new file mode 100644 index 000000000..7f8ee1543 --- /dev/null +++ b/urllib3/packages/ordered_dict.py @@ -0,0 +1,260 @@ +# Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy. +# Passes Python2.7's test suite and incorporates all the latest updates. +# Copyright 2009 Raymond Hettinger, released under the MIT License. +# http://code.activestate.com/recipes/576693/ + +try: + from thread import get_ident as _get_ident +except ImportError: + from dummy_thread import get_ident as _get_ident + +try: + from _abcoll import KeysView, ValuesView, ItemsView +except ImportError: + pass + + +class OrderedDict(dict): + 'Dictionary that remembers insertion order' + # An inherited dict maps keys to values. + # The inherited dict provides __getitem__, __len__, __contains__, and get. + # The remaining methods are order-aware. + # Big-O running times for all methods are the same as for regular dictionaries. + + # The internal self.__map dictionary maps keys to links in a doubly linked list. + # The circular doubly linked list starts and ends with a sentinel element. + # The sentinel element never gets deleted (this simplifies the algorithm). + # Each link is stored as a list of length three: [PREV, NEXT, KEY]. + + def __init__(self, *args, **kwds): + '''Initialize an ordered dictionary. Signature is the same as for + regular dictionaries, but keyword arguments are not recommended + because their insertion order is arbitrary. + + ''' + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__root + except AttributeError: + self.__root = root = [] # sentinel node + root[:] = [root, root, None] + self.__map = {} + self.__update(*args, **kwds) + + def __setitem__(self, key, value, dict_setitem=dict.__setitem__): + 'od.__setitem__(i, y) <==> od[i]=y' + # Setting a new item creates a new link which goes at the end of the linked + # list, and the inherited dictionary is updated with the new key/value pair. + if key not in self: + root = self.__root + last = root[0] + last[1] = root[0] = self.__map[key] = [last, root, key] + dict_setitem(self, key, value) + + def __delitem__(self, key, dict_delitem=dict.__delitem__): + 'od.__delitem__(y) <==> del od[y]' + # Deleting an existing item uses self.__map to find the link which is + # then removed by updating the links in the predecessor and successor nodes. + dict_delitem(self, key) + link_prev, link_next, key = self.__map.pop(key) + link_prev[1] = link_next + link_next[0] = link_prev + + def __iter__(self): + 'od.__iter__() <==> iter(od)' + root = self.__root + curr = root[1] + while curr is not root: + yield curr[2] + curr = curr[1] + + def __reversed__(self): + 'od.__reversed__() <==> reversed(od)' + root = self.__root + curr = root[0] + while curr is not root: + yield curr[2] + curr = curr[0] + + def clear(self): + 'od.clear() -> None. Remove all items from od.' + try: + for node in self.__map.itervalues(): + del node[:] + root = self.__root + root[:] = [root, root, None] + self.__map.clear() + except AttributeError: + pass + dict.clear(self) + + def popitem(self, last=True): + '''od.popitem() -> (k, v), return and remove a (key, value) pair. + Pairs are returned in LIFO order if last is true or FIFO order if false. + + ''' + if not self: + raise KeyError('dictionary is empty') + root = self.__root + if last: + link = root[0] + link_prev = link[0] + link_prev[1] = root + root[0] = link_prev + else: + link = root[1] + link_next = link[1] + root[1] = link_next + link_next[0] = root + key = link[2] + del self.__map[key] + value = dict.pop(self, key) + return key, value + + # -- the following methods do not depend on the internal structure -- + + def keys(self): + 'od.keys() -> list of keys in od' + return list(self) + + def values(self): + 'od.values() -> list of values in od' + return [self[key] for key in self] + + def items(self): + 'od.items() -> list of (key, value) pairs in od' + return [(key, self[key]) for key in self] + + def iterkeys(self): + 'od.iterkeys() -> an iterator over the keys in od' + return iter(self) + + def itervalues(self): + 'od.itervalues -> an iterator over the values in od' + for k in self: + yield self[k] + + def iteritems(self): + 'od.iteritems -> an iterator over the (key, value) items in od' + for k in self: + yield (k, self[k]) + + def update(*args, **kwds): + '''od.update(E, **F) -> None. Update od from dict/iterable E and F. + + If E is a dict instance, does: for k in E: od[k] = E[k] + If E has a .keys() method, does: for k in E.keys(): od[k] = E[k] + Or if E is an iterable of items, does: for k, v in E: od[k] = v + In either case, this is followed by: for k, v in F.items(): od[k] = v + + ''' + if len(args) > 2: + raise TypeError('update() takes at most 2 positional ' + 'arguments (%d given)' % (len(args),)) + elif not args: + raise TypeError('update() takes at least 1 argument (0 given)') + self = args[0] + # Make progressively weaker assumptions about "other" + other = () + if len(args) == 2: + other = args[1] + if isinstance(other, dict): + for key in other: + self[key] = other[key] + elif hasattr(other, 'keys'): + for key in other.keys(): + self[key] = other[key] + else: + for key, value in other: + self[key] = value + for key, value in kwds.items(): + self[key] = value + + __update = update # let subclasses override update without breaking __init__ + + __marker = object() + + def pop(self, key, default=__marker): + '''od.pop(k[,d]) -> v, remove specified key and return the corresponding value. + If key is not found, d is returned if given, otherwise KeyError is raised. + + ''' + if key in self: + result = self[key] + del self[key] + return result + if default is self.__marker: + raise KeyError(key) + return default + + def setdefault(self, key, default=None): + 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od' + if key in self: + return self[key] + self[key] = default + return default + + def __repr__(self, _repr_running={}): + 'od.__repr__() <==> repr(od)' + call_key = id(self), _get_ident() + if call_key in _repr_running: + return '...' + _repr_running[call_key] = 1 + try: + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, self.items()) + finally: + del _repr_running[call_key] + + def __reduce__(self): + 'Return state information for pickling' + items = [[k, self[k]] for k in self] + inst_dict = vars(self).copy() + for k in vars(OrderedDict()): + inst_dict.pop(k, None) + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) + + def copy(self): + 'od.copy() -> a shallow copy of od' + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S + and values equal to v (which defaults to None). + + ''' + d = cls() + for key in iterable: + d[key] = value + return d + + def __eq__(self, other): + '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive + while comparison to a regular mapping is order-insensitive. + + ''' + if isinstance(other, OrderedDict): + return len(self)==len(other) and self.items() == other.items() + return dict.__eq__(self, other) + + def __ne__(self, other): + return not self == other + + # -- the following methods are only used in Python 2.7 -- + + def viewkeys(self): + "od.viewkeys() -> a set-like object providing a view on od's keys" + return KeysView(self) + + def viewvalues(self): + "od.viewvalues() -> an object providing a view on od's values" + return ValuesView(self) + + def viewitems(self): + "od.viewitems() -> a set-like object providing a view on od's items" + return ItemsView(self) diff --git a/urllib3/packages/six.py b/urllib3/packages/six.py new file mode 100644 index 000000000..27d80112b --- /dev/null +++ b/urllib3/packages/six.py @@ -0,0 +1,385 @@ +"""Utilities for writing code that runs on Python 2 and 3""" + +#Copyright (c) 2010-2011 Benjamin Peterson + +#Permission is hereby granted, free of charge, to any person obtaining a copy of +#this software and associated documentation files (the "Software"), to deal in +#the Software without restriction, including without limitation the rights to +#use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +#the Software, and to permit persons to whom the Software is furnished to do so, +#subject to the following conditions: + +#The above copyright notice and this permission notice shall be included in all +#copies or substantial portions of the Software. + +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +#FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +#COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +#IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +#CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import operator +import sys +import types + +__author__ = "Benjamin Peterson " +__version__ = "1.2.0" # Revision 41c74fef2ded + + +# True if we are running on Python 3. +PY3 = sys.version_info[0] == 3 + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + if sys.platform.startswith("java"): + # Jython always uses 32 bits. + MAXSIZE = int((1 << 31) - 1) + else: + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X + + +def _add_doc(func, doc): + """Add documentation to a function.""" + func.__doc__ = doc + + +def _import_module(name): + """Import module, returning the module after the last dot.""" + __import__(name) + return sys.modules[name] + + +class _LazyDescr(object): + + def __init__(self, name): + self.name = name + + def __get__(self, obj, tp): + result = self._resolve() + setattr(obj, self.name, result) + # This is a bit ugly, but it avoids running this again. + delattr(tp, self.name) + return result + + +class MovedModule(_LazyDescr): + + def __init__(self, name, old, new=None): + super(MovedModule, self).__init__(name) + if PY3: + if new is None: + new = name + self.mod = new + else: + self.mod = old + + def _resolve(self): + return _import_module(self.mod) + + +class MovedAttribute(_LazyDescr): + + def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): + super(MovedAttribute, self).__init__(name) + if PY3: + if new_mod is None: + new_mod = name + self.mod = new_mod + if new_attr is None: + if old_attr is None: + new_attr = name + else: + new_attr = old_attr + self.attr = new_attr + else: + self.mod = old_mod + if old_attr is None: + old_attr = name + self.attr = old_attr + + def _resolve(self): + module = _import_module(self.mod) + return getattr(module, self.attr) + + + +class _MovedItems(types.ModuleType): + """Lazy loading of moved objects""" + + +_moved_attributes = [ + MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), + MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), + MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("reload_module", "__builtin__", "imp", "reload"), + MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), + + MovedModule("builtins", "__builtin__"), + MovedModule("configparser", "ConfigParser"), + MovedModule("copyreg", "copy_reg"), + MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), + MovedModule("http_cookies", "Cookie", "http.cookies"), + MovedModule("html_entities", "htmlentitydefs", "html.entities"), + MovedModule("html_parser", "HTMLParser", "html.parser"), + MovedModule("http_client", "httplib", "http.client"), + MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), + MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), + MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), + MovedModule("cPickle", "cPickle", "pickle"), + MovedModule("queue", "Queue"), + MovedModule("reprlib", "repr"), + MovedModule("socketserver", "SocketServer"), + MovedModule("tkinter", "Tkinter"), + MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), + MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), + MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), + MovedModule("tkinter_colorchooser", "tkColorChooser", + "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", + "tkinter.commondialog"), + MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), + MovedModule("tkinter_font", "tkFont", "tkinter.font"), + MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), + MovedModule("winreg", "_winreg"), +] +for attr in _moved_attributes: + setattr(_MovedItems, attr.name, attr) +del attr + +moves = sys.modules[__name__ + ".moves"] = _MovedItems("moves") + + +def add_move(move): + """Add an item to six.moves.""" + setattr(_MovedItems, move.name, move) + + +def remove_move(name): + """Remove item from six.moves.""" + try: + delattr(_MovedItems, name) + except AttributeError: + try: + del moves.__dict__[name] + except KeyError: + raise AttributeError("no such move, %r" % (name,)) + + +if PY3: + _meth_func = "__func__" + _meth_self = "__self__" + + _func_code = "__code__" + _func_defaults = "__defaults__" + + _iterkeys = "keys" + _itervalues = "values" + _iteritems = "items" +else: + _meth_func = "im_func" + _meth_self = "im_self" + + _func_code = "func_code" + _func_defaults = "func_defaults" + + _iterkeys = "iterkeys" + _itervalues = "itervalues" + _iteritems = "iteritems" + + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + + +if PY3: + def get_unbound_function(unbound): + return unbound + + Iterator = object + + def callable(obj): + return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) +else: + def get_unbound_function(unbound): + return unbound.im_func + + class Iterator(object): + + def next(self): + return type(self).__next__(self) + + callable = callable +_add_doc(get_unbound_function, + """Get the function out of a possibly unbound function""") + + +get_method_function = operator.attrgetter(_meth_func) +get_method_self = operator.attrgetter(_meth_self) +get_function_code = operator.attrgetter(_func_code) +get_function_defaults = operator.attrgetter(_func_defaults) + + +def iterkeys(d): + """Return an iterator over the keys of a dictionary.""" + return iter(getattr(d, _iterkeys)()) + +def itervalues(d): + """Return an iterator over the values of a dictionary.""" + return iter(getattr(d, _itervalues)()) + +def iteritems(d): + """Return an iterator over the (key, value) pairs of a dictionary.""" + return iter(getattr(d, _iteritems)()) + + +if PY3: + def b(s): + return s.encode("latin-1") + def u(s): + return s + if sys.version_info[1] <= 1: + def int2byte(i): + return bytes((i,)) + else: + # This is about 2x faster than the implementation above on 3.2+ + int2byte = operator.methodcaller("to_bytes", 1, "big") + import io + StringIO = io.StringIO + BytesIO = io.BytesIO +else: + def b(s): + return s + def u(s): + return unicode(s, "unicode_escape") + int2byte = chr + import StringIO + StringIO = BytesIO = StringIO.StringIO +_add_doc(b, """Byte literal""") +_add_doc(u, """Text literal""") + + +if PY3: + import builtins + exec_ = getattr(builtins, "exec") + + + def reraise(tp, value, tb=None): + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + + + print_ = getattr(builtins, "print") + del builtins + +else: + def exec_(code, globs=None, locs=None): + """Execute code in a namespace.""" + if globs is None: + frame = sys._getframe(1) + globs = frame.f_globals + if locs is None: + locs = frame.f_locals + del frame + elif locs is None: + locs = globs + exec("""exec code in globs, locs""") + + + exec_("""def reraise(tp, value, tb=None): + raise tp, value, tb +""") + + + def print_(*args, **kwargs): + """The new-style print function.""" + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + def write(data): + if not isinstance(data, basestring): + data = str(data) + fp.write(data) + want_unicode = False + sep = kwargs.pop("sep", None) + if sep is not None: + if isinstance(sep, unicode): + want_unicode = True + elif not isinstance(sep, str): + raise TypeError("sep must be None or a string") + end = kwargs.pop("end", None) + if end is not None: + if isinstance(end, unicode): + want_unicode = True + elif not isinstance(end, str): + raise TypeError("end must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + if not want_unicode: + for arg in args: + if isinstance(arg, unicode): + want_unicode = True + break + if want_unicode: + newline = unicode("\n") + space = unicode(" ") + else: + newline = "\n" + space = " " + if sep is None: + sep = space + if end is None: + end = newline + for i, arg in enumerate(args): + if i: + write(sep) + write(arg) + write(end) + +_add_doc(reraise, """Reraise an exception.""") + + +def with_metaclass(meta, base=object): + """Create a base class with a metaclass.""" + return meta("NewBase", (base,), {}) diff --git a/urllib3/packages/ssl_match_hostname/__init__.py b/urllib3/packages/ssl_match_hostname/__init__.py new file mode 100644 index 000000000..dd59a75fd --- /dev/null +++ b/urllib3/packages/ssl_match_hostname/__init__.py @@ -0,0 +1,13 @@ +try: + # Python 3.2+ + from ssl import CertificateError, match_hostname +except ImportError: + try: + # Backport of the function from a pypi module + from backports.ssl_match_hostname import CertificateError, match_hostname + except ImportError: + # Our vendored copy + from ._implementation import CertificateError, match_hostname + +# Not needed, but documenting what we provide. +__all__ = ('CertificateError', 'match_hostname') diff --git a/urllib3/packages/ssl_match_hostname/_implementation.py b/urllib3/packages/ssl_match_hostname/_implementation.py new file mode 100644 index 000000000..52f428733 --- /dev/null +++ b/urllib3/packages/ssl_match_hostname/_implementation.py @@ -0,0 +1,105 @@ +"""The match_hostname() function from Python 3.3.3, essential when using SSL.""" + +# Note: This file is under the PSF license as the code comes from the python +# stdlib. http://docs.python.org/3/license.html + +import re + +__version__ = '3.4.0.2' + +class CertificateError(ValueError): + pass + + +def _dnsname_match(dn, hostname, max_wildcards=1): + """Matching according to RFC 6125, section 6.4.3 + + http://tools.ietf.org/html/rfc6125#section-6.4.3 + """ + pats = [] + if not dn: + return False + + # Ported from python3-syntax: + # leftmost, *remainder = dn.split(r'.') + parts = dn.split(r'.') + leftmost = parts[0] + remainder = parts[1:] + + wildcards = leftmost.count('*') + if wildcards > max_wildcards: + # Issue #17980: avoid denials of service by refusing more + # than one wildcard per fragment. A survey of established + # policy among SSL implementations showed it to be a + # reasonable choice. + raise CertificateError( + "too many wildcards in certificate DNS name: " + repr(dn)) + + # speed up common case w/o wildcards + if not wildcards: + return dn.lower() == hostname.lower() + + # RFC 6125, section 6.4.3, subitem 1. + # The client SHOULD NOT attempt to match a presented identifier in which + # the wildcard character comprises a label other than the left-most label. + if leftmost == '*': + # When '*' is a fragment by itself, it matches a non-empty dotless + # fragment. + pats.append('[^.]+') + elif leftmost.startswith('xn--') or hostname.startswith('xn--'): + # RFC 6125, section 6.4.3, subitem 3. + # The client SHOULD NOT attempt to match a presented identifier + # where the wildcard character is embedded within an A-label or + # U-label of an internationalized domain name. + pats.append(re.escape(leftmost)) + else: + # Otherwise, '*' matches any dotless string, e.g. www* + pats.append(re.escape(leftmost).replace(r'\*', '[^.]*')) + + # add the remaining fragments, ignore any wildcards + for frag in remainder: + pats.append(re.escape(frag)) + + pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE) + return pat.match(hostname) + + +def match_hostname(cert, hostname): + """Verify that *cert* (in decoded format as returned by + SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125 + rules are followed, but IP addresses are not accepted for *hostname*. + + CertificateError is raised on failure. On success, the function + returns nothing. + """ + if not cert: + raise ValueError("empty or no certificate") + dnsnames = [] + san = cert.get('subjectAltName', ()) + for key, value in san: + if key == 'DNS': + if _dnsname_match(value, hostname): + return + dnsnames.append(value) + if not dnsnames: + # The subject is only checked when there is no dNSName entry + # in subjectAltName + for sub in cert.get('subject', ()): + for key, value in sub: + # XXX according to RFC 2818, the most specific Common Name + # must be used. + if key == 'commonName': + if _dnsname_match(value, hostname): + return + dnsnames.append(value) + if len(dnsnames) > 1: + raise CertificateError("hostname %r " + "doesn't match either of %s" + % (hostname, ', '.join(map(repr, dnsnames)))) + elif len(dnsnames) == 1: + raise CertificateError("hostname %r " + "doesn't match %r" + % (hostname, dnsnames[0])) + else: + raise CertificateError("no appropriate commonName or " + "subjectAltName fields were found") diff --git a/urllib3/poolmanager.py b/urllib3/poolmanager.py new file mode 100644 index 000000000..f18ff2bb7 --- /dev/null +++ b/urllib3/poolmanager.py @@ -0,0 +1,258 @@ +# urllib3/poolmanager.py +# Copyright 2008-2014 Andrey Petrov and contributors (see CONTRIBUTORS.txt) +# +# This module is part of urllib3 and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +import logging + +try: # Python 3 + from urllib.parse import urljoin +except ImportError: + from urlparse import urljoin + +from ._collections import RecentlyUsedContainer +from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool +from .connectionpool import port_by_scheme +from .request import RequestMethods +from .util import parse_url + + +__all__ = ['PoolManager', 'ProxyManager', 'proxy_from_url'] + + +pool_classes_by_scheme = { + 'http': HTTPConnectionPool, + 'https': HTTPSConnectionPool, +} + +log = logging.getLogger(__name__) + +SSL_KEYWORDS = ('key_file', 'cert_file', 'cert_reqs', 'ca_certs', + 'ssl_version') + + +class PoolManager(RequestMethods): + """ + Allows for arbitrary requests while transparently keeping track of + necessary connection pools for you. + + :param num_pools: + Number of connection pools to cache before discarding the least + recently used pool. + + :param headers: + Headers to include with all requests, unless other headers are given + explicitly. + + :param \**connection_pool_kw: + Additional parameters are used to create fresh + :class:`urllib3.connectionpool.ConnectionPool` instances. + + Example: :: + + >>> manager = PoolManager(num_pools=2) + >>> r = manager.request('GET', 'http://google.com/') + >>> r = manager.request('GET', 'http://google.com/mail') + >>> r = manager.request('GET', 'http://yahoo.com/') + >>> len(manager.pools) + 2 + + """ + + proxy = None + + def __init__(self, num_pools=10, headers=None, **connection_pool_kw): + RequestMethods.__init__(self, headers) + self.connection_pool_kw = connection_pool_kw + self.pools = RecentlyUsedContainer(num_pools, + dispose_func=lambda p: p.close()) + + def _new_pool(self, scheme, host, port): + """ + Create a new :class:`ConnectionPool` based on host, port and scheme. + + This method is used to actually create the connection pools handed out + by :meth:`connection_from_url` and companion methods. It is intended + to be overridden for customization. + """ + pool_cls = pool_classes_by_scheme[scheme] + kwargs = self.connection_pool_kw + if scheme == 'http': + kwargs = self.connection_pool_kw.copy() + for kw in SSL_KEYWORDS: + kwargs.pop(kw, None) + + return pool_cls(host, port, **kwargs) + + def clear(self): + """ + Empty our store of pools and direct them all to close. + + This will not affect in-flight connections, but they will not be + re-used after completion. + """ + self.pools.clear() + + def connection_from_host(self, host, port=None, scheme='http'): + """ + Get a :class:`ConnectionPool` based on the host, port, and scheme. + + If ``port`` isn't given, it will be derived from the ``scheme`` using + ``urllib3.connectionpool.port_by_scheme``. + """ + + scheme = scheme or 'http' + + port = port or port_by_scheme.get(scheme, 80) + + pool_key = (scheme, host, port) + + with self.pools.lock: + # If the scheme, host, or port doesn't match existing open + # connections, open a new ConnectionPool. + pool = self.pools.get(pool_key) + if pool: + return pool + + # Make a fresh ConnectionPool of the desired type + pool = self._new_pool(scheme, host, port) + self.pools[pool_key] = pool + return pool + + def connection_from_url(self, url): + """ + Similar to :func:`urllib3.connectionpool.connection_from_url` but + doesn't pass any additional parameters to the + :class:`urllib3.connectionpool.ConnectionPool` constructor. + + Additional parameters are taken from the :class:`.PoolManager` + constructor. + """ + u = parse_url(url) + return self.connection_from_host(u.host, port=u.port, scheme=u.scheme) + + def urlopen(self, method, url, redirect=True, **kw): + """ + Same as :meth:`urllib3.connectionpool.HTTPConnectionPool.urlopen` + with custom cross-host redirect logic and only sends the request-uri + portion of the ``url``. + + The given ``url`` parameter must be absolute, such that an appropriate + :class:`urllib3.connectionpool.ConnectionPool` can be chosen for it. + """ + u = parse_url(url) + conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme) + + kw['assert_same_host'] = False + kw['redirect'] = False + if 'headers' not in kw: + kw['headers'] = self.headers + + if self.proxy is not None and u.scheme == "http": + response = conn.urlopen(method, url, **kw) + else: + response = conn.urlopen(method, u.request_uri, **kw) + + redirect_location = redirect and response.get_redirect_location() + if not redirect_location: + return response + + # Support relative URLs for redirecting. + redirect_location = urljoin(url, redirect_location) + + # RFC 2616, Section 10.3.4 + if response.status == 303: + method = 'GET' + + log.info("Redirecting %s -> %s" % (url, redirect_location)) + kw['retries'] = kw.get('retries', 3) - 1 # Persist retries countdown + kw['redirect'] = redirect + return self.urlopen(method, redirect_location, **kw) + + +class ProxyManager(PoolManager): + """ + Behaves just like :class:`PoolManager`, but sends all requests through + the defined proxy, using the CONNECT method for HTTPS URLs. + + :param proxy_url: + The URL of the proxy to be used. + + :param proxy_headers: + A dictionary contaning headers that will be sent to the proxy. In case + of HTTP they are being sent with each request, while in the + HTTPS/CONNECT case they are sent only once. Could be used for proxy + authentication. + + Example: + >>> proxy = urllib3.ProxyManager('http://localhost:3128/') + >>> r1 = proxy.request('GET', 'http://google.com/') + >>> r2 = proxy.request('GET', 'http://httpbin.org/') + >>> len(proxy.pools) + 1 + >>> r3 = proxy.request('GET', 'https://httpbin.org/') + >>> r4 = proxy.request('GET', 'https://twitter.com/') + >>> len(proxy.pools) + 3 + + """ + + def __init__(self, proxy_url, num_pools=10, headers=None, + proxy_headers=None, **connection_pool_kw): + + if isinstance(proxy_url, HTTPConnectionPool): + proxy_url = '%s://%s:%i' % (proxy_url.scheme, proxy_url.host, + proxy_url.port) + proxy = parse_url(proxy_url) + if not proxy.port: + port = port_by_scheme.get(proxy.scheme, 80) + proxy = proxy._replace(port=port) + self.proxy = proxy + self.proxy_headers = proxy_headers or {} + assert self.proxy.scheme in ("http", "https"), \ + 'Not supported proxy scheme %s' % self.proxy.scheme + connection_pool_kw['_proxy'] = self.proxy + connection_pool_kw['_proxy_headers'] = self.proxy_headers + super(ProxyManager, self).__init__( + num_pools, headers, **connection_pool_kw) + + def connection_from_host(self, host, port=None, scheme='http'): + if scheme == "https": + return super(ProxyManager, self).connection_from_host( + host, port, scheme) + + return super(ProxyManager, self).connection_from_host( + self.proxy.host, self.proxy.port, self.proxy.scheme) + + def _set_proxy_headers(self, url, headers=None): + """ + Sets headers needed by proxies: specifically, the Accept and Host + headers. Only sets headers not provided by the user. + """ + headers_ = {'Accept': '*/*'} + + netloc = parse_url(url).netloc + if netloc: + headers_['Host'] = netloc + + if headers: + headers_.update(headers) + return headers_ + + def urlopen(self, method, url, redirect=True, **kw): + "Same as HTTP(S)ConnectionPool.urlopen, ``url`` must be absolute." + u = parse_url(url) + + if u.scheme == "http": + # For proxied HTTPS requests, httplib sets the necessary headers + # on the CONNECT to the proxy. For HTTP, we'll definitely + # need to set 'Host' at the very least. + kw['headers'] = self._set_proxy_headers(url, kw.get('headers', + self.headers)) + + return super(ProxyManager, self).urlopen(method, url, redirect, **kw) + + +def proxy_from_url(url, **kw): + return ProxyManager(proxy_url=url, **kw) diff --git a/urllib3/request.py b/urllib3/request.py new file mode 100644 index 000000000..2a92cc208 --- /dev/null +++ b/urllib3/request.py @@ -0,0 +1,141 @@ +# urllib3/request.py +# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt) +# +# This module is part of urllib3 and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +try: + from urllib.parse import urlencode +except ImportError: + from urllib import urlencode + +from .filepost import encode_multipart_formdata + + +__all__ = ['RequestMethods'] + + +class RequestMethods(object): + """ + Convenience mixin for classes who implement a :meth:`urlopen` method, such + as :class:`~urllib3.connectionpool.HTTPConnectionPool` and + :class:`~urllib3.poolmanager.PoolManager`. + + Provides behavior for making common types of HTTP request methods and + decides which type of request field encoding to use. + + Specifically, + + :meth:`.request_encode_url` is for sending requests whose fields are encoded + in the URL (such as GET, HEAD, DELETE). + + :meth:`.request_encode_body` is for sending requests whose fields are + encoded in the *body* of the request using multipart or www-form-urlencoded + (such as for POST, PUT, PATCH). + + :meth:`.request` is for making any kind of request, it will look up the + appropriate encoding format and use one of the above two methods to make + the request. + + Initializer parameters: + + :param headers: + Headers to include with all requests, unless other headers are given + explicitly. + """ + + _encode_url_methods = set(['DELETE', 'GET', 'HEAD', 'OPTIONS']) + + def __init__(self, headers=None): + self.headers = headers or {} + + def urlopen(self, method, url, body=None, headers=None, + encode_multipart=True, multipart_boundary=None, + **kw): # Abstract + raise NotImplemented("Classes extending RequestMethods must implement " + "their own ``urlopen`` method.") + + def request(self, method, url, fields=None, headers=None, **urlopen_kw): + """ + Make a request using :meth:`urlopen` with the appropriate encoding of + ``fields`` based on the ``method`` used. + + This is a convenience method that requires the least amount of manual + effort. It can be used in most situations, while still having the option + to drop down to more specific methods when necessary, such as + :meth:`request_encode_url`, :meth:`request_encode_body`, + or even the lowest level :meth:`urlopen`. + """ + method = method.upper() + + if method in self._encode_url_methods: + return self.request_encode_url(method, url, fields=fields, + headers=headers, + **urlopen_kw) + else: + return self.request_encode_body(method, url, fields=fields, + headers=headers, + **urlopen_kw) + + def request_encode_url(self, method, url, fields=None, **urlopen_kw): + """ + Make a request using :meth:`urlopen` with the ``fields`` encoded in + the url. This is useful for request methods like GET, HEAD, DELETE, etc. + """ + if fields: + url += '?' + urlencode(fields) + return self.urlopen(method, url, **urlopen_kw) + + def request_encode_body(self, method, url, fields=None, headers=None, + encode_multipart=True, multipart_boundary=None, + **urlopen_kw): + """ + Make a request using :meth:`urlopen` with the ``fields`` encoded in + the body. This is useful for request methods like POST, PUT, PATCH, etc. + + When ``encode_multipart=True`` (default), then + :meth:`urllib3.filepost.encode_multipart_formdata` is used to encode the + payload with the appropriate content type. Otherwise + :meth:`urllib.urlencode` is used with the + 'application/x-www-form-urlencoded' content type. + + Multipart encoding must be used when posting files, and it's reasonably + safe to use it in other times too. However, it may break request signing, + such as with OAuth. + + Supports an optional ``fields`` parameter of key/value strings AND + key/filetuple. A filetuple is a (filename, data, MIME type) tuple where + the MIME type is optional. For example: :: + + fields = { + 'foo': 'bar', + 'fakefile': ('foofile.txt', 'contents of foofile'), + 'realfile': ('barfile.txt', open('realfile').read()), + 'typedfile': ('bazfile.bin', open('bazfile').read(), + 'image/jpeg'), + 'nonamefile': 'contents of nonamefile field', + } + + When uploading a file, providing a filename (the first parameter of the + tuple) is optional but recommended to best mimick behavior of browsers. + + Note that if ``headers`` are supplied, the 'Content-Type' header will be + overwritten because it depends on the dynamic random boundary string + which is used to compose the body of the request. The random boundary + string can be explicitly set with the ``multipart_boundary`` parameter. + """ + if encode_multipart: + body, content_type = encode_multipart_formdata(fields or {}, + boundary=multipart_boundary) + else: + body, content_type = (urlencode(fields or {}), + 'application/x-www-form-urlencoded') + + if headers is None: + headers = self.headers + + headers_ = {'Content-Type': content_type} + headers_.update(headers) + + return self.urlopen(method, url, body=body, headers=headers_, + **urlopen_kw) diff --git a/urllib3/response.py b/urllib3/response.py new file mode 100644 index 000000000..db441828a --- /dev/null +++ b/urllib3/response.py @@ -0,0 +1,308 @@ +# urllib3/response.py +# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt) +# +# This module is part of urllib3 and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + + +import logging +import zlib +import io + +from ._collections import HTTPHeaderDict +from .exceptions import DecodeError +from .packages.six import string_types as basestring, binary_type +from .util import is_fp_closed + + +log = logging.getLogger(__name__) + + +class DeflateDecoder(object): + + def __init__(self): + self._first_try = True + self._data = binary_type() + self._obj = zlib.decompressobj() + + def __getattr__(self, name): + return getattr(self._obj, name) + + def decompress(self, data): + if not self._first_try: + return self._obj.decompress(data) + + self._data += data + try: + return self._obj.decompress(data) + except zlib.error: + self._first_try = False + self._obj = zlib.decompressobj(-zlib.MAX_WBITS) + try: + return self.decompress(self._data) + finally: + self._data = None + + +def _get_decoder(mode): + if mode == 'gzip': + return zlib.decompressobj(16 + zlib.MAX_WBITS) + + return DeflateDecoder() + + +class HTTPResponse(io.IOBase): + """ + HTTP Response container. + + Backwards-compatible to httplib's HTTPResponse but the response ``body`` is + loaded and decoded on-demand when the ``data`` property is accessed. + + Extra parameters for behaviour not present in httplib.HTTPResponse: + + :param preload_content: + If True, the response's body will be preloaded during construction. + + :param decode_content: + If True, attempts to decode specific content-encoding's based on headers + (like 'gzip' and 'deflate') will be skipped and raw data will be used + instead. + + :param original_response: + When this HTTPResponse wrapper is generated from an httplib.HTTPResponse + object, it's convenient to include the original for debug purposes. It's + otherwise unused. + """ + + CONTENT_DECODERS = ['gzip', 'deflate'] + REDIRECT_STATUSES = [301, 302, 303, 307, 308] + + def __init__(self, body='', headers=None, status=0, version=0, reason=None, + strict=0, preload_content=True, decode_content=True, + original_response=None, pool=None, connection=None): + + self.headers = HTTPHeaderDict() + if headers: + self.headers.update(headers) + self.status = status + self.version = version + self.reason = reason + self.strict = strict + self.decode_content = decode_content + + self._decoder = None + self._body = body if body and isinstance(body, basestring) else None + self._fp = None + self._original_response = original_response + self._fp_bytes_read = 0 + + self._pool = pool + self._connection = connection + + if hasattr(body, 'read'): + self._fp = body + + if preload_content and not self._body: + self._body = self.read(decode_content=decode_content) + + def get_redirect_location(self): + """ + Should we redirect and where to? + + :returns: Truthy redirect location string if we got a redirect status + code and valid location. ``None`` if redirect status and no + location. ``False`` if not a redirect status code. + """ + if self.status in self.REDIRECT_STATUSES: + return self.headers.get('location') + + return False + + def release_conn(self): + if not self._pool or not self._connection: + return + + self._pool._put_conn(self._connection) + self._connection = None + + @property + def data(self): + # For backwords-compat with earlier urllib3 0.4 and earlier. + if self._body: + return self._body + + if self._fp: + return self.read(cache_content=True) + + def tell(self): + """ + Obtain the number of bytes pulled over the wire so far. May differ from + the amount of content returned by :meth:``HTTPResponse.read`` if bytes + are encoded on the wire (e.g, compressed). + """ + return self._fp_bytes_read + + def read(self, amt=None, decode_content=None, cache_content=False): + """ + Similar to :meth:`httplib.HTTPResponse.read`, but with two additional + parameters: ``decode_content`` and ``cache_content``. + + :param amt: + How much of the content to read. If specified, caching is skipped + because it doesn't make sense to cache partial content as the full + response. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + + :param cache_content: + If True, will save the returned data such that the same result is + returned despite of the state of the underlying file object. This + is useful if you want the ``.data`` property to continue working + after having ``.read()`` the file object. (Overridden if ``amt`` is + set.) + """ + # Note: content-encoding value should be case-insensitive, per RFC 2616 + # Section 3.5 + content_encoding = self.headers.get('content-encoding', '').lower() + if self._decoder is None: + if content_encoding in self.CONTENT_DECODERS: + self._decoder = _get_decoder(content_encoding) + if decode_content is None: + decode_content = self.decode_content + + if self._fp is None: + return + + flush_decoder = False + + try: + if amt is None: + # cStringIO doesn't like amt=None + data = self._fp.read() + flush_decoder = True + else: + cache_content = False + data = self._fp.read(amt) + if amt != 0 and not data: # Platform-specific: Buggy versions of Python. + # Close the connection when no data is returned + # + # This is redundant to what httplib/http.client _should_ + # already do. However, versions of python released before + # December 15, 2012 (http://bugs.python.org/issue16298) do not + # properly close the connection in all cases. There is no harm + # in redundantly calling close. + self._fp.close() + flush_decoder = True + + self._fp_bytes_read += len(data) + + try: + if decode_content and self._decoder: + data = self._decoder.decompress(data) + except (IOError, zlib.error) as e: + raise DecodeError( + "Received response with content-encoding: %s, but " + "failed to decode it." % content_encoding, + e) + + if flush_decoder and decode_content and self._decoder: + buf = self._decoder.decompress(binary_type()) + data += buf + self._decoder.flush() + + if cache_content: + self._body = data + + return data + + finally: + if self._original_response and self._original_response.isclosed(): + self.release_conn() + + def stream(self, amt=2**16, decode_content=None): + """ + A generator wrapper for the read() method. A call will block until + ``amt`` bytes have been read from the connection or until the + connection is closed. + + :param amt: + How much of the content to read. The generator will return up to + much data per iteration, but may return less. This is particularly + likely when using compressed data. However, the empty string will + never be returned. + + :param decode_content: + If True, will attempt to decode the body based on the + 'content-encoding' header. + """ + while not is_fp_closed(self._fp): + data = self.read(amt=amt, decode_content=decode_content) + + if data: + yield data + + + @classmethod + def from_httplib(ResponseCls, r, **response_kw): + """ + Given an :class:`httplib.HTTPResponse` instance ``r``, return a + corresponding :class:`urllib3.response.HTTPResponse` object. + + Remaining parameters are passed to the HTTPResponse constructor, along + with ``original_response=r``. + """ + + headers = HTTPHeaderDict() + for k, v in r.getheaders(): + headers.add(k, v) + + # HTTPResponse objects in Python 3 don't have a .strict attribute + strict = getattr(r, 'strict', 0) + return ResponseCls(body=r, + headers=headers, + status=r.status, + version=r.version, + reason=r.reason, + strict=strict, + original_response=r, + **response_kw) + + # Backwards-compatibility methods for httplib.HTTPResponse + def getheaders(self): + return self.headers + + def getheader(self, name, default=None): + return self.headers.get(name, default) + + # Overrides from io.IOBase + def close(self): + if not self.closed: + self._fp.close() + + @property + def closed(self): + if self._fp is None: + return True + elif hasattr(self._fp, 'closed'): + return self._fp.closed + elif hasattr(self._fp, 'isclosed'): # Python 2 + return self._fp.isclosed() + else: + return True + + def fileno(self): + if self._fp is None: + raise IOError("HTTPResponse has no file to get a fileno from") + elif hasattr(self._fp, "fileno"): + return self._fp.fileno() + else: + raise IOError("The file-like object this HTTPResponse is wrapped " + "around has no file descriptor") + + def flush(self): + if self._fp is not None and hasattr(self._fp, 'flush'): + return self._fp.flush() + + def readable(self): + return True diff --git a/urllib3/util.py b/urllib3/util.py new file mode 100644 index 000000000..bd266317f --- /dev/null +++ b/urllib3/util.py @@ -0,0 +1,648 @@ +# urllib3/util.py +# Copyright 2008-2013 Andrey Petrov and contributors (see CONTRIBUTORS.txt) +# +# This module is part of urllib3 and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + + +from base64 import b64encode +from binascii import hexlify, unhexlify +from collections import namedtuple +from hashlib import md5, sha1 +from socket import error as SocketError, _GLOBAL_DEFAULT_TIMEOUT +import time + +try: + from select import poll, POLLIN +except ImportError: # `poll` doesn't exist on OSX and other platforms + poll = False + try: + from select import select + except ImportError: # `select` doesn't exist on AppEngine. + select = False + +try: # Test for SSL features + SSLContext = None + HAS_SNI = False + + import ssl + from ssl import wrap_socket, CERT_NONE, PROTOCOL_SSLv23 + from ssl import SSLContext # Modern SSL? + from ssl import HAS_SNI # Has SNI? +except ImportError: + pass + +from .packages import six +from .exceptions import LocationParseError, SSLError, TimeoutStateError + + +_Default = object() +# The default timeout to use for socket connections. This is the attribute used +# by httplib to define the default timeout + + +def current_time(): + """ + Retrieve the current time, this function is mocked out in unit testing. + """ + return time.time() + + +class Timeout(object): + """ + Utility object for storing timeout values. + + Example usage: + + .. code-block:: python + + timeout = urllib3.util.Timeout(connect=2.0, read=7.0) + pool = HTTPConnectionPool('www.google.com', 80, timeout=timeout) + pool.request(...) # Etc, etc + + :param connect: + The maximum amount of time to wait for a connection attempt to a server + to succeed. Omitting the parameter will default the connect timeout to + the system default, probably `the global default timeout in socket.py + `_. + None will set an infinite timeout for connection attempts. + + :type connect: integer, float, or None + + :param read: + The maximum amount of time to wait between consecutive + read operations for a response from the server. Omitting + the parameter will default the read timeout to the system + default, probably `the global default timeout in socket.py + `_. + None will set an infinite timeout. + + :type read: integer, float, or None + + :param total: + This combines the connect and read timeouts into one; the read timeout + will be set to the time leftover from the connect attempt. In the + event that both a connect timeout and a total are specified, or a read + timeout and a total are specified, the shorter timeout will be applied. + + Defaults to None. + + :type total: integer, float, or None + + .. note:: + + Many factors can affect the total amount of time for urllib3 to return + an HTTP response. Specifically, Python's DNS resolver does not obey the + timeout specified on the socket. Other factors that can affect total + request time include high CPU load, high swap, the program running at a + low priority level, or other behaviors. The observed running time for + urllib3 to return a response may be greater than the value passed to + `total`. + + In addition, the read and total timeouts only measure the time between + read operations on the socket connecting the client and the server, + not the total amount of time for the request to return a complete + response. For most requests, the timeout is raised because the server + has not sent the first byte in the specified time. This is not always + the case; if a server streams one byte every fifteen seconds, a timeout + of 20 seconds will not ever trigger, even though the request will + take several minutes to complete. + + If your goal is to cut off any request after a set amount of wall clock + time, consider having a second "watcher" thread to cut off a slow + request. + """ + + #: A sentinel object representing the default timeout value + DEFAULT_TIMEOUT = _GLOBAL_DEFAULT_TIMEOUT + + def __init__(self, total=None, connect=_Default, read=_Default): + self._connect = self._validate_timeout(connect, 'connect') + self._read = self._validate_timeout(read, 'read') + self.total = self._validate_timeout(total, 'total') + self._start_connect = None + + def __str__(self): + return '%s(connect=%r, read=%r, total=%r)' % ( + type(self).__name__, self._connect, self._read, self.total) + + + @classmethod + def _validate_timeout(cls, value, name): + """ Check that a timeout attribute is valid + + :param value: The timeout value to validate + :param name: The name of the timeout attribute to validate. This is used + for clear error messages + :return: the value + :raises ValueError: if the type is not an integer or a float, or if it + is a numeric value less than zero + """ + if value is _Default: + return cls.DEFAULT_TIMEOUT + + if value is None or value is cls.DEFAULT_TIMEOUT: + return value + + try: + float(value) + except (TypeError, ValueError): + raise ValueError("Timeout value %s was %s, but it must be an " + "int or float." % (name, value)) + + try: + if value < 0: + raise ValueError("Attempted to set %s timeout to %s, but the " + "timeout cannot be set to a value less " + "than 0." % (name, value)) + except TypeError: # Python 3 + raise ValueError("Timeout value %s was %s, but it must be an " + "int or float." % (name, value)) + + return value + + @classmethod + def from_float(cls, timeout): + """ Create a new Timeout from a legacy timeout value. + + The timeout value used by httplib.py sets the same timeout on the + connect(), and recv() socket requests. This creates a :class:`Timeout` + object that sets the individual timeouts to the ``timeout`` value passed + to this function. + + :param timeout: The legacy timeout value + :type timeout: integer, float, sentinel default object, or None + :return: a Timeout object + :rtype: :class:`Timeout` + """ + return Timeout(read=timeout, connect=timeout) + + def clone(self): + """ Create a copy of the timeout object + + Timeout properties are stored per-pool but each request needs a fresh + Timeout object to ensure each one has its own start/stop configured. + + :return: a copy of the timeout object + :rtype: :class:`Timeout` + """ + # We can't use copy.deepcopy because that will also create a new object + # for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to + # detect the user default. + return Timeout(connect=self._connect, read=self._read, + total=self.total) + + def start_connect(self): + """ Start the timeout clock, used during a connect() attempt + + :raises urllib3.exceptions.TimeoutStateError: if you attempt + to start a timer that has been started already. + """ + if self._start_connect is not None: + raise TimeoutStateError("Timeout timer has already been started.") + self._start_connect = current_time() + return self._start_connect + + def get_connect_duration(self): + """ Gets the time elapsed since the call to :meth:`start_connect`. + + :return: the elapsed time + :rtype: float + :raises urllib3.exceptions.TimeoutStateError: if you attempt + to get duration for a timer that hasn't been started. + """ + if self._start_connect is None: + raise TimeoutStateError("Can't get connect duration for timer " + "that has not started.") + return current_time() - self._start_connect + + @property + def connect_timeout(self): + """ Get the value to use when setting a connection timeout. + + This will be a positive float or integer, the value None + (never timeout), or the default system timeout. + + :return: the connect timeout + :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None + """ + if self.total is None: + return self._connect + + if self._connect is None or self._connect is self.DEFAULT_TIMEOUT: + return self.total + + return min(self._connect, self.total) + + @property + def read_timeout(self): + """ Get the value for the read timeout. + + This assumes some time has elapsed in the connection timeout and + computes the read timeout appropriately. + + If self.total is set, the read timeout is dependent on the amount of + time taken by the connect timeout. If the connection time has not been + established, a :exc:`~urllib3.exceptions.TimeoutStateError` will be + raised. + + :return: the value to use for the read timeout + :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None + :raises urllib3.exceptions.TimeoutStateError: If :meth:`start_connect` + has not yet been called on this object. + """ + if (self.total is not None and + self.total is not self.DEFAULT_TIMEOUT and + self._read is not None and + self._read is not self.DEFAULT_TIMEOUT): + # in case the connect timeout has not yet been established. + if self._start_connect is None: + return self._read + return max(0, min(self.total - self.get_connect_duration(), + self._read)) + elif self.total is not None and self.total is not self.DEFAULT_TIMEOUT: + return max(0, self.total - self.get_connect_duration()) + else: + return self._read + + +class Url(namedtuple('Url', ['scheme', 'auth', 'host', 'port', 'path', 'query', 'fragment'])): + """ + Datastructure for representing an HTTP URL. Used as a return value for + :func:`parse_url`. + """ + slots = () + + def __new__(cls, scheme=None, auth=None, host=None, port=None, path=None, query=None, fragment=None): + return super(Url, cls).__new__(cls, scheme, auth, host, port, path, query, fragment) + + @property + def hostname(self): + """For backwards-compatibility with urlparse. We're nice like that.""" + return self.host + + @property + def request_uri(self): + """Absolute path including the query string.""" + uri = self.path or '/' + + if self.query is not None: + uri += '?' + self.query + + return uri + + @property + def netloc(self): + """Network location including host and port""" + if self.port: + return '%s:%d' % (self.host, self.port) + return self.host + + +def split_first(s, delims): + """ + Given a string and an iterable of delimiters, split on the first found + delimiter. Return two split parts and the matched delimiter. + + If not found, then the first part is the full input string. + + Example: :: + + >>> split_first('foo/bar?baz', '?/=') + ('foo', 'bar?baz', '/') + >>> split_first('foo/bar?baz', '123') + ('foo/bar?baz', '', None) + + Scales linearly with number of delims. Not ideal for large number of delims. + """ + min_idx = None + min_delim = None + for d in delims: + idx = s.find(d) + if idx < 0: + continue + + if min_idx is None or idx < min_idx: + min_idx = idx + min_delim = d + + if min_idx is None or min_idx < 0: + return s, '', None + + return s[:min_idx], s[min_idx+1:], min_delim + + +def parse_url(url): + """ + Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is + performed to parse incomplete urls. Fields not provided will be None. + + Partly backwards-compatible with :mod:`urlparse`. + + Example: :: + + >>> parse_url('http://google.com/mail/') + Url(scheme='http', host='google.com', port=None, path='/', ...) + >>> parse_url('google.com:80') + Url(scheme=None, host='google.com', port=80, path=None, ...) + >>> parse_url('/foo?bar') + Url(scheme=None, host=None, port=None, path='/foo', query='bar', ...) + """ + + # While this code has overlap with stdlib's urlparse, it is much + # simplified for our needs and less annoying. + # Additionally, this implementations does silly things to be optimal + # on CPython. + + scheme = None + auth = None + host = None + port = None + path = None + fragment = None + query = None + + # Scheme + if '://' in url: + scheme, url = url.split('://', 1) + + # Find the earliest Authority Terminator + # (http://tools.ietf.org/html/rfc3986#section-3.2) + url, path_, delim = split_first(url, ['/', '?', '#']) + + if delim: + # Reassemble the path + path = delim + path_ + + # Auth + if '@' in url: + # Last '@' denotes end of auth part + auth, url = url.rsplit('@', 1) + + # IPv6 + if url and url[0] == '[': + host, url = url.split(']', 1) + host += ']' + + # Port + if ':' in url: + _host, port = url.split(':', 1) + + if not host: + host = _host + + if port: + # If given, ports must be integers. + if not port.isdigit(): + raise LocationParseError("Failed to parse: %s" % url) + port = int(port) + else: + # Blank ports are cool, too. (rfc3986#section-3.2.3) + port = None + + elif not host and url: + host = url + + if not path: + return Url(scheme, auth, host, port, path, query, fragment) + + # Fragment + if '#' in path: + path, fragment = path.split('#', 1) + + # Query + if '?' in path: + path, query = path.split('?', 1) + + return Url(scheme, auth, host, port, path, query, fragment) + + +def get_host(url): + """ + Deprecated. Use :func:`.parse_url` instead. + """ + p = parse_url(url) + return p.scheme or 'http', p.hostname, p.port + + +def make_headers(keep_alive=None, accept_encoding=None, user_agent=None, + basic_auth=None, proxy_basic_auth=None): + """ + Shortcuts for generating request headers. + + :param keep_alive: + If ``True``, adds 'connection: keep-alive' header. + + :param accept_encoding: + Can be a boolean, list, or string. + ``True`` translates to 'gzip,deflate'. + List will get joined by comma. + String will be used as provided. + + :param user_agent: + String representing the user-agent you want, such as + "python-urllib3/0.6" + + :param basic_auth: + Colon-separated username:password string for 'authorization: basic ...' + auth header. + + :param proxy_basic_auth: + Colon-separated username:password string for 'proxy-authorization: basic ...' + auth header. + + Example: :: + + >>> make_headers(keep_alive=True, user_agent="Batman/1.0") + {'connection': 'keep-alive', 'user-agent': 'Batman/1.0'} + >>> make_headers(accept_encoding=True) + {'accept-encoding': 'gzip,deflate'} + """ + headers = {} + if accept_encoding: + if isinstance(accept_encoding, str): + pass + elif isinstance(accept_encoding, list): + accept_encoding = ','.join(accept_encoding) + else: + accept_encoding = 'gzip,deflate' + headers['accept-encoding'] = accept_encoding + + if user_agent: + headers['user-agent'] = user_agent + + if keep_alive: + headers['connection'] = 'keep-alive' + + if basic_auth: + headers['authorization'] = 'Basic ' + \ + b64encode(six.b(basic_auth)).decode('utf-8') + + if proxy_basic_auth: + headers['proxy-authorization'] = 'Basic ' + \ + b64encode(six.b(proxy_basic_auth)).decode('utf-8') + + return headers + + +def is_connection_dropped(conn): # Platform-specific + """ + Returns True if the connection is dropped and should be closed. + + :param conn: + :class:`httplib.HTTPConnection` object. + + Note: For platforms like AppEngine, this will always return ``False`` to + let the platform handle connection recycling transparently for us. + """ + sock = getattr(conn, 'sock', False) + if not sock: # Platform-specific: AppEngine + return False + + if not poll: + if not select: # Platform-specific: AppEngine + return False + + try: + return select([sock], [], [], 0.0)[0] + except SocketError: + return True + + # This version is better on platforms that support it. + p = poll() + p.register(sock, POLLIN) + for (fno, ev) in p.poll(0.0): + if fno == sock.fileno(): + # Either data is buffered (bad), or the connection is dropped. + return True + + +def resolve_cert_reqs(candidate): + """ + Resolves the argument to a numeric constant, which can be passed to + the wrap_socket function/method from the ssl module. + Defaults to :data:`ssl.CERT_NONE`. + If given a string it is assumed to be the name of the constant in the + :mod:`ssl` module or its abbrevation. + (So you can specify `REQUIRED` instead of `CERT_REQUIRED`. + If it's neither `None` nor a string we assume it is already the numeric + constant which can directly be passed to wrap_socket. + """ + if candidate is None: + return CERT_NONE + + if isinstance(candidate, str): + res = getattr(ssl, candidate, None) + if res is None: + res = getattr(ssl, 'CERT_' + candidate) + return res + + return candidate + + +def resolve_ssl_version(candidate): + """ + like resolve_cert_reqs + """ + if candidate is None: + return PROTOCOL_SSLv23 + + if isinstance(candidate, str): + res = getattr(ssl, candidate, None) + if res is None: + res = getattr(ssl, 'PROTOCOL_' + candidate) + return res + + return candidate + + +def assert_fingerprint(cert, fingerprint): + """ + Checks if given fingerprint matches the supplied certificate. + + :param cert: + Certificate as bytes object. + :param fingerprint: + Fingerprint as string of hexdigits, can be interspersed by colons. + """ + + # Maps the length of a digest to a possible hash function producing + # this digest. + hashfunc_map = { + 16: md5, + 20: sha1 + } + + fingerprint = fingerprint.replace(':', '').lower() + + digest_length, rest = divmod(len(fingerprint), 2) + + if rest or digest_length not in hashfunc_map: + raise SSLError('Fingerprint is of invalid length.') + + # We need encode() here for py32; works on py2 and p33. + fingerprint_bytes = unhexlify(fingerprint.encode()) + + hashfunc = hashfunc_map[digest_length] + + cert_digest = hashfunc(cert).digest() + + if not cert_digest == fingerprint_bytes: + raise SSLError('Fingerprints did not match. Expected "{0}", got "{1}".' + .format(hexlify(fingerprint_bytes), + hexlify(cert_digest))) + +def is_fp_closed(obj): + """ + Checks whether a given file-like object is closed. + + :param obj: + The file-like object to check. + """ + if hasattr(obj, 'fp'): + # Object is a container for another file-like object that gets released + # on exhaustion (e.g. HTTPResponse) + return obj.fp is None + + return obj.closed + + +if SSLContext is not None: # Python 3.2+ + def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None, + ca_certs=None, server_hostname=None, + ssl_version=None): + """ + All arguments except `server_hostname` have the same meaning as for + :func:`ssl.wrap_socket` + + :param server_hostname: + Hostname of the expected certificate + """ + context = SSLContext(ssl_version) + context.verify_mode = cert_reqs + + # Disable TLS compression to migitate CRIME attack (issue #309) + OP_NO_COMPRESSION = 0x20000 + context.options |= OP_NO_COMPRESSION + + if ca_certs: + try: + context.load_verify_locations(ca_certs) + # Py32 raises IOError + # Py33 raises FileNotFoundError + except Exception as e: # Reraise as SSLError + raise SSLError(e) + if certfile: + # FIXME: This block needs a test. + context.load_cert_chain(certfile, keyfile) + if HAS_SNI: # Platform-specific: OpenSSL with enabled SNI + return context.wrap_socket(sock, server_hostname=server_hostname) + return context.wrap_socket(sock) + +else: # Python 3.1 and earlier + def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None, + ca_certs=None, server_hostname=None, + ssl_version=None): + return wrap_socket(sock, keyfile=keyfile, certfile=certfile, + ca_certs=ca_certs, cert_reqs=cert_reqs, + ssl_version=ssl_version) diff --git a/windowsbuild/Armory_Setup_32bit.warsetup b/windowsbuild/Armory_Setup_32bit.warsetup new file mode 100644 index 000000000..9957467b0 --- /dev/null +++ b/windowsbuild/Armory_Setup_32bit.warsetup @@ -0,0 +1,419 @@ + + + + C:\Program Files\Jgaa's Internet\War Setup\Licenses + + false + + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + 3 + + + + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + 0 + + 3 + + 0 + + true + GNU Affero General Public License (AGPL) Version 3.0 + + + + + + + + \ No newline at end of file diff --git a/windowsbuild/Armory_Setup_64bit.warsetup b/windowsbuild/Armory_Setup_64bit.warsetup new file mode 100644 index 000000000..080bb2d54 --- /dev/null +++ b/windowsbuild/Armory_Setup_64bit.warsetup @@ -0,0 +1,641 @@ + + + + C:\Program Files (x86)\Jgaa's Internet\War Setup\Licenses + + false + + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + 0 + + + + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + + + + + false + + 0 + + 3 + + 0 + + true + GNU Affero General Public License (AGPL) Version 3.0 + + + + + + + + \ No newline at end of file diff --git a/windowsbuild/GNU Affero General Public License (AGPL) Version 3.0.rtf b/windowsbuild/GNU Affero General Public License (AGPL) Version 3.0.rtf new file mode 100644 index 000000000..500d173f7 --- /dev/null +++ b/windowsbuild/GNU Affero General Public License (AGPL) Version 3.0.rtf @@ -0,0 +1,455 @@ +{\rtf1\ansi\ansicpg1252\uc1\deff0\stshfdbch0\stshfloch0\stshfhich0\stshfbi0\deflang1033\deflangfe1033{\fonttbl{\f0\froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;} +{\f2\fmodern\fcharset0\fprq1{\*\panose 02070309020205020404}Courier New;}{\f36\froman\fcharset238\fprq2 Times New Roman CE;}{\f37\froman\fcharset204\fprq2 Times New Roman Cyr;}{\f39\froman\fcharset161\fprq2 Times New Roman Greek;} +{\f40\froman\fcharset162\fprq2 Times New Roman Tur;}{\f41\froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\f42\froman\fcharset178\fprq2 Times New Roman (Arabic);}{\f43\froman\fcharset186\fprq2 Times New Roman Baltic;} +{\f44\froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\f56\fmodern\fcharset238\fprq1 Courier New CE;}{\f57\fmodern\fcharset204\fprq1 Courier New Cyr;}{\f59\fmodern\fcharset161\fprq1 Courier New Greek;} +{\f60\fmodern\fcharset162\fprq1 Courier New Tur;}{\f61\fmodern\fcharset177\fprq1 Courier New (Hebrew);}{\f62\fmodern\fcharset178\fprq1 Courier New (Arabic);}{\f63\fmodern\fcharset186\fprq1 Courier New Baltic;} +{\f64\fmodern\fcharset163\fprq1 Courier New (Vietnamese);}}{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255; +\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;\red204\green204\blue204;}{\stylesheet{ +\ql \li0\ri0\widctlpar\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \fs24\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \snext0 Normal;}{\*\cs10 \additive \ssemihidden Default Paragraph Font;}{\* +\ts11\tsrowd\trftsWidthB3\trpaddl108\trpaddr108\trpaddfl3\trpaddft3\trpaddfb3\trpaddfr3\trcbpat1\trcfpat1\tscellwidthfts0\tsvertalt\tsbrdrt\tsbrdrl\tsbrdrb\tsbrdrr\tsbrdrdgl\tsbrdrdgr\tsbrdrh\tsbrdrv +\ql \li0\ri0\widctlpar\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \fs20\lang1024\langfe1024\cgrid\langnp1024\langfenp1024 \snext11 \ssemihidden Normal Table;}{\s15\ql \li0\ri0\widctlpar +\tx916\tx1832\tx2748\tx3664\tx4580\tx5496\tx6412\tx7328\tx8244\tx9160\tx10076\tx10992\tx11908\tx12824\tx13740\tx14656\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \f2\fs20\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 +\sbasedon0 \snext15 \styrsid3877315 HTML Preformatted;}}{\*\latentstyles\lsdstimax156\lsdlockeddef0}{\*\pgptbl {\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0 +\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0 +\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1 +\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0 +\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp239\itap0\li0\ri0\sb0\sa0\brdrt +\brdrs\brdrw15\brdrcf17 \brdrl\brdrs\brdrw15\brdrcf17 \brdrb\brdrs\brdrw15\brdrcf17 \brdrr\brdrs\brdrw15\brdrcf17 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1 +\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0 +\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp120\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp223\itap1 +\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0 +\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp140\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp223\itap1 +\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0 +\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp208\itap1\li0\ri0\sb0\sa0}{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1 +\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0 +\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp259\itap0\li0\ri0\sb0\sa0\brdrt\brdrnone\brsp40 \brdrl\brdrnone\brsp40 \brdrb\brdrnone\brsp40 \brdrr\brdrnone\brsp40 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl +\brdrnone\brsp240 }{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }{\pgp\ipgp155\itap0\li0\ri1500\sb0\sa0}{\pgp\ipgp223\itap1\li0\ri0\sb0\sa0\brdrl\brdrnone\brsp240 }}{\*\rsidtbl \rsid3877315\rsid7430913\rsid7825306\rsid8616255\rsid15162665} +{\*\generator Microsoft Word 11.0.5604;}{\info{\title ********************************************************************************}{\author vbox}{\operator vbox}{\creatim\yr2012\mo4\dy18\hr15\min47}{\revtim\yr2012\mo4\dy18\hr15\min49}{\version5} +{\edmins6}{\nofpages8}{\nofwords4960}{\nofchars28276}{\nofcharsws33170}{\vern24689}}\margr1620 \widowctrl\ftnbj\aenddoc\noxlattoyen\expshrtn\noultrlspc\dntblnsbdb\nospaceforul\hyphcaps0\formshade\horzdoc\dgmargin\dghspace180\dgvspace180\dghorigin1800 +\dgvorigin1440\dghshow1\dgvshow1\jexpand\viewkind1\viewscale100\pgbrdrhead\pgbrdrfoot\splytwnine\ftnlytwnine\htmautsp\nolnhtadjtbl\useltbaln\alntblind\lytcalctblwd\lyttblrtgr\lnbrkrule\nobrkwrptbl\snaptogridincell\allowfieldendsel\wrppunct +\asianbrkrule\rsidroot3877315\newtblstyruls\nogrowautofit \fet0\sectd \linex0\endnhere\sectlinegrid360\sectdefaultcl\sectrsid3877315\sftnbj {\*\pnseclvl1\pnucrm\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl2\pnucltr\pnstart1\pnindent720\pnhang +{\pntxta .}}{\*\pnseclvl3\pndec\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl4\pnlcltr\pnstart1\pnindent720\pnhang {\pntxta )}}{\*\pnseclvl5\pndec\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl6\pnlcltr\pnstart1\pnindent720\pnhang +{\pntxtb (}{\pntxta )}}{\*\pnseclvl7\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl8\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl9\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}\pard\plain +\s15\ql \li0\ri0\widctlpar\tx916\tx1832\tx2748\tx3664\tx4580\tx5496\tx6412\tx7328\tx8244\tx9160\tx10076\tx10992\tx11908\tx12824\tx13740\tx14656\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid3877315 \cbpat8 +\f2\fs20\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\b\fs16\cf2\insrsid3877315\charrsid3877315 ***************************************}{\b\fs16\cf2\insrsid7825306 +\par }{\fs16\cf2\insrsid3877315\charrsid3877315 \~\~Armory -- Bitcoin Client Software +\par +\par \~\~Copyright (C) 2011-2012, }{\fs16\cf2\insrsid7825306 +\par }{\fs16\cf2\lang1035\langfe1033\langnp1035\insrsid7825306 }{\fs16\cf2\lang1035\langfe1033\langnp1035\insrsid15162665 }{\fs16\cf2\lang1035\langfe1033\langnp1035\insrsid3877315\charrsid7825306 Alan C. Reiner }{ +\fs16\cf2\lang1035\langfe1033\langnp1035\insrsid7825306 +\par }{\fs16\cf2\lang1035\langfe1033\langnp1035\insrsid15162665 }{\fs16\cf2\lang1035\langfe1033\langnp1035\insrsid7825306 }{\fs16\cf2\lang1035\langfe1033\langnp1035\insrsid3877315\charrsid7825306 +\par }{\fs16\lang1035\langfe1033\langnp1035\insrsid3877315\charrsid7825306 +\par }{\fs16\insrsid3877315\charrsid3877315 This program is free software: you can redistribute it and/or modify}{\fs16\insrsid7825306 }{\fs16\insrsid3877315\charrsid3877315 it under the terms of the GNU Affero General Public License as}{\fs16\insrsid7825306 + }{\fs16\insrsid3877315\charrsid3877315 published by the Free Software Foundation, either version 3 of the}{\fs16\insrsid7825306 }{\fs16\insrsid3877315\charrsid3877315 License, or (at your option) any later version. +\par +\par This program is distributed in the hope that it will be useful,}{\fs16\insrsid7825306 }{\fs16\insrsid3877315\charrsid3877315 but WITHOUT ANY WARRANTY; without even the implied warranty of}{\fs16\insrsid7825306 }{\fs16\insrsid3877315\charrsid3877315 +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the}{\fs16\insrsid7825306 }{\fs16\insrsid3877315\charrsid3877315 GNU Affero General Public License }{\fs16\insrsid7825306 }{\fs16\insrsid3877315\charrsid3877315 +for more details. +\par +\par }\pard \s15\ql \li0\ri0\widctlpar\tx916\tx1832\tx2748\tx3664\tx4580\tx5496\tx6412\tx7328\tx8244\tx9160\tx10076\tx10992\tx11908\tx12824\tx13740\tx14656\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid7825306 \cbpat8 {\b\fs16\cf2\insrsid7430913 + +\par }{\b\fs16\cf2\insrsid7825306\charrsid3877315 ***************************************}{\b\fs16\cf2\insrsid7825306 +\par }\pard \s15\ql \li0\ri0\widctlpar\tx916\tx1832\tx2748\tx3664\tx4580\tx5496\tx6412\tx7328\tx8244\tx9160\tx10076\tx10992\tx11908\tx12824\tx13740\tx14656\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid3877315 \cbpat8 { +\fs16\insrsid3877315\charrsid3877315 \~\~\~ +\par If you wish to use this work in a way not compliant with the AGPLv3 license, please contact me to work out an agreement for}{\fs16\insrsid7825306 }{\fs16\insrsid3877315\charrsid3877315 dual-licensing. alan.reiner@gmail.com}{\fs16\insrsid3877315 +\par +\par }{\fs16\insrsid3877315\charrsid3877315 +\par +\par }\pard \s15\ql \li0\ri0\widctlpar\tx916\tx1832\tx2748\tx3664\tx4580\tx5496\tx6412\tx7328\tx8244\tx9160\tx10076\tx10992\tx11908\tx12824\tx13740\tx14656\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid7825306 \cbpat8 { +\b\fs16\cf2\insrsid7825306\charrsid3877315 ***************************************}{\b\fs16\cf2\insrsid7825306 +\par }\pard \s15\ql \li0\ri0\widctlpar\tx916\tx1832\tx2748\tx3664\tx4580\tx5496\tx6412\tx7328\tx8244\tx9160\tx10076\tx10992\tx11908\tx12824\tx13740\tx14656\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid3877315 \cbpat8 { +\b\fs16\cf2\insrsid7825306\charrsid3877315 +\par }{\b\fs16\insrsid3877315\charrsid3877315 \~}{\b\fs16\insrsid8616255 }{\b\fs16\insrsid3877315\charrsid3877315 \~}{\b\fs16\cf2\insrsid3877315\charrsid3877315 GNU AFFERO GENERAL PUBLIC LICENSE +\par }{\b\fs16\cf2\insrsid7825306 }{\b\fs16\cf2\insrsid3877315\charrsid3877315 \~}{\b\fs16\cf2\insrsid8616255 }{\b\fs16\cf2\insrsid3877315\charrsid3877315 \~\~\~Version 3, 19 November 2007 +\par }{\b\fs16\cf2\insrsid7825306 +\par }{\b\fs16\cf2\insrsid3877315\charrsid3877315 -}{\b\fs16\cf2\insrsid7825306 }{\b\fs16\cf2\insrsid3877315\charrsid3877315 -}{\b\fs16\cf2\insrsid7825306 }{\b\fs16\cf2\insrsid3877315\charrsid3877315 -}{\b\fs16\cf2\insrsid7825306 }{ +\b\fs16\cf2\insrsid3877315\charrsid3877315 -}{\b\fs16\cf2\insrsid7825306 }{\b\fs16\cf2\insrsid3877315\charrsid3877315 -}{\b\fs16\cf2\insrsid7825306 }{\b\fs16\cf2\insrsid3877315\charrsid3877315 -}{\b\fs16\cf2\insrsid7825306 }{ +\b\fs16\cf2\insrsid3877315\charrsid3877315 -}{\b\fs16\cf2\insrsid7825306 }{\b\fs16\cf2\insrsid3877315\charrsid3877315 -}{\b\fs16\cf2\insrsid7825306 }{\b\fs16\cf2\insrsid3877315\charrsid3877315 -}{\b\fs16\cf2\insrsid7825306 }{ +\b\fs16\cf2\insrsid3877315\charrsid3877315 -}{\b\fs16\cf2\insrsid7825306 }{\b\fs16\cf2\insrsid3877315\charrsid3877315 -}{\b\fs16\cf2\insrsid7825306 }{\b\fs16\cf2\insrsid3877315\charrsid3877315 -}{\b\fs16\cf2\insrsid7825306 }{ +\b\fs16\cf2\insrsid3877315\charrsid3877315 -}{\b\fs16\cf2\insrsid7825306 }{\b\fs16\cf2\insrsid3877315\charrsid3877315 -}{\b\fs16\cf2\insrsid7825306 }{\b\fs16\cf2\insrsid3877315\charrsid3877315 -}{\b\fs16\cf2\insrsid7825306 }{ +\b\fs16\cf2\insrsid3877315\charrsid3877315 -}{\b\fs16\cf2\insrsid7825306 }{\b\fs16\cf2\insrsid3877315\charrsid3877315 -}{\b\fs16\cf2\insrsid7825306 }{\b\fs16\cf2\insrsid3877315\charrsid3877315 -}{\b\fs16\cf2\insrsid7825306 }{ +\b\fs16\cf2\insrsid3877315\charrsid3877315 -}{\b\fs16\cf2\insrsid7825306 }{\fs16\insrsid3877315\charrsid3877315 +\par }{\fs16\insrsid7825306 +\par +\par }{\fs16\insrsid3877315\charrsid3877315 Copyright \'a9 2007 Free Software Foundation, Inc. +\par Everyone is permitted to copy and distribute verbatim copies of this license +\par document, but changing it is not allowed. +\par +\par Preamble +\par +\par The GNU Affero General Public License is a free, copyleft license for software +\par and other kinds of works, specifically designed to ensure cooperation with the +\par community in the case of network server software. +\par +\par The licenses for most software and other practical works are designed to take +\par away your freedom to share and change the works. By contrast, our General Public +\par Licenses are intended to guarantee your freedom to share and change all versions +\par of a program--to make sure it remains free software for all its users. +\par +\par When we speak of free software, we are referring to freedom, not price. Our +\par General Public Licenses are designed to make sure that you have the freedom to +\par distribute copies of free software (and charge for them if you wish), that you +\par receive source code or can get it if you want it, that you can change the +\par software or use pieces of it in new free programs, and that you know you can +\par do these things. +\par +\par Developers that use our General Public Licenses protect your rights with two +\par steps: (1) assert copyright on the software, and (2) offer you this License +\par which gives you legal permission to copy, distribute and/or modify the software. +\par +\par A secondary benefit of defending all users' freedom is that improvements made in +\par alternate versions of the program, if they receive widespread use, become +\par available for other developers to incorporate. Many developers of free software +\par are heartened and encouraged by the resulting cooperation. However, in the case +\par of software used on network servers, this result may fail to come about. The GNU +\par General Public License permits making a modified version and letting the public +\par access it on a server without ever releasing its source code to the public. +\par +\par The GNU Affero General Public License is designed specifically to ensure that, +\par in such cases, the modified source code becomes available to the community. It +\par requires the operator of a network server to provide the source code of the +\par modified version running there to the users of that server. Therefore, public +\par use of a modified version, on a publicly accessible server, gives the public +\par access to the source code of the modified version. +\par +\par An older license, called the Affero General Public License and published by +\par Affero, was designed to accomplish similar goals. This is a different license, +\par not a version of the Affero GPL, but Affero has released a new version of the +\par Affero GPL which permits relicensing under this license. +\par +\par The terms and conditions for copying, distribution and modification follow: +\par +\par ******************************************************************************** +\par +\par TERMS AND CONDITIONS +\par +\par 0. Definitions. +\par "This License" refers to version 3 of the GNU Affero General Public License. +\par +\par "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. +\par +\par "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. +\par +\par To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based +on" the earlier work. +\par +\par A "covered work" means either the unmodified Program or a work based on the Program. +\par +\par To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under a +pplicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. +\par +\par To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. +\par +\par An interactive user interface displays "Appropriate Legal Not +ices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that li +censees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. +\par +\par 1. Source Code. +\par The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. +\par +\par A "Standard Interface" means an interface that either is an official standard defined by a recognized stan +dards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. +\par +\par The "System Libraries" of an executable work include anything, other than the work as a whole, th +at (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation +i +s available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to pr +oduce the work, or an object code interpreter used to run it. +\par +\par The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, includin +g scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For + +example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate + data communication or control flow between those subprograms and other parts of the work. +\par +\par The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. +\par +\par The Corresponding Source for a work in source code form is that same work. +\par +\par 2. Basic Permissions. +\par All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms y +our unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equival +ent, as provided by copyright law. +\par +\par You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make +modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered w +orks for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. +\par +\par Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. +\par +\par 3. Protecting Users' Legal Rights From Anti-Circumvention Law. +\par No covered work shall be deemed part of an effective technological measure under any app +licable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. +\par +\par When you convey a covered work, you waive any legal power to forbid ci +rcumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforc +ing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. +\par +\par 4. Conveying Verbatim Copies. +\par You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absen +ce of any warranty; and give all recipients a copy of this License along with the Program. +\par +\par You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. +\par +\par 5. Conveying Modified Source Versions. +\par You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: +\par +\par a) The work must carry prominent notices stating that you modified it, and giving a relevant date. +\par b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". +\par c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to +the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. +\par d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. +\par A compilation of a covered work with other separate and +independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and it +s + resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +\par +\par 6. Conveying Non-Source Forms. +\par You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: +\par +\par a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. +\par b) Convey the object code in, or embod +ied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses t +h +e object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physi +cally performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. +\par c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternati +ve is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. +\par d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equival +ent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the +C +orresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of wh +at server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. +\par e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. +\par A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. +\par +\par A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into +a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, +r +egardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industria +l or non-consumer uses, unless such uses represent the only significant mode of use of the product. +\par +\par "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modifi +ed versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely bec +ause modification has been made. +\par +\par If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is + transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not a +pply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). +\par +\par The requirement to provide Installation Information does not include a requirement to continu +e to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself material +ly and adversely affects the operation of the network or violates the rules and protocols for communication across the network. +\par +\par Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special passwor +d or key for unpacking, reading or copying. +\par +\par 7. Additional Terms. +\par "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire +Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the e +ntire Program remains governed by this License without regard to the additional permissions. +\par +\par When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions +may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. +\par +\par Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: +\par +\par a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or +\par b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or +\par c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or +\par d) Limiting the use for publicity purposes of names of licensors or authors of the material; or +\par e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or +\par f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contra +ctual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. +\par All other non-permissive additional terms are considered "further restrictions" within the meaning of sect +ion 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but + permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. +\par +\par If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. +\par +\par Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. +\par +\par 8. Termination. +\par You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise +to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). +\par +\par However, if you cease all violation of this License, then your license from + a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable mean +s prior to 60 days after the cessation. +\par +\par Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of v +iolation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. +\par +\par Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new +licenses for the same material under section 10. +\par +\par 9. Acceptance Not Required for Having Copies. +\par You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a conse +quence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept + this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. +\par +\par 10. Automatic Licensing of Downstream Recipients. +\par Each time you convey a covered work, the recipient automatically receives a licen +se from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. +\par +\par An "entity transaction" is a transaction transferring control of an organ +ization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatev +e +r licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with re +asonable efforts. +\par +\par You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, a +nd you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. +\par +\par 11. Patents. +\par A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". +\par +\par A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infrin +ged only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. +\par +\par Each contributor grants yo +u a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. +\par +\par In the following three p +aragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a p +arty means to make such an agreement or commitment not to enforce a patent against the party. +\par +\par If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge +and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent + +license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, yo +ur conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. +\par +\par If, pursuant to or in connection with a single +transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the cover +ed work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. +\par +\par A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is + conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, und +e +r which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in conne +c +tion with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent +license was granted, prior to 28 March 2007. +\par +\par Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. +\par +\par 12. No Surrender of Others' Freedom. +\par If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covere +d work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conve +ying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. +\par +\par 13. Remote Network Interaction; Use with the GNU General Public License. +\par Notwithstandi +ng any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Cor +r +esponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding So +urce for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. +\par +\par Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work lice +nsed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remai +n governed by version 3 of the GNU General Public License. +\par +\par 14. Revised Versions of this License. +\par The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be simi +lar in spirit to the present version, but may differ in detail to address new problems or concerns. +\par +\par Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "o +r any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Af +fero General Public License, you may choose any version ever published by the Free Software Foundation. +\par +\par If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. +\par +\par Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. +\par +\par 15. Disclaimer of Warranty. +\par THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE + PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS +WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. +\par +\par 16. Limitation of Liability. +\par IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER P +ARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF + DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +\par +\par 17. Interpretation of Sections 15 and 16. +\par If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civ +il liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. +\par +\par }\pard\plain \ql \li0\ri0\widctlpar\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid3877315 \fs24\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\fs16\insrsid3877315\charrsid3877315 END OF TERMS AND CONDITIONS}{\fs16\insrsid3877315 +\par }{\fs16\insrsid7430913 +\par +\par +\par }\pard\plain \s15\ql \li0\ri0\widctlpar\tx916\tx1832\tx2748\tx3664\tx4580\tx5496\tx6412\tx7328\tx8244\tx9160\tx10076\tx10992\tx11908\tx12824\tx13740\tx14656\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid7430913 \cbpat8 +\f2\fs20\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\b\fs16\cf2\insrsid7430913\charrsid3877315 ***************************************}{\b\fs16\cf2\insrsid7430913 +\par }{\fs16\insrsid7430913\charrsid3877315 +\par This li}{\fs16\insrsid7430913 cense applies to all source code +\par in this project EXCEPT}{\fs16\insrsid7430913\charrsid3877315 : +\par \~\~\~\~ +\par \~\~\~\~\~\~\~\~\~\~cppForSwig/cryptopp/* +\par \~\~\~\~\~\~\~\~\~\~qtreactor4.py +\par \~\~\~\~\~\~\~\~\~\~qrcodenative.py +\par \~\~\~\~ +\par Everything in the cryptopp directory }{\fs16\insrsid7430913 +\par }{\fs16\insrsid7430913\charrsid3877315 is considered public domain according +\par t}{\fs16\insrsid7430913 o http://www.cryptopp.com and the two +\par python files are copyrighted by others +\par but freely distributed with original +\par copyright notices intact. +\par }{\fs16\insrsid7430913\charrsid3877315 +\par +\par }\pard\plain \ql \li0\ri0\widctlpar\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid3877315 \fs24\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\fs16\insrsid7430913\charrsid3877315 +\par }} \ No newline at end of file diff --git a/writeNSISCompilerArgs.py b/writeNSISCompilerArgs.py index e38f3c1c7..4104d25f8 100644 --- a/writeNSISCompilerArgs.py +++ b/writeNSISCompilerArgs.py @@ -6,7 +6,7 @@ import sys from string import join sys.argv.append('--nologging') -from armoryengine import BTCARMORY_VERSION +from armoryengine.ArmoryUtils import BTCARMORY_VERSION # need back up 2 directories because this is run from # \cppForSwig\BitcoinArmory_SwigDLL and the output is # expected in the base directory