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:
parent
0190c349aa
commit
0029905d43
25 changed files with 434 additions and 109 deletions
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
2
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
|
||||
|
|
|
@ -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)
|
||||
|
|
12
mining.go
12
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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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--
|
||||
|
|
|
@ -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)
|
||||
|
|
113
txscript/sigcache.go
Normal file
113
txscript/sigcache.go
Normal 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
140
txscript/sigcache_test.go
Normal 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))
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue