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)