From bfdf7a2cf2507dea8e284f580d5d9497f3bbc99f Mon Sep 17 00:00:00 2001 From: stasatdaglabs <39559713+stasatdaglabs@users.noreply.github.com> Date: Mon, 5 Aug 2019 16:04:24 +0300 Subject: [PATCH] [NOD-237] Implement transaction mass (#355) * [NOD-237] Implemented transaction mass. * [NOD-237] Added transaction mass validation to the mempool. * [NOD-237] Made blockMaxMassMax not rely on MaxBlockPayload. * [NOD-237] Added comments describing the new constants in validate.go. * [NOD-237] Changed the default blockmaxmass to 10,000,000. * [NOD-237] Fixed a comment that erroneously didn't refer to mass. * [NOD-237] Added comments to ValidateTxMass and CalcTxMass. * [NOD-237] Renamed "size" to "byte". Made validateBlockMass exit early if validation fails. Fixed unit names in comments. In CalcTxMass, moved summing of mass to the bottom of the function. * [NOD-237] Instead of ErrMassTooHigh, renamed ErrBlockTooBig and ErrTxTooBig. Replaced wire.MaxBlockPayload with MaxMassPerBlock. * [NOD-237] Fixed sanity checks related to block size in commands. * [NOD-237] To use up less memory during testing, made the mass in the "too big" test come from pkScripts rather than input bytes. * [NOD-237] Added an overflow check to validateBlockMass. --- blockdag/error.go | 16 ++-- blockdag/error_test.go | 4 +- blockdag/fullblocktests/generate.go | 2 +- blockdag/validate.go | 120 +++++++++++++++++++++----- blockdag/validate_test.go | 2 +- btcjson/dagsvrcmds.go | 12 +-- btcjson/dagsvrcmds_test.go | 20 ++--- btcjson/dagsvrresults.go | 3 +- cmd/addblock/import.go | 4 +- config/config.go | 29 +++---- database/cmd/dbtool/insecureimport.go | 4 +- mempool/mempool.go | 10 +++ mining/mining.go | 67 +++++++------- mining/mining_test.go | 2 +- mining/policy.go | 8 +- mining/test_utils.go | 2 +- server/rpc/rpcserver.go | 12 +-- server/rpc/rpcserverhelp.go | 7 +- server/server.go | 3 +- wire/msgblock.go | 11 +-- wire/msgblock_test.go | 3 +- wire/msgmerkleblock.go | 2 +- wire/msgmerkleblock_test.go | 3 +- wire/msgtx.go | 2 +- wire/msgtx_test.go | 2 +- 25 files changed, 218 insertions(+), 132 deletions(-) diff --git a/blockdag/error.go b/blockdag/error.go index 4314ed353..b1d169148 100644 --- a/blockdag/error.go +++ b/blockdag/error.go @@ -37,9 +37,9 @@ const ( // exists. ErrDuplicateBlock ErrorCode = iota - // ErrBlockTooBig indicates the serialized block size exceeds the - // maximum allowed size. - ErrBlockTooBig + // ErrBlockMassTooHigh indicates the mass of a block exceeds the maximum + // allowed limits. + ErrBlockMassTooHigh // ErrBlockVersionTooOld indicates the block version is too old and is // no longer accepted since the majority of the network has upgraded @@ -105,9 +105,9 @@ const ( // valid transaction must have at least one input. ErrNoTxInputs - // ErrTxTooBig indicates a transaction exceeds the maximum allowed size - // when serialized. - ErrTxTooBig + // ErrTxMassTooHigh indicates the mass of a transaction exceeds the maximum + // allowed limits. + ErrTxMassTooHigh // ErrBadTxOutValue indicates an output value for a transaction is // invalid in some way such as being out of range. @@ -227,7 +227,7 @@ const ( // Map of ErrorCode values back to their constant names for pretty printing. var errorCodeStrings = map[ErrorCode]string{ ErrDuplicateBlock: "ErrDuplicateBlock", - ErrBlockTooBig: "ErrBlockTooBig", + ErrBlockMassTooHigh: "ErrBlockMassTooHigh", ErrBlockVersionTooOld: "ErrBlockVersionTooOld", ErrInvalidTime: "ErrInvalidTime", ErrTimeTooOld: "ErrTimeTooOld", @@ -242,7 +242,7 @@ var errorCodeStrings = map[ErrorCode]string{ ErrFinalityPointTimeTooOld: "ErrFinalityPointTimeTooOld", ErrNoTransactions: "ErrNoTransactions", ErrNoTxInputs: "ErrNoTxInputs", - ErrTxTooBig: "ErrTxTooBig", + ErrTxMassTooHigh: "ErrTxMassTooHigh", ErrBadTxOutValue: "ErrBadTxOutValue", ErrDuplicateTxInputs: "ErrDuplicateTxInputs", ErrBadTxInput: "ErrBadTxInput", diff --git a/blockdag/error_test.go b/blockdag/error_test.go index 5bb1f0ea3..f96b80924 100644 --- a/blockdag/error_test.go +++ b/blockdag/error_test.go @@ -16,7 +16,7 @@ func TestErrorCodeStringer(t *testing.T) { want string }{ {ErrDuplicateBlock, "ErrDuplicateBlock"}, - {ErrBlockTooBig, "ErrBlockTooBig"}, + {ErrBlockMassTooHigh, "ErrBlockMassTooHigh"}, {ErrBlockVersionTooOld, "ErrBlockVersionTooOld"}, {ErrInvalidTime, "ErrInvalidTime"}, {ErrTimeTooOld, "ErrTimeTooOld"}, @@ -31,7 +31,7 @@ func TestErrorCodeStringer(t *testing.T) { {ErrFinalityPointTimeTooOld, "ErrFinalityPointTimeTooOld"}, {ErrNoTransactions, "ErrNoTransactions"}, {ErrNoTxInputs, "ErrNoTxInputs"}, - {ErrTxTooBig, "ErrTxTooBig"}, + {ErrTxMassTooHigh, "ErrTxMassTooHigh"}, {ErrBadTxOutValue, "ErrBadTxOutValue"}, {ErrDuplicateTxInputs, "ErrDuplicateTxInputs"}, {ErrBadTxInput, "ErrBadTxInput"}, diff --git a/blockdag/fullblocktests/generate.go b/blockdag/fullblocktests/generate.go index d666954d1..dbafdd11a 100644 --- a/blockdag/fullblocktests/generate.go +++ b/blockdag/fullblocktests/generate.go @@ -1106,7 +1106,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) { replaceSpendScript(sizePadScript)(b) }) g.assertTipBlockSize(maxBlockSize + 1) - rejected(blockdag.ErrBlockTooBig) + rejected(blockdag.ErrBlockMassTooHigh) // Parent was rejected, so this block must either be an orphan or // outright rejected due to an invalid parent. diff --git a/blockdag/validate.go b/blockdag/validate.go index 502308eef..a42b7dd0b 100644 --- a/blockdag/validate.go +++ b/blockdag/validate.go @@ -20,8 +20,8 @@ import ( const ( // MaxSigOpsPerBlock is the maximum number of signature operations - // allowed for a block. It is a fraction of the max block payload size. - MaxSigOpsPerBlock = wire.MaxBlockPayload / 50 + // allowed for a block. It is a fraction of the max block transaction mass. + MaxSigOpsPerBlock = wire.MaxMassPerBlock / 50 // MaxCoinbasePayloadLen is the maximum length a coinbase payload can be. MaxCoinbasePayloadLen = 150 @@ -29,6 +29,11 @@ const ( // baseSubsidy is the starting subsidy amount for mined blocks. This // value is halved every SubsidyHalvingInterval blocks. baseSubsidy = 50 * util.SatoshiPerBitcoin + + // the following are used when calculating a transaction's mass + massPerTxByte = 1 + massPerPKScriptByte = 10 + massPerSigOp = 10000 ) // isNullOutpoint determines whether or not a previous transaction outpoint @@ -121,13 +126,13 @@ func CheckTransactionSanity(tx *util.Tx, subnetworkID *subnetworkid.SubnetworkID return ruleError(ErrNoTxInputs, "transaction has no inputs") } - // A transaction must not exceed the maximum allowed block payload when + // A transaction must not exceed the maximum allowed block mass when // serialized. serializedTxSize := msgTx.SerializeSize() - if serializedTxSize > wire.MaxBlockPayload { + if serializedTxSize > wire.MaxMassPerBlock { str := fmt.Sprintf("serialized transaction is too big - got "+ - "%d, max %d", serializedTxSize, wire.MaxBlockPayload) - return ruleError(ErrTxTooBig, str) + "%d, max %d", serializedTxSize, wire.MaxMassPerBlock) + return ruleError(ErrTxMassTooHigh, str) } // Ensure the transaction amounts are in range. Each transaction @@ -361,6 +366,86 @@ func CountP2SHSigOps(tx *util.Tx, isCoinbase bool, utxoSet UTXOSet) (int, error) return totalSigOps, nil } +// ValidateTxMass makes sure that the given transaction's mass does not exceed +// the maximum allowed limit. Currently, it is equivalent to the block mass limit. +// See CalcTxMass for further details. +func ValidateTxMass(tx *util.Tx, utxoSet UTXOSet) error { + txMass, err := CalcTxMass(tx, utxoSet) + if err != nil { + return err + } + if txMass > wire.MaxMassPerBlock { + str := fmt.Sprintf("tx %s has mass %d, which is above the "+ + "allowed limit of %d", tx.ID(), txMass, wire.MaxMassPerBlock) + return ruleError(ErrTxMassTooHigh, str) + } + return nil +} + +func validateBlockMass(pastUTXO UTXOSet, transactions []*util.Tx) error { + totalMass := uint64(0) + for _, tx := range transactions { + txMass, err := CalcTxMass(tx, pastUTXO) + if err != nil { + return err + } + totalMass += txMass + + // We could potentially overflow the accumulator so check for + // overflow as well. + if totalMass < txMass || totalMass > wire.MaxMassPerBlock { + str := fmt.Sprintf("block has total mass %d, which is "+ + "above the allowed limit of %d", totalMass, wire.MaxMassPerBlock) + return ruleError(ErrBlockMassTooHigh, str) + } + } + return nil +} + +// CalcTxMass sums up and returns the "mass" of a transaction. This number +// is an approximation of how many resources (CPU, RAM, etc.) it would take +// to process the transaction. +// The following properties are considered in the calculation: +// * The transaction length in bytes +// * The length of all output scripts in bytes +// * The count of all input sigOps +func CalcTxMass(tx *util.Tx, utxoSet UTXOSet) (uint64, error) { + txSize := tx.MsgTx().SerializeSize() + + if tx.IsCoinBase() { + return uint64(txSize * massPerTxByte), nil + } + + pkScriptSize := 0 + for _, txOut := range tx.MsgTx().TxOut { + pkScriptSize += len(txOut.PkScript) + } + + sigOpsCount := 0 + for txInIndex, txIn := range tx.MsgTx().TxIn { + // Ensure the referenced input transaction is available. + entry, ok := utxoSet.Get(txIn.PreviousOutpoint) + if !ok { + str := fmt.Sprintf("output %s referenced from "+ + "transaction %s:%d either does not exist or "+ + "has already been spent", txIn.PreviousOutpoint, + tx.ID(), txInIndex) + return 0, ruleError(ErrMissingTxOut, str) + } + + // Count the precise number of signature operations in the + // referenced public key script. + pkScript := entry.PkScript() + sigScript := txIn.SignatureScript + isP2SH := txscript.IsPayToScriptHash(pkScript) + sigOpsCount += txscript.GetPreciseSigOpCount(sigScript, pkScript, isP2SH) + } + + return uint64(txSize*massPerTxByte + + pkScriptSize*massPerPKScriptByte + + sigOpsCount*massPerSigOp), nil +} + // checkBlockHeaderSanity performs some preliminary checks on a block header to // ensure it is sane before continuing with processing. These checks are // context free. @@ -444,21 +529,12 @@ func (dag *BlockDAG) checkBlockSanity(block *util.Block, flags BehaviorFlags) (t "any transactions") } - // A block must not have more transactions than the max block payload or - // else it is certainly over the block size limit. - if numTx > wire.MaxBlockPayload { + // A block must not have more transactions than the max block mass or + // else it is certainly over the block mass limit. + if numTx > wire.MaxMassPerBlock { str := fmt.Sprintf("block contains too many transactions - "+ - "got %d, max %d", numTx, wire.MaxBlockPayload) - return 0, ruleError(ErrBlockTooBig, str) - } - - // A block must not exceed the maximum allowed block payload when - // serialized. - serializedSize := msgBlock.SerializeSize() - if serializedSize > wire.MaxBlockPayload { - str := fmt.Sprintf("serialized block is too big - got %d, "+ - "max %d", serializedSize, wire.MaxBlockPayload) - return 0, ruleError(ErrBlockTooBig, str) + "got %d, max %d", numTx, wire.MaxMassPerBlock) + return 0, ruleError(ErrBlockMassTooHigh, str) } // The first transaction in a block must be a coinbase. @@ -861,6 +937,10 @@ func (dag *BlockDAG) checkConnectToPastUTXO(block *blockNode, pastUTXO UTXOSet, if err := validateSigopsCount(pastUTXO, transactions); err != nil { return nil, err } + + if err := validateBlockMass(pastUTXO, transactions); err != nil { + return nil, err + } } // Perform several checks on the inputs for each transaction. Also diff --git a/blockdag/validate_test.go b/blockdag/validate_test.go index 54216263b..4a5ee00a8 100644 --- a/blockdag/validate_test.go +++ b/blockdag/validate_test.go @@ -633,7 +633,7 @@ func TestCheckTransactionSanity(t *testing.T) { {"good one", 1, 1, 1, *subnetworkid.SubnetworkIDNative, nil, nil, nil}, {"no inputs", 0, 1, 1, *subnetworkid.SubnetworkIDNative, nil, nil, ruleError(ErrNoTxInputs, "")}, {"no outputs", 1, 0, 1, *subnetworkid.SubnetworkIDNative, nil, nil, nil}, - {"too big", 100000, 1, 1, *subnetworkid.SubnetworkIDNative, nil, nil, ruleError(ErrTxTooBig, "")}, + {"too massive", 1, 1000000, 1, *subnetworkid.SubnetworkIDNative, nil, nil, ruleError(ErrTxMassTooHigh, "")}, {"too much satoshi in one output", 1, 1, util.MaxSatoshi + 1, *subnetworkid.SubnetworkIDNative, nil, diff --git a/btcjson/dagsvrcmds.go b/btcjson/dagsvrcmds.go index 575664627..d98bddfcc 100644 --- a/btcjson/dagsvrcmds.go +++ b/btcjson/dagsvrcmds.go @@ -222,10 +222,10 @@ type TemplateRequest struct { // Optional long polling. LongPollID string `json:"longPollId,omitempty"` - // Optional template tweaking. SigOpLimit and SizeLimit can be int64 + // Optional template tweaking. SigOpLimit and MassLimit can be int64 // or bool. SigOpLimit interface{} `json:"sigOpLimit,omitempty"` - SizeLimit interface{} `json:"sizeLimit,omitempty"` + MassLimit interface{} `json:"massLimit,omitempty"` MaxVersion uint32 `json:"maxVersion,omitempty"` // Basic pool extension from BIP 0023. @@ -257,7 +257,7 @@ func convertTemplateRequestField(fieldName string, iface interface{}) (interface } // UnmarshalJSON provides a custom Unmarshal method for TemplateRequest. This -// is necessary because the SigOpLimit and SizeLimit fields can only be specific +// is necessary because the SigOpLimit and MassLimit fields can only be specific // types. func (t *TemplateRequest) UnmarshalJSON(data []byte) error { type templateRequest TemplateRequest @@ -274,12 +274,12 @@ func (t *TemplateRequest) UnmarshalJSON(data []byte) error { } request.SigOpLimit = val - // The SizeLimit field can only be nil, bool, or int64. - val, err = convertTemplateRequestField("sizeLimit", request.SizeLimit) + // The MassLimit field can only be nil, bool, or int64. + val, err = convertTemplateRequestField("massLimit", request.MassLimit) if err != nil { return err } - request.SizeLimit = val + request.MassLimit = val return nil } diff --git a/btcjson/dagsvrcmds_test.go b/btcjson/dagsvrcmds_test.go index 5e591dd63..05421631e 100644 --- a/btcjson/dagsvrcmds_test.go +++ b/btcjson/dagsvrcmds_test.go @@ -271,25 +271,25 @@ func TestDAGSvrCmds(t *testing.T) { { name: "getBlockTemplate optional - template request with tweaks", newCmd: func() (interface{}, error) { - return btcjson.NewCmd("getBlockTemplate", `{"mode":"template","capabilities":["longPoll","coinbaseTxn"],"sigOpLimit":500,"sizeLimit":100000000,"maxVersion":1}`) + return btcjson.NewCmd("getBlockTemplate", `{"mode":"template","capabilities":["longPoll","coinbaseTxn"],"sigOpLimit":500,"massLimit":100000000,"maxVersion":1}`) }, staticCmd: func() interface{} { template := btcjson.TemplateRequest{ Mode: "template", Capabilities: []string{"longPoll", "coinbaseTxn"}, SigOpLimit: 500, - SizeLimit: 100000000, + MassLimit: 100000000, MaxVersion: 1, } return btcjson.NewGetBlockTemplateCmd(&template) }, - marshalled: `{"jsonrpc":"1.0","method":"getBlockTemplate","params":[{"mode":"template","capabilities":["longPoll","coinbaseTxn"],"sigOpLimit":500,"sizeLimit":100000000,"maxVersion":1}],"id":1}`, + marshalled: `{"jsonrpc":"1.0","method":"getBlockTemplate","params":[{"mode":"template","capabilities":["longPoll","coinbaseTxn"],"sigOpLimit":500,"massLimit":100000000,"maxVersion":1}],"id":1}`, unmarshalled: &btcjson.GetBlockTemplateCmd{ Request: &btcjson.TemplateRequest{ Mode: "template", Capabilities: []string{"longPoll", "coinbaseTxn"}, SigOpLimit: int64(500), - SizeLimit: int64(100000000), + MassLimit: int64(100000000), MaxVersion: 1, }, }, @@ -297,25 +297,25 @@ func TestDAGSvrCmds(t *testing.T) { { name: "getBlockTemplate optional - template request with tweaks 2", newCmd: func() (interface{}, error) { - return btcjson.NewCmd("getBlockTemplate", `{"mode":"template","capabilities":["longPoll","coinbaseTxn"],"sigOpLimit":true,"sizeLimit":100000000,"maxVersion":1}`) + return btcjson.NewCmd("getBlockTemplate", `{"mode":"template","capabilities":["longPoll","coinbaseTxn"],"sigOpLimit":true,"massLimit":100000000,"maxVersion":1}`) }, staticCmd: func() interface{} { template := btcjson.TemplateRequest{ Mode: "template", Capabilities: []string{"longPoll", "coinbaseTxn"}, SigOpLimit: true, - SizeLimit: 100000000, + MassLimit: 100000000, MaxVersion: 1, } return btcjson.NewGetBlockTemplateCmd(&template) }, - marshalled: `{"jsonrpc":"1.0","method":"getBlockTemplate","params":[{"mode":"template","capabilities":["longPoll","coinbaseTxn"],"sigOpLimit":true,"sizeLimit":100000000,"maxVersion":1}],"id":1}`, + marshalled: `{"jsonrpc":"1.0","method":"getBlockTemplate","params":[{"mode":"template","capabilities":["longPoll","coinbaseTxn"],"sigOpLimit":true,"massLimit":100000000,"maxVersion":1}],"id":1}`, unmarshalled: &btcjson.GetBlockTemplateCmd{ Request: &btcjson.TemplateRequest{ Mode: "template", Capabilities: []string{"longPoll", "coinbaseTxn"}, SigOpLimit: true, - SizeLimit: int64(100000000), + MassLimit: int64(100000000), MaxVersion: 1, }, }, @@ -1152,9 +1152,9 @@ func TestDAGSvrCmdErrors(t *testing.T) { err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, }, { - name: "invalid template request sizelimit field", + name: "invalid template request masslimit field", result: &btcjson.TemplateRequest{}, - marshalled: `{"sizelimit":"invalid"}`, + marshalled: `{"masslimit":"invalid"}`, err: btcjson.Error{ErrorCode: btcjson.ErrInvalidType}, }, } diff --git a/btcjson/dagsvrresults.go b/btcjson/dagsvrresults.go index 6b6d748db..f4e3ecf6a 100644 --- a/btcjson/dagsvrresults.go +++ b/btcjson/dagsvrresults.go @@ -121,6 +121,7 @@ type GetBlockTemplateResultTx struct { Data string `json:"data"` ID string `json:"id"` Depends []int64 `json:"depends"` + Mass uint64 `json:"mass"` Fee uint64 `json:"fee"` SigOps int64 `json:"sigOps"` } @@ -141,7 +142,7 @@ type GetBlockTemplateResult struct { Height uint64 `json:"height"` ParentHashes []string `json:"parentHashes"` SigOpLimit int64 `json:"sigOpLimit,omitempty"` - SizeLimit int64 `json:"sizeLimit,omitempty"` + MassLimit int64 `json:"massLimit,omitempty"` Transactions []GetBlockTemplateResultTx `json:"transactions"` AcceptedIDMerkleRoot string `json:"acceptedIdMerkleRoot"` UTXOCommitment string `json:"utxoCommitment"` diff --git a/cmd/addblock/import.go b/cmd/addblock/import.go index 3f9cb216b..b0ad6425b 100644 --- a/cmd/addblock/import.go +++ b/cmd/addblock/import.go @@ -69,10 +69,10 @@ func (bi *blockImporter) readBlock() ([]byte, error) { if err := binary.Read(bi.r, binary.LittleEndian, &blockLen); err != nil { return nil, err } - if blockLen > wire.MaxBlockPayload { + if blockLen > wire.MaxMessagePayload { return nil, fmt.Errorf("block payload of %d bytes is larger "+ "than the max allowed %d bytes", blockLen, - wire.MaxBlockPayload) + wire.MaxMessagePayload) } serializedBlock := make([]byte, blockLen) diff --git a/config/config.go b/config/config.go index a0860c8ee..23272b0ae 100644 --- a/config/config.go +++ b/config/config.go @@ -28,7 +28,6 @@ import ( "github.com/daglabs/btcd/util/network" "github.com/daglabs/btcd/util/subnetworkid" "github.com/daglabs/btcd/version" - "github.com/daglabs/btcd/wire" "github.com/jessevdk/go-flags" ) @@ -47,10 +46,9 @@ const ( defaultMaxRPCWebsockets = 25 defaultMaxRPCConcurrentReqs = 20 defaultDbType = "ffldb" - defaultBlockMinSize = 0 - defaultBlockMaxSize = 750000 - blockMaxSizeMin = 1000 - blockMaxSizeMax = wire.MaxBlockPayload - 1000 + defaultBlockMaxMass = 10000000 + blockMaxMassMin = 1000 + blockMaxMassMax = 10000000 defaultGenerate = false defaultMaxOrphanTransactions = 100 //DefaultMaxOrphanTxSize is the default maximum size for an orphan transaction @@ -148,8 +146,7 @@ type configFlags struct { MaxOrphanTxs int `long:"maxorphantx" description:"Max number of orphan transactions to keep in memory"` Generate bool `long:"generate" description:"Generate (mine) bitcoins using the CPU"` MiningAddrs []string `long:"miningaddr" description:"Add the specified payment address to the list of addresses to use for generated blocks -- At least one address is required if the generate option is set"` - BlockMinSize uint32 `long:"blockminsize" description:"Mininum block size in bytes to be used when creating a block"` - BlockMaxSize uint32 `long:"blockmaxsize" description:"Maximum block size in bytes to be used when creating a block"` + BlockMaxMass uint64 `long:"blockmaxmass" description:"Maximum transaction mass to be used when creating a block"` UserAgentComments []string `long:"uacomment" description:"Comment to add to the user agent -- See BIP 14 for more information."` NoPeerBloomFilters bool `long:"nopeerbloomfilters" description:"Disable bloom filtering support"` EnableCFilters bool `long:"enablecfilters" description:"Enable committed filtering (CF) support"` @@ -312,8 +309,7 @@ func loadConfig() (*Config, []string, error) { DbType: defaultDbType, RPCKey: defaultRPCKeyFile, RPCCert: defaultRPCCertFile, - BlockMinSize: defaultBlockMinSize, - BlockMaxSize: defaultBlockMaxSize, + BlockMaxMass: defaultBlockMaxMass, MaxOrphanTxs: defaultMaxOrphanTransactions, SigCacheMaxSize: defaultSigCacheMaxSize, Generate: defaultGenerate, @@ -659,14 +655,14 @@ func loadConfig() (*Config, []string, error) { return nil, nil, err } - // Limit the max block size to a sane value. - if cfg.BlockMaxSize < blockMaxSizeMin || cfg.BlockMaxSize > - blockMaxSizeMax { + // Limit the max block mass to a sane value. + if cfg.BlockMaxMass < blockMaxMassMin || cfg.BlockMaxMass > + blockMaxMassMax { - str := "%s: The blockmaxsize option must be in between %d " + + str := "%s: The blockmaxmass option must be in between %d " + "and %d -- parsed [%d]" - err := fmt.Errorf(str, funcName, blockMaxSizeMin, - blockMaxSizeMax, cfg.BlockMaxSize) + err := fmt.Errorf(str, funcName, blockMaxMassMin, + blockMaxMassMax, cfg.BlockMaxMass) fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, usageMessage) return nil, nil, err @@ -682,9 +678,6 @@ func loadConfig() (*Config, []string, error) { return nil, nil, err } - // Limit minimum block sizes to max block size. - cfg.BlockMinSize = minUint32(cfg.BlockMinSize, cfg.BlockMaxSize) - // Look for illegal characters in the user agent comments. for _, uaComment := range cfg.UserAgentComments { if strings.ContainsAny(uaComment, "/:()") { diff --git a/database/cmd/dbtool/insecureimport.go b/database/cmd/dbtool/insecureimport.go index 43d0cc878..640d192a7 100644 --- a/database/cmd/dbtool/insecureimport.go +++ b/database/cmd/dbtool/insecureimport.go @@ -81,10 +81,10 @@ func (bi *blockImporter) readBlock() ([]byte, error) { if err := binary.Read(bi.r, binary.LittleEndian, &blockLen); err != nil { return nil, err } - if blockLen > wire.MaxBlockPayload { + if blockLen > wire.MaxMessagePayload { return nil, fmt.Errorf("block payload of %d bytes is larger "+ "than the max allowed %d bytes", blockLen, - wire.MaxBlockPayload) + wire.MaxMessagePayload) } serializedBlock := make([]byte, blockLen) diff --git a/mempool/mempool.go b/mempool/mempool.go index 53d6aeb29..d96412d64 100644 --- a/mempool/mempool.go +++ b/mempool/mempool.go @@ -938,6 +938,16 @@ func (mp *TxPool) maybeAcceptTransaction(tx *util.Tx, isNew, rejectDupOrphans bo "transaction's sequence locks on inputs not met") } + // Don't allow transactions that exceed the maximum allowed + // transaction mass. + err = blockdag.ValidateTxMass(tx, mp.mpUTXOSet) + if err != nil { + if ruleError, ok := err.(blockdag.RuleError); ok { + return nil, nil, dagRuleError(ruleError) + } + return nil, nil, err + } + // Perform several checks on the transaction inputs using the invariant // rules in blockchain for what transactions are allowed into blocks. // Also returns the fees associated with the transaction which will be diff --git a/mining/mining.go b/mining/mining.go index e486c3db5..6533604fd 100644 --- a/mining/mining.go +++ b/mining/mining.go @@ -162,6 +162,10 @@ type BlockTemplate struct { // requirement. Block *wire.MsgBlock + // TxMasses contains the mass of each transaction in the generated + // template performs. + TxMasses []uint64 + // Fees contains the amount of fees each transaction in the generated // template pays in base units. Since the first transaction is the // coinbase, the first entry (offset 0) will contain the negative of the @@ -279,7 +283,7 @@ func NewBlkTmplGenerator(policy *Policy, params *dagconfig.Params, // nonzero, in which case the block will be filled with the low-fee/free // transactions until the block size reaches that minimum size. // -// Any transactions which would cause the block to exceed the BlockMaxSize +// Any transactions which would cause the block to exceed the BlockMaxMass // policy setting, exceed the maximum allowed signature operations per block, or // otherwise cause the block to be invalid are skipped. // @@ -294,7 +298,7 @@ func NewBlkTmplGenerator(policy *Policy, params *dagconfig.Params, // |-----------------------------------| | -- // | | | // | | | -// | | |--- policy.BlockMaxSize +// | | |--- policy.BlockMaxMass // | Transactions prioritized by fee | | // | until <= policy.TxMinFreeFee | | // | | | @@ -310,6 +314,8 @@ func (g *BlkTmplGenerator) NewBlockTemplate(payToAddress util.Address) (*BlockTe defer g.dag.RUnlock() nextBlockBlueScore := g.dag.VirtualBlueScore() + nextBlockUTXO := g.dag.UTXOSet() + coinbasePayloadPkScript, err := txscript.PayToAddrScript(payToAddress) if err != nil { return nil, err @@ -328,6 +334,10 @@ func (g *BlkTmplGenerator) NewBlockTemplate(payToAddress util.Address) (*BlockTe if err != nil { return nil, err } + coinbaseTxMass, err := blockdag.CalcTxMass(coinbaseTx, nextBlockUTXO) + if err != nil { + return nil, err + } numCoinbaseSigOps := int64(blockdag.CountSigOps(coinbaseTx)) // Get the current source transactions and create a priority queue to @@ -346,21 +356,20 @@ func (g *BlkTmplGenerator) NewBlockTemplate(payToAddress util.Address) (*BlockTe blockTxns := make([]*util.Tx, 0, len(sourceTxns)+1) blockTxns = append(blockTxns, coinbaseTx) - // The starting block size is the size of the block header plus the max - // possible transaction count size, plus the size of the coinbase - // transaction. - blockSize := blockHeaderOverhead + uint32(coinbaseTx.MsgTx().SerializeSize()) + blockMass := coinbaseTxMass blockSigOps := numCoinbaseSigOps totalFees := uint64(0) - // Create slices to hold the fees and number of signature operations - // for each of the selected transactions and add an entry for the - // coinbase. This allows the code below to simply append details about - // a transaction as it is selected for inclusion in the final block. + // Create slices to hold the mass, the fees, and number of signature + // operations for each of the selected transactions and add an entry for + // the coinbase. This allows the code below to simply append details + // about a transaction as it is selected for inclusion in the final block. // However, since the total fees aren't known yet, use a dummy value for // the coinbase fee which will be updated later. + txMasses := make([]uint64, 0, len(sourceTxns)+1) txFees := make([]uint64, 0, len(sourceTxns)+1) txSigOpCounts := make([]int64, 0, len(sourceTxns)+1) + txMasses = append(txMasses, coinbaseTxMass) txFees = append(txFees, 0) // For coinbase tx txSigOpCounts = append(txSigOpCounts, numCoinbaseSigOps) @@ -423,18 +432,22 @@ func (g *BlkTmplGenerator) NewBlockTemplate(payToAddress util.Address) (*BlockTe gasUsageMap[subnetworkID] = gasUsage + txGas } - // Enforce maximum block size. Also check for overflow. - txSize := uint32(tx.MsgTx().SerializeSize()) - blockPlusTxSize := blockSize + txSize - if blockPlusTxSize < blockSize || - blockPlusTxSize >= g.policy.BlockMaxSize { - + // Enforce maximum transaction mass per block. Also check + // for overflow. + txMass, err := blockdag.CalcTxMass(tx, g.dag.UTXOSet()) + if err != nil { + log.Tracef("Skipping tx %s due to error in "+ + "CalcTxMass: %s", tx.ID(), err) + continue + } + if blockMass+txMass < blockMass || + blockMass >= g.policy.BlockMaxMass { log.Tracef("Skipping tx %s because it would exceed "+ - "the max block size", tx.ID()) + "the max block mass", tx.ID()) continue } - // Enforce maximum signature operations per block. Also check + // Enforce maximum signature operations per block. Also check // for overflow. numSigOps := int64(blockdag.CountSigOps(tx)) if blockSigOps+numSigOps < blockSigOps || @@ -476,12 +489,13 @@ func (g *BlkTmplGenerator) NewBlockTemplate(payToAddress util.Address) (*BlockTe } // Add the transaction to the block, increment counters, and - // save the fees and signature operation counts to the block + // save the masses, fees, and signature operation counts to the block // template. blockTxns = append(blockTxns, tx) - blockSize += txSize - blockSigOps += int64(numSigOps) + blockMass += txMass + blockSigOps += numSigOps totalFees += prioItem.fee + txMasses = append(txMasses, txMass) txFees = append(txFees, prioItem.fee) txSigOpCounts = append(txSigOpCounts, numSigOps) @@ -489,12 +503,6 @@ func (g *BlkTmplGenerator) NewBlockTemplate(payToAddress util.Address) (*BlockTe prioItem.tx.ID(), prioItem.feePerKB) } - // Now that the actual transactions have been selected, update the - // block size for the real transaction count and coinbase value with - // the total fees accordingly. - blockSize -= wire.MaxVarIntPayload - - uint32(wire.VarIntSerializeSize(uint64(len(blockTxns)))) - // Calculate the required difficulty for the block. The timestamp // is potentially adjusted to ensure it comes after the median time of // the last several blocks per the chain consensus rules. @@ -553,12 +561,13 @@ func (g *BlkTmplGenerator) NewBlockTemplate(payToAddress util.Address) (*BlockTe } log.Debugf("Created new block template (%d transactions, %d in fees, "+ - "%d signature operations, %d bytes, target difficulty %064x)", - len(msgBlock.Transactions), totalFees, blockSigOps, blockSize, + "%d signature operations, %d mass, target difficulty %064x)", + len(msgBlock.Transactions), totalFees, blockSigOps, blockMass, util.CompactToBig(msgBlock.Header.Bits)) return &BlockTemplate{ Block: &msgBlock, + TxMasses: txMasses, Fees: txFees, SigOpCounts: txSigOpCounts, ValidPayAddress: payToAddress != nil, diff --git a/mining/mining_test.go b/mining/mining_test.go index dfc37b6ee..19f432a42 100644 --- a/mining/mining_test.go +++ b/mining/mining_test.go @@ -89,7 +89,7 @@ func TestNewBlockTemplate(t *testing.T) { } policy := Policy{ - BlockMaxSize: 50000, + BlockMaxMass: 50000, } // First we create a block to have coinbase funds for the rest of the test. diff --git a/mining/policy.go b/mining/policy.go index 932375dc9..5def9b7ea 100644 --- a/mining/policy.go +++ b/mining/policy.go @@ -8,11 +8,7 @@ package mining // the generation of block templates. See the documentation for // NewBlockTemplate for more details on each of these parameters are used. type Policy struct { - // BlockMinSize is the minimum block size to be used when generating - // a block template. - BlockMinSize uint32 - - // BlockMaxSize is the maximum block size to be used when generating a + // BlockMaxMass is the maximum block mass to be used when generating a // block template. - BlockMaxSize uint32 + BlockMaxMass uint64 } diff --git a/mining/test_utils.go b/mining/test_utils.go index 7911bd308..3ae4ba426 100644 --- a/mining/test_utils.go +++ b/mining/test_utils.go @@ -46,7 +46,7 @@ func PrepareBlockForTest(dag *blockdag.BlockDAG, params *dagconfig.Params, paren oldVirtual := blockdag.SetVirtualForTest(dag, newVirtual) defer blockdag.SetVirtualForTest(dag, oldVirtual) policy := Policy{ - BlockMaxSize: 50000, + BlockMaxMass: 50000, } txSource := &fakeTxSource{ diff --git a/server/rpc/rpcserver.go b/server/rpc/rpcserver.go index 5db6a7951..8a746dd2b 100644 --- a/server/rpc/rpcserver.go +++ b/server/rpc/rpcserver.go @@ -1761,6 +1761,7 @@ func (state *gbtWorkState) blockTemplateResult(dag *blockdag.BlockDAG, useCoinba Data: hex.EncodeToString(txBuf.Bytes()), ID: txID.String(), Depends: depends, + Mass: template.TxMasses[i], Fee: template.Fees[i], SigOps: template.SigOpCounts[i], } @@ -1779,7 +1780,7 @@ func (state *gbtWorkState) blockTemplateResult(dag *blockdag.BlockDAG, useCoinba Height: template.Height, ParentHashes: daghash.Strings(header.ParentHashes), SigOpLimit: blockdag.MaxSigOpsPerBlock, - SizeLimit: wire.MaxBlockPayload, + MassLimit: wire.MaxMassPerBlock, Transactions: transactions, AcceptedIDMerkleRoot: header.AcceptedIDMerkleRoot.String(), UTXOCommitment: header.UTXOCommitment.String(), @@ -1822,6 +1823,7 @@ func (state *gbtWorkState) blockTemplateResult(dag *blockdag.BlockDAG, useCoinba Data: hex.EncodeToString(txBuf.Bytes()), ID: tx.TxID().String(), Depends: []int64{}, + Mass: template.TxMasses[0], Fee: template.Fees[0], SigOps: template.SigOpCounts[0], } @@ -2028,8 +2030,8 @@ func chainErrToGBTErrString(err error) string { switch ruleErr.ErrorCode { case blockdag.ErrDuplicateBlock: return "duplicate" - case blockdag.ErrBlockTooBig: - return "bad-blk-length" + case blockdag.ErrBlockMassTooHigh: + return "bad-blk-mass" case blockdag.ErrBlockVersionTooOld: return "bad-version" case blockdag.ErrInvalidTime: @@ -2054,8 +2056,8 @@ func chainErrToGBTErrString(err error) string { return "bad-txns-none" case blockdag.ErrNoTxInputs: return "bad-txns-noinputs" - case blockdag.ErrTxTooBig: - return "bad-txns-size" + case blockdag.ErrTxMassTooHigh: + return "bad-txns-mass" case blockdag.ErrBadTxOutValue: return "bad-txns-outputvalue" case blockdag.ErrDuplicateTxInputs: diff --git a/server/rpc/rpcserverhelp.go b/server/rpc/rpcserverhelp.go index 4746be689..1395815b8 100644 --- a/server/rpc/rpcserverhelp.go +++ b/server/rpc/rpcserverhelp.go @@ -283,7 +283,7 @@ var helpDescsEnUS = map[string]string{ "templateRequest-capabilities": "List of capabilities", "templateRequest-longPollId": "The long poll ID of a job to monitor for expiration; required and valid only for long poll requests ", "templateRequest-sigOpLimit": "Number of signature operations allowed in blocks (this parameter is ignored)", - "templateRequest-sizeLimit": "Number of bytes allowed in blocks (this parameter is ignored)", + "templateRequest-massLimit": "Max transaction mass allowed in blocks (this parameter is ignored)", "templateRequest-maxVersion": "Highest supported block version number (this parameter is ignored)", "templateRequest-target": "The desired target for the block template (this parameter is ignored)", "templateRequest-data": "Hex-encoded block data (only for mode=proposal)", @@ -294,6 +294,7 @@ var helpDescsEnUS = map[string]string{ "getBlockTemplateResultTx-hash": "Hex-encoded transaction hash (little endian if treated as a 256-bit number)", "getBlockTemplateResultTx-id": "Hex-encoded transaction ID (little endian if treated as a 256-bit number)", "getBlockTemplateResultTx-depends": "Other transactions before this one (by 1-based index in the 'transactions' list) that must be present in the final block if this one is", + "getBlockTemplateResultTx-mass": "Total mass of all transactions in the block", "getBlockTemplateResultTx-fee": "Difference in value between transaction inputs and outputs (in Satoshi)", "getBlockTemplateResultTx-sigOps": "Total number of signature operations as counted for purposes of block limits", @@ -305,8 +306,8 @@ var helpDescsEnUS = map[string]string{ "getBlockTemplateResult-curTime": "Current time as seen by the server (recommended for block time); must fall within mintime/maxtime rules", "getBlockTemplateResult-height": "Height of the block to be solved", "getBlockTemplateResult-parentHashes": "Hex-encoded big-endian hashes of the parent blocks", - "getBlockTemplateResult-sigOpLimit": "Number of sigops allowed in blocks ", - "getBlockTemplateResult-sizeLimit": "Number of bytes allowed in blocks", + "getBlockTemplateResult-sigOpLimit": "Number of sigops allowed in blocks", + "getBlockTemplateResult-massLimit": "Max transaction mass allowed in blocks", "getBlockTemplateResult-transactions": "Array of transactions as JSON objects", "getBlockTemplateResult-acceptedIdMerkleRoot": "The root of the merkle tree of transaction IDs accepted by this block", "getBlockTemplateResult-utxoCommitment": "An ECMH UTXO commitment of this block", diff --git a/server/server.go b/server/server.go index 0c8cfb412..e4ce46303 100644 --- a/server/server.go +++ b/server/server.go @@ -101,8 +101,7 @@ func NewServer(listenAddrs []string, db database.DB, dagParams *dagconfig.Params // NOTE: The CPU miner relies on the mempool, so the mempool has to be // created before calling the function to create the CPU miner. policy := mining.Policy{ - BlockMinSize: cfg.BlockMinSize, - BlockMaxSize: cfg.BlockMaxSize, + BlockMaxMass: cfg.BlockMaxMass, } blockTemplateGenerator := mining.NewBlkTmplGenerator(&policy, s.p2pServer.DAGParams, s.p2pServer.TxMemPool, s.p2pServer.DAG, s.p2pServer.TimeSource, s.p2pServer.SigCache) diff --git a/wire/msgblock.go b/wire/msgblock.go index 20d831ab3..74dee560d 100644 --- a/wire/msgblock.go +++ b/wire/msgblock.go @@ -21,12 +21,12 @@ import ( // backing array multiple times. const defaultTransactionAlloc = 2048 -// MaxBlockPayload is the maximum bytes a block message can be in bytes. -const MaxBlockPayload = 1000000 +// maxMassPerBlock is the maximum total transaction mass a block may contain. +const MaxMassPerBlock = 10000000 // maxTxPerBlock is the maximum number of transactions that could // possibly fit into a block. -const maxTxPerBlock = (MaxBlockPayload / minTxPayload) + 1 +const maxTxPerBlock = (MaxMassPerBlock / minTxPayload) + 1 // TxLoc holds locator data for the offset and length of where a transaction is // located within a MsgBlock data buffer. @@ -217,10 +217,7 @@ func (msg *MsgBlock) Command() string { // MaxPayloadLength returns the maximum length the payload can be for the // receiver. This is part of the Message interface implementation. func (msg *MsgBlock) MaxPayloadLength(pver uint32) uint32 { - // Block header at 80 bytes + transaction count + max transactions - // which can vary up to the MaxBlockPayload (including the block header - // and transaction count). - return MaxBlockPayload + return MaxMessagePayload } // BlockHash computes the block identifier hash for this block. diff --git a/wire/msgblock_test.go b/wire/msgblock_test.go index 0992bafd9..0c34ed8e3 100644 --- a/wire/msgblock_test.go +++ b/wire/msgblock_test.go @@ -40,8 +40,7 @@ func TestBlock(t *testing.T) { } // Ensure max payload is expected value for latest protocol version. - // Num addresses (varInt) + max allowed addresses. - wantPayload := uint32(1000000) + wantPayload := uint32(1024 * 1024 * 32) maxPayload := msg.MaxPayloadLength(pver) if maxPayload != wantPayload { t.Errorf("MaxPayloadLength: wrong max payload length for "+ diff --git a/wire/msgmerkleblock.go b/wire/msgmerkleblock.go index 9f451234b..fa193d047 100644 --- a/wire/msgmerkleblock.go +++ b/wire/msgmerkleblock.go @@ -133,7 +133,7 @@ func (msg *MsgMerkleBlock) Command() string { // MaxPayloadLength returns the maximum length the payload can be for the // receiver. This is part of the Message interface implementation. func (msg *MsgMerkleBlock) MaxPayloadLength(pver uint32) uint32 { - return MaxBlockPayload + return MaxMessagePayload } // NewMsgMerkleBlock returns a new bitcoin merkleblock message that conforms to diff --git a/wire/msgmerkleblock_test.go b/wire/msgmerkleblock_test.go index 30d4c45d5..16fef0ba7 100644 --- a/wire/msgmerkleblock_test.go +++ b/wire/msgmerkleblock_test.go @@ -38,8 +38,7 @@ func TestMerkleBlock(t *testing.T) { } // Ensure max payload is expected value for latest protocol version. - // Num addresses (varInt) + max allowed addresses. - wantPayload := uint32(1000000) + wantPayload := uint32(1024 * 1024 * 32) maxPayload := msg.MaxPayloadLength(pver) if maxPayload != wantPayload { t.Errorf("MaxPayloadLength: wrong max payload length for "+ diff --git a/wire/msgtx.go b/wire/msgtx.go index f434c806b..991236972 100644 --- a/wire/msgtx.go +++ b/wire/msgtx.go @@ -765,7 +765,7 @@ func (msg *MsgTx) Command() string { // MaxPayloadLength returns the maximum length the payload can be for the // receiver. This is part of the Message interface implementation. func (msg *MsgTx) MaxPayloadLength(pver uint32) uint32 { - return MaxBlockPayload + return MaxMessagePayload } // PkScriptLocs returns a slice containing the start of each public key script diff --git a/wire/msgtx_test.go b/wire/msgtx_test.go index 1844b367b..f73197ecf 100644 --- a/wire/msgtx_test.go +++ b/wire/msgtx_test.go @@ -37,7 +37,7 @@ func TestTx(t *testing.T) { } // Ensure max payload is expected value for latest protocol version. - wantPayload := uint32(1000 * 1000) + wantPayload := uint32(1024 * 1024 * 32) maxPayload := msg.MaxPayloadLength(pver) if maxPayload != wantPayload { t.Errorf("MaxPayloadLength: wrong max payload length for "+