From 0029905d43589ea74d11dd574a92bf45e297e949 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 24 Sep 2015 16:22:00 -0700 Subject: [PATCH] Integrate a valid ECDSA signature cache into btcd Introduce an ECDSA signature verification into btcd in order to mitigate a certain DoS attack and as a performance optimization. The benefits of SigCache are two fold. Firstly, usage of SigCache mitigates a DoS attack wherein an attacker causes a victim's client to hang due to worst-case behavior triggered while processing attacker crafted invalid transactions. A detailed description of the mitigated DoS attack can be found here: https://bitslog.wordpress.com/2013/01/23/fixed-bitcoin-vulnerability-explanation-why-the-signature-cache-is-a-dos-protection/ Secondly, usage of the SigCache introduces a signature verification optimization which speeds up the validation of transactions within a block, if they've already been seen and verified within the mempool. The server itself manages the sigCache instance. The blockManager and txMempool respectively now receive pointers to the created sigCache instance. All read (sig triplet existence) operations on the sigCache will not block unless a separate goroutine is adding an entry (writing) to the sigCache. GetBlockTemplate generation now also utilizes the sigCache in order to avoid unnecessarily double checking signatures when generating a template after previously accepting a txn to the mempool. Consequently, the CPU miner now also employs the same optimization. The maximum number of entries for the sigCache has been introduced as a config parameter in order to allow users to configure the amount of memory consumed by this new additional caching. --- blockchain/chain.go | 5 +- blockchain/common_test.go | 2 +- blockchain/example_test.go | 7 +- blockchain/scriptval.go | 14 ++- blockchain/scriptval_test.go | 2 +- blockchain/validate.go | 2 +- blockmanager.go | 3 +- cmd/addblock/import.go | 2 +- cmd/findcheckpoint/findcheckpoint.go | 2 +- config.go | 3 + cpuminer.go | 4 +- doc.go | 2 + mempool.go | 2 +- mining.go | 12 +- rpcserver.go | 4 +- sample-btcd.conf | 7 ++ server.go | 3 + txscript/engine.go | 5 +- txscript/engine_test.go | 6 +- txscript/example_test.go | 2 +- txscript/opcode.go | 33 +++++- txscript/reference_test.go | 164 +++++++++++++++------------ txscript/sigcache.go | 113 ++++++++++++++++++ txscript/sigcache_test.go | 140 +++++++++++++++++++++++ txscript/sign_test.go | 4 +- 25 files changed, 434 insertions(+), 109 deletions(-) create mode 100644 txscript/sigcache.go create mode 100644 txscript/sigcache_test.go diff --git a/blockchain/chain.go b/blockchain/chain.go index dae879d7..b4775a59 100644 --- a/blockchain/chain.go +++ b/blockchain/chain.go @@ -15,6 +15,7 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/database" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" ) @@ -159,6 +160,7 @@ type BlockChain struct { noCheckpoints bool nextCheckpoint *chaincfg.Checkpoint checkpointBlock *btcutil.Block + sigCache *txscript.SigCache } // DisableVerify provides a mechanism to disable transaction script validation @@ -1067,7 +1069,7 @@ func (b *BlockChain) IsCurrent(timeSource MedianTimeSource) bool { // Notification and NotificationType for details on the types and contents of // notifications. The provided callback can be nil if the caller is not // interested in receiving notifications. -func New(db database.Db, params *chaincfg.Params, c NotificationCallback) *BlockChain { +func New(db database.Db, params *chaincfg.Params, c NotificationCallback, sigCache *txscript.SigCache) *BlockChain { // Generate a checkpoint by height map from the provided checkpoints. var checkpointsByHeight map[int32]*chaincfg.Checkpoint if len(params.Checkpoints) > 0 { @@ -1080,6 +1082,7 @@ func New(db database.Db, params *chaincfg.Params, c NotificationCallback) *Block b := BlockChain{ db: db, + sigCache: sigCache, chainParams: params, checkpointsByHeight: checkpointsByHeight, notifications: c, diff --git a/blockchain/common_test.go b/blockchain/common_test.go index 8963f6f2..2d8e8c97 100644 --- a/blockchain/common_test.go +++ b/blockchain/common_test.go @@ -116,7 +116,7 @@ func chainSetup(dbName string) (*blockchain.BlockChain, func(), error) { return nil, nil, err } - chain := blockchain.New(db, &chaincfg.MainNetParams, nil) + chain := blockchain.New(db, &chaincfg.MainNetParams, nil, nil) return chain, teardown, nil } diff --git a/blockchain/example_test.go b/blockchain/example_test.go index 046043dc..01e14a66 100644 --- a/blockchain/example_test.go +++ b/blockchain/example_test.go @@ -42,9 +42,10 @@ func ExampleBlockChain_ProcessBlock() { return } - // Create a new BlockChain instance using the underlying database for - // the main bitcoin network and ignore notifications. - chain := blockchain.New(db, &chaincfg.MainNetParams, nil) + // Create a new BlockChain instance without an initialized signature + // verification cache, using the underlying database for the main + // bitcoin network and ignore notifications. + chain := blockchain.New(db, &chaincfg.MainNetParams, nil, nil) // Create a new median time source that is required by the upcoming // call to ProcessBlock. Ordinarily this would also add time values diff --git a/blockchain/scriptval.go b/blockchain/scriptval.go index ebfea08f..a96c9bb5 100644 --- a/blockchain/scriptval.go +++ b/blockchain/scriptval.go @@ -30,6 +30,7 @@ type txValidator struct { resultChan chan error txStore TxStore flags txscript.ScriptFlags + sigCache *txscript.SigCache } // sendResult sends the result of a script pair validation on the internal @@ -84,7 +85,7 @@ out: sigScript := txIn.SignatureScript pkScript := originMsgTx.TxOut[originTxIndex].PkScript vm, err := txscript.NewEngine(pkScript, txVI.tx.MsgTx(), - txVI.txInIndex, v.flags) + txVI.txInIndex, v.flags, v.sigCache) if err != nil { str := fmt.Sprintf("failed to parse input "+ "%s:%d which references output %s:%d - "+ @@ -179,19 +180,20 @@ func (v *txValidator) Validate(items []*txValidateItem) error { // newTxValidator returns a new instance of txValidator to be used for // validating transaction scripts asynchronously. -func newTxValidator(txStore TxStore, flags txscript.ScriptFlags) *txValidator { +func newTxValidator(txStore TxStore, flags txscript.ScriptFlags, sigCache *txscript.SigCache) *txValidator { return &txValidator{ validateChan: make(chan *txValidateItem), quitChan: make(chan struct{}), resultChan: make(chan error), txStore: txStore, + sigCache: sigCache, flags: flags, } } // ValidateTransactionScripts validates the scripts for the passed transaction // using multiple goroutines. -func ValidateTransactionScripts(tx *btcutil.Tx, txStore TxStore, flags txscript.ScriptFlags) error { +func ValidateTransactionScripts(tx *btcutil.Tx, txStore TxStore, flags txscript.ScriptFlags, sigCache *txscript.SigCache) error { // Collect all of the transaction inputs and required information for // validation. txIns := tx.MsgTx().TxIn @@ -211,7 +213,7 @@ func ValidateTransactionScripts(tx *btcutil.Tx, txStore TxStore, flags txscript. } // Validate all of the inputs. - validator := newTxValidator(txStore, flags) + validator := newTxValidator(txStore, flags, sigCache) if err := validator.Validate(txValItems); err != nil { return err } @@ -222,7 +224,7 @@ func ValidateTransactionScripts(tx *btcutil.Tx, txStore TxStore, flags txscript. // checkBlockScripts executes and validates the scripts for all transactions in // the passed block. func checkBlockScripts(block *btcutil.Block, txStore TxStore, - scriptFlags txscript.ScriptFlags) error { + scriptFlags txscript.ScriptFlags, sigCache *txscript.SigCache) error { // Collect all of the transaction inputs and required information for // validation for all transactions in the block into a single slice. @@ -248,7 +250,7 @@ func checkBlockScripts(block *btcutil.Block, txStore TxStore, } // Validate all of the inputs. - validator := newTxValidator(txStore, scriptFlags) + validator := newTxValidator(txStore, scriptFlags, sigCache) if err := validator.Validate(txValItems); err != nil { return err } diff --git a/blockchain/scriptval_test.go b/blockchain/scriptval_test.go index 0c0e26a4..de9af1a0 100644 --- a/blockchain/scriptval_test.go +++ b/blockchain/scriptval_test.go @@ -37,7 +37,7 @@ func TestCheckBlockScripts(t *testing.T) { } scriptFlags := txscript.ScriptBip16 - err = blockchain.TstCheckBlockScripts(blocks[0], txStore, scriptFlags) + err = blockchain.TstCheckBlockScripts(blocks[0], txStore, scriptFlags, nil) if err != nil { t.Errorf("Transaction script validation failed: %v\n", err) diff --git a/blockchain/validate.go b/blockchain/validate.go index fc00be45..4b79a607 100644 --- a/blockchain/validate.go +++ b/blockchain/validate.go @@ -1138,7 +1138,7 @@ func (b *BlockChain) checkConnectBlock(node *blockNode, block *btcutil.Block) er // expensive ECDSA signature check scripts. Doing this last helps // prevent CPU exhaustion attacks. if runScripts { - err := checkBlockScripts(block, txInputStore, scriptFlags) + err := checkBlockScripts(block, txInputStore, scriptFlags, b.sigCache) if err != nil { return err } diff --git a/blockmanager.go b/blockmanager.go index e3e3b4a1..6e544dff 100644 --- a/blockmanager.go +++ b/blockmanager.go @@ -1446,7 +1446,8 @@ func newBlockManager(s *server) (*blockManager, error) { quit: make(chan struct{}), } bm.progressLogger = newBlockProgressLogger("Processed", bmgrLog) - bm.blockChain = blockchain.New(s.db, s.chainParams, bm.handleNotifyMsg) + bm.blockChain = blockchain.New(s.db, s.chainParams, bm.handleNotifyMsg, + s.sigCache) bm.blockChain.DisableCheckpoints(cfg.DisableCheckpoints) if !cfg.DisableCheckpoints { // Initialize the next checkpoint based on the current height. diff --git a/cmd/addblock/import.go b/cmd/addblock/import.go index f47178ec..4964b613 100644 --- a/cmd/addblock/import.go +++ b/cmd/addblock/import.go @@ -303,7 +303,7 @@ func newBlockImporter(db database.Db, r io.ReadSeeker) *blockImporter { doneChan: make(chan bool), errChan: make(chan error), quit: make(chan struct{}), - chain: blockchain.New(db, activeNetParams, nil), + chain: blockchain.New(db, activeNetParams, nil, nil), medianTime: blockchain.NewMedianTime(), lastLogTime: time.Now(), } diff --git a/cmd/findcheckpoint/findcheckpoint.go b/cmd/findcheckpoint/findcheckpoint.go index a2d446b8..00ee6f04 100644 --- a/cmd/findcheckpoint/findcheckpoint.go +++ b/cmd/findcheckpoint/findcheckpoint.go @@ -53,7 +53,7 @@ func findCandidates(db database.Db, latestHash *wire.ShaHash) ([]*chaincfg.Check // Setup chain and get the latest checkpoint. Ignore notifications // since they aren't needed for this util. - chain := blockchain.New(db, activeNetParams, nil) + chain := blockchain.New(db, activeNetParams, nil, nil) latestCheckpoint := chain.LatestCheckpoint() if latestCheckpoint == nil { return nil, fmt.Errorf("unable to retrieve latest checkpoint") diff --git a/config.go b/config.go index 5bff8899..53d56693 100644 --- a/config.go +++ b/config.go @@ -45,6 +45,7 @@ const ( defaultBlockPrioritySize = 50000 defaultGenerate = false defaultAddrIndex = false + defaultSigCacheMaxSize = 50000 ) var ( @@ -117,6 +118,7 @@ type config struct { AddrIndex bool `long:"addrindex" description:"Build and maintain a full address index. Currently only supported by leveldb."` DropAddrIndex bool `long:"dropaddrindex" description:"Deletes the address-based transaction index from the database on start up, and the exits."` NoPeerBloomFilters bool `long:"nopeerbloomfilters" description:"Disable bloom filtering support."` + SigCacheMaxSize uint `long:"sigcachemaxsize" description:"The maximum number of entries in the signature verification cache."` onionlookup func(string) ([]net.IP, error) lookup func(string) ([]net.IP, error) oniondial func(string, string) (net.Conn, error) @@ -322,6 +324,7 @@ func loadConfig() (*config, []string, error) { BlockMinSize: defaultBlockMinSize, BlockMaxSize: defaultBlockMaxSize, BlockPrioritySize: defaultBlockPrioritySize, + SigCacheMaxSize: defaultSigCacheMaxSize, MaxOrphanTxs: maxOrphanTransactions, Generate: defaultGenerate, AddrIndex: defaultAddrIndex, diff --git a/cpuminer.go b/cpuminer.go index f8e54afb..e6fc0839 100644 --- a/cpuminer.go +++ b/cpuminer.go @@ -302,7 +302,7 @@ out: // Create a new block template using the available transactions // in the memory pool as a source of transactions to potentially // include in the block. - template, err := NewBlockTemplate(m.server.txMemPool, payToAddr) + template, err := NewBlockTemplate(m.server, payToAddr) m.submitBlockLock.Unlock() if err != nil { errStr := fmt.Sprintf("Failed to create new block "+ @@ -564,7 +564,7 @@ func (m *CPUMiner) GenerateNBlocks(n uint32) ([]*wire.ShaHash, error) { // Create a new block template using the available transactions // in the memory pool as a source of transactions to potentially // include in the block. - template, err := NewBlockTemplate(m.server.txMemPool, payToAddr) + template, err := NewBlockTemplate(m.server, payToAddr) m.submitBlockLock.Unlock() if err != nil { errStr := fmt.Sprintf("Failed to create new block "+ diff --git a/doc.go b/doc.go index c3386235..90038461 100644 --- a/doc.go +++ b/doc.go @@ -105,6 +105,8 @@ Application Options: --dropaddrindex Deletes the address-based transaction index from the database on start up, and the exits. --nopeerbloomfilters Disable bloom filtering support. + --sigcachemaxsize= The maximum number of entries in the signature + verification cache. Help Options: -h, --help Show this help message diff --git a/mempool.go b/mempool.go index 9ba9fb46..bd43c72a 100644 --- a/mempool.go +++ b/mempool.go @@ -1223,7 +1223,7 @@ func (mp *txMemPool) maybeAcceptTransaction(tx *btcutil.Tx, isNew, rateLimit boo // Verify crypto signatures for each input and reject the transaction if // any don't verify. err = blockchain.ValidateTransactionScripts(tx, txStore, - txscript.StandardVerifyFlags) + txscript.StandardVerifyFlags, mp.server.sigCache) if err != nil { if cerr, ok := err.(blockchain.RuleError); ok { return nil, chainRuleError(cerr) diff --git a/mining.go b/mining.go index 40f6d85b..384da0a6 100644 --- a/mining.go +++ b/mining.go @@ -366,9 +366,9 @@ func medianAdjustedTime(chainState *chainState, timeSource blockchain.MedianTime // | transactions (while block size | | // | <= cfg.BlockMinSize) | | // ----------------------------------- -- -func NewBlockTemplate(mempool *txMemPool, payToAddress btcutil.Address) (*BlockTemplate, error) { - blockManager := mempool.server.blockManager - timeSource := mempool.server.timeSource +func NewBlockTemplate(server *server, payToAddress btcutil.Address) (*BlockTemplate, error) { + blockManager := server.blockManager + timeSource := server.timeSource chainState := &blockManager.chainState // Extend the most recently known best block. @@ -404,7 +404,7 @@ func NewBlockTemplate(mempool *txMemPool, payToAddress btcutil.Address) (*BlockT // Also, choose the initial sort order for the priority queue based on // whether or not there is an area allocated for high-priority // transactions. - mempoolTxns := mempool.TxDescs() + mempoolTxns := server.txMemPool.TxDescs() sortedByFee := cfg.BlockPrioritySize == 0 priorityQueue := newTxPriorityQueue(len(mempoolTxns), sortedByFee) @@ -474,7 +474,7 @@ mempoolLoop: originIndex := txIn.PreviousOutPoint.Index txData, exists := txStore[*originHash] if !exists || txData.Err != nil || txData.Tx == nil { - if !mempool.HaveTransaction(originHash) { + if !server.txMemPool.HaveTransaction(originHash) { minrLog.Tracef("Skipping tx %s because "+ "it references tx %s which is "+ "not available", tx.Sha, @@ -656,7 +656,7 @@ mempoolLoop: continue } err = blockchain.ValidateTransactionScripts(tx, blockTxStore, - txscript.StandardVerifyFlags) + txscript.StandardVerifyFlags, server.sigCache) if err != nil { minrLog.Tracef("Skipping tx %s due to error in "+ "ValidateTransactionScripts: %v", tx.Sha(), err) diff --git a/rpcserver.go b/rpcserver.go index d0be4fb5..99928219 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -1449,7 +1449,7 @@ func (state *gbtWorkState) updateBlockTemplate(s *rpcServer, useCoinbaseValue bo // block template doesn't include the coinbase, so the caller // will ultimately create their own coinbase which pays to the // appropriate address(es). - blkTemplate, err := NewBlockTemplate(s.server.txMemPool, payAddr) + blkTemplate, err := NewBlockTemplate(s.server, payAddr) if err != nil { return internalRPCError("Failed to create new block "+ "template: "+err.Error(), "") @@ -2609,7 +2609,7 @@ func handleGetWorkRequest(s *rpcServer) (interface{}, error) { // Choose a payment address at random. payToAddr := cfg.miningAddrs[rand.Intn(len(cfg.miningAddrs))] - template, err := NewBlockTemplate(s.server.txMemPool, payToAddr) + template, err := NewBlockTemplate(s.server, payToAddr) if err != nil { context := "Failed to create new block template" return nil, internalRPCError(err.Error(), context) diff --git a/sample-btcd.conf b/sample-btcd.conf index f09e7d05..1febab98 100644 --- a/sample-btcd.conf +++ b/sample-btcd.conf @@ -233,6 +233,13 @@ ; Delete the entire address index on start up, then exit. ; dropaddrindex=0 +; ------------------------------------------------------------------------------ +; Signature Verification Cache +; ------------------------------------------------------------------------------ + +; Limit the signature cache to a max of 50000 entries. +; sigcachemaxsize=50000 + ; ------------------------------------------------------------------------------ ; Coin Generation (Mining) Settings - The following options control the ; generation of block templates used by external mining applications through RPC diff --git a/server.go b/server.go index a842c5dd..9b1a6e7d 100644 --- a/server.go +++ b/server.go @@ -23,6 +23,7 @@ import ( "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/database" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" ) @@ -90,6 +91,7 @@ type server struct { bytesReceived uint64 // Total bytes received from all peers since start. bytesSent uint64 // Total bytes sent by all peers since start. addrManager *addrmgr.AddrManager + sigCache *txscript.SigCache rpcServer *rpcServer blockManager *blockManager addrIndexer *addrIndexer @@ -1401,6 +1403,7 @@ func newServer(listenAddrs []string, db database.Db, chainParams *chaincfg.Param db: db, timeSource: blockchain.NewMedianTime(), services: services, + sigCache: txscript.NewSigCache(cfg.SigCacheMaxSize), } bm, err := newBlockManager(&s) if err != nil { diff --git a/txscript/engine.go b/txscript/engine.go index 93855ea2..a2def673 100644 --- a/txscript/engine.go +++ b/txscript/engine.go @@ -86,6 +86,7 @@ type Engine struct { condStack []int numOps int flags ScriptFlags + sigCache *SigCache bip16 bool // treat execution as pay-to-script-hash savedFirstStack [][]byte // stack from first script for bip16 scripts } @@ -573,7 +574,7 @@ func (vm *Engine) SetAltStack(data [][]byte) { // NewEngine returns a new script engine for the provided public key script, // transaction, and input index. The flags modify the behavior of the script // engine according to the description provided by each flag. -func NewEngine(scriptPubKey []byte, tx *wire.MsgTx, txIdx int, flags ScriptFlags) (*Engine, error) { +func NewEngine(scriptPubKey []byte, tx *wire.MsgTx, txIdx int, flags ScriptFlags, sigCache *SigCache) (*Engine, error) { // The provided transaction input index must refer to a valid input. if txIdx < 0 || txIdx >= len(tx.TxIn) { return nil, ErrInvalidIndex @@ -588,7 +589,7 @@ func NewEngine(scriptPubKey []byte, tx *wire.MsgTx, txIdx int, flags ScriptFlags // allowing the clean stack flag without the P2SH flag would make it // possible to have a situation where P2SH would not be a soft fork when // it should be. - vm := Engine{flags: flags} + vm := Engine{flags: flags, sigCache: sigCache} if vm.hasFlag(ScriptVerifyCleanStack) && !vm.hasFlag(ScriptBip16) { return nil, ErrInvalidFlags } diff --git a/txscript/engine_test.go b/txscript/engine_test.go index 5d47fc6e..bcc5bee4 100644 --- a/txscript/engine_test.go +++ b/txscript/engine_test.go @@ -62,7 +62,7 @@ func TestBadPC(t *testing.T) { pkScript := []byte{txscript.OP_NOP} for _, test := range pcTests { - vm, err := txscript.NewEngine(pkScript, tx, 0, 0) + vm, err := txscript.NewEngine(pkScript, tx, 0, 0, nil) if err != nil { t.Errorf("Failed to create script: %v", err) } @@ -133,7 +133,7 @@ func TestCheckErrorCondition(t *testing.T) { txscript.OP_TRUE, } - vm, err := txscript.NewEngine(pkScript, tx, 0, 0) + vm, err := txscript.NewEngine(pkScript, tx, 0, 0, nil) if err != nil { t.Errorf("failed to create script: %v", err) } @@ -214,7 +214,7 @@ func TestInvalidFlagCombinations(t *testing.T) { pkScript := []byte{txscript.OP_NOP} for i, test := range tests { - _, err := txscript.NewEngine(pkScript, tx, 0, test) + _, err := txscript.NewEngine(pkScript, tx, 0, test, nil) if err != txscript.ErrInvalidFlags { t.Fatalf("TestInvalidFlagCombinations #%d unexpected "+ "error: %v", i, err) diff --git a/txscript/example_test.go b/txscript/example_test.go index bb3f6ac9..5ef2eea6 100644 --- a/txscript/example_test.go +++ b/txscript/example_test.go @@ -165,7 +165,7 @@ func ExampleSignTxOutput() { txscript.ScriptStrictMultiSig | txscript.ScriptDiscourageUpgradableNops vm, err := txscript.NewEngine(originTx.TxOut[0].PkScript, redeemTx, 0, - flags) + flags, nil) if err != nil { fmt.Println(err) return diff --git a/txscript/opcode.go b/txscript/opcode.go index 2c31098f..631fab30 100644 --- a/txscript/opcode.go +++ b/txscript/opcode.go @@ -1850,8 +1850,21 @@ func opcodeCheckSig(op *parsedOpcode, vm *Engine) error { return nil } - ok := signature.Verify(hash, pubKey) - vm.dstack.PushBool(ok) + var valid bool + if vm.sigCache != nil { + var sigHash wire.ShaHash + copy(sigHash[:], hash) + + valid = vm.sigCache.Exists(sigHash, signature, pubKey) + if !valid && signature.Verify(hash, pubKey) { + vm.sigCache.Add(sigHash, signature, pubKey) + valid = true + } + } else { + valid = signature.Verify(hash, pubKey) + } + + vm.dstack.PushBool(valid) return nil } @@ -2051,7 +2064,21 @@ func opcodeCheckMultiSig(op *parsedOpcode, vm *Engine) error { // Generate the signature hash based on the signature hash type. hash := calcSignatureHash(script, hashType, &vm.tx, vm.txIdx) - if parsedSig.Verify(hash, parsedPubKey) { + var valid bool + if vm.sigCache != nil { + var sigHash wire.ShaHash + copy(sigHash[:], hash) + + valid = vm.sigCache.Exists(sigHash, parsedSig, parsedPubKey) + if !valid && parsedSig.Verify(hash, parsedPubKey) { + vm.sigCache.Add(sigHash, parsedSig, parsedPubKey) + valid = true + } + } else { + valid = parsedSig.Verify(hash, parsedPubKey) + } + + if valid { // PubKey verified, move on to the next signature. signatureIdx++ numSignatures-- diff --git a/txscript/reference_test.go b/txscript/reference_test.go index 50d5531a..4ef94e83 100644 --- a/txscript/reference_test.go +++ b/txscript/reference_test.go @@ -190,40 +190,51 @@ func TestScriptInvalidTests(t *testing.T) { err) return } - for i, test := range tests { - // Skip comments - if len(test) == 1 { - continue - } - name, err := testName(test) - if err != nil { - t.Errorf("TestBitcoindInvalidTests: invalid test #%d", - i) - continue - } - scriptSig, err := parseShortForm(test[0]) - if err != nil { - t.Errorf("%s: can't parse scriptSig; %v", name, err) - continue - } - scriptPubKey, err := parseShortForm(test[1]) - if err != nil { - t.Errorf("%s: can't parse scriptPubkey; %v", name, err) - continue - } - flags, err := parseScriptFlags(test[2]) - if err != nil { - t.Errorf("%s: %v", name, err) - continue - } - tx := createSpendingTx(scriptSig, scriptPubKey) - vm, err := NewEngine(scriptPubKey, tx, 0, flags) - if err == nil { - if err := vm.Execute(); err == nil { - t.Errorf("%s test succeeded when it "+ - "should have failed\n", name) + sigCache := NewSigCache(10) + sigCacheToggle := []bool{true, false} + for _, useSigCache := range sigCacheToggle { + for i, test := range tests { + // Skip comments + if len(test) == 1 { + continue + } + name, err := testName(test) + if err != nil { + t.Errorf("TestBitcoindInvalidTests: invalid test #%d", + i) + continue + } + scriptSig, err := parseShortForm(test[0]) + if err != nil { + t.Errorf("%s: can't parse scriptSig; %v", name, err) + continue + } + scriptPubKey, err := parseShortForm(test[1]) + if err != nil { + t.Errorf("%s: can't parse scriptPubkey; %v", name, err) + continue + } + flags, err := parseScriptFlags(test[2]) + if err != nil { + t.Errorf("%s: %v", name, err) + continue + } + tx := createSpendingTx(scriptSig, scriptPubKey) + + var vm *Engine + if useSigCache { + vm, err = NewEngine(scriptPubKey, tx, 0, flags, sigCache) + } else { + vm, err = NewEngine(scriptPubKey, tx, 0, flags, nil) + } + + if err == nil { + if err := vm.Execute(); err == nil { + t.Errorf("%s test succeeded when it "+ + "should have failed\n", name) + } + continue } - continue } } } @@ -244,42 +255,53 @@ func TestScriptValidTests(t *testing.T) { err) return } - for i, test := range tests { - // Skip comments - if len(test) == 1 { - continue - } - name, err := testName(test) - if err != nil { - t.Errorf("TestBitcoindValidTests: invalid test #%d", - i) - continue - } - scriptSig, err := parseShortForm(test[0]) - if err != nil { - t.Errorf("%s: can't parse scriptSig; %v", name, err) - continue - } - scriptPubKey, err := parseShortForm(test[1]) - if err != nil { - t.Errorf("%s: can't parse scriptPubkey; %v", name, err) - continue - } - flags, err := parseScriptFlags(test[2]) - if err != nil { - t.Errorf("%s: %v", name, err) - continue - } - tx := createSpendingTx(scriptSig, scriptPubKey) - vm, err := NewEngine(scriptPubKey, tx, 0, flags) - if err != nil { - t.Errorf("%s failed to create script: %v", name, err) - continue - } - err = vm.Execute() - if err != nil { - t.Errorf("%s failed to execute: %v", name, err) - continue + sigCache := NewSigCache(10) + sigCacheToggle := []bool{true, false} + for _, useSigCache := range sigCacheToggle { + for i, test := range tests { + // Skip comments + if len(test) == 1 { + continue + } + name, err := testName(test) + if err != nil { + t.Errorf("TestBitcoindValidTests: invalid test #%d", + i) + continue + } + scriptSig, err := parseShortForm(test[0]) + if err != nil { + t.Errorf("%s: can't parse scriptSig; %v", name, err) + continue + } + scriptPubKey, err := parseShortForm(test[1]) + if err != nil { + t.Errorf("%s: can't parse scriptPubkey; %v", name, err) + continue + } + flags, err := parseScriptFlags(test[2]) + if err != nil { + t.Errorf("%s: %v", name, err) + continue + } + tx := createSpendingTx(scriptSig, scriptPubKey) + + var vm *Engine + if useSigCache { + vm, err = NewEngine(scriptPubKey, tx, 0, flags, sigCache) + } else { + vm, err = NewEngine(scriptPubKey, tx, 0, flags, nil) + } + + if err != nil { + t.Errorf("%s failed to create script: %v", name, err) + continue + } + err = vm.Execute() + if err != nil { + t.Errorf("%s failed to execute: %v", name, err) + continue + } } } } @@ -414,7 +436,7 @@ testloop: // These are meant to fail, so as soon as the first // input fails the transaction has failed. (some of the // test txns have good inputs, too.. - vm, err := NewEngine(pkScript, tx.MsgTx(), k, flags) + vm, err := NewEngine(pkScript, tx.MsgTx(), k, flags, nil) if err != nil { continue testloop } @@ -555,7 +577,7 @@ testloop: k, i, test) continue testloop } - vm, err := NewEngine(pkScript, tx.MsgTx(), k, flags) + vm, err := NewEngine(pkScript, tx.MsgTx(), k, flags, nil) if err != nil { t.Errorf("test (%d:%v:%d) failed to create "+ "script: %v", i, test, k, err) diff --git a/txscript/sigcache.go b/txscript/sigcache.go new file mode 100644 index 00000000..16484fc6 --- /dev/null +++ b/txscript/sigcache.go @@ -0,0 +1,113 @@ +// Copyright (c) 2015 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package txscript + +import ( + "bytes" + "crypto/rand" + "sync" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/wire" +) + +// sigInfo represents an entry in the SigCache. Entries in the sigcache are a +// 3-tuple: (sigHash, sig, pubKey). +type sigInfo struct { + sigHash wire.ShaHash + sig string + pubKey string +} + +// SigCache implements an ECDSA signature verification cache with a randomized +// entry eviction policy. Only valid signatures will be added to the cache. The +// benefits of SigCache are two fold. Firstly, usage of SigCache mitigates a DoS +// attack wherein an attack causes a victim's client to hang due to worst-case +// behavior triggered while processing attacker crafted invalid transactions. A +// detailed description of the mitigated DoS attack can be found here: +// https://bitslog.wordpress.com/2013/01/23/fixed-bitcoin-vulnerability-explanation-why-the-signature-cache-is-a-dos-protection/. +// Secondly, usage of the SigCache introduces a signature verification +// optimization which speeds up the validation of transactions within a block, +// if they've already been seen and verified within the mempool. +type SigCache struct { + sync.RWMutex + validSigs map[sigInfo]struct{} + maxEntries uint +} + +// NewSigCache creates and initializes a new instance of SigCache. Its sole +// parameter 'maxEntries' represents the maximum number of entries allowed to +// exist in the SigCache and any particular moment. Random entries are evicted +// to make room for new entries that would cause the number of entries in the +// cache to exceed the max. +func NewSigCache(maxEntries uint) *SigCache { + return &SigCache{validSigs: make(map[sigInfo]struct{}), maxEntries: maxEntries} +} + +// Exists returns true if an existing entry of 'sig' over 'sigHash' for public +// key 'pubKey' is found within the SigCache. Otherwise, false is returned. +// +// NOTE: This function is safe for concurrent access. Readers won't be blocked +// unless there exists a writer, adding an entry to the SigCache. +func (s *SigCache) Exists(sigHash wire.ShaHash, sig *btcec.Signature, pubKey *btcec.PublicKey) bool { + info := sigInfo{sigHash, string(sig.Serialize()), + string(pubKey.SerializeCompressed())} + + s.RLock() + _, ok := s.validSigs[info] + s.RUnlock() + return ok +} + +// Add adds an entry for a signature over 'sigHash' under public key 'pubKey' +// to the signature cache. In the event that the SigCache is 'full', an +// existing entry it randomly chosen to be evicted in order to make space for +// the new entry. +// +// NOTE: This function is safe for concurrent access. Writers will block +// simultaneous readers until function execution has concluded. +func (s *SigCache) Add(sigHash wire.ShaHash, sig *btcec.Signature, pubKey *btcec.PublicKey) { + s.Lock() + defer s.Unlock() + + if s.maxEntries <= 0 { + return + } + + // If adding this new entry will put us over the max number of allowed + // entries, then evict an entry. + if uint(len(s.validSigs)+1) > s.maxEntries { + // Generate a cryptographically random hash. + randHashBytes := make([]byte, wire.HashSize) + _, err := rand.Read(randHashBytes) + if err != nil { + // Failure to read a random hash results in the proposed + // entry not being added to the cache since we are + // unable to evict any existing entries. + return + } + + // Try to find the first entry that is greater than the random + // hash. Use the first entry (which is already pseudo random due + // to Go's range statement over maps) as a fall back if none of + // the hashes in the rejected transactions pool are larger than + // the random hash. + var foundEntry sigInfo + for sigEntry := range s.validSigs { + if foundEntry.sig == "" { + foundEntry = sigEntry + } + if bytes.Compare(sigEntry.sigHash.Bytes(), randHashBytes) > 0 { + foundEntry = sigEntry + break + } + } + delete(s.validSigs, foundEntry) + } + + info := sigInfo{sigHash, string(sig.Serialize()), + string(pubKey.SerializeCompressed())} + s.validSigs[info] = struct{}{} +} diff --git a/txscript/sigcache_test.go b/txscript/sigcache_test.go new file mode 100644 index 00000000..f1f7d3d3 --- /dev/null +++ b/txscript/sigcache_test.go @@ -0,0 +1,140 @@ +// Copyright (c) 2015 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package txscript + +import ( + "crypto/rand" + "testing" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/wire" +) + +// genRandomSig returns a random message, public key, and a signature of the +// message under the public key. This function is used to generate randomized +// test data. +func genRandomSig() (*wire.ShaHash, *btcec.Signature, *btcec.PublicKey, error) { + privKey, err := btcec.NewPrivateKey(btcec.S256()) + if err != nil { + return nil, nil, nil, err + } + + var msgHash wire.ShaHash + if _, err := rand.Read(msgHash[:]); err != nil { + return nil, nil, nil, err + } + + sig, err := privKey.Sign(msgHash[:]) + if err != nil { + return nil, nil, nil, err + } + + return &msgHash, sig, privKey.PubKey(), nil +} + +// TestSigCacheAddExists tests the ability to add, and later check the +// existence of a signature triplet in the signature cache. +func TestSigCacheAddExists(t *testing.T) { + sigCache := NewSigCache(200) + + // Generate a random sigCache entry triplet. + msg1, sig1, key1, err := genRandomSig() + if err != nil { + t.Errorf("unable to generate random signature test data") + } + + // Add the triplet to the signature cache. + sigCache.Add(*msg1, sig1, key1) + + // The previously added triplet should now be found within the sigcache. + sig1Copy, _ := btcec.ParseSignature(sig1.Serialize(), btcec.S256()) + key1Copy, _ := btcec.ParsePubKey(key1.SerializeCompressed(), btcec.S256()) + if !sigCache.Exists(*msg1, sig1Copy, key1Copy) { + t.Errorf("previously added item not found in signature cache") + } +} + +// TestSigCacheAddEvictEntry tests the eviction case where a new signature +// triplet is added to a full signature cache which should trigger randomized +// eviction, followed by adding the new element to the cache. +func TestSigCacheAddEvictEntry(t *testing.T) { + // Create a sigcache that can hold up to 100 entries. + sigCacheSize := uint(100) + sigCache := NewSigCache(sigCacheSize) + + // Fill the sigcache up with some random sig triplets. + for i := uint(0); i < sigCacheSize; i++ { + msg, sig, key, err := genRandomSig() + if err != nil { + t.Fatalf("unable to generate random signature test data") + } + + sigCache.Add(*msg, sig, key) + + sigCopy, _ := btcec.ParseSignature(sig.Serialize(), btcec.S256()) + keyCopy, _ := btcec.ParsePubKey(key.SerializeCompressed(), btcec.S256()) + if !sigCache.Exists(*msg, sigCopy, keyCopy) { + t.Errorf("previously added item not found in signature" + + "cache") + } + } + + // The sigcache should now have sigCacheSize entries within it. + if uint(len(sigCache.validSigs)) != sigCacheSize { + t.Fatalf("sigcache should now have %v entries, instead it has %v", + sigCacheSize, len(sigCache.validSigs)) + } + + // Add a new entry, this should cause eviction of a randomly chosen + // previously entry. + msgNew, sigNew, keyNew, err := genRandomSig() + if err != nil { + t.Fatalf("unable to generate random signature test data") + } + sigCache.Add(*msgNew, sigNew, keyNew) + + // The sigcache should still have sigCache entries. + if uint(len(sigCache.validSigs)) != sigCacheSize { + t.Fatalf("sigcache should now have %v entries, instead it has %v", + sigCacheSize, len(sigCache.validSigs)) + } + + // The entry added above should be found within the sigcache. + sigNewCopy, _ := btcec.ParseSignature(sigNew.Serialize(), btcec.S256()) + keyNewCopy, _ := btcec.ParsePubKey(keyNew.SerializeCompressed(), btcec.S256()) + if !sigCache.Exists(*msgNew, sigNewCopy, keyNewCopy) { + t.Fatalf("previously added item not found in signature cache") + } +} + +// TestSigCacheAddMaxEntriesZeroOrNegative tests that if a sigCache is created +// with a max size <= 0, then no entries are added to the sigcache at all. +func TestSigCacheAddMaxEntriesZeroOrNegative(t *testing.T) { + // Create a sigcache that can hold up to 0 entries. + sigCache := NewSigCache(0) + + // Generate a random sigCache entry triplet. + msg1, sig1, key1, err := genRandomSig() + if err != nil { + t.Errorf("unable to generate random signature test data") + } + + // Add the triplet to the signature cache. + sigCache.Add(*msg1, sig1, key1) + + // The generated triplet should not be found. + sig1Copy, _ := btcec.ParseSignature(sig1.Serialize(), btcec.S256()) + key1Copy, _ := btcec.ParsePubKey(key1.SerializeCompressed(), btcec.S256()) + if sigCache.Exists(*msg1, sig1Copy, key1Copy) { + t.Errorf("previously added signature found in sigcache, but" + + "shouldn't have been") + } + + // There shouldn't be any entries in the sigCache. + if len(sigCache.validSigs) != 0 { + t.Errorf("%v items found in sigcache, no items should have"+ + "been added", len(sigCache.validSigs)) + } +} diff --git a/txscript/sign_test.go b/txscript/sign_test.go index 1da4cba5..43063521 100644 --- a/txscript/sign_test.go +++ b/txscript/sign_test.go @@ -58,7 +58,7 @@ func mkGetScript(scripts map[string][]byte) txscript.ScriptDB { func checkScripts(msg string, tx *wire.MsgTx, idx int, sigScript, pkScript []byte) error { tx.TxIn[idx].SignatureScript = sigScript vm, err := txscript.NewEngine(pkScript, tx, idx, - txscript.ScriptBip16|txscript.ScriptVerifyDERSignatures) + txscript.ScriptBip16|txscript.ScriptVerifyDERSignatures, nil) if err != nil { return fmt.Errorf("failed to make script engine for %s: %v", msg, err) @@ -1692,7 +1692,7 @@ nexttest: scriptFlags := txscript.ScriptBip16 | txscript.ScriptVerifyDERSignatures for j := range tx.TxIn { vm, err := txscript.NewEngine(sigScriptTests[i]. - inputs[j].txout.PkScript, tx, j, scriptFlags) + inputs[j].txout.PkScript, tx, j, scriptFlags, nil) if err != nil { t.Errorf("cannot create script vm for test %v: %v", sigScriptTests[i].name, err)