Skip to content

Commit

Permalink
Merge pull request #436 from iotaledger/fix/mana-calculation-overflow
Browse files Browse the repository at this point in the history
Fix mana calculation overflow.
  • Loading branch information
piotrm50 authored Oct 19, 2023
2 parents 4947202 + 5a2edd8 commit c98efdf
Show file tree
Hide file tree
Showing 4 changed files with 327 additions and 90 deletions.
19 changes: 0 additions & 19 deletions pkg/protocol/engine/accounts/mana.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,6 @@ func NewMana(value iotago.Mana, excessBaseTokens iotago.BaseToken, updateTime io
}
}

// Update is applied when the account output is transitioned, updating the total value and excess base tokens.
func (m *Mana) Update(value iotago.Mana, excessBaseTokens iotago.BaseToken, updateTime iotago.SlotIndex) {
m.mutex.Lock()
defer m.mutex.Unlock()

m.value = value
m.excessBaseTokens = excessBaseTokens
m.updateTime = updateTime
}

// UpdateValue is applied when the total decayed value is updated but the account output is not changed.
func (m *Mana) UpdateValue(value iotago.Mana, updateTime iotago.SlotIndex) {
m.mutex.Lock()
defer m.mutex.Unlock()

m.value = value
m.updateTime = updateTime
}

func (m *Mana) Value() iotago.Mana {
m.mutex.RLock()
defer m.mutex.RUnlock()
Expand Down
184 changes: 118 additions & 66 deletions pkg/protocol/engine/accounts/mana/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,134 +41,186 @@ func NewManager(apiProvider iotago.APIProvider, accountOutputResolveFunc func(io
}
}

func (m *Manager) GetManaOnAccount(accountID iotago.AccountID, currentSlot iotago.SlotIndex) (iotago.Mana, error) {
func (m *Manager) GetManaOnAccount(accountID iotago.AccountID, slot iotago.SlotIndex) (iotago.Mana, error) {
m.mutex.Lock()
defer m.mutex.Unlock()

apiForSlot := m.apiProvider.APIForSlot(currentSlot)
apiForSlot := m.apiProvider.APIForSlot(slot)

mana, exists := m.manaVectorCache.Get(accountID)
if !exists || mana.UpdateTime() > currentSlot {
output, err := m.accountOutputResolveFunc(accountID, currentSlot)
if err != nil {
return 0, ierrors.Errorf("failed to resolve AccountOutput for %s in slot %s: %w", accountID, currentSlot, err)
}
minDeposit, err := apiForSlot.StorageScoreStructure().MinDeposit(output.Output())
// if entry does not exist or required slot is earlier than the one stored in the cache,
// then need to calculate it from scratch
if !exists || mana.UpdateTime() > slot {
output, err := m.accountOutputResolveFunc(accountID, slot)
if err != nil {
return 0, ierrors.Errorf("failed to get min deposit for %s: %w", accountID, err)
}
excessBaseTokens, err := safemath.SafeSub(output.BaseTokenAmount(), minDeposit)
if err != nil {
excessBaseTokens = 0
return 0, ierrors.Wrapf(err, "failed to resolve AccountOutput for %s in slot %s", accountID, slot)
}

decayedBIC, err := m.getDecayedBIC(accountID, currentSlot, apiForSlot)
if err != nil {
return 0, ierrors.Wrapf(err, "failed to get decayed BIC for %s", accountID)
}
totalMana, err := safemath.SafeAdd(output.StoredMana(), decayedBIC)
mana, err = m.getMana(accountID, output, slot)
if err != nil {
return 0, ierrors.Wrapf(err, "overflow when adding stored mana and decayed BIC for account %s", accountID)
return 0, ierrors.Wrapf(err, "failed to calculate mana for %s in slot %s", accountID, slot)
}

mana = accounts.NewMana(totalMana, excessBaseTokens, output.SlotCreated())

// If it did not exist in cache, then add an entry to cache.
// If it did exist and the requested slot is earlier,
// then don't add it as it could lead to inaccurate results if any later slot is requested afterward.
if !exists {
m.manaVectorCache.Put(accountID, mana)
}
}

if currentSlot == mana.UpdateTime() {
manaDecayProvider := apiForSlot.ManaDecayProvider()

// If the requested slot is the same as the update time the cached mana, then return how it is.
// Otherwise, need to calculate decay and potential mana generation before returning.
if slot == mana.UpdateTime() {
return mana.Value(), nil
}

manaDecayProvider := apiForSlot.ManaDecayProvider()
// apply decay to stored, allotted and potential that was added on last update
manaStored, err := manaDecayProvider.ManaWithDecay(mana.Value(), mana.UpdateTime(), currentSlot)
// Apply decay to stored, potential and BIC mana that was calculated when adding the entry to cache
// so that it's correct for the requested slot.
manaWithDecay, err := manaDecayProvider.ManaWithDecay(mana.Value(), mana.UpdateTime(), slot)
if err != nil {
return 0, err
}
updatedValue := manaStored

// get newly generated potential since last update and apply decay
manaPotential, err := manaDecayProvider.ManaGenerationWithDecay(mana.ExcessBaseTokens(), mana.UpdateTime(), currentSlot)
// Calculate newly generated potential mana since last update time.
manaPotential, err := manaDecayProvider.ManaGenerationWithDecay(mana.ExcessBaseTokens(), mana.UpdateTime(), slot)
if err != nil {
return 0, err
}
updatedValue, err = safemath.SafeAdd(updatedValue, manaPotential)

// Add potential mana to the decayed mana.
manaWithDecayAndGeneration, err := safemath.SafeAdd(manaWithDecay, manaPotential)
if err != nil {
return 0, ierrors.Wrapf(err, "overflow when adding stored and potential mana for account %s", accountID)
}

decayedBIC, err := m.getDecayedBIC(accountID, currentSlot, apiForSlot)
return manaWithDecayAndGeneration, nil
}

func (m *Manager) getMana(accountID iotago.AccountID, output *utxoledger.Output, slot iotago.SlotIndex) (*accounts.Mana, error) {
bic, bicUpdateTime, err := m.getBIC(accountID, slot)
if err != nil {
return 0, ierrors.Wrapf(err, "failed to get decayed BIC for %s", accountID)
return nil, ierrors.Wrap(err, "failed to retrieve BIC")
}
updatedValue, err = safemath.SafeAdd(updatedValue, decayedBIC)

manaDecayProvider := m.apiProvider.APIForSlot(slot).ManaDecayProvider()
minDeposit := lo.PanicOnErr(m.apiProvider.APIForSlot(slot).StorageScoreStructure().MinDeposit(output.Output()))
excessBaseTokens, err := safemath.SafeSub(output.BaseTokenAmount(), minDeposit)
if err != nil {
return 0, ierrors.Wrapf(err, "overflow when adding stored, potential and decayed BIC for account %s", accountID)
// if subtraction underflows, then excess base tokens is 0
excessBaseTokens = 0
}

if output.SlotCreated() > slot || bicUpdateTime > slot {
return nil, ierrors.Errorf("BIC update time (%d) or output creation slot (%d) later than requested slot (%d)", bicUpdateTime, output.SlotCreated(), slot)
}

mana.UpdateValue(updatedValue, currentSlot)
// Decay and generate stored mana to match either the BIC update time or Output creation slot(bigger of the two),
// so that subsequent decay calculations don't need to consider the fact that the two values should be decayed for different periods,
// but instead can be added and decayed together.

var manaUpdateTime iotago.SlotIndex
var totalMana iotago.Mana
if bicUpdateTime > output.SlotCreated() {
manaPotential, err := manaDecayProvider.ManaGenerationWithDecay(excessBaseTokens, output.SlotCreated(), bicUpdateTime)
if err != nil {
return nil, ierrors.Wrapf(err, "failed to calculate mana generation with decay (excessBaseTokens: %d; outputSlotCreated: %d; targetSlot: %d)", excessBaseTokens, output.SlotCreated(), bicUpdateTime)
}

manaStored, err := manaDecayProvider.ManaWithDecay(output.StoredMana(), output.SlotCreated(), bicUpdateTime)
if err != nil {
return nil, ierrors.Wrapf(err, "failed to calculate mana with decay (storedMana: %d; outputSlotCreated: %d; targetSlot: %d)", output.StoredMana(), output.SlotCreated(), bicUpdateTime)
}

manaStoredPotential, err := safemath.SafeAdd(manaStored, manaPotential)
if err != nil {
return nil, ierrors.Wrapf(err, "overflow when adding decayed stored and potential mana (storedMana: %d; potentialMana: %d)", manaStored, manaPotential)
}

totalMana, err = safemath.SafeAdd(bic, manaStoredPotential)
if err != nil {
return nil, ierrors.Wrapf(err, "overflow when adding decayed stored, potential mana and BIC (storedPotentialMana: %d; BIC: %d)", manaStoredPotential, bic)
}

manaUpdateTime = bicUpdateTime
} else if output.SlotCreated() > bicUpdateTime {
// Decay BIC to match the Output creation time.
bicWithDecay, err := manaDecayProvider.ManaWithDecay(bic, bicUpdateTime, output.SlotCreated())
if err != nil {
return nil, err
}

totalMana, err = safemath.SafeAdd(bicWithDecay, output.StoredMana())
if err != nil {
return nil, ierrors.Wrapf(err, "overflow when adding stored mana and decayed BIC (storedMana: %d; decayed BIC: %d)", output.StoredMana(), bicWithDecay)
}

manaUpdateTime = output.SlotCreated()
} else {
totalMana, err = safemath.SafeAdd(bic, output.StoredMana())
if err != nil {
return nil, ierrors.Wrapf(err, "overflow when adding stored mana and BIC (storedMana: %d; BIC: %d)", output.StoredMana(), bic)
}

manaUpdateTime = output.SlotCreated()
}

return mana.Value(), nil
return accounts.NewMana(totalMana, excessBaseTokens, manaUpdateTime), nil
}

func (m *Manager) ApplyDiff(slot iotago.SlotIndex, destroyedAccounts ds.Set[iotago.AccountID], accountOutputs map[iotago.AccountID]*utxoledger.Output, accountDiffs map[iotago.AccountID]*model.AccountDiff) error {
m.mutex.Lock()
defer m.mutex.Unlock()

apiForSlot := m.apiProvider.APIForSlot(slot)

destroyedAccounts.Range(func(accountID iotago.AccountID) {
m.manaVectorCache.Remove(accountID)
})

for accountID := range accountDiffs {
mana, exists := m.manaVectorCache.Get(accountID)
if exists {
var excessBaseTokens iotago.BaseToken
var storedMana iotago.Mana
var err error
for accountID, accountDiff := range accountDiffs {
if _, exists := m.manaVectorCache.Get(accountID); exists {
// If the account output was spent and a new one was created,
// or if BIC amount changed, then mana entry in the cache needs to be updated.

var accountOutput *utxoledger.Output
// If the account output was updated, then we can use the output directly from the diff;
// otherwise it needs to be retrieved from the UTXO ledger.
if output, has := accountOutputs[accountID]; has {
minDeposit := lo.PanicOnErr(apiForSlot.StorageScoreStructure().MinDeposit(output.Output()))
excessBaseTokens, err = safemath.SafeSub(output.BaseTokenAmount(), minDeposit)
if err != nil {
excessBaseTokens = 0
accountOutput = output
} else if accountDiff.BICChange != 0 {
var err error
if accountOutput, err = m.accountOutputResolveFunc(accountID, slot); err != nil {
return ierrors.Errorf("failed to resolve AccountOutput for %s in slot %s: %w", accountID, slot, err)
}
storedMana = output.StoredMana()
}
decayedBIC, err := m.getDecayedBIC(accountID, slot, apiForSlot)
if err != nil {
return err
}
totalMana, err := safemath.SafeAdd(decayedBIC, storedMana)
if err != nil {
return ierrors.Wrapf(err, "overflow when adding stored mana and decayed BIC for account %s", accountID)
}

mana.Update(totalMana, excessBaseTokens, slot)
// If account output was not retrieved, that means that the applied diff affected neither BIC nor AccountOutput.
if accountOutput != nil {
mana, err := m.getMana(accountID, accountOutput, slot)
if err != nil {
return ierrors.Wrapf(err, "failed to calculate mana on an account %s", accountID)
}

m.manaVectorCache.Put(accountID, mana)
}
}
}

return nil
}

func (m *Manager) getDecayedBIC(accountID iotago.AccountID, slot iotago.SlotIndex, apiForSlot iotago.API) (iotago.Mana, error) {
func (m *Manager) getBIC(accountID iotago.AccountID, slot iotago.SlotIndex) (bic iotago.Mana, updateTime iotago.SlotIndex, err error) {
accountBIC, exists, err := m.accountRetrieveFunc(accountID, slot)
if err != nil {
return 0, ierrors.Wrapf(err, "failed to retrieve account data for %s in slot %s", accountID, slot)
return 0, 0, ierrors.Wrapf(err, "failed to retrieve account data for %s in slot %s", accountID, slot)
}
if !exists {
return 0, ierrors.Errorf("account data for %s in slot %s does not exist", accountID, slot)
return 0, 0, ierrors.Errorf("account data for %s in slot %s does not exist", accountID, slot)
}
if accountBIC.Credits.Value <= 0 {
return 0, nil
}
decayedBIC, err := apiForSlot.ManaDecayProvider().ManaWithDecay(iotago.Mana(accountBIC.Credits.Value), accountBIC.Credits.UpdateTime, slot)
if err != nil {
return 0, ierrors.Wrapf(err, "failed to apply mana decay for account %s", accountID)
return 0, 0, nil
}

return decayedBIC, nil
return iotago.Mana(accountBIC.Credits.Value), accountBIC.Credits.UpdateTime, nil
}
Loading

0 comments on commit c98efdf

Please sign in to comment.