Skip to content
This repository was archived by the owner on Jan 24, 2025. It is now read-only.

Properly export and rollback accounts that were created after the target slot #943

Merged
merged 4 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 26 additions & 16 deletions pkg/protocol/engine/accounts/accountsledger/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ func (m *Manager) account(accountID iotago.AccountID, targetSlot iotago.SlotInde
loadedAccount = accounts.NewAccountData(accountID, accounts.WithCredits(accounts.NewBlockIssuanceCredits(0, targetSlot)))
}

wasDestroyed, err := m.rollbackAccountTo(loadedAccount, targetSlot)
_, wasDestroyed, err := m.rollbackAccountTo(loadedAccount, targetSlot)
if err != nil {
return nil, false, err
}
Expand Down Expand Up @@ -250,7 +250,7 @@ func (m *Manager) PastAccounts(accountIDs iotago.AccountIDs, targetSlot iotago.S
if !exists {
loadedAccount = accounts.NewAccountData(accountID, accounts.WithCredits(accounts.NewBlockIssuanceCredits(0, targetSlot)))
}
wasDestroyed, err := m.rollbackAccountTo(loadedAccount, targetSlot)
_, wasDestroyed, err := m.rollbackAccountTo(loadedAccount, targetSlot)
if err != nil {
continue
}
Expand Down Expand Up @@ -290,7 +290,7 @@ func (m *Manager) Rollback(targetSlot iotago.SlotIndex) error {
accountData = accounts.NewAccountData(accountID)
}

if _, err := m.rollbackAccountTo(accountData, targetSlot); err != nil {
if _, _, err := m.rollbackAccountTo(accountData, targetSlot); err != nil {
internalErr = ierrors.Wrapf(err, "unable to rollback account %s to target slot %d", accountID, targetSlot)

return false
Expand All @@ -314,7 +314,11 @@ func (m *Manager) Rollback(targetSlot iotago.SlotIndex) error {
}
}

return m.accountsTree.Commit()
if err := m.accountsTree.Commit(); err != nil {
return ierrors.Wrap(err, "unable to commit account tree")
}

return nil
}

// AddAccount adds a new account to the Account tree, allotting to it the balance on the given output.
Expand Down Expand Up @@ -368,17 +372,17 @@ func (m *Manager) Reset() {
m.latestSupportedVersionSignals.Clear()
}

func (m *Manager) rollbackAccountTo(accountData *accounts.AccountData, targetSlot iotago.SlotIndex) (wasDestroyed bool, err error) {
func (m *Manager) rollbackAccountTo(accountData *accounts.AccountData, targetSlot iotago.SlotIndex) (wasCreatedAfterTargetSlot bool, wasDestroyed bool, err error) {
// to reach targetSlot, we need to rollback diffs from the current latestCommittedSlot down to targetSlot + 1
for diffSlot := m.latestCommittedSlot; diffSlot > targetSlot; diffSlot-- {
diffStore, err := m.slotDiff(diffSlot)
if err != nil {
return false, ierrors.Errorf("can't retrieve account, could not find diff store for slot %d", diffSlot)
return false, false, ierrors.Errorf("can't retrieve account, could not find diff store for slot %d", diffSlot)
}

found, err := diffStore.Has(accountData.ID)
if err != nil {
return false, ierrors.Wrapf(err, "can't retrieve account, could not check if diff store for slot %d has account %s", diffSlot, accountData.ID)
return false, false, ierrors.Wrapf(err, "can't retrieve account, could not check if diff store for slot %d has account %s", diffSlot, accountData.ID)
}

// no changes for this account in this slot
Expand All @@ -388,7 +392,7 @@ func (m *Manager) rollbackAccountTo(accountData *accounts.AccountData, targetSlo

diffChange, destroyed, err := diffStore.Load(accountData.ID)
if err != nil {
return false, ierrors.Wrapf(err, "can't retrieve account, could not load diff for account %s in slot %d", accountData.ID, diffSlot)
return false, false, ierrors.Wrapf(err, "can't retrieve account, could not load diff for account %s in slot %d", accountData.ID, diffSlot)
}

// update the account data with the diff
Expand All @@ -397,34 +401,40 @@ func (m *Manager) rollbackAccountTo(accountData *accounts.AccountData, targetSlo
if diffChange.PreviousExpirySlot != diffChange.NewExpirySlot {
accountData.ExpirySlot = diffChange.PreviousExpirySlot
}
// update the outputID only if the account got actually transitioned, not if it was only an allotment target
if diffChange.PreviousOutputID != iotago.EmptyOutputID {
accountData.OutputID = diffChange.PreviousOutputID

if diffChange.PreviousOutputID == iotago.EmptyOutputID && diffChange.NewOutputID != iotago.EmptyOutputID {
// Account was created in this slot, so we need to remove it
m.LogDebug("Account was created in this slot, so we need to remove it", "accountID", accountData.ID, "slot", diffSlot, "diffChange.PreviousOutputID", diffChange.PreviousOutputID, "diffChange.NewOutputID", diffChange.NewOutputID)
return true, false, nil
}

// update the output ID of the account if it was changed
accountData.OutputID = diffChange.PreviousOutputID

accountData.AddBlockIssuerKeys(diffChange.BlockIssuerKeysRemoved...)
accountData.RemoveBlockIssuerKey(diffChange.BlockIssuerKeysAdded...)

validatorStake, err := safemath.SafeSub(int64(accountData.ValidatorStake), diffChange.ValidatorStakeChange)
if err != nil {
return false, ierrors.Wrapf(err, "can't retrieve account, validator stake underflow for account %s in slot %d: %d - %d", accountData.ID, diffSlot, accountData.ValidatorStake, diffChange.ValidatorStakeChange)
return false, false, ierrors.Wrapf(err, "can't retrieve account, validator stake underflow for account %s in slot %d: %d - %d", accountData.ID, diffSlot, accountData.ValidatorStake, diffChange.ValidatorStakeChange)
}
accountData.ValidatorStake = iotago.BaseToken(validatorStake)

delegationStake, err := safemath.SafeSub(int64(accountData.DelegationStake), diffChange.DelegationStakeChange)
if err != nil {
return false, ierrors.Wrapf(err, "can't retrieve account, delegation stake underflow for account %s in slot %d: %d - %d", accountData.ID, diffSlot, accountData.DelegationStake, diffChange.DelegationStakeChange)
return false, false, ierrors.Wrapf(err, "can't retrieve account, delegation stake underflow for account %s in slot %d: %d - %d", accountData.ID, diffSlot, accountData.DelegationStake, diffChange.DelegationStakeChange)
}
accountData.DelegationStake = iotago.BaseToken(delegationStake)

stakeEpochEnd, err := safemath.SafeSub(int64(accountData.StakeEndEpoch), diffChange.StakeEndEpochChange)
if err != nil {
return false, ierrors.Wrapf(err, "can't retrieve account, stake end epoch underflow for account %s in slot %d: %d - %d", accountData.ID, diffSlot, accountData.StakeEndEpoch, diffChange.StakeEndEpochChange)
return false, false, ierrors.Wrapf(err, "can't retrieve account, stake end epoch underflow for account %s in slot %d: %d - %d", accountData.ID, diffSlot, accountData.StakeEndEpoch, diffChange.StakeEndEpochChange)
}
accountData.StakeEndEpoch = iotago.EpochIndex(stakeEpochEnd)

fixedCost, err := safemath.SafeSub(int64(accountData.FixedCost), diffChange.FixedCostChange)
if err != nil {
return false, ierrors.Wrapf(err, "can't retrieve account, fixed cost underflow for account %s in slot %d: %d - %d", accountData.ID, diffSlot, accountData.FixedCost, diffChange.FixedCostChange)
return false, false, ierrors.Wrapf(err, "can't retrieve account, fixed cost underflow for account %s in slot %d: %d - %d", accountData.ID, diffSlot, accountData.FixedCost, diffChange.FixedCostChange)
}
accountData.FixedCost = iotago.Mana(fixedCost)
if diffChange.PrevLatestSupportedVersionAndHash != diffChange.NewLatestSupportedVersionAndHash {
Expand All @@ -435,7 +445,7 @@ func (m *Manager) rollbackAccountTo(accountData *accounts.AccountData, targetSlo
wasDestroyed = wasDestroyed || destroyed
}

return wasDestroyed, nil
return false, wasDestroyed, nil
}

func (m *Manager) preserveDestroyedAccountData(accountID iotago.AccountID) (accountDiff *model.AccountDiff, err error) {
Expand Down
34 changes: 30 additions & 4 deletions pkg/protocol/engine/accounts/accountsledger/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ func (m *Manager) Import(reader io.ReadSeeker) error {
return ierrors.Wrapf(err, "unable to set account %s", accountData.ID)
}

m.LogDebug("Imported account", "accountID", accountData.ID, "outputID", accountData.OutputID, "credits.value", accountData.Credits.Value, "credits.updateSlot", accountData.Credits.UpdateSlot)

return nil
}); err != nil {
return ierrors.Wrap(err, "failed to read account data")
Expand All @@ -36,6 +38,10 @@ func (m *Manager) Import(reader io.ReadSeeker) error {
return ierrors.Wrap(err, "unable to import slot diffs")
}

if err := m.accountsTree.Commit(); err != nil {
return ierrors.Wrap(err, "unable to commit account tree")
}

return nil
}

Expand Down Expand Up @@ -73,14 +79,24 @@ func (m *Manager) exportAccountTree(writer io.WriteSeeker, targetIndex iotago.Sl
var accountCount int

if err := m.accountsTree.Stream(func(accountID iotago.AccountID, accountData *accounts.AccountData) error {
if _, err := m.rollbackAccountTo(accountData, targetIndex); err != nil {
m.LogDebug("Exporting account", "accountID", accountID, "outputID", accountData.OutputID, "credits.value", accountData.Credits.Value, "credits.updateSlot", accountData.Credits.UpdateSlot)

wasCreatedAfterTargetSlot, _, err := m.rollbackAccountTo(accountData, targetIndex)
if err != nil {
return ierrors.Wrapf(err, "unable to rollback account %s", accountID)
}

m.LogDebug("Exporting account after rollback", "accountID", accountID, "outputID", accountData.OutputID, "credits.value", accountData.Credits.Value, "credits.updateSlot", accountData.Credits.UpdateSlot)

// Account was created after the target slot, so we don't need to export it.
if wasCreatedAfterTargetSlot {
m.LogDebug("Exporting account was created after target slot", "accountID", accountID, "targetSlot", targetIndex)
return nil
}

if err := stream.WriteObject(writer, accountData, (*accounts.AccountData).Bytes); err != nil {
return ierrors.Wrapf(err, "unable to write account %s", accountID)
}

accountCount++

return nil
Expand All @@ -105,7 +121,6 @@ func (m *Manager) recreateDestroyedAccounts(writer io.WriteSeeker, targetSlot io
accountData := accounts.NewAccountData(accountID)

destroyedAccounts[accountID] = accountData
recreatedAccountsCount++

return true
})
Expand All @@ -115,15 +130,26 @@ func (m *Manager) recreateDestroyedAccounts(writer io.WriteSeeker, targetSlot io
}

for accountID, accountData := range destroyedAccounts {
if wasDestroyed, err := m.rollbackAccountTo(accountData, targetSlot); err != nil {
m.LogDebug("Exporting recreated destroyed account", "accountID", accountID, "outputID", accountData.OutputID, "credits.value", accountData.Credits.Value, "credits.updateSlot", accountData.Credits.UpdateSlot)

if wasCreatedAfterTargetSlot, wasDestroyed, err := m.rollbackAccountTo(accountData, targetSlot); err != nil {
return 0, ierrors.Wrapf(err, "unable to rollback account %s to target slot %d", accountID, targetSlot)
} else if wasCreatedAfterTargetSlot {
// Account was created after the target slot, so we don't need to export it.
m.LogDebug("Exporting recreated destroyed account was created after target slot", "accountID", accountID, "targetSlot", targetSlot)

continue
} else if !wasDestroyed {
return 0, ierrors.Errorf("account %s was not destroyed", accountID)
}

m.LogDebug("Exporting recreated destroyed account after rollback", "accountID", accountID, "outputID", accountData.OutputID, "credits.value", accountData.Credits.Value, "credits.updateSlot", accountData.Credits.UpdateSlot)

if err := stream.WriteObject(writer, accountData, (*accounts.AccountData).Bytes); err != nil {
return 0, ierrors.Wrapf(err, "unable to write account %s", accountID)
}

recreatedAccountsCount++
}

return recreatedAccountsCount, nil
Expand Down
63 changes: 37 additions & 26 deletions pkg/protocol/engine/accounts/accountsledger/snapshot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ func TestManager_Import_Export(t *testing.T) {
ts := NewTestSuite(t)
latestSupportedVersionHash1 := tpkg.Rand32ByteArray()
latestSupportedVersionHash2 := tpkg.Rand32ByteArray()

accountTreeRoots := []iotago.Identifier{}

accountTreeRoots = append(accountTreeRoots, ts.Instance.AccountsTreeRoot())

ts.ApplySlotActions(1, 5, map[string]*AccountActions{
"A": {
TotalAllotments: 10,
Expand Down Expand Up @@ -44,6 +49,8 @@ func TestManager_Import_Export(t *testing.T) {
},
})

accountTreeRoots = append(accountTreeRoots, ts.Instance.AccountsTreeRoot())

ts.AssertAccountLedgerUntil(1, map[string]*AccountState{
"A": {
BICUpdatedTime: 1,
Expand Down Expand Up @@ -90,6 +97,8 @@ func TestManager_Import_Export(t *testing.T) {
},
})

accountTreeRoots = append(accountTreeRoots, ts.Instance.AccountsTreeRoot())

ts.AssertAccountLedgerUntil(2, map[string]*AccountState{
"A": {
BICUpdatedTime: 2,
Expand Down Expand Up @@ -145,6 +154,8 @@ func TestManager_Import_Export(t *testing.T) {
},
})

accountTreeRoots = append(accountTreeRoots, ts.Instance.AccountsTreeRoot())

ts.AssertAccountLedgerUntil(3, map[string]*AccountState{
"A": {
Destroyed: true,
Expand Down Expand Up @@ -178,31 +189,31 @@ func TestManager_Import_Export(t *testing.T) {

// Export and import the account ledger into new manager for the latest slot.
{
writer := stream.NewByteBuffer()

err := ts.Instance.Export(writer, iotago.SlotIndex(3))
require.NoError(t, err)

ts.Instance = ts.initAccountLedger()
err = ts.Instance.Import(writer.Reader())
require.NoError(t, err)
ts.Instance.SetLatestCommittedSlot(3)

ts.AssertAccountLedgerUntilWithoutNewState(3)
}

// Export and import for pre-latest slot.
{
writer := stream.NewByteBuffer()

err := ts.Instance.Export(writer, iotago.SlotIndex(2))
require.NoError(t, err)

ts.Instance = ts.initAccountLedger()
err = ts.Instance.Import(writer.Reader())
require.NoError(t, err)
ts.Instance.SetLatestCommittedSlot(2)

ts.AssertAccountLedgerUntilWithoutNewState(2)
writer := []*stream.ByteBuffer{
stream.NewByteBuffer(),
stream.NewByteBuffer(),
stream.NewByteBuffer(),
stream.NewByteBuffer(),
}

latestSlot := iotago.SlotIndex(3)

// Export snapshots at all slots including genesis.
for i := iotago.SlotIndex(0); i <= latestSlot; i++ {
err := ts.Instance.Export(writer[i], i)
require.NoError(t, err)
}

// Import all of the created snapshots into a new manager, assert the tree root and the states.
for i := iotago.SlotIndex(0); i <= latestSlot; i++ {
ts.Instance = ts.initAccountLedger()
err := ts.Instance.Import(writer[i].Reader())
require.NoError(t, err)
ts.Instance.SetLatestCommittedSlot(i)

ts.AssertAccountLedgerUntilWithoutNewState(i)

require.Equal(t, accountTreeRoots[i], ts.Instance.AccountsTreeRoot())
}
}
}
Loading