Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deduplicate contract names migration #5143

Merged
merged 7 commits into from
Jan 9, 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
133 changes: 133 additions & 0 deletions cmd/util/ledger/migrations/deduplicate_contract_names_migration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package migrations

import (
"context"
"fmt"

"github.com/fxamacker/cbor/v2"
"github.com/rs/zerolog"

"github.com/onflow/cadence/runtime/common"

"github.com/onflow/flow-go/ledger"
"github.com/onflow/flow-go/ledger/common/convert"
"github.com/onflow/flow-go/model/flow"
)

// DeduplicateContractNamesMigration checks if the contract names have been duplicated and
// removes the duplicate ones.
//
// This migration de-syncs storage used, so it should be run before the StorageUsedMigration.
type DeduplicateContractNamesMigration struct {
log zerolog.Logger
}

func (d *DeduplicateContractNamesMigration) Close() error {
return nil
}

func (d *DeduplicateContractNamesMigration) InitMigration(
log zerolog.Logger,
_ []*ledger.Payload,
_ int,
) error {
d.log = log.
With().
Str("migration", "DeduplicateContractNamesMigration").
Logger()

return nil
}

func (d *DeduplicateContractNamesMigration) MigrateAccount(
ctx context.Context,
address common.Address,
payloads []*ledger.Payload,
) ([]*ledger.Payload, error) {
flowAddress := flow.ConvertAddress(address)
contractNamesID := flow.ContractNamesRegisterID(flowAddress)

var contractNamesPayload *ledger.Payload
contractNamesPayloadIndex := 0
for i, payload := range payloads {
key, err := payload.Key()
if err != nil {
return nil, err
}
id, err := convert.LedgerKeyToRegisterID(key)
if err != nil {
return nil, err
}
if id == contractNamesID {
contractNamesPayload = payload
contractNamesPayloadIndex = i
break
}
}
if contractNamesPayload == nil {
return payloads, nil
}

value := contractNamesPayload.Value()
if len(value) == 0 {
// Remove the empty payload
copy(payloads[contractNamesPayloadIndex:], payloads[contractNamesPayloadIndex+1:])
payloads = payloads[:len(payloads)-1]

return payloads, nil
janezpodhostnik marked this conversation as resolved.
Show resolved Hide resolved
}

var contractNames []string
err := cbor.Unmarshal(value, &contractNames)
if err != nil {
return nil, fmt.Errorf("failed to get contract names: %w", err)
}

var foundDuplicate bool
fxamacker marked this conversation as resolved.
Show resolved Hide resolved
i := 1
for i < len(contractNames) {
if contractNames[i-1] != contractNames[i] {

if contractNames[i-1] > contractNames[i] {
// this is not a valid state and we should fail.
// Contract names must be sorted by definition.
return nil, fmt.Errorf(
"contract names for account %s are not sorted: %s",
address.Hex(),
contractNames,
)
}

i++
continue
}
// Found duplicate (contactNames[i-1] == contactNames[i])
// Remove contractNames[i]
copy(contractNames[i:], contractNames[i+1:])
contractNames = contractNames[:len(contractNames)-1]
foundDuplicate = true
}

if !foundDuplicate {
return payloads, nil
}

d.log.Info().
Str("address", address.Hex()).
Strs("contract_names", contractNames).
Msg("removing duplicate contract names")

newContractNames, err := cbor.Marshal(contractNames)
if err != nil {
return nil, fmt.Errorf(
"cannot encode contract names: %s",
contractNames,
)
}

payloads[contractNamesPayloadIndex] = ledger.NewPayload(convert.RegisterIDToLedgerKey(contractNamesID), newContractNames)
return payloads, nil

}

var _ AccountBasedMigration = &DeduplicateContractNamesMigration{}
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package migrations_test

import (
"context"
"fmt"
"math/rand"
"sort"
"testing"

"github.com/fxamacker/cbor/v2"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"

"github.com/onflow/cadence/runtime/common"

"github.com/onflow/flow-go/cmd/util/ledger/migrations"
"github.com/onflow/flow-go/fvm/environment"
"github.com/onflow/flow-go/ledger"
"github.com/onflow/flow-go/ledger/common/convert"
"github.com/onflow/flow-go/model/flow"
)

func TestDeduplicateContractNamesMigration(t *testing.T) {
migration := migrations.DeduplicateContractNamesMigration{}
log := zerolog.New(zerolog.NewTestWriter(t))
err := migration.InitMigration(log, nil, 0)
require.NoError(t, err)

address, err := common.HexToAddress("0x1")
require.NoError(t, err)

ctx := context.Background()

accountStatus := environment.NewAccountStatus()
accountStatus.SetStorageUsed(1000)
accountStatusPayload := ledger.NewPayload(
convert.RegisterIDToLedgerKey(
flow.AccountStatusRegisterID(flow.ConvertAddress(address)),
),
accountStatus.ToBytes(),
)

contractNamesPayload := func(contractNames []byte) *ledger.Payload {
return ledger.NewPayload(
convert.RegisterIDToLedgerKey(
flow.RegisterID{
Owner: string(address.Bytes()),
Key: flow.ContractNamesKey,
},
),
contractNames,
)
}

requireContractNames := func(payloads []*ledger.Payload, f func([]string)) {
for _, payload := range payloads {
key, err := payload.Key()
require.NoError(t, err)
id, err := convert.LedgerKeyToRegisterID(key)
require.NoError(t, err)

if id.Key != flow.ContractNamesKey {
continue
}

contracts := make([]string, 0)
err = cbor.Unmarshal(payload.Value(), &contracts)
require.NoError(t, err)

f(contracts)

}
}

t.Run("no contract names", func(t *testing.T) {
payloads, err := migration.MigrateAccount(ctx, address,
[]*ledger.Payload{
accountStatusPayload,
},
)

require.NoError(t, err)
require.Equal(t, 1, len(payloads))
})

t.Run("one contract", func(t *testing.T) {
contractNames := []string{"test"}
newContractNames, err := cbor.Marshal(contractNames)
require.NoError(t, err)

payloads, err := migration.MigrateAccount(ctx, address,
[]*ledger.Payload{
accountStatusPayload,
contractNamesPayload(newContractNames),
},
)

require.NoError(t, err)
require.Equal(t, 2, len(payloads))

requireContractNames(payloads, func(contracts []string) {
require.Equal(t, 1, len(contracts))
require.Equal(t, "test", contracts[0])
})
})

t.Run("two unique contracts", func(t *testing.T) {
contractNames := []string{"test", "test2"}
newContractNames, err := cbor.Marshal(contractNames)
require.NoError(t, err)

payloads, err := migration.MigrateAccount(ctx, address,
[]*ledger.Payload{
accountStatusPayload,
contractNamesPayload(newContractNames),
},
)

require.NoError(t, err)
require.Equal(t, 2, len(payloads))

requireContractNames(payloads, func(contracts []string) {
require.Equal(t, 2, len(contracts))
require.Equal(t, "test", contracts[0])
require.Equal(t, "test2", contracts[1])
})
})

t.Run("two contracts", func(t *testing.T) {
contractNames := []string{"test", "test"}
newContractNames, err := cbor.Marshal(contractNames)
require.NoError(t, err)

payloads, err := migration.MigrateAccount(ctx, address,
[]*ledger.Payload{
accountStatusPayload,
contractNamesPayload(newContractNames),
},
)

require.NoError(t, err)
require.Equal(t, 2, len(payloads))

requireContractNames(payloads, func(contracts []string) {
require.Equal(t, 1, len(contracts))
require.Equal(t, "test", contracts[0])
})
})

t.Run("not sorted contracts", func(t *testing.T) {
contractNames := []string{"test2", "test"}
newContractNames, err := cbor.Marshal(contractNames)
require.NoError(t, err)

_, err = migration.MigrateAccount(ctx, address,
[]*ledger.Payload{
accountStatusPayload,
contractNamesPayload(newContractNames),
},
)

require.Error(t, err)
})

t.Run("duplicate contracts", func(t *testing.T) {
contractNames := []string{"test", "test", "test2", "test3", "test3"}
newContractNames, err := cbor.Marshal(contractNames)
require.NoError(t, err)

payloads, err := migration.MigrateAccount(ctx, address,
[]*ledger.Payload{
accountStatusPayload,
contractNamesPayload(newContractNames),
},
)

require.NoError(t, err)
require.Equal(t, 2, len(payloads))

requireContractNames(payloads, func(contracts []string) {
require.Equal(t, 3, len(contracts))
require.Equal(t, "test", contracts[0])
require.Equal(t, "test2", contracts[1])
require.Equal(t, "test3", contracts[2])
})
})

t.Run("random contracts", func(t *testing.T) {
contractNames := make([]string, 1000)
uniqueContracts := 1
for i := 0; i < 1000; i++ {
// i > 0 so it's easier to know how many unique contracts there are
if i > 0 && rand.Float32() < 0.5 {
uniqueContracts++
}
contractNames[i] = fmt.Sprintf("test%d", uniqueContracts)
}

// sort contractNames alphabetically, because they are not sorted
sort.Slice(contractNames, func(i, j int) bool {
return contractNames[i] < contractNames[j]
})

newContractNames, err := cbor.Marshal(contractNames)
require.NoError(t, err)

payloads, err := migration.MigrateAccount(ctx, address,
[]*ledger.Payload{
accountStatusPayload,
contractNamesPayload(newContractNames),
},
)

require.NoError(t, err)
require.Equal(t, 2, len(payloads))

requireContractNames(payloads, func(contracts []string) {
require.Equal(t, uniqueContracts, len(contracts))
})
})
}
Loading