Skip to content

Commit

Permalink
Merge pull request #599 from iotaledger/max-mana-check
Browse files Browse the repository at this point in the history
Add Stored Mana and Allotments do not exceed Max Mana check
  • Loading branch information
PhilippGackstatter authored Nov 9, 2023
2 parents c08a4ff + 06b269e commit 0fc3bed
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 9 deletions.
38 changes: 32 additions & 6 deletions output.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/ethereum/go-ethereum/common"

"github.com/iotaledger/hive.go/constraints"
"github.com/iotaledger/hive.go/core/safemath"
"github.com/iotaledger/hive.go/ierrors"
"github.com/iotaledger/hive.go/lo"
"github.com/iotaledger/hive.go/serializer/v2"
Expand Down Expand Up @@ -332,7 +333,6 @@ type OutputsSyntacticalValidationFunc func(index int, output Output) error

// OutputsSyntacticalDepositAmount returns an OutputsSyntacticalValidationFunc which checks that:
// - every output has base token amount more than zero
// - every output has base token amount less than the total supply
// - the sum of base token amounts does not exceed the total supply
// - the base token amount fulfills the minimum storage deposit as calculated from the storage score of the output
// - if the output contains a StorageDepositReturnUnlockCondition, it must "return" bigger equal than the minimum storage deposit
Expand All @@ -343,12 +343,16 @@ func OutputsSyntacticalDepositAmount(protoParams ProtocolParameters, storageScor
return func(index int, output Output) error {
amount := output.BaseTokenAmount()

switch {
case amount == 0:
if amount == 0 {
return ierrors.Wrapf(ErrAmountMustBeGreaterThanZero, "output %d", index)
case amount > protoParams.TokenSupply():
return ierrors.Wrapf(ErrOutputAmountMoreThanTotalSupply, "output %d", index)
case sum+amount > protoParams.TokenSupply():
}

var err error
sum, err = safemath.SafeAdd(sum, amount)
if err != nil {
return ierrors.Wrapf(ErrOutputsSumExceedsTotalSupply, "%w: output %d", err, index)
}
if sum > protoParams.TokenSupply() {
return ierrors.Wrapf(ErrOutputsSumExceedsTotalSupply, "output %d", index)
}

Expand Down Expand Up @@ -394,6 +398,28 @@ func OutputsSyntacticalNativeTokens() OutputsSyntacticalValidationFunc {
}
}

// OutputsSyntacticalStoredMana returns an OutputsSyntacticalValidationFunc which checks that:
// - the sum of all stored mana fields does not exceed 2^(Mana Bits Count) - 1.
func OutputsSyntacticalStoredMana(maxManaValue Mana) OutputsSyntacticalValidationFunc {
var sum Mana

return func(index int, output Output) error {
storedMana := output.StoredMana()

var err error
sum, err = safemath.SafeAdd(sum, storedMana)
if err != nil {
return ierrors.Wrapf(ErrMaxManaExceeded, "%w: stored mana sum calculation failed at output %d", err, index)
}

if sum > maxManaValue {
return ierrors.Wrapf(ErrMaxManaExceeded, "sum of stored mana exceeds max value with output %d", index)
}

return nil
}
}

// OutputsSyntacticalExpirationAndTimelock returns an OutputsSyntacticalValidationFunc which checks that:
// That ExpirationUnlockCondition and TimelockUnlockCondition does not have its unix criteria set to zero.
func OutputsSyntacticalExpirationAndTimelock() OutputsSyntacticalValidationFunc {
Expand Down
2 changes: 1 addition & 1 deletion output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ func TestOutputsSyntacticalDepositAmount(t *testing.T) {
},
},
},
wantErr: iotago.ErrOutputAmountMoreThanTotalSupply,
wantErr: iotago.ErrOutputsSumExceedsTotalSupply,
},
{
name: "fail - sum more than total supply over multiple outputs",
Expand Down
30 changes: 28 additions & 2 deletions transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"golang.org/x/crypto/blake2b"

"github.com/iotaledger/hive.go/core/safemath"
"github.com/iotaledger/hive.go/ierrors"
"github.com/iotaledger/hive.go/lo"
"github.com/iotaledger/hive.go/serializer/v2/byteutils"
Expand Down Expand Up @@ -46,12 +47,12 @@ var (
ErrNFTOutputCyclicAddress = ierrors.New("NFT output's ID corresponds to address field")
// ErrOutputsSumExceedsTotalSupply gets returned if the sum of the output deposits exceeds the total supply of tokens.
ErrOutputsSumExceedsTotalSupply = ierrors.New("accumulated output balance exceeds total supply")
// ErrOutputAmountMoreThanTotalSupply gets returned if an output base token amount is more than the total supply.
ErrOutputAmountMoreThanTotalSupply = ierrors.New("an output's base token amount cannot exceed the total supply")
// ErrStorageDepositLessThanMinReturnOutputStorageDeposit gets returned when the storage deposit condition's amount is less than the min storage deposit for the return output.
ErrStorageDepositLessThanMinReturnOutputStorageDeposit = ierrors.New("storage deposit return amount is less than the min storage deposit needed for the return output")
// ErrStorageDepositExceedsTargetOutputAmount gets returned when the storage deposit condition's amount exceeds the target output's base token amount.
ErrStorageDepositExceedsTargetOutputAmount = ierrors.New("storage deposit return amount exceeds target output's base token amount")
// ErrMaxManaExceeded gets returned when the sum of stored mana in all outputs or the sum of Mana in all allotments exceeds the maximum Mana value.
ErrMaxManaExceeded = ierrors.New("max mana value exceeded")
)

type (
Expand Down Expand Up @@ -247,6 +248,25 @@ func (t *Transaction) Size() int {
return t.TransactionEssence.Size() + t.Outputs.Size()
}

// allotmentSyntacticValidation checks that the sum of all allotted mana does not exceed 2^(Mana Bits Count) - 1.
func (t *Transaction) allotmentSyntacticValidation(maxManaValue Mana) error {
var sum Mana

for index, allotment := range t.Allotments {
var err error
sum, err = safemath.SafeAdd(sum, allotment.Mana)
if err != nil {
return ierrors.Errorf("%w: %w: allotment mana sum calculation failed at allotment %d", ErrMaxManaExceeded, err, index)
}

if sum > maxManaValue {
return ierrors.Wrapf(ErrMaxManaExceeded, "sum of allotted mana exceeds max value with allotment %d", index)
}
}

return nil
}

// syntacticallyValidate checks whether the transaction essence is syntactically valid.
// The function does not syntactically validate the input or outputs themselves.
func (t *Transaction) SyntacticallyValidate(api API) error {
Expand All @@ -256,10 +276,16 @@ func (t *Transaction) SyntacticallyValidate(api API) error {
return err
}

var maxManaValue Mana = (1 << protoParams.ManaParameters().BitsCount) - 1
if err := t.allotmentSyntacticValidation(maxManaValue); err != nil {
return err
}

return SyntacticallyValidateOutputs(t.Outputs,
OutputsSyntacticalDepositAmount(protoParams, api.StorageScoreStructure()),
OutputsSyntacticalExpirationAndTimelock(),
OutputsSyntacticalNativeTokens(),
OutputsSyntacticalStoredMana(maxManaValue),
OutputsSyntacticalChainConstrainedOutputUniqueness(),
OutputsSyntacticalFoundry(),
OutputsSyntacticalAccount(),
Expand Down
97 changes: 97 additions & 0 deletions transaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,3 +299,100 @@ func TestTransactionEssenceCapabilitiesBitMask(t *testing.T) {
})
}
}

func TestTransactionSyntacticMaxMana(t *testing.T) {
type test struct {
name string
tx *iotago.Transaction
wantErr error
}

basicOutputWithMana := func(mana iotago.Mana) *iotago.BasicOutput {
return &iotago.BasicOutput{
Amount: OneIOTA,
Mana: mana,
Conditions: iotago.BasicOutputUnlockConditions{
&iotago.AddressUnlockCondition{
Address: tpkg.RandEd25519Address(),
},
},
}
}

allotmentWithMana := func(mana iotago.Mana) *iotago.Allotment {
return &iotago.Allotment{
Mana: mana,
AccountID: tpkg.RandAccountID(),
}
}

var maxManaValue iotago.Mana = (1 << tpkg.TestAPI.ProtocolParameters().ManaParameters().BitsCount) - 1
tests := []*test{
{
name: "ok - stored mana sum below max value",
tx: tpkg.RandTransactionWithOptions(tpkg.TestAPI,
func(tx *iotago.Transaction) {
tx.Outputs = iotago.TxEssenceOutputs{basicOutputWithMana(1), basicOutputWithMana(maxManaValue - 1)}
},
),
wantErr: nil,
},
{
name: "fail - one output's stored mana exceeds max mana value",
tx: tpkg.RandTransactionWithOptions(tpkg.TestAPI,
func(tx *iotago.Transaction) {
tx.Outputs = iotago.TxEssenceOutputs{basicOutputWithMana(maxManaValue + 1)}
},
),
wantErr: iotago.ErrMaxManaExceeded,
},
{
name: "fail - sum of stored mana exceeds max mana value",
tx: tpkg.RandTransactionWithOptions(tpkg.TestAPI,
func(tx *iotago.Transaction) {
tx.Outputs = iotago.TxEssenceOutputs{basicOutputWithMana(maxManaValue - 1), basicOutputWithMana(maxManaValue - 1)}
},
),
wantErr: iotago.ErrMaxManaExceeded,
},
{
name: "ok - allotted mana sum below max value",
tx: tpkg.RandTransactionWithOptions(tpkg.TestAPI,
func(tx *iotago.Transaction) {
tx.Allotments = iotago.Allotments{allotmentWithMana(1), allotmentWithMana(maxManaValue - 1)}
},
),
wantErr: nil,
},
{
name: "fail - one allotment's mana exceeds max value",
tx: tpkg.RandTransactionWithOptions(tpkg.TestAPI,
func(tx *iotago.Transaction) {
tx.Allotments = iotago.Allotments{allotmentWithMana(maxManaValue + 1)}
},
),
wantErr: iotago.ErrMaxManaExceeded,
},
{
name: "fail - sum of allotted mana exceeds max value",
tx: tpkg.RandTransactionWithOptions(tpkg.TestAPI,
func(tx *iotago.Transaction) {
tx.Allotments = iotago.Allotments{allotmentWithMana(maxManaValue - 1), allotmentWithMana(maxManaValue - 1)}
},
),
wantErr: iotago.ErrMaxManaExceeded,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.tx.SyntacticallyValidate(tpkg.TestAPI)
if test.wantErr != nil {
require.ErrorIs(t, err, test.wantErr)

return
}
require.NoError(t, err)
})
}
}

0 comments on commit 0fc3bed

Please sign in to comment.