204 lines
7.9 KiB
Go
204 lines
7.9 KiB
Go
// Copyright (c) 2015-2016 The btcsuite developers
|
|
// Copyright (c) 2016-2020 The Decred 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"
|
|
"encoding/binary"
|
|
"sync"
|
|
|
|
"github.com/dchest/siphash"
|
|
"github.com/decred/dcrd/chaincfg/chainhash"
|
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
|
"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
|
|
"github.com/decred/dcrd/wire"
|
|
)
|
|
|
|
// ProactiveEvictionDepth is the depth of the block at which the signatures for
|
|
// the transactions within the block are nearly guaranteed to no longer be
|
|
// useful.
|
|
const ProactiveEvictionDepth = 2
|
|
|
|
// shortTxHashKeySize is the size of the byte array required for key material
|
|
// for the SipHash keyed shortTxHash function.
|
|
const shortTxHashKeySize = 16
|
|
|
|
// sigCacheEntry represents an entry in the SigCache. Entries within the
|
|
// SigCache are keyed according to the sigHash of the signature. In the
|
|
// scenario of a cache-hit (according to the sigHash), an additional comparison
|
|
// of the signature and public key will be executed in order to ensure a complete
|
|
// match. In the occasion that two sigHashes collide, the newer sigHash will
|
|
// simply overwrite the existing entry.
|
|
type sigCacheEntry struct {
|
|
sig *ecdsa.Signature
|
|
pubKey *secp256k1.PublicKey
|
|
shortTxHash uint64
|
|
}
|
|
|
|
// 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[chainhash.Hash]sigCacheEntry
|
|
maxEntries uint
|
|
shortTxHashKey [shortTxHashKeySize]byte
|
|
}
|
|
|
|
// 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 at 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, error) {
|
|
// Create a cryptographically secure random key for generating short tx hashes.
|
|
shortTxHashKey, err := createShortTxHashKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &SigCache{
|
|
validSigs: make(map[chainhash.Hash]sigCacheEntry, maxEntries),
|
|
maxEntries: maxEntries,
|
|
shortTxHashKey: shortTxHashKey,
|
|
}, nil
|
|
}
|
|
|
|
// 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 chainhash.Hash, sig *ecdsa.Signature, pubKey *secp256k1.PublicKey) bool {
|
|
s.RLock()
|
|
entry, ok := s.validSigs[sigHash]
|
|
s.RUnlock()
|
|
|
|
return ok && entry.pubKey.IsEqual(pubKey) && entry.sig.IsEqual(sig)
|
|
}
|
|
|
|
// 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 is 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 chainhash.Hash, sig *ecdsa.Signature, pubKey *secp256k1.PublicKey, tx *wire.MsgTx) {
|
|
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 {
|
|
// Remove a random entry from the map. Relying on the random
|
|
// starting point of Go's map iteration. It's worth noting that
|
|
// the random iteration starting point is not 100% guaranteed
|
|
// by the spec, however most Go compilers support it.
|
|
// Ultimately, the iteration order isn't important here because
|
|
// in order to manipulate which items are evicted, an adversary
|
|
// would need to be able to execute preimage attacks on the
|
|
// hashing function in order to start eviction at a specific
|
|
// entry.
|
|
for sigEntry := range s.validSigs {
|
|
delete(s.validSigs, sigEntry)
|
|
break
|
|
}
|
|
}
|
|
s.validSigs[sigHash] = sigCacheEntry{sig, pubKey, shortTxHash(tx, s.shortTxHashKey)}
|
|
}
|
|
|
|
// createShortTxHashKey returns a cryptographically secure random key of size
|
|
// shortTxHashKeySize that can be used for generating short transaction hashes
|
|
// with the shortTxHash function.
|
|
func createShortTxHashKey() ([shortTxHashKeySize]byte, error) {
|
|
var key [shortTxHashKeySize]byte
|
|
_, err := rand.Read(key[:])
|
|
if err != nil {
|
|
return key, err
|
|
}
|
|
|
|
return key, nil
|
|
}
|
|
|
|
// shortTxHash generates a short hash from the standard transaction hash. The
|
|
// hash function used is SipHash-2-4, a keyed function, and it produces a 64-bit
|
|
// hash. The key that is used must be a cryptographically secure random key.
|
|
func shortTxHash(msg *wire.MsgTx, key [shortTxHashKeySize]byte) uint64 {
|
|
k0 := binary.LittleEndian.Uint64(key[0:8])
|
|
k1 := binary.LittleEndian.Uint64(key[8:16])
|
|
txHash := msg.TxHash()
|
|
return siphash.Hash(k0, k1, txHash[:])
|
|
}
|
|
|
|
// EvictEntries removes all entries from the SigCache that correspond to the
|
|
// transactions in the given block. The block that is passed should be
|
|
// ProactiveEvictionDepth blocks deep, which is the depth at which the
|
|
// signatures for the transactions within the block are nearly guaranteed to no
|
|
// longer be useful.
|
|
//
|
|
// EvictEntries wraps the unexported evictEntries method, which is run from a
|
|
// goroutine. evictEntries is only invoked if validSigs is not empty. This
|
|
// avoids starting a new goroutine when there is nothing to evict, such as when
|
|
// syncing is ongoing.
|
|
func (s *SigCache) EvictEntries(block *wire.MsgBlock) {
|
|
s.RLock()
|
|
if len(s.validSigs) == 0 {
|
|
s.RUnlock()
|
|
return
|
|
}
|
|
s.RUnlock()
|
|
|
|
go s.evictEntries(block)
|
|
}
|
|
|
|
// evictEntries removes all entries from the SigCache that correspond to the
|
|
// transactions in the given block. The block that is passed should be
|
|
// ProactiveEvictionDepth blocks deep, which is the depth at which the
|
|
// signatures for the transactions within the block are nearly guaranteed to no
|
|
// longer be useful.
|
|
//
|
|
// Proactively evicting entries reduces the likelihood of the SigCache reaching
|
|
// maximum capacity quickly and then relying on random eviction, which may
|
|
// randomly evict entries that are still useful.
|
|
//
|
|
// This method must be run from a goroutine and should not be run during block
|
|
// validation.
|
|
func (s *SigCache) evictEntries(block *wire.MsgBlock) {
|
|
// Create a set consisting of the short tx hashes that are in the block.
|
|
numTxns := len(block.Transactions) + len(block.STransactions)
|
|
shortTxHashSet := make(map[uint64]struct{}, numTxns)
|
|
for _, tx := range block.Transactions {
|
|
shortTxHashSet[shortTxHash(tx, s.shortTxHashKey)] = struct{}{}
|
|
}
|
|
for _, stx := range block.STransactions {
|
|
shortTxHashSet[shortTxHash(stx, s.shortTxHashKey)] = struct{}{}
|
|
}
|
|
|
|
// Iterate through the entries in validSigs and remove any that are associated
|
|
// with a transaction in the block. This is done by iterating through every
|
|
// entry in validSigs, since the alternative of also keying the map by the
|
|
// shortTxHash would take extra space.
|
|
s.Lock()
|
|
for sigHash, sigEntry := range s.validSigs {
|
|
if _, ok := shortTxHashSet[sigEntry.shortTxHash]; ok {
|
|
delete(s.validSigs, sigHash)
|
|
}
|
|
}
|
|
s.Unlock()
|
|
}
|