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.
This commit is contained in:
Olaoluwa Osuntokun 2015-09-24 16:22:00 -07:00
parent 0190c349aa
commit 0029905d43
25 changed files with 434 additions and 109 deletions

View file

@ -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,

View file

@ -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
}

View file

@ -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

View file

@ -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
}

View file

@ -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)

View file

@ -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
}

View file

@ -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.

View file

@ -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(),
}

View file

@ -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")

View file

@ -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,

View file

@ -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 "+

2
doc.go
View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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 {

View file

@ -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
}

View file

@ -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)

View file

@ -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

View file

@ -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--

View file

@ -190,6 +190,9 @@ func TestScriptInvalidTests(t *testing.T) {
err)
return
}
sigCache := NewSigCache(10)
sigCacheToggle := []bool{true, false}
for _, useSigCache := range sigCacheToggle {
for i, test := range tests {
// Skip comments
if len(test) == 1 {
@ -217,7 +220,14 @@ func TestScriptInvalidTests(t *testing.T) {
continue
}
tx := createSpendingTx(scriptSig, scriptPubKey)
vm, err := NewEngine(scriptPubKey, tx, 0, flags)
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 "+
@ -227,6 +237,7 @@ func TestScriptInvalidTests(t *testing.T) {
}
}
}
}
// TestScriptValidTests ensures all of the tests in script_valid.json pass as
// expected.
@ -244,6 +255,9 @@ func TestScriptValidTests(t *testing.T) {
err)
return
}
sigCache := NewSigCache(10)
sigCacheToggle := []bool{true, false}
for _, useSigCache := range sigCacheToggle {
for i, test := range tests {
// Skip comments
if len(test) == 1 {
@ -271,7 +285,14 @@ func TestScriptValidTests(t *testing.T) {
continue
}
tx := createSpendingTx(scriptSig, scriptPubKey)
vm, err := NewEngine(scriptPubKey, tx, 0, flags)
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
@ -283,6 +304,7 @@ func TestScriptValidTests(t *testing.T) {
}
}
}
}
// TestTxInvalidTests ensures all of the tests in tx_invalid.json fail as
// expected.
@ -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)

113
txscript/sigcache.go Normal file
View file

@ -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{}{}
}

140
txscript/sigcache_test.go Normal file
View file

@ -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))
}
}

View file

@ -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)