diff --git a/data/pools/errors.go b/data/pools/errors.go new file mode 100644 index 0000000000..e465983fa9 --- /dev/null +++ b/data/pools/errors.go @@ -0,0 +1,47 @@ +// Copyright (C) 2019-2022 Algorand, Inc. +// This file is part of go-algorand +// +// go-algorand is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// go-algorand is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with go-algorand. If not, see . + +package pools + +import ( + "errors" + "fmt" + + "github.com/algorand/go-algorand/data/basics" +) + +// ErrStaleBlockAssemblyRequest returned by AssembleBlock when requested block number is older than the current transaction pool round +// i.e. typically it means that we're trying to make a proposal for an older round than what the ledger is currently pointing at. +var ErrStaleBlockAssemblyRequest = errors.New("AssembleBlock: requested block assembly specified a round that is older than current transaction pool round") + +// ErrPendingQueueReachedMaxCap indicates the current transaction pool has reached its max capacity +var ErrPendingQueueReachedMaxCap = errors.New("TransactionPool.checkPendingQueueSize: transaction pool have reached capacity") + +// ErrNoPendingBlockEvaluator indicates there is no pending block evaluator to accept a new tx group +var ErrNoPendingBlockEvaluator = errors.New("TransactionPool.ingest: no pending block evaluator") + +// ErrTxPoolFeeError is an error type for txpool fee escalation checks +type ErrTxPoolFeeError struct { + fee basics.MicroAlgos + feeThreshold uint64 + feePerByte uint64 + encodedLength int +} + +func (e *ErrTxPoolFeeError) Error() string { + return fmt.Sprintf("fee %d below threshold %d (%d per byte * %d bytes)", + e.fee, e.feeThreshold, e.feePerByte, e.encodedLength) +} diff --git a/data/pools/transactionPool.go b/data/pools/transactionPool.go index 4302d14a2d..a4670c3020 100644 --- a/data/pools/transactionPool.go +++ b/data/pools/transactionPool.go @@ -168,10 +168,6 @@ const ( generateBlockTransactionDuration = 2155 * time.Nanosecond ) -// ErrStaleBlockAssemblyRequest returned by AssembleBlock when requested block number is older than the current transaction pool round -// i.e. typically it means that we're trying to make a proposal for an older round than what the ledger is currently pointing at. -var ErrStaleBlockAssemblyRequest = fmt.Errorf("AssembleBlock: requested block assembly specified a round that is older than current transaction pool round") - // Reset resets the content of the transaction pool func (pool *TransactionPool) Reset() { pool.mu.Lock() @@ -291,7 +287,7 @@ func (pool *TransactionPool) checkPendingQueueSize(txnGroup []transactions.Signe return nil } } - return fmt.Errorf("TransactionPool.checkPendingQueueSize: transaction pool have reached capacity") + return ErrPendingQueueReachedMaxCap } return nil } @@ -360,8 +356,12 @@ func (pool *TransactionPool) checkSufficientFee(txgroup []transactions.SignedTxn for _, t := range txgroup { feeThreshold := feePerByte * uint64(t.GetEncodedLength()) if t.Txn.Fee.Raw < feeThreshold { - return fmt.Errorf("fee %d below threshold %d (%d per byte * %d bytes)", - t.Txn.Fee, feeThreshold, feePerByte, t.GetEncodedLength()) + return &ErrTxPoolFeeError{ + fee: t.Txn.Fee, + feeThreshold: feeThreshold, + feePerByte: feePerByte, + encodedLength: t.GetEncodedLength(), + } } } @@ -415,7 +415,7 @@ func (pool *TransactionPool) add(txgroup []transactions.SignedTxn, stats *teleme // while it waits for OnNewBlock() to be called. func (pool *TransactionPool) ingest(txgroup []transactions.SignedTxn, params poolIngestParams) error { if pool.pendingBlockEvaluator == nil { - return fmt.Errorf("TransactionPool.ingest: no pending block evaluator") + return ErrNoPendingBlockEvaluator } if !params.recomputing { @@ -427,7 +427,7 @@ func (pool *TransactionPool) ingest(txgroup []transactions.SignedTxn, params poo for pool.pendingBlockEvaluator.Round() <= latest && time.Now().Before(waitExpires) { condvar.TimedWait(&pool.cond, timeoutOnNewBlock) if pool.pendingBlockEvaluator == nil { - return fmt.Errorf("TransactionPool.ingest: no pending block evaluator") + return ErrNoPendingBlockEvaluator } } @@ -467,7 +467,7 @@ func (pool *TransactionPool) Remember(txgroup []transactions.SignedTxn) error { err := pool.remember(txgroup) if err != nil { - return fmt.Errorf("TransactionPool.Remember: %v", err) + return fmt.Errorf("TransactionPool.Remember: %w", err) } pool.rememberCommit(false) @@ -581,7 +581,7 @@ func (pool *TransactionPool) addToPendingBlockEvaluatorOnce(txgroup []transactio r := pool.pendingBlockEvaluator.Round() + pool.numPendingWholeBlocks for _, tx := range txgroup { if tx.Txn.LastValid < r { - return transactions.TxnDeadError{ + return &transactions.TxnDeadError{ Round: r, FirstValid: tx.Txn.FirstValid, LastValid: tx.Txn.LastValid, @@ -600,7 +600,7 @@ func (pool *TransactionPool) addToPendingBlockEvaluatorOnce(txgroup []transactio if recomputing { if !pool.assemblyResults.assemblyCompletedOrAbandoned { - transactionGroupDuration := time.Now().Sub(transactionGroupStartsTime) + transactionGroupDuration := time.Since(transactionGroupStartsTime) pool.assemblyMu.Lock() defer pool.assemblyMu.Unlock() if pool.assemblyRound > pool.pendingBlockEvaluator.Round() { @@ -630,7 +630,7 @@ func (pool *TransactionPool) addToPendingBlockEvaluatorOnce(txgroup []transactio } else { pool.assemblyResults.blk = lvb } - stats.BlockGenerationDuration = uint64(time.Now().Sub(blockGenerationStarts)) + stats.BlockGenerationDuration = uint64(time.Since(blockGenerationStarts)) pool.assemblyResults.stats = *stats pool.assemblyCond.Broadcast() } else { @@ -742,7 +742,7 @@ func (pool *TransactionPool) recomputeBlockEvaluator(committedTxIds map[transact case *ledgercore.TransactionInLedgerError: asmStats.CommittedCount++ stats.RemovedInvalidCount++ - case transactions.TxnDeadError: + case *transactions.TxnDeadError: if int(terr.LastValid-terr.FirstValid) > 20 { // cutoff value here is picked as a somewhat arbitrary cutoff trying to separate longer lived transactions from very short lived ones asmStats.ExpiredLongLivedCount++ @@ -753,7 +753,7 @@ func (pool *TransactionPool) recomputeBlockEvaluator(committedTxIds map[transact asmStats.LeaseErrorCount++ stats.RemovedInvalidCount++ pool.log.Infof("Cannot re-add pending transaction to pool: %v", err) - case transactions.MinFeeError: + case *transactions.MinFeeError: asmStats.MinFeeErrorCount++ stats.RemovedInvalidCount++ pool.log.Infof("Cannot re-add pending transaction to pool: %v", err) @@ -784,7 +784,7 @@ func (pool *TransactionPool) recomputeBlockEvaluator(committedTxIds map[transact } else { pool.assemblyResults.blk = lvb } - asmStats.BlockGenerationDuration = uint64(time.Now().Sub(blockGenerationStarts)) + asmStats.BlockGenerationDuration = uint64(time.Since(blockGenerationStarts)) pool.assemblyResults.stats = asmStats pool.assemblyCond.Broadcast() } @@ -835,7 +835,7 @@ func (pool *TransactionPool) AssembleBlock(round basics.Round, deadline time.Tim } // Measure time here because we want to know how close to deadline we are - dt := time.Now().Sub(start) + dt := time.Since(start) stats.Nanoseconds = dt.Nanoseconds() payset := assembled.Block().Payset @@ -906,7 +906,7 @@ func (pool *TransactionPool) AssembleBlock(round basics.Round, deadline time.Tim pool.assemblyDeadline = deadline pool.assemblyRound = round for time.Now().Before(deadline) && (!pool.assemblyResults.ok || pool.assemblyResults.roundStartedEvaluating != round) { - condvar.TimedWait(&pool.assemblyCond, deadline.Sub(time.Now())) + condvar.TimedWait(&pool.assemblyCond, time.Until(deadline)) } if !pool.assemblyResults.ok { @@ -919,7 +919,7 @@ func (pool *TransactionPool) AssembleBlock(round basics.Round, deadline time.Tim if pool.assemblyResults.roundStartedEvaluating > round { // this case is expected to happen only if the transaction pool was able to construct *two* rounds during the time we were trying to assemble the empty block. - // while this is extreamly unlikely, we need to handle this. the handling it quite straight-forward : + // while this is extremely unlikely, we need to handle this. the handling it quite straight-forward : // since the network is already ahead of us, there is no issue here in not generating a block ( since the block would get discarded anyway ) pool.log.Infof("AssembleBlock: requested round is behind transaction pool round after timing out %d < %d", round, pool.assemblyResults.roundStartedEvaluating) return nil, ErrStaleBlockAssemblyRequest @@ -927,7 +927,7 @@ func (pool *TransactionPool) AssembleBlock(round basics.Round, deadline time.Tim deadline = deadline.Add(assemblyWaitEps) for time.Now().Before(deadline) && (!pool.assemblyResults.ok || pool.assemblyResults.roundStartedEvaluating != round) { - condvar.TimedWait(&pool.assemblyCond, deadline.Sub(time.Now())) + condvar.TimedWait(&pool.assemblyCond, time.Until(deadline)) } // check to see if the extra time helped us to get a block. @@ -949,7 +949,7 @@ func (pool *TransactionPool) AssembleBlock(round basics.Round, deadline time.Tim if pool.assemblyResults.roundStartedEvaluating > round { // this scenario should not happen unless the txpool is receiving the new blocks via OnNewBlock // with "jumps" between consecutive blocks ( which is why it's a warning ) - // The "normal" usecase is evaluated on the top of the function. + // The "normal" use case is evaluated on the top of the function. pool.log.Warnf("AssembleBlock: requested round is behind transaction pool round %d < %d", round, pool.assemblyResults.roundStartedEvaluating) return nil, ErrStaleBlockAssemblyRequest } else if pool.assemblyResults.roundStartedEvaluating == round.SubSaturate(1) { diff --git a/data/pools/transactionPool_test.go b/data/pools/transactionPool_test.go index acbc5a9bc1..be0546c3b1 100644 --- a/data/pools/transactionPool_test.go +++ b/data/pools/transactionPool_test.go @@ -100,7 +100,7 @@ func mockLedger(t TestingT, initAccounts map[basics.Address]basics.AccountData, genesisInitState := ledgercore.InitState{Block: initBlock, Accounts: initAccounts, GenesisHash: hash} cfg := config.GetDefaultLocal() cfg.Archival = true - l, err := ledger.OpenLedger(logging.Base(), fn, true, genesisInitState, cfg) + l, err := ledger.OpenLedger(logging.Base(), fn, inMem, genesisInitState, cfg) require.NoError(t, err) return l } @@ -967,7 +967,7 @@ func TestTransactionPool_CurrentFeePerByte(t *testing.T) { Amount: basics.MicroAlgos{Raw: proto.MinBalance}, }, } - tx.Note = make([]byte, 8, 8) + tx.Note = make([]byte, 8) crypto.RandBytes(tx.Note) signedTx := tx.Sign(secrets[i]) err := transactionPool.RememberOne(signedTx) @@ -1018,7 +1018,7 @@ func BenchmarkTransactionPoolRememberOne(b *testing.B) { Amount: basics.MicroAlgos{Raw: proto.MinBalance}, }, } - tx.Note = make([]byte, 8, 8) + tx.Note = make([]byte, 8) crypto.RandBytes(tx.Note) signedTx := tx.Sign(secrets[i]) signedTransactions = append(signedTransactions, signedTx) @@ -1081,7 +1081,7 @@ func BenchmarkTransactionPoolPending(b *testing.B) { Amount: basics.MicroAlgos{Raw: proto.MinBalance}, }, } - tx.Note = make([]byte, 8, 8) + tx.Note = make([]byte, 8) crypto.RandBytes(tx.Note) signedTx := tx.Sign(secrets[i]) err := transactionPool.RememberOne(signedTx) @@ -1247,7 +1247,7 @@ func BenchmarkTransactionPoolSteadyState(b *testing.B) { Amount: basics.MicroAlgos{Raw: proto.MinBalance}, }, } - tx.Note = make([]byte, 8, 8) + tx.Note = make([]byte, 8) crypto.RandBytes(tx.Note) signedTx, err := transactions.AssembleSignedTxn(tx, crypto.Signature{}, crypto.MultisigSig{}) @@ -1453,7 +1453,7 @@ func TestStateProofLogging(t *testing.T) { require.NoError(t, err) b.BlockHeader.Branch = phdr.Hash() - eval, err := mockLedger.StartEvaluator(b.BlockHeader, 0, 10000) + _, err = mockLedger.StartEvaluator(b.BlockHeader, 0, 10000) require.NoError(t, err) // Simulate the blocks up to round 512 without any transactions @@ -1477,7 +1477,7 @@ func TestStateProofLogging(t *testing.T) { break } - eval, err = mockLedger.StartEvaluator(b.BlockHeader, 0, 10000) + _, err = mockLedger.StartEvaluator(b.BlockHeader, 0, 10000) require.NoError(t, err) } @@ -1520,7 +1520,7 @@ func TestStateProofLogging(t *testing.T) { require.NoError(t, err) // Add it to the transaction pool and assemble the block - eval, err = mockLedger.StartEvaluator(b.BlockHeader, 0, 1000000) + eval, err := mockLedger.StartEvaluator(b.BlockHeader, 0, 1000000) require.NoError(t, err) err = eval.Transaction(stxn, transactions.ApplyData{}) diff --git a/data/transactions/error.go b/data/transactions/error.go index a95deeb3f1..d29db15fc8 100644 --- a/data/transactions/error.go +++ b/data/transactions/error.go @@ -23,14 +23,16 @@ import ( ) // MinFeeError defines an error type which could be returned from the method WellFormed +//msgp:ignore MinFeeError type MinFeeError string -func (err MinFeeError) Error() string { - return string(err) +func (err *MinFeeError) Error() string { + return string(*err) } -func makeMinFeeErrorf(format string, args ...interface{}) MinFeeError { - return MinFeeError(fmt.Sprintf(format, args...)) +func makeMinFeeErrorf(format string, args ...interface{}) *MinFeeError { + err := MinFeeError(fmt.Sprintf(format, args...)) + return &err } // TxnDeadError defines an error type which indicates a transaction is outside of the @@ -41,6 +43,6 @@ type TxnDeadError struct { LastValid basics.Round } -func (err TxnDeadError) Error() string { +func (err *TxnDeadError) Error() string { return fmt.Sprintf("txn dead: round %d outside of %d--%d", err.Round, err.FirstValid, err.LastValid) } diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index 34db841c12..f89da449bc 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -778,11 +778,12 @@ func EvalApp(program []byte, gi int, aid basics.AppIndex, params *EvalParams) (b return pass, err } -// EvalSignature evaluates the logicsig of the ith transaction in params. +// EvalSignatureFull evaluates the logicsig of the ith transaction in params. // A program passes successfully if it finishes with one int element on the stack that is non-zero. -func EvalSignature(gi int, params *EvalParams) (pass bool, err error) { +// It returns EvalContext suitable for obtaining additional info about the execution. +func EvalSignatureFull(gi int, params *EvalParams) (pass bool, pcx *EvalContext, err error) { if params.SigLedger == nil { - return false, errors.New("no sig ledger in signature eval") + return false, nil, errors.New("no sig ledger in signature eval") } cx := EvalContext{ EvalParams: params, @@ -790,7 +791,15 @@ func EvalSignature(gi int, params *EvalParams) (pass bool, err error) { groupIndex: gi, txn: ¶ms.TxnGroup[gi], } - return eval(cx.txn.Lsig.Logic, &cx) + pass, err = eval(cx.txn.Lsig.Logic, &cx) + return pass, &cx, err +} + +// EvalSignature evaluates the logicsig of the ith transaction in params. +// A program passes successfully if it finishes with one int element on the stack that is non-zero. +func EvalSignature(gi int, params *EvalParams) (pass bool, err error) { + pass, _, err = EvalSignatureFull(gi, params) + return pass, err } // eval implementation @@ -997,6 +1006,11 @@ func boolToSV(x bool) stackValue { return stackValue{Uint: boolToUint(x)} } +// Cost return cost incurred so far +func (cx *EvalContext) Cost() int { + return cx.cost +} + func (cx *EvalContext) remainingBudget() int { if cx.runModeFlags == modeSig { return int(cx.Proto.LogicSigMaxCost) - cx.cost diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index 9fa1753734..1529186e33 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -389,9 +389,10 @@ byte base64 5rZMNsevs5sULO+54aN+OvU6lQ503z2X+SSYUABIx7E= ep := defaultEvalParams(txn) err := CheckSignature(0, ep) require.NoError(t, err) - pass, err := EvalSignature(0, ep) + pass, cx, err := EvalSignatureFull(0, ep) require.True(t, pass) require.NoError(t, err) + require.Greater(t, cx.Cost(), 0) }) } } @@ -454,10 +455,12 @@ func TestTLHC(t *testing.T) { t.Log(ep.Trace.String()) } require.NoError(t, err) - pass, err := EvalSignature(0, ep) + pass, cx, err := EvalSignatureFull(0, ep) if pass { t.Log(hex.EncodeToString(ops.Program)) t.Log(ep.Trace.String()) + require.Greater(t, cx.cost, 0) + require.Greater(t, cx.Cost(), 0) } require.False(t, pass) isNotPanic(t, err) diff --git a/data/transactions/msgp_gen.go b/data/transactions/msgp_gen.go index bcb87f64e0..641e541c7b 100644 --- a/data/transactions/msgp_gen.go +++ b/data/transactions/msgp_gen.go @@ -93,14 +93,6 @@ import ( // |-----> (*) Msgsize // |-----> (*) MsgIsZero // -// MinFeeError -// |-----> MarshalMsg -// |-----> CanMarshalMsg -// |-----> (*) UnmarshalMsg -// |-----> (*) CanUnmarshalMsg -// |-----> Msgsize -// |-----> MsgIsZero -// // OnCompletion // |-----> MarshalMsg // |-----> CanMarshalMsg @@ -3082,52 +3074,6 @@ func (z *LogicSig) MsgIsZero() bool { return (len((*z).Logic) == 0) && ((*z).Sig.MsgIsZero()) && ((*z).Msig.MsgIsZero()) && (len((*z).Args) == 0) } -// MarshalMsg implements msgp.Marshaler -func (z MinFeeError) MarshalMsg(b []byte) (o []byte) { - o = msgp.Require(b, z.Msgsize()) - o = msgp.AppendString(o, string(z)) - return -} - -func (_ MinFeeError) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(MinFeeError) - if !ok { - _, ok = (z).(*MinFeeError) - } - return ok -} - -// UnmarshalMsg implements msgp.Unmarshaler -func (z *MinFeeError) UnmarshalMsg(bts []byte) (o []byte, err error) { - { - var zb0001 string - zb0001, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - (*z) = MinFeeError(zb0001) - } - o = bts - return -} - -func (_ *MinFeeError) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*MinFeeError) - return ok -} - -// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z MinFeeError) Msgsize() (s int) { - s = msgp.StringPrefixSize + len(string(z)) - return -} - -// MsgIsZero returns whether this is a zero value -func (z MinFeeError) MsgIsZero() bool { - return z == "" -} - // MarshalMsg implements msgp.Marshaler func (z OnCompletion) MarshalMsg(b []byte) (o []byte) { o = msgp.Require(b, z.Msgsize()) diff --git a/data/transactions/transaction.go b/data/transactions/transaction.go index 62b615f542..8aa93740c7 100644 --- a/data/transactions/transaction.go +++ b/data/transactions/transaction.go @@ -235,7 +235,7 @@ func (tx Header) Alive(tc TxnContext) error { // Check round validity round := tc.Round() if round < tx.FirstValid || round > tx.LastValid { - return TxnDeadError{ + return &TxnDeadError{ Round: round, FirstValid: tx.FirstValid, LastValid: tx.LastValid, diff --git a/data/transactions/verify/txn.go b/data/transactions/verify/txn.go index b3c71e4a2b..4dc8bc6563 100644 --- a/data/transactions/verify/txn.go +++ b/data/transactions/verify/txn.go @@ -36,6 +36,7 @@ import ( var logicGoodTotal = metrics.MakeCounter(metrics.MetricName{Name: "algod_ledger_logic_ok", Description: "Total transaction scripts executed and accepted"}) var logicRejTotal = metrics.MakeCounter(metrics.MetricName{Name: "algod_ledger_logic_rej", Description: "Total transaction scripts executed and rejected"}) var logicErrTotal = metrics.MakeCounter(metrics.MetricName{Name: "algod_ledger_logic_err", Description: "Total transaction scripts executed and errored"}) +var logicCostTotal = metrics.MakeCounter(metrics.MetricName{Name: "algod_ledger_logic_cost", Description: "Total cost of transaction scripts executed"}) var msigLessOrEqual4 = metrics.MakeCounter(metrics.MetricName{Name: "algod_verify_msig_4", Description: "Total transactions with 1-4 msigs"}) var msigLessOrEqual10 = metrics.MakeCounter(metrics.MetricName{Name: "algod_verify_msig_5_10", Description: "Total transactions with 5-10 msigs"}) var msigMore10 = metrics.MakeCounter(metrics.MetricName{Name: "algod_verify_msig_16", Description: "Total transactions with 11+ msigs"}) @@ -104,20 +105,20 @@ const ( TxGroupErrorReasonNumValues ) -// ErrTxGroupError is an error from txn pre-validation (well form-ness, signature format, etc). +// TxGroupError is an error from txn pre-validation (well form-ness, signature format, etc). // It can be unwrapped into underlying error, as well as has a specific failure reason code. -type ErrTxGroupError struct { +type TxGroupError struct { err error Reason TxGroupErrorReason } // Error returns an error message from the underlying error -func (e *ErrTxGroupError) Error() string { +func (e *TxGroupError) Error() string { return e.err.Error() } // Unwrap returns an underlying error -func (e *ErrTxGroupError) Unwrap() error { +func (e *TxGroupError) Unwrap() error { return e.err } @@ -154,13 +155,13 @@ func (g *GroupContext) Equal(other *GroupContext) bool { // txnBatchPrep verifies a SignedTxn having no obviously inconsistent data. // Block-assembly time checks of LogicSig and accounting rules may still block the txn. // It is the caller responsibility to call batchVerifier.Verify(). -func txnBatchPrep(s *transactions.SignedTxn, txnIdx int, groupCtx *GroupContext, verifier *crypto.BatchVerifier) *ErrTxGroupError { +func txnBatchPrep(s *transactions.SignedTxn, txnIdx int, groupCtx *GroupContext, verifier *crypto.BatchVerifier) *TxGroupError { if !groupCtx.consensusParams.SupportRekeying && (s.AuthAddr != basics.Address{}) { - return &ErrTxGroupError{err: errRekeyingNotSupported, Reason: TxGroupErrorReasonGeneric} + return &TxGroupError{err: errRekeyingNotSupported, Reason: TxGroupErrorReasonGeneric} } if err := s.Txn.WellFormed(groupCtx.specAddrs, groupCtx.consensusParams); err != nil { - return &ErrTxGroupError{err: err, Reason: TxGroupErrorReasonNotWellFormed} + return &TxGroupError{err: err, Reason: TxGroupErrorReasonNotWellFormed} } return stxnCoreChecks(s, txnIdx, groupCtx, verifier) @@ -209,14 +210,14 @@ func txnGroupBatchPrep(stxs []transactions.SignedTxn, contextHdr bookkeeping.Blo } feeNeeded, overflow := basics.OMul(groupCtx.consensusParams.MinTxnFee, minFeeCount) if overflow { - err = &ErrTxGroupError{err: errTxGroupInvalidFee, Reason: TxGroupErrorReasonInvalidFee} + err = &TxGroupError{err: errTxGroupInvalidFee, Reason: TxGroupErrorReasonInvalidFee} return nil, err } // feesPaid may have saturated. That's ok. Since we know // feeNeeded did not overflow, simple comparison tells us // feesPaid was enough. if feesPaid < feeNeeded { - err = &ErrTxGroupError{ + err = &TxGroupError{ err: fmt.Errorf( "txgroup had %d in fees, which is less than the minimum %d * %d", feesPaid, minFeeCount, groupCtx.consensusParams.MinTxnFee), @@ -229,7 +230,7 @@ func txnGroupBatchPrep(stxs []transactions.SignedTxn, contextHdr bookkeeping.Blo } // stxnCoreChecks runs signatures validity checks and enqueues signature into batchVerifier for verification. -func stxnCoreChecks(s *transactions.SignedTxn, txnIdx int, groupCtx *GroupContext, batchVerifier *crypto.BatchVerifier) *ErrTxGroupError { +func stxnCoreChecks(s *transactions.SignedTxn, txnIdx int, groupCtx *GroupContext, batchVerifier *crypto.BatchVerifier) *TxGroupError { numSigs := 0 hasSig := false hasMsig := false @@ -254,10 +255,10 @@ func stxnCoreChecks(s *transactions.SignedTxn, txnIdx int, groupCtx *GroupContex if s.Txn.Sender == transactions.StateProofSender && s.Txn.Type == protocol.StateProofTx { return nil } - return &ErrTxGroupError{err: errTxnSigHasNoSig, Reason: TxGroupErrorReasonHasNoSig} + return &TxGroupError{err: errTxnSigHasNoSig, Reason: TxGroupErrorReasonHasNoSig} } if numSigs > 1 { - return &ErrTxGroupError{err: errTxnSigNotWellFormed, Reason: TxGroupErrorReasonSigNotWellFormed} + return &TxGroupError{err: errTxnSigNotWellFormed, Reason: TxGroupErrorReasonSigNotWellFormed} } if hasSig { @@ -266,7 +267,7 @@ func stxnCoreChecks(s *transactions.SignedTxn, txnIdx int, groupCtx *GroupContex } if hasMsig { if err := crypto.MultisigBatchPrep(s.Txn, crypto.Digest(s.Authorizer()), s.Msig, batchVerifier); err != nil { - return &ErrTxGroupError{err: fmt.Errorf("multisig validation failed: %w", err), Reason: TxGroupErrorReasonMsigNotWellFormed} + return &TxGroupError{err: fmt.Errorf("multisig validation failed: %w", err), Reason: TxGroupErrorReasonMsigNotWellFormed} } counter := 0 for _, subsigi := range s.Msig.Subsigs { @@ -285,11 +286,11 @@ func stxnCoreChecks(s *transactions.SignedTxn, txnIdx int, groupCtx *GroupContex } if hasLogicSig { if err := logicSigVerify(s, txnIdx, groupCtx); err != nil { - return &ErrTxGroupError{err: err, Reason: TxGroupErrorReasonLogicSigFailed} + return &TxGroupError{err: err, Reason: TxGroupErrorReasonLogicSigFailed} } return nil } - return &ErrTxGroupError{err: errUnknownSignature, Reason: TxGroupErrorReasonGeneric} + return &TxGroupError{err: errUnknownSignature, Reason: TxGroupErrorReasonGeneric} } // LogicSigSanityCheck checks that the signature is valid and that the program is basically well formed. @@ -404,7 +405,7 @@ func logicSigVerify(txn *transactions.SignedTxn, groupIndex int, groupCtx *Group MinAvmVersion: &groupCtx.minAvmVersion, SigLedger: groupCtx.ledger, } - pass, err := logic.EvalSignature(groupIndex, &ep) + pass, cx, err := logic.EvalSignatureFull(groupIndex, &ep) if err != nil { logicErrTotal.Inc(nil) return fmt.Errorf("transaction %v: rejected by logic err=%v", txn.ID(), err) @@ -414,6 +415,7 @@ func logicSigVerify(txn *transactions.SignedTxn, groupIndex int, groupCtx *Group return fmt.Errorf("transaction %v: rejected by logic", txn.ID()) } logicGoodTotal.Inc(nil) + logicCostTotal.AddUint64(uint64(cx.Cost()), nil) return nil } diff --git a/data/transactions/verify/txn_test.go b/data/transactions/verify/txn_test.go index 8988f7aeae..3e23c8f352 100644 --- a/data/transactions/verify/txn_test.go +++ b/data/transactions/verify/txn_test.go @@ -609,7 +609,7 @@ func TestTxnGroupCacheUpdateFailLogic(t *testing.T) { _, signedTxn, _, _ := generateTestObjects(100, 20, 50) blkHdr := createDummyBlockHeader() - // sign the transcation with logic + // sign the transaction with logic for i := 0; i < len(signedTxn); i++ { // add a simple logic that verifies this condition: // sha256(arg0) == base64decode(5rZMNsevs5sULO+54aN+OvU6lQ503z2X+SSYUABIx7E=) @@ -637,8 +637,10 @@ byte base64 5rZMNsevs5sULO+54aN+OvU6lQ503z2X+SSYUABIx7E= restoreSignatureFunc := func(txn *transactions.SignedTxn) { txn.Lsig.Args[0][0]-- } + initCounter := logicCostTotal.GetUint64Value() verifyGroup(t, txnGroups, blkHdr, breakSignatureFunc, restoreSignatureFunc, "rejected by logic") - + currentCounter := logicCostTotal.GetUint64Value() + require.Greater(t, currentCounter, initCounter) } // TestTxnGroupCacheUpdateLogicWithSig makes sure that a payment transaction contains logicsig signed with single signature is valid (and added to the cache) only diff --git a/data/txHandler.go b/data/txHandler.go index 118f0979b3..eaa235652a 100644 --- a/data/txHandler.go +++ b/data/txHandler.go @@ -31,6 +31,7 @@ import ( "github.com/algorand/go-algorand/data/pools" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/data/transactions/verify" + "github.com/algorand/go-algorand/ledger/ledgercore" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/network" "github.com/algorand/go-algorand/protocol" @@ -66,6 +67,25 @@ var transactionGroupTxSyncHandled = metrics.MakeCounter(metrics.TransactionGroup var transactionGroupTxSyncRemember = metrics.MakeCounter(metrics.TransactionGroupTxSyncRemember) var transactionGroupTxSyncAlreadyCommitted = metrics.MakeCounter(metrics.TransactionGroupTxSyncAlreadyCommitted) +var transactionMessageTxPoolRememberCounter = metrics.NewTagCounter( + "algod_transaction_messages_txpool_remember_{TAG}", "Number of transaction messages not remembered by txpool b/c if {TAG}", + txPoolRememberTagCap, txPoolRememberPendingEval, txPoolRememberTagNoSpace, txPoolRememberTagFee, txPoolRememberTagTxnDead, txPoolRememberTagTooLarge, txPoolRememberTagGroupID, + txPoolRememberTagTxID, txPoolRememberTagLease, txPoolRememberTagEvalGeneric, +) + +const ( + txPoolRememberTagCap = "cap" + txPoolRememberPendingEval = "pending_eval" + txPoolRememberTagNoSpace = "no_space" + txPoolRememberTagFee = "fee" + txPoolRememberTagTxnDead = "txn_dead" + txPoolRememberTagTooLarge = "too_large" + txPoolRememberTagGroupID = "groupid" + txPoolRememberTagTxID = "txid" + txPoolRememberTagLease = "lease" + txPoolRememberTagEvalGeneric = "eval" +) + // The txBacklogMsg structure used to track a single incoming transaction from the gossip network, type txBacklogMsg struct { rawmsg *network.IncomingMessage // the raw message from the network @@ -231,7 +251,7 @@ func (handler *TxHandler) postProcessReportErrors(err error) { return } - var txGroupErr *verify.ErrTxGroupError + var txGroupErr *verify.TxGroupError if errors.As(err, &txGroupErr) { switch txGroupErr.Reason { case verify.TxGroupErrorReasonNotWellFormed: @@ -254,6 +274,56 @@ func (handler *TxHandler) postProcessReportErrors(err error) { } } +func (handler *TxHandler) rememberReportErrors(err error) { + if errors.Is(err, pools.ErrPendingQueueReachedMaxCap) { + transactionMessageTxPoolRememberCounter.Add(txPoolRememberTagCap, 1) + return + } + + if errors.Is(err, pools.ErrNoPendingBlockEvaluator) { + transactionMessageTxPoolRememberCounter.Add(txPoolRememberPendingEval, 1) + return + } + + if errors.Is(err, ledgercore.ErrNoSpace) { + transactionMessageTxPoolRememberCounter.Add(txPoolRememberTagNoSpace, 1) + return + } + + // it is possible to call errors.As but it requires additional allocations + // instead, unwrap and type assert. + underlyingErr := errors.Unwrap(err) + if underlyingErr == nil { + // something went wrong + return + } + + switch err := underlyingErr.(type) { + case *pools.ErrTxPoolFeeError: + transactionMessageTxPoolRememberCounter.Add(txPoolRememberTagFee, 1) + return + case *transactions.TxnDeadError: + transactionMessageTxPoolRememberCounter.Add(txPoolRememberTagTxnDead, 1) + return + case *ledgercore.TransactionInLedgerError: + transactionMessageTxPoolRememberCounter.Add(txPoolRememberTagTxID, 1) + return + case *ledgercore.LeaseInLedgerError: + transactionMessageTxPoolRememberCounter.Add(txPoolRememberTagLease, 1) + return + case *ledgercore.TxGroupMalformedError: + switch err.Reason { + case ledgercore.TxGroupMalformedErrorReasonExceedMaxSize: + transactionMessageTxPoolRememberCounter.Add(txPoolRememberTagTooLarge, 1) + default: + transactionMessageTxPoolRememberCounter.Add(txPoolRememberTagGroupID, 1) + } + return + } + + transactionMessageTxPoolRememberCounter.Add(txPoolRememberTagEvalGeneric, 1) +} + func (handler *TxHandler) postProcessCheckedTxn(wi *txBacklogMsg) { if wi.verificationErr != nil { // disconnect from peer. @@ -272,6 +342,7 @@ func (handler *TxHandler) postProcessCheckedTxn(wi *txBacklogMsg) { // save the transaction, if it has high enough fee and not already in the cache err := handler.txPool.Remember(verifiedTxGroup) if err != nil { + handler.rememberReportErrors(err) logging.Base().Debugf("could not remember tx: %v", err) return } diff --git a/data/txHandler_test.go b/data/txHandler_test.go index dd9f5fea1f..3b33406b71 100644 --- a/data/txHandler_test.go +++ b/data/txHandler_test.go @@ -33,6 +33,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/components/mocks" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" @@ -41,12 +42,15 @@ import ( "github.com/algorand/go-algorand/data/pools" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/data/transactions/verify" + realledger "github.com/algorand/go-algorand/ledger" + "github.com/algorand/go-algorand/ledger/ledgercore" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/network" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" "github.com/algorand/go-algorand/util/execpool" "github.com/algorand/go-algorand/util/metrics" + "github.com/algorand/go-deadlock" ) @@ -1525,13 +1529,13 @@ func TestTxHandlerPostProcessError(t *testing.T) { continue } - errTxGroup := &verify.ErrTxGroupError{Reason: i} + errTxGroup := &verify.TxGroupError{Reason: i} txh.postProcessReportErrors(errTxGroup) result = collect() if i == verify.TxGroupErrorReasonSigNotWellFormed { // TxGroupErrorReasonSigNotWellFormed and TxGroupErrorReasonHasNoSig increment the same metric counter-- - require.Equal(t, result[metrics.TransactionMessagesTxnSigNotWellFormed.Name], float64(2)) + require.Equal(t, float64(2), result[metrics.TransactionMessagesTxnSigNotWellFormed.Name]) } require.Len(t, result, counter) counter++ @@ -1561,7 +1565,7 @@ func TestTxHandlerPostProcessErrorWithVerify(t *testing.T) { }, } _, err := verify.TxnGroup([]transactions.SignedTxn{stxn}, hdr, nil, nil) - var txGroupErr *verify.ErrTxGroupError + var txGroupErr *verify.TxGroupError require.ErrorAs(t, err, &txGroupErr) result := map[string]float64{} @@ -1573,3 +1577,230 @@ func TestTxHandlerPostProcessErrorWithVerify(t *testing.T) { transactionMessagesTxnNotWellFormed.AddMetric(result) require.Len(t, result, 1) } + +// TestTxHandlerRememberReportErrors checks Is and As statements work as expected +func TestTxHandlerRememberReportErrors(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + var txh TxHandler + result := map[string]float64{} + + getMetricName := func(tag string) string { + return strings.ReplaceAll(transactionMessageTxPoolRememberCounter.Name, "{TAG}", tag) + } + getMetricCounter := func(tag string) int { + transactionMessageTxPoolRememberCounter.AddMetric(result) + return int(result[getMetricName(tag)]) + } + + noSpaceErr := ledgercore.ErrNoSpace + txh.rememberReportErrors(noSpaceErr) + transactionMessageTxPoolRememberCounter.AddMetric(result) + require.Equal(t, 1, getMetricCounter(txPoolRememberTagNoSpace)) + + wrapped := fmt.Errorf("wrap: %w", noSpaceErr) // simulate wrapping + txh.rememberReportErrors(wrapped) + + transactionMessageTxPoolRememberCounter.AddMetric(result) + require.Equal(t, 2, getMetricCounter(txPoolRememberTagNoSpace)) + + feeErr := pools.ErrTxPoolFeeError{} + wrapped = fmt.Errorf("wrap: %w", &feeErr) // simulate wrapping + txh.rememberReportErrors(wrapped) + + transactionMessageTxPoolRememberCounter.AddMetric(result) + require.Equal(t, 1, getMetricCounter(txPoolRememberTagFee)) +} + +type blockTicker struct { + cond sync.Cond +} + +func (t *blockTicker) OnNewBlock(block bookkeeping.Block, delta ledgercore.StateDelta) { + t.cond.L.Lock() + defer t.cond.L.Unlock() + t.cond.Broadcast() +} + +func (t *blockTicker) Wait() { + t.cond.L.Lock() + defer t.cond.L.Unlock() + t.cond.Wait() +} + +func TestTxHandlerRememberReportErrorsWithTxPool(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + result := map[string]float64{} + getMetricName := func(tag string) string { + return strings.ReplaceAll(transactionMessageTxPoolRememberCounter.Name, "{TAG}", tag) + } + getMetricCounter := func(tag string) int { + transactionMessageTxPoolRememberCounter.AddMetric(result) + return int(result[getMetricName(tag)]) + } + + log := logging.TestingLog(t) + log.SetLevel(logging.Warn) + + const numAccts = 2 + genesis := make(map[basics.Address]basics.AccountData, numAccts+1) + addresses := make([]basics.Address, numAccts) + secrets := make([]*crypto.SignatureSecrets, numAccts) + + for i := 0; i < numAccts; i++ { + secret := keypair() + addr := basics.Address(secret.SignatureVerifier) + secrets[i] = secret + addresses[i] = addr + genesis[addr] = basics.AccountData{ + Status: basics.Online, + MicroAlgos: basics.MicroAlgos{Raw: 10000000000000}, + } + } + genesis[poolAddr] = basics.AccountData{ + Status: basics.NotParticipating, + MicroAlgos: basics.MicroAlgos{Raw: config.Consensus[protocol.ConsensusCurrentVersion].MinBalance}, + } + + genBal := bookkeeping.MakeGenesisBalances(genesis, sinkAddr, poolAddr) + + ledgerName := fmt.Sprintf("%s-mem-%d", t.Name(), rand.Int()) + const inMem = true + cfg := config.GetDefaultLocal() + cfg.Archival = true + cfg.TxPoolSize = config.MaxTxGroupSize + 1 + ledger, err := LoadLedger(log, ledgerName, inMem, protocol.ConsensusCurrentVersion, genBal, genesisID, genesisHash, nil, cfg) + require.NoError(t, err) + + handler := makeTestTxHandler(ledger, cfg) + // since Start is not called, set the context here + handler.ctx, handler.ctxCancel = context.WithCancel(context.Background()) + defer handler.ctxCancel() + + var wi txBacklogMsg + wi.unverifiedTxGroup = []transactions.SignedTxn{{}} + handler.postProcessCheckedTxn(&wi) + require.Equal(t, 1, getMetricCounter(txPoolRememberTagTxnDead)) + + // trigger max pool capacity metric + hdr := bookkeeping.BlockHeader{ + Round: 1, + UpgradeState: bookkeeping.UpgradeState{ + CurrentProtocol: protocol.ConsensusCurrentVersion, + }, + } + + txn1 := transactions.Transaction{ + Type: protocol.PaymentTx, + Header: transactions.Header{ + Sender: addresses[0], + Fee: basics.MicroAlgos{Raw: proto.MinTxnFee * 2}, + FirstValid: 0, + LastValid: basics.Round(proto.MaxTxnLife), + GenesisHash: genesisHash, + }, + PaymentTxnFields: transactions.PaymentTxnFields{ + Receiver: poolAddr, + Amount: basics.MicroAlgos{Raw: mockBalancesMinBalance + (rand.Uint64() % 10000)}, + }, + } + + wi.unverifiedTxGroup = []transactions.SignedTxn{txn1.Sign(secrets[0])} + for i := 0; i <= cfg.TxPoolSize; i++ { + txn := txn1 + crypto.RandBytes(txn.Note[:]) + wi.unverifiedTxGroup = append(wi.unverifiedTxGroup, txn.Sign(secrets[0])) + } + handler.postProcessCheckedTxn(&wi) + require.Equal(t, 1, getMetricCounter(txPoolRememberTagCap)) + + // trigger group id error + txn2 := txn1 + crypto.RandBytes(txn2.Group[:]) + wi.unverifiedTxGroup = []transactions.SignedTxn{txn1.Sign(secrets[0]), txn2.Sign(secrets[0])} + handler.postProcessCheckedTxn(&wi) + require.Equal(t, 1, getMetricCounter(txPoolRememberTagGroupID)) + + // trigger group too large error + wi.unverifiedTxGroup = []transactions.SignedTxn{txn1.Sign(secrets[0])} + for i := 0; i < config.MaxTxGroupSize; i++ { + txn := txn1 + crypto.RandBytes(txn.Note[:]) + wi.unverifiedTxGroup = append(wi.unverifiedTxGroup, txn.Sign(secrets[0])) + } + handler.postProcessCheckedTxn(&wi) + require.Equal(t, 1, getMetricCounter(txPoolRememberTagTooLarge)) + + // trigger eval error + secret := keypair() + addr := basics.Address(secret.SignatureVerifier) + txn2 = txn1 + txn2.Sender = addr + wi.unverifiedTxGroup = []transactions.SignedTxn{txn2.Sign(secret)} + handler.postProcessCheckedTxn(&wi) + require.Equal(t, 1, getMetricCounter(txPoolRememberTagEvalGeneric)) + + // trigger TxnDeadErr from the evaluator + txn2 = txn1 + txn2.FirstValid = ledger.LastRound() + 10 + prevTxnDead := getMetricCounter(txPoolRememberTagTxnDead) + wi.unverifiedTxGroup = []transactions.SignedTxn{txn2.Sign(secrets[0])} + handler.postProcessCheckedTxn(&wi) + require.Equal(t, prevTxnDead+1, getMetricCounter(txPoolRememberTagTxnDead)) + + // trigger TransactionInLedgerError (txid) error + wi.unverifiedTxGroup = []transactions.SignedTxn{txn1.Sign(secrets[0])} + wi.rawmsg = &network.IncomingMessage{} + handler.postProcessCheckedTxn(&wi) + handler.postProcessCheckedTxn(&wi) + require.Equal(t, 1, getMetricCounter(txPoolRememberTagTxID)) + + // trigger LeaseInLedgerError (lease) error + txn2 = txn1 + crypto.RandBytes(txn2.Lease[:]) + txn3 := txn2 + txn3.Receiver = addr + wi.unverifiedTxGroup = []transactions.SignedTxn{txn2.Sign(secrets[0])} + handler.postProcessCheckedTxn(&wi) + wi.unverifiedTxGroup = []transactions.SignedTxn{txn3.Sign(secrets[0])} + handler.postProcessCheckedTxn(&wi) + require.Equal(t, 1, getMetricCounter(txPoolRememberTagLease)) + + // TODO: not sure how to trigger fee error - need to return ErrNoSpace from ledger + // trigger pool fee error + // txn1.Fee = basics.MicroAlgos{Raw: proto.MinTxnFee / 2} + // wi.unverifiedTxGroup = []transactions.SignedTxn{txn1.Sign(secrets[0])} + // handler.postProcessCheckedTxn(&wi) + // require.Equal(t, 1, getMetricCounter(txPoolRememberFee)) + + // make an invalid block to fail recompute pool and expose transactionMessageTxGroupRememberNoPendingEval metric + blockTicker := &blockTicker{cond: *sync.NewCond(&deadlock.Mutex{})} + blockListeners := []realledger.BlockListener{ + handler.txPool, + blockTicker, + } + ledger.RegisterBlockListeners(blockListeners) + + hdr = bookkeeping.BlockHeader{ + Round: 1, + UpgradeState: bookkeeping.UpgradeState{ + CurrentProtocol: "test", + }, + } + + blk := bookkeeping.Block{ + BlockHeader: hdr, + Payset: []transactions.SignedTxnInBlock{{}}, + } + vb := ledgercore.MakeValidatedBlock(blk, ledgercore.StateDelta{}) + err = ledger.AddValidatedBlock(vb, agreement.Certificate{}) + require.NoError(t, err) + blockTicker.Wait() + + wi.unverifiedTxGroup = []transactions.SignedTxn{} + handler.postProcessCheckedTxn(&wi) + require.Equal(t, 1, getMetricCounter(txPoolRememberPendingEval)) +} diff --git a/ledger/internal/eval.go b/ledger/internal/eval.go index ae0a0797e3..7c8cd60524 100644 --- a/ledger/internal/eval.go +++ b/ledger/internal/eval.go @@ -926,7 +926,10 @@ func (eval *BlockEvaluator) transactionGroup(txgroup []transactions.SignedTxnWit } if len(txgroup) > eval.proto.MaxTxGroupSize { - return fmt.Errorf("group size %d exceeds maximum %d", len(txgroup), eval.proto.MaxTxGroupSize) + return &ledgercore.TxGroupMalformedError{ + Msg: fmt.Sprintf("group size %d exceeds maximum %d", len(txgroup), eval.proto.MaxTxGroupSize), + Reason: ledgercore.TxGroupMalformedErrorReasonExceedMaxSize, + } } var txibs []transactions.SignedTxnInBlock @@ -959,8 +962,11 @@ func (eval *BlockEvaluator) transactionGroup(txgroup []transactions.SignedTxnWit // Make sure all transactions in group have the same group value if txad.SignedTxn.Txn.Group != txgroup[0].SignedTxn.Txn.Group { - return fmt.Errorf("transactionGroup: inconsistent group values: %v != %v", - txad.SignedTxn.Txn.Group, txgroup[0].SignedTxn.Txn.Group) + return &ledgercore.TxGroupMalformedError{ + Msg: fmt.Sprintf("transactionGroup: inconsistent group values: %v != %v", + txad.SignedTxn.Txn.Group, txgroup[0].SignedTxn.Txn.Group), + Reason: ledgercore.TxGroupMalformedErrorReasonInconsistentGroupID, + } } if !txad.SignedTxn.Txn.Group.IsZero() { @@ -969,15 +975,21 @@ func (eval *BlockEvaluator) transactionGroup(txgroup []transactions.SignedTxnWit group.TxGroupHashes = append(group.TxGroupHashes, crypto.Digest(txWithoutGroup.ID())) } else if len(txgroup) > 1 { - return fmt.Errorf("transactionGroup: [%d] had zero Group but was submitted in a group of %d", gi, len(txgroup)) + return &ledgercore.TxGroupMalformedError{ + Msg: fmt.Sprintf("transactionGroup: [%d] had zero Group but was submitted in a group of %d", gi, len(txgroup)), + Reason: ledgercore.TxGroupMalformedErrorReasonEmptyGroupID, + } } } // If we had a non-zero Group value, check that all group members are present. if group.TxGroupHashes != nil { if txgroup[0].SignedTxn.Txn.Group != crypto.HashObj(group) { - return fmt.Errorf("transactionGroup: incomplete group: %v != %v (%v)", - txgroup[0].SignedTxn.Txn.Group, crypto.HashObj(group), group) + return &ledgercore.TxGroupMalformedError{ + Msg: fmt.Sprintf("transactionGroup: incomplete group: %v != %v (%v)", + txgroup[0].SignedTxn.Txn.Group, crypto.HashObj(group), group), + Reason: ledgercore.TxGroupMalformedErrorReasonIncompleteGroup, + } } } diff --git a/ledger/ledgercore/error.go b/ledger/ledgercore/error.go index 5ce0898b7e..05eccfa8f2 100644 --- a/ledger/ledgercore/error.go +++ b/ledger/ledgercore/error.go @@ -107,3 +107,30 @@ type ErrNonSequentialBlockEval struct { func (err ErrNonSequentialBlockEval) Error() string { return fmt.Sprintf("block evaluation for round %d requires sequential evaluation while the latest round is %d", err.EvaluatorRound, err.LatestRound) } + +// TxGroupMalformedErrorReasonCode is a reason code for TxGroupMalformed +//msgp:ignore TxGroupMalformedErrorReasonCode +type TxGroupMalformedErrorReasonCode int + +const ( + // TxGroupMalformedErrorReasonGeneric is a generic (not specific) reason code + TxGroupMalformedErrorReasonGeneric TxGroupMalformedErrorReasonCode = iota + // TxGroupMalformedErrorReasonExceedMaxSize indicates too large txgroup + TxGroupMalformedErrorReasonExceedMaxSize + // TxGroupMalformedErrorReasonInconsistentGroupID indicates different group IDs in a txgroup + TxGroupMalformedErrorReasonInconsistentGroupID + // TxGroupMalformedErrorReasonEmptyGroupID is for empty group ID but multiple transactions in a txgroup + TxGroupMalformedErrorReasonEmptyGroupID + // TxGroupMalformedErrorReasonIncompleteGroup indicates expected group ID does not match to provided + TxGroupMalformedErrorReasonIncompleteGroup +) + +// TxGroupMalformedError indicates txgroup has group ID problems or too large +type TxGroupMalformedError struct { + Msg string + Reason TxGroupMalformedErrorReasonCode +} + +func (e *TxGroupMalformedError) Error() string { + return e.Msg +}