diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 4cae2ba98..4e45d2aa9 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1268,11 +1268,97 @@ func (bc *Blockchain) verifyTx(t *transaction.Transaction, block *block.Block) e if bc.dao.IsDoubleClaim(claim) { return errors.New("double claim") } + if err := bc.verifyClaims(t); err != nil { + return err + } } return bc.verifyTxWitnesses(t, block) } +func (bc *Blockchain) verifyClaims(tx *transaction.Transaction) (err error) { + t := tx.Data.(*transaction.ClaimTX) + var result *transaction.Result + results := bc.GetTransactionResults(tx) + for i := range results { + if results[i].AssetID == UtilityTokenID() { + result = results[i] + break + } + } + + if result == nil || result.Amount.GreaterThan(0) { + return errors.New("invalid output in claim tx") + } + + bonus, err := bc.calculateBonus(t.Claims) + if err == nil && bonus != -result.Amount { + return fmt.Errorf("wrong bonus calculated in claim tx: %s != %s", + bonus.String(), (-result.Amount).String()) + } + + return err +} + +func (bc *Blockchain) calculateBonus(claims []transaction.Input) (util.Fixed8, error) { + unclaimed := []*spentCoin{} + inputs := transaction.GroupInputsByPrevHash(claims) + + for _, group := range inputs { + h := group[0].PrevHash + claimable, err := bc.getUnclaimed(h) + if err != nil || len(claimable) == 0 { + return 0, errors.New("no unclaimed inputs") + } + + for _, c := range group { + s, ok := claimable[c.PrevIndex] + if !ok { + return 0, fmt.Errorf("can't find spent coins for %s (%d)", c.PrevHash.StringLE(), c.PrevIndex) + } + unclaimed = append(unclaimed, s) + } + } + + return bc.calculateBonusInternal(unclaimed) +} + +func (bc *Blockchain) calculateBonusInternal(scs []*spentCoin) (util.Fixed8, error) { + var claimed util.Fixed8 + for _, sc := range scs { + gen, sys, err := bc.CalculateClaimable(sc.Output.Amount, sc.StartHeight, sc.EndHeight) + if err != nil { + return 0, err + } + claimed += gen + sys + } + + return claimed, nil +} + +func (bc *Blockchain) getUnclaimed(h util.Uint256) (map[uint16]*spentCoin, error) { + tx, txHeight, err := bc.GetTransaction(h) + if err != nil { + return nil, err + } + + scs, err := bc.dao.GetSpentCoinState(h) + if err != nil { + return nil, err + } + + result := make(map[uint16]*spentCoin) + for i, height := range scs.items { + result[i] = &spentCoin{ + Output: &tx.Outputs[i], + StartHeight: txHeight, + EndHeight: height, + } + } + + return result, nil +} + // isTxStillRelevant is a callback for mempool transaction filtering after the // new block addition. It returns false for transactions already present in the // chain (added by the new block), transactions using some inputs that are diff --git a/pkg/core/spent_coin_state.go b/pkg/core/spent_coin_state.go index def3cbbaa..53cbb8eee 100644 --- a/pkg/core/spent_coin_state.go +++ b/pkg/core/spent_coin_state.go @@ -1,6 +1,7 @@ package core import ( + "github.com/CityOfZion/neo-go/pkg/core/transaction" "github.com/CityOfZion/neo-go/pkg/io" "github.com/CityOfZion/neo-go/pkg/util" ) @@ -14,6 +15,13 @@ type SpentCoinState struct { items map[uint16]uint32 } +// spentCoin represents the state of a single spent coin output. +type spentCoin struct { + Output *transaction.Output + StartHeight uint32 + EndHeight uint32 +} + // NewSpentCoinState returns a new SpentCoinState object. func NewSpentCoinState(hash util.Uint256, height uint32) *SpentCoinState { return &SpentCoinState{