From 11c5505240e00b6cddea2e807fcb13b38b31f5b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daria=20Dziuba=C5=82towska?= Date: Thu, 12 Oct 2023 16:33:10 +0200 Subject: [PATCH 1/3] Add AllotAllMana to txBuilder --- builder/transaction_builder.go | 152 ++++++++++++++++++++------------- 1 file changed, 92 insertions(+), 60 deletions(-) diff --git a/builder/transaction_builder.go b/builder/transaction_builder.go index 5bdaef780..089938214 100644 --- a/builder/transaction_builder.go +++ b/builder/transaction_builder.go @@ -146,42 +146,66 @@ func (b *TransactionBuilder) AllotRequiredManaAndStoreRemainingManaInOutput(targ return b } - minManalockedSlot := b.transaction.CreationSlot + 2*b.api.ProtocolParameters().MaxCommittableAge() + if storedManaOutputIndex >= len(b.transaction.Outputs) { + return setBuildError(ierrors.Errorf("given storedManaOutputIndex does not exist: %d", storedManaOutputIndex)) + } - // check if the output is locked for a certain time to an account. - hasManalockCondition := func(output iotago.Output) (iotago.AccountID, bool) { - if !output.UnlockConditionSet().HasTimelockUntil(minManalockedSlot) { - return iotago.EmptyAccountID, false - } + // calculate the minimum required mana to issue the block + minRequiredMana, err := b.MinRequiredAllotedMana(b.api.ProtocolParameters().WorkScoreStructure(), rmc, blockIssuerAccountID) + if err != nil { + return setBuildError(ierrors.Wrap(err, "failed to calculate the minimum required mana to issue the block")) + } - unlockAddress := output.UnlockConditionSet().Address() - if unlockAddress == nil { - return iotago.EmptyAccountID, false - } + unboundManaInputsLeftoverBalance, err := b.calculateMaximumPossibleAllotment(targetSlot, minRequiredMana, blockIssuerAccountID) + if err != nil { + return setBuildError(err) + } - if unlockAddress.Address.Type() != iotago.AddressAccount { - return iotago.EmptyAccountID, false - } - //nolint:forcetypeassert // we can safely assume that this is an AccountAddress - accountAddress := unlockAddress.Address.(*iotago.AccountAddress) + // allot the mana to the block issuer account (we increase the value, so we don't interfere with the already alloted value) + b.IncreaseAllotment(blockIssuerAccountID, minRequiredMana) - return accountAddress.AccountID(), true + // move the remaining mana to stored mana on the specified output index + switch output := b.transaction.Outputs[storedManaOutputIndex].(type) { + case *iotago.BasicOutput: + output.Mana += unboundManaInputsLeftoverBalance + case *iotago.AccountOutput: + output.Mana += unboundManaInputsLeftoverBalance + case *iotago.NFTOutput: + output.Mana += unboundManaInputsLeftoverBalance + default: + return setBuildError(ierrors.Wrapf(iotago.ErrUnknownOutputType, "output type %T does not support stored mana", output)) } - // calculate the available mana on input side - _, unboundManaInputs, accountBoundManaInputs, err := CalculateAvailableMana(b.api.ProtocolParameters(), b.inputs, targetSlot) + return b +} + +func (b *TransactionBuilder) AllotAllMana(targetSlot iotago.SlotIndex, rmc iotago.Mana, blockIssuerAccountID iotago.AccountID) *TransactionBuilder { + setBuildError := func(err error) *TransactionBuilder { + b.occurredBuildErr = err + return b + } + // calculate the minimum required mana to issue the block + minRequiredMana, err := b.MinRequiredAllotedMana(b.api.ProtocolParameters().WorkScoreStructure(), rmc, blockIssuerAccountID) if err != nil { - return setBuildError(ierrors.Wrap(err, "failed to calculate the available mana on input side")) + return setBuildError(ierrors.Wrap(err, "failed to calculate the minimum required mana to issue the block")) } - // update the unbound mana balance - updateUnboundManaBalance := func(manaOut iotago.Mana) error { - if unboundManaInputs < manaOut { - return ierrors.New("not enough unbound mana available on the input side") - } - unboundManaInputs -= manaOut + unboundManaInputsLeftoverBalance, err := b.calculateMaximumPossibleAllotment(targetSlot, minRequiredMana, blockIssuerAccountID) + if err != nil { + return setBuildError(err) + } - return nil + // allot the mana to the block issuer account (we increase the value, so we don't interfere with the already alloted value) + b.IncreaseAllotment(blockIssuerAccountID, unboundManaInputsLeftoverBalance+minRequiredMana) + + return b +} + +func (b *TransactionBuilder) calculateMaximumPossibleAllotment(targetSlot iotago.SlotIndex, minRequiredMana iotago.Mana, blockIssuerAccountID iotago.AccountID) (iotago.Mana, error) { + // calculate the available mana on input side + _, unboundManaInputs, accountBoundManaInputs, err := CalculateAvailableMana(b.api.ProtocolParameters(), b.inputs, targetSlot) + if err != nil { + return 0, ierrors.Wrap(err, "failed to calculate the available mana on input side") } // update the account bound mana balances if they exist and/or the onbound mana balance @@ -195,7 +219,12 @@ func (b *TransactionBuilder) AllotRequiredManaAndStoreRemainingManaInOutput(targ accountBoundManaInputs[accountID] = 0 // subtract the remainder from the unbound mana - return updateUnboundManaBalance(accountBoundManaOut - accountBalance) + unboundManaInputs, err = safemath.SafeSub(unboundManaInputs, accountBoundManaOut-accountBalance) + if err != nil { + return ierrors.Wrapf(err, "not enough unbound mana on the input side for account %s", accountID.String()) + } + + return nil } // there is enough account bound mana for this account, subtract it from there @@ -205,11 +234,12 @@ func (b *TransactionBuilder) AllotRequiredManaAndStoreRemainingManaInOutput(targ } // no account bound mana available for the given account, subtract it from the unbounded mana - return updateUnboundManaBalance(accountBoundManaOut) - } + unboundManaInputs, err = safemath.SafeSub(unboundManaInputs, accountBoundManaOut) + if err != nil { + return ierrors.Wrapf(err, "not enough unbound mana on the input side for account %s", accountID.String()) + } - if storedManaOutputIndex >= len(b.transaction.Outputs) { - return setBuildError(ierrors.Errorf("given storedManaOutputIndex does not exist: %d", storedManaOutputIndex)) + return nil } // subtract the stored mana on the outputs side @@ -217,19 +247,20 @@ func (b *TransactionBuilder) AllotRequiredManaAndStoreRemainingManaInOutput(targ switch output := o.(type) { case *iotago.AccountOutput: // mana on account outputs is locked to this account - if err := updateUnboundAndAccountBoundManaBalances(output.AccountID, output.StoredMana()); err != nil { - return setBuildError(ierrors.Wrap(err, "failed to subtract the stored mana on the outputs side")) + if err = updateUnboundAndAccountBoundManaBalances(output.AccountID, output.StoredMana()); err != nil { + return 0, ierrors.Wrap(err, "failed to subtract the stored mana on the outputs side") } default: // check if the output locked mana to a certain account - if accountID, isManaLocked := hasManalockCondition(output); isManaLocked { - if err := updateUnboundAndAccountBoundManaBalances(accountID, output.StoredMana()); err != nil { - return setBuildError(ierrors.Wrap(err, "failed to subtract the stored mana on the outputs side")) + if accountID, isManaLocked := b.hasManalockCondition(output); isManaLocked { + if err = updateUnboundAndAccountBoundManaBalances(accountID, output.StoredMana()); err != nil { + return 0, ierrors.Wrap(err, "failed to subtract the stored mana on the outputs side") } } else { - if err := updateUnboundManaBalance(output.StoredMana()); err != nil { - return setBuildError(ierrors.Wrap(err, "failed to subtract the stored mana on the outputs side")) + unboundManaInputs, err = safemath.SafeSub(unboundManaInputs, output.StoredMana()) + if err != nil { + return 0, ierrors.Wrap(err, "failed to subtract the stored mana on the outputs side") } } } @@ -237,38 +268,39 @@ func (b *TransactionBuilder) AllotRequiredManaAndStoreRemainingManaInOutput(targ // subtract the already alloted mana for _, allotment := range b.transaction.Allotments { - if err := updateUnboundAndAccountBoundManaBalances(allotment.AccountID, allotment.Value); err != nil { - return setBuildError(ierrors.Wrap(err, "failed to subtract the already alloted mana")) + if err = updateUnboundAndAccountBoundManaBalances(allotment.AccountID, allotment.Value); err != nil { + return 0, ierrors.Wrap(err, "failed to subtract the already alloted mana") } } - // calculate the minimum required mana to issue the block - minRequiredMana, err := b.MinRequiredAllotedMana(b.api.ProtocolParameters().WorkScoreStructure(), rmc, blockIssuerAccountID) - if err != nil { - return setBuildError(ierrors.Wrap(err, "failed to calculate the minimum required mana to issue the block")) + // subtract the minimum required mana to issue the block + if err = updateUnboundAndAccountBoundManaBalances(blockIssuerAccountID, minRequiredMana); err != nil { + return 0, ierrors.Wrap(err, "failed to subtract the minimum required mana to issue the block") } - // subtract the minimum required mana to issue the block - if err := updateUnboundAndAccountBoundManaBalances(blockIssuerAccountID, minRequiredMana); err != nil { - return setBuildError(ierrors.Wrap(err, "failed to subtract the minimum required mana to issue the block")) + return unboundManaInputs, nil +} + +// hasManalockCondition checks if the output is locked for a certain time to an account. +func (b *TransactionBuilder) hasManalockCondition(output iotago.Output) (iotago.AccountID, bool) { + minManalockedSlot := b.transaction.CreationSlot + 2*b.api.ProtocolParameters().MaxCommittableAge() + + if !output.UnlockConditionSet().HasTimelockUntil(minManalockedSlot) { + return iotago.EmptyAccountID, false } - // allot the mana to the block issuer account (we increase the value, so we don't interfere with the already alloted value) - b.IncreaseAllotment(blockIssuerAccountID, minRequiredMana) + unlockAddress := output.UnlockConditionSet().Address() + if unlockAddress == nil { + return iotago.EmptyAccountID, false + } - // move the remaining mana to stored mana on the specified output index - switch output := b.transaction.Outputs[storedManaOutputIndex].(type) { - case *iotago.BasicOutput: - output.Mana += unboundManaInputs - case *iotago.AccountOutput: - output.Mana += unboundManaInputs - case *iotago.NFTOutput: - output.Mana += unboundManaInputs - default: - return setBuildError(ierrors.Wrapf(iotago.ErrUnknownOutputType, "output type %T does not support stored mana", output)) + if unlockAddress.Address.Type() != iotago.AddressAccount { + return iotago.EmptyAccountID, false } + //nolint:forcetypeassert // we can safely assume that this is an AccountAddress + accountAddress := unlockAddress.Address.(*iotago.AccountAddress) - return b + return accountAddress.AccountID(), true } // BuildAndSwapToBlockBuilder builds the transaction and then swaps to a BasicBlockBuilder with From ad2a52b5ac9a2825737672d7f69117237f51cff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daria=20Dziuba=C5=82towska?= Date: Fri, 13 Oct 2023 11:21:00 +0200 Subject: [PATCH 2/3] AllotAllMana does not check if we met minimum required mana --- builder/transaction_builder.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/builder/transaction_builder.go b/builder/transaction_builder.go index 089938214..99cb0de80 100644 --- a/builder/transaction_builder.go +++ b/builder/transaction_builder.go @@ -179,24 +179,20 @@ func (b *TransactionBuilder) AllotRequiredManaAndStoreRemainingManaInOutput(targ return b } -func (b *TransactionBuilder) AllotAllMana(targetSlot iotago.SlotIndex, rmc iotago.Mana, blockIssuerAccountID iotago.AccountID) *TransactionBuilder { +// AllotAllMana allots all loose mana to the provided account, even if alloted value is less than required paymnet. +func (b *TransactionBuilder) AllotAllMana(targetSlot iotago.SlotIndex, blockIssuerAccountID iotago.AccountID) *TransactionBuilder { setBuildError := func(err error) *TransactionBuilder { b.occurredBuildErr = err return b } - // calculate the minimum required mana to issue the block - minRequiredMana, err := b.MinRequiredAllotedMana(b.api.ProtocolParameters().WorkScoreStructure(), rmc, blockIssuerAccountID) - if err != nil { - return setBuildError(ierrors.Wrap(err, "failed to calculate the minimum required mana to issue the block")) - } - unboundManaInputsLeftoverBalance, err := b.calculateMaximumPossibleAllotment(targetSlot, minRequiredMana, blockIssuerAccountID) + unboundManaInputsLeftoverBalance, err := b.calculateMaximumPossibleAllotment(targetSlot, 0, blockIssuerAccountID) if err != nil { return setBuildError(err) } // allot the mana to the block issuer account (we increase the value, so we don't interfere with the already alloted value) - b.IncreaseAllotment(blockIssuerAccountID, unboundManaInputsLeftoverBalance+minRequiredMana) + b.IncreaseAllotment(blockIssuerAccountID, unboundManaInputsLeftoverBalance) return b } @@ -221,7 +217,7 @@ func (b *TransactionBuilder) calculateMaximumPossibleAllotment(targetSlot iotago // subtract the remainder from the unbound mana unboundManaInputs, err = safemath.SafeSub(unboundManaInputs, accountBoundManaOut-accountBalance) if err != nil { - return ierrors.Wrapf(err, "not enough unbound mana on the input side for account %s", accountID.String()) + return ierrors.Wrapf(err, "not enough unbound mana on the input side for account %s while subtracting remainder", accountID.String()) } return nil @@ -248,14 +244,14 @@ func (b *TransactionBuilder) calculateMaximumPossibleAllotment(targetSlot iotago case *iotago.AccountOutput: // mana on account outputs is locked to this account if err = updateUnboundAndAccountBoundManaBalances(output.AccountID, output.StoredMana()); err != nil { - return 0, ierrors.Wrap(err, "failed to subtract the stored mana on the outputs side") + return 0, ierrors.Wrap(err, "failed to subtract the stored mana on the outputs side for account output") } default: // check if the output locked mana to a certain account if accountID, isManaLocked := b.hasManalockCondition(output); isManaLocked { if err = updateUnboundAndAccountBoundManaBalances(accountID, output.StoredMana()); err != nil { - return 0, ierrors.Wrap(err, "failed to subtract the stored mana on the outputs side") + return 0, ierrors.Wrap(err, "failed to subtract the stored mana on the outputs side because of lock") } } else { unboundManaInputs, err = safemath.SafeSub(unboundManaInputs, output.StoredMana()) From a9e95d577461eaa04177de470c2f0223a8c8c4b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daria=20Dziuba=C5=82towska?= Date: Fri, 13 Oct 2023 14:43:35 +0200 Subject: [PATCH 3/3] Apply code review suggestion --- builder/transaction_builder.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/builder/transaction_builder.go b/builder/transaction_builder.go index 99cb0de80..a31f0842c 100644 --- a/builder/transaction_builder.go +++ b/builder/transaction_builder.go @@ -156,7 +156,7 @@ func (b *TransactionBuilder) AllotRequiredManaAndStoreRemainingManaInOutput(targ return setBuildError(ierrors.Wrap(err, "failed to calculate the minimum required mana to issue the block")) } - unboundManaInputsLeftoverBalance, err := b.calculateMaximumPossibleAllotment(targetSlot, minRequiredMana, blockIssuerAccountID) + unboundManaInputsLeftoverBalance, err := b.calculateAvailableManaLeftover(targetSlot, minRequiredMana, blockIssuerAccountID) if err != nil { return setBuildError(err) } @@ -179,14 +179,14 @@ func (b *TransactionBuilder) AllotRequiredManaAndStoreRemainingManaInOutput(targ return b } -// AllotAllMana allots all loose mana to the provided account, even if alloted value is less than required paymnet. +// AllotAllMana allots all available mana to the provided account, even if the alloted value is less than the minimum required mana value to issue the block. func (b *TransactionBuilder) AllotAllMana(targetSlot iotago.SlotIndex, blockIssuerAccountID iotago.AccountID) *TransactionBuilder { setBuildError := func(err error) *TransactionBuilder { b.occurredBuildErr = err return b } - unboundManaInputsLeftoverBalance, err := b.calculateMaximumPossibleAllotment(targetSlot, 0, blockIssuerAccountID) + unboundManaInputsLeftoverBalance, err := b.calculateAvailableManaLeftover(targetSlot, 0, blockIssuerAccountID) if err != nil { return setBuildError(err) } @@ -197,7 +197,7 @@ func (b *TransactionBuilder) AllotAllMana(targetSlot iotago.SlotIndex, blockIssu return b } -func (b *TransactionBuilder) calculateMaximumPossibleAllotment(targetSlot iotago.SlotIndex, minRequiredMana iotago.Mana, blockIssuerAccountID iotago.AccountID) (iotago.Mana, error) { +func (b *TransactionBuilder) calculateAvailableManaLeftover(targetSlot iotago.SlotIndex, minRequiredMana iotago.Mana, blockIssuerAccountID iotago.AccountID) (iotago.Mana, error) { // calculate the available mana on input side _, unboundManaInputs, accountBoundManaInputs, err := CalculateAvailableMana(b.api.ProtocolParameters(), b.inputs, targetSlot) if err != nil { @@ -251,7 +251,7 @@ func (b *TransactionBuilder) calculateMaximumPossibleAllotment(targetSlot iotago // check if the output locked mana to a certain account if accountID, isManaLocked := b.hasManalockCondition(output); isManaLocked { if err = updateUnboundAndAccountBoundManaBalances(accountID, output.StoredMana()); err != nil { - return 0, ierrors.Wrap(err, "failed to subtract the stored mana on the outputs side because of lock") + return 0, ierrors.Wrap(err, "failed to subtract the stored mana on the outputs side, while checking locked mana") } } else { unboundManaInputs, err = safemath.SafeSub(unboundManaInputs, output.StoredMana())