Skip to content

Commit

Permalink
feat(x/accounts)!: make address generation more robust and add predic…
Browse files Browse the repository at this point in the history
…table address creation (#22776)

Co-authored-by: Julien Robert <[email protected]>
(cherry picked from commit ecd53f8)

# Conflicts:
#	api/cosmos/accounts/v1/tx.pulsar.go
#	tests/integration/v2/auth/accounts_retro_compatibility_test.go
  • Loading branch information
testinginprod authored and mergify[bot] committed Dec 9, 2024
1 parent d65bc30 commit 9e57cea
Show file tree
Hide file tree
Showing 19 changed files with 5,389 additions and 81 deletions.
4,987 changes: 4,987 additions & 0 deletions api/cosmos/accounts/v1/tx.pulsar.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion tests/integration/accounts/base_account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func TestBaseAccount(t *testing.T) {

_, baseAccountAddr, err := ak.Init(ctx, "base", accCreator, &baseaccountv1.MsgInit{
PubKey: toAnyPb(t, privKey.PubKey()),
}, nil)
}, nil, nil)
require.NoError(t, err)

// fund base account! this will also cause an auth base account to be created
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/accounts/fixture_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ func initFixture(t *testing.T, f func(ctx context.Context, msg *account_abstract
banktypes.RegisterMsgServer(router, bankkeeper.NewMsgServerImpl(bankKeeper))

// init account
_, addr, err := accountsKeeper.Init(integrationApp.Context(), "mock", []byte("system"), &gogotypes.Empty{}, nil)
_, addr, err := accountsKeeper.Init(integrationApp.Context(), "mock", []byte("system"), &gogotypes.Empty{}, nil, nil)
require.NoError(t, err)

fixture := &fixture{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func (s *IntegrationTestSuite) TestContinuousLockingAccount() {
StartTime: currentTime,
// end time in 1 minutes
EndTime: currentTime.Add(time.Minute),
}, sdk.Coins{sdk.NewCoin("stake", math.NewInt(1000))})
}, sdk.Coins{sdk.NewCoin("stake", math.NewInt(1000))}, nil)
require.NoError(t, err)

addr, err := app.AuthKeeper.AddressCodec().BytesToString(randAcc)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (s *IntegrationTestSuite) TestDelayedLockingAccount() {
Owner: ownerAddrStr,
// end time in 1 minutes
EndTime: currentTime.Add(time.Minute),
}, sdk.Coins{sdk.NewCoin("stake", math.NewInt(1000))})
}, sdk.Coins{sdk.NewCoin("stake", math.NewInt(1000))}, nil)
require.NoError(t, err)

addr, err := app.AuthKeeper.AddressCodec().BytesToString(randAcc)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (s *IntegrationTestSuite) TestPeriodicLockingAccount() {
Length: time.Minute,
},
},
}, sdk.Coins{sdk.NewCoin("stake", math.NewInt(1500))})
}, sdk.Coins{sdk.NewCoin("stake", math.NewInt(1500))}, nil)
require.NoError(t, err)

addr, err := app.AuthKeeper.AddressCodec().BytesToString(randAcc)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func (s *IntegrationTestSuite) TestPermanentLockingAccount() {

_, accountAddr, err := app.AccountsKeeper.Init(ctx, lockupaccount.PERMANENT_LOCKING_ACCOUNT, accOwner, &types.MsgInitLockupAccount{
Owner: ownerAddrStr,
}, sdk.Coins{sdk.NewCoin("stake", math.NewInt(1000))})
}, sdk.Coins{sdk.NewCoin("stake", math.NewInt(1000))}, nil)
require.NoError(t, err)

addr, err := app.AuthKeeper.AddressCodec().BytesToString(randAcc)
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/accounts/multisig/test_suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func (s *IntegrationTestSuite) initAccount(ctx context.Context, sender []byte, m
Revote: false,
EarlyExecution: true,
},
}, sdk.Coins{sdk.NewCoin("stake", math.NewInt(1000))})
}, sdk.Coins{sdk.NewCoin("stake", math.NewInt(1000))}, nil)
s.NoError(err)

accountAddrStr, err := s.app.AuthKeeper.AddressCodec().BytesToString(accountAddr)
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/accounts/wiring_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestDependencies(t *testing.T) {

_, counterAddr, err := ak.Init(ctx, "counter", accCreator, &counterv1.MsgInit{
InitialValue: 0,
}, nil)
}, nil, nil)
require.NoError(t, err)
// test dependencies
creatorInitFunds := sdk.NewCoins(sdk.NewInt64Coin("stake", 100_000))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func TestAuthToAccountsGRPCCompat(t *testing.T) {

// init three accounts
for n, a := range accs {
_, addr, err := f.accountsKeeper.Init(f.app.Context(), n, []byte("me"), &gogotypes.Empty{}, nil)
_, addr, err := f.accountsKeeper.Init(f.app.Context(), n, []byte("me"), &gogotypes.Empty{}, nil, nil)
require.NoError(t, err)
a.(*mockRetroCompatAccount).address = addr
}
Expand Down Expand Up @@ -132,10 +132,10 @@ func TestAccountsBaseAccountRetroCompat(t *testing.T) {
require.NoError(t, err)

// we init two accounts to have account num not be zero.
_, _, err = f.accountsKeeper.Init(f.app.Context(), "base", []byte("me"), &basev1.MsgInit{PubKey: anyPk}, nil)
_, _, err = f.accountsKeeper.Init(f.app.Context(), "base", []byte("me"), &basev1.MsgInit{PubKey: anyPk}, nil, nil)
require.NoError(t, err)

_, addr, err := f.accountsKeeper.Init(f.app.Context(), "base", []byte("me"), &basev1.MsgInit{PubKey: anyPk}, nil)
_, addr, err := f.accountsKeeper.Init(f.app.Context(), "base", []byte("me"), &basev1.MsgInit{PubKey: anyPk}, nil, nil)
require.NoError(t, err)

// try to query it via auth
Expand Down
185 changes: 185 additions & 0 deletions tests/integration/v2/auth/accounts_retro_compatibility_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package auth

import (
"context"
"errors"
"testing"

gogotypes "github.com/cosmos/gogoproto/types"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"cosmossdk.io/x/accounts/accountstd"
basev1 "cosmossdk.io/x/accounts/defaults/base/v1"

codectypes "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
)

var _ accountstd.Interface = mockRetroCompatAccount{}

type mockRetroCompatAccount struct {
retroCompat *authtypes.QueryLegacyAccountResponse
address []byte
}

var (
valid = &mockRetroCompatAccount{
retroCompat: &authtypes.QueryLegacyAccountResponse{
Account: &codectypes.Any{},
Base: &authtypes.BaseAccount{
Address: "test",
PubKey: nil,
AccountNumber: 10,
Sequence: 20,
},
},
}

noInfo = &mockRetroCompatAccount{
retroCompat: &authtypes.QueryLegacyAccountResponse{
Account: &codectypes.Any{},
},
}
noImplement = &mockRetroCompatAccount{
retroCompat: nil,
}
)

func newMockRetroCompatAccount(name string, acc accountstd.Interface) accountstd.AccountCreatorFunc {
return func(_ accountstd.Dependencies) (string, accountstd.Interface, error) {
_, ok := acc.(*mockRetroCompatAccount)
if !ok {
return name, nil, errors.New("invalid account type")
}
return name, acc, nil
}
}

func ProvideMockRetroCompatAccountValid() accountstd.DepinjectAccount {
return accountstd.DepinjectAccount{MakeAccount: newMockRetroCompatAccount("valid", valid)}
}

func ProvideMockRetroCompatAccountNoInfo() accountstd.DepinjectAccount {
return accountstd.DepinjectAccount{MakeAccount: newMockRetroCompatAccount("no_info", noInfo)}
}

func ProvideMockRetroCompatAccountNoImplement() accountstd.DepinjectAccount {
return accountstd.DepinjectAccount{MakeAccount: newMockRetroCompatAccount("no_implement", noImplement)}
}

func (m mockRetroCompatAccount) RegisterInitHandler(builder *accountstd.InitBuilder) {
accountstd.RegisterInitHandler(builder, func(ctx context.Context, req *gogotypes.Empty) (*gogotypes.Empty, error) {
return &gogotypes.Empty{}, nil
})
}

func (m mockRetroCompatAccount) RegisterExecuteHandlers(_ *accountstd.ExecuteBuilder) {}

func (m mockRetroCompatAccount) RegisterQueryHandlers(builder *accountstd.QueryBuilder) {
if m.retroCompat == nil {
return
}
accountstd.RegisterQueryHandler(builder, func(ctx context.Context, req *authtypes.QueryLegacyAccount) (*authtypes.QueryLegacyAccountResponse, error) {
return m.retroCompat, nil
})
}

func TestAuthToAccountsGRPCCompat(t *testing.T) {
accs := map[string]accountstd.Interface{
"valid": valid,
"no_info": noInfo,
"no_implement": noImplement,
}

f := createTestSuite(t)

// init three accounts
for n, a := range accs {
_, addr, err := f.accountsKeeper.Init(f.ctx, n, []byte("me"), &gogotypes.Empty{}, nil, nil)
require.NoError(t, err)
a.(*mockRetroCompatAccount).address = addr
}

qs := authkeeper.NewQueryServer(f.authKeeper)

t.Run("account supports info and account query", func(t *testing.T) {
infoResp, err := qs.AccountInfo(f.ctx, &authtypes.QueryAccountInfoRequest{
Address: f.mustAddr(valid.address),
})
require.NoError(t, err)
require.Equal(t, infoResp.Info, valid.retroCompat.Base)

accountResp, err := qs.Account(f.ctx, &authtypes.QueryAccountRequest{
Address: f.mustAddr(noInfo.address),
})
require.NoError(t, err)
require.Equal(t, accountResp.Account, valid.retroCompat.Account)
})

t.Run("account only supports account query, not info", func(t *testing.T) {
_, err := qs.AccountInfo(f.ctx, &authtypes.QueryAccountInfoRequest{
Address: f.mustAddr(noInfo.address),
})
require.Error(t, err)
require.Equal(t, status.Code(err), codes.NotFound)

resp, err := qs.Account(f.ctx, &authtypes.QueryAccountRequest{
Address: f.mustAddr(noInfo.address),
})
require.NoError(t, err)
require.Equal(t, resp.Account, valid.retroCompat.Account)
})

t.Run("account does not support any retro compat", func(t *testing.T) {
_, err := qs.AccountInfo(f.ctx, &authtypes.QueryAccountInfoRequest{
Address: f.mustAddr(noImplement.address),
})
require.Error(t, err)
require.Equal(t, status.Code(err), codes.NotFound)

_, err = qs.Account(f.ctx, &authtypes.QueryAccountRequest{
Address: f.mustAddr(noImplement.address),
})

require.Error(t, err)
require.Equal(t, status.Code(err), codes.NotFound)
})
}

func TestAccountsBaseAccountRetroCompat(t *testing.T) {
f := createTestSuite(t)
// init a base acc
anyPk, err := codectypes.NewAnyWithValue(secp256k1.GenPrivKey().PubKey())
require.NoError(t, err)

// we init two accounts. Account number should start with 4
// since the first three accounts are fee_collector, bonded_tokens_pool, not_bonded_tokens_pool
// generated by init genesis plus one more genesis account, which make the current account number 4.
_, _, err = f.accountsKeeper.Init(f.ctx, "base", []byte("me"), &basev1.MsgInit{PubKey: anyPk}, nil, nil)
require.NoError(t, err)

_, addr, err := f.accountsKeeper.Init(f.ctx, "base", []byte("me"), &basev1.MsgInit{PubKey: anyPk}, nil, nil)
require.NoError(t, err)

// try to query it via auth
qs := authkeeper.NewQueryServer(f.authKeeper)

r, err := qs.Account(f.ctx, &authtypes.QueryAccountRequest{
Address: f.mustAddr(addr),
})
require.NoError(t, err)
require.NotNil(t, r.Account)

info, err := qs.AccountInfo(f.ctx, &authtypes.QueryAccountInfoRequest{
Address: f.mustAddr(addr),
})
require.NoError(t, err)
require.NotNil(t, info.Info)
require.Equal(t, info.Info.PubKey, anyPk)
// Account number should be 5
require.Equal(t, info.Info.AccountNumber, uint64(5))
}
26 changes: 26 additions & 0 deletions x/accounts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,32 @@ func (a Account) AuthRetroCompatibility(ctx context.Context, _ *authtypes.QueryL
* Implement this handler only for account types you want to expose via x/auth gRPC methods.
* The `info` field in the response can be nil if your account doesn't fit the `BaseAccount` structure.

## Address Derivation

The x/accounts module offers two methods for deriving addresses, both ensuring non-squattability. This means each address is uniquely tied to its creator, preventing address collisions between different creators (e.g., Alice cannot create addresses that would conflict with Bob's addresses).

### Method 1: Using Address Seeds

When creating an account via `MsgInit`, you can provide an `address_seed`. The address is derived using:

```bash
address = sha256(ModuleName || address_seed || creator_address)
```

### Method 2: Using Account Numbers
If no address seed is provided, the address is derived using:

```
address = sha256(ModuleName || creator_address || next_account_number)
```

### Address Seed Best Practices

1. Address seeds must be unique per creator (not globally unique)
2. Reusing an address seed will cause account creation to fail
3. For programmatic account creation, use an incrementing sequence number as the address seed
4. This is particularly useful for contracts or modules that need deterministic address generation

## Genesis

### Creating accounts on genesis
Expand Down
2 changes: 2 additions & 0 deletions x/accounts/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ var (
ErrBundlerPayment = errors.New(ModuleName, 2, "bundler payment failed")
// ErrExecution is returned when the execution fails.
ErrExecution = errors.New(ModuleName, 3, "execution failed")
// ErrAccountAlreadyExists is returned when the account already exists in state.
ErrAccountAlreadyExists = errors.New(ModuleName, 4, "account already exists")
)
6 changes: 3 additions & 3 deletions x/accounts/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ func TestGenesis(t *testing.T) {
// we init two accounts of the same type

// we set counter to 10
_, addr1, err := k.Init(ctx, testAccountType, []byte("sender"), &types.Empty{}, nil)
_, addr1, err := k.Init(ctx, testAccountType, []byte("sender"), &types.Empty{}, nil, nil)
require.NoError(t, err)
_, err = k.Execute(ctx, addr1, []byte("sender"), &types.UInt64Value{Value: 10}, nil)
require.NoError(t, err)

// we set counter to 20
_, addr2, err := k.Init(ctx, testAccountType, []byte("sender"), &types.Empty{}, nil)
_, addr2, err := k.Init(ctx, testAccountType, []byte("sender"), &types.Empty{}, nil, nil)
require.NoError(t, err)
_, err = k.Execute(ctx, addr2, []byte("sender"), &types.UInt64Value{Value: 20}, nil)
require.NoError(t, err)
Expand Down Expand Up @@ -62,7 +62,7 @@ func TestGenesis(t *testing.T) {
require.Equal(t, &types.UInt64Value{Value: 20}, resp)

// check initted on genesis account
addr3, err := k.makeAddress(2)
addr3, err := k.makeAddress([]byte("sender-2"), 2, nil)
require.NoError(t, err)
resp, err = k.Query(ctx, addr3, &types.DoubleValue{})
require.NoError(t, err)
Expand Down
Loading

0 comments on commit 9e57cea

Please sign in to comment.