diff --git a/config.go b/config.go index 1ebaa670..1e6e8fe0 100644 --- a/config.go +++ b/config.go @@ -37,6 +37,7 @@ const ( defaultMaxRPCWebsockets = 25 defaultVerifyEnabled = false defaultDbType = "leveldb" + defaultFreeTxRelayLimit = 15.0 ) var ( @@ -93,6 +94,7 @@ type config struct { CpuProfile string `long:"cpuprofile" description:"Write CPU profile to the specified file"` DebugLevel string `short:"d" long:"debuglevel" description:"Logging level for all subsystems {trace, debug, info, warn, error, critical} -- You may also specify =,=,... to set the log level for individual subsystems -- Use show to list available subsystems"` Upnp bool `long:"upnp" description:"Use UPnP to map our listening port outside of NAT"` + FreeTxRelayLimit float64 `long:"limitfreerelay" description:"Limit relay of transactions with no transaction fee to the given amount in thousands of bytes per minute"` onionlookup func(string) ([]net.IP, error) lookup func(string) ([]net.IP, error) oniondial func(string, string) (net.Conn, error) @@ -293,6 +295,7 @@ func loadConfig() (*config, []string, error) { DbType: defaultDbType, RPCKey: defaultRPCKeyFile, RPCCert: defaultRPCCertFile, + FreeTxRelayLimit: defaultFreeTxRelayLimit, } // Service options which are only added on Windows. diff --git a/doc.go b/doc.go index f507c698..7f4fd5de 100644 --- a/doc.go +++ b/doc.go @@ -74,6 +74,9 @@ Application Options: the log level for individual subsystems -- Use show to list available subsystems (info) --upnp Use UPnP to map our listening port outside of NAT + --limitfreerelay= Limit relay of transactions with no transaction fee + to the given amount in thousands of bytes per minute + (15) Help Options: -h, --help Show this help message diff --git a/mempool.go b/mempool.go index b95f0b53..bc867b97 100644 --- a/mempool.go +++ b/mempool.go @@ -97,6 +97,9 @@ type txMemPool struct { orphans map[btcwire.ShaHash]*btcutil.Tx orphansByPrev map[btcwire.ShaHash]*list.List outpoints map[btcwire.OutPoint]*btcutil.Tx + pennyTotal float64 // exponentially decaying total for penny spends. + lastPennyUnix int64 // unix time of last ``penny spend'' + } // isDust returns whether or not the passed transaction output amount is @@ -858,9 +861,29 @@ func (mp *txMemPool) maybeAcceptTransaction(tx *btcutil.Tx, isOrphan *bool, isNe return TxRuleError(str) } - // TODO(davec): Rate-limit 'free' transactions. That is to say - // transactions which are less than the minimum relay fee and are - // therefore considered free. + // Free-to-relay transactions are rate limited here to prevent + // penny-flooding with tiny transactions as a form of attack. + if minRequiredFee == 0 { + nowUnix := time.Now().Unix() + // we decay passed data with an exponentially decaying ~10 + // minutes window - matches bitcoind handling. + mp.pennyTotal *= math.Pow(1.0-1.0/600.0, + float64(nowUnix-mp.lastPennyUnix)) + mp.lastPennyUnix = nowUnix + + // Are we still over the limit? + if mp.pennyTotal >= cfg.FreeTxRelayLimit*10*1000 { + str := fmt.Sprintf("transaction %v has 0 fees and has "+ + "been rejected by the rate limiter", txHash) + return TxRuleError(str) + } + oldTotal := mp.pennyTotal + + mp.pennyTotal += float64(tx.MsgTx().SerializeSize()) + txmpLog.Debugf("rate limit: curTotal %v, nextTotal: %v, "+ + "limit %v", oldTotal, mp.pennyTotal, + cfg.FreeTxRelayLimit*10*1000) + } // Verify crypto signatures for each input and reject the transaction if // any don't verify.