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

chore: simplify ADR-028 and address.Module #14017

Merged
merged 26 commits into from
Jan 16, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fb63a19
chore: simplify ADR-028 and address.Module
robert-zaremba Nov 24, 2022
a76c8e9
formatting
robert-zaremba Nov 24, 2022
8758fb7
Apply suggestions from code review
robert-zaremba Nov 25, 2022
a4feb8c
update example 2
robert-zaremba Nov 25, 2022
a69b7ab
Merge branch 'main' into robert/update-adr28
robert-zaremba Nov 25, 2022
bcd66d1
review: typos
robert-zaremba Dec 5, 2022
24dc097
Merge remote-tracking branch 'origin/main' into robert/update-adr28
robert-zaremba Dec 5, 2022
50580f0
Apply suggestions from code review
robert-zaremba Dec 8, 2022
cf22468
Merge remote-tracking branch 'origin/main' into robert/update-adr28
robert-zaremba Dec 18, 2022
8e9ef45
update NewModuleCredentialto return an error
robert-zaremba Dec 18, 2022
8882546
changed NewModuleCredential interface
robert-zaremba Dec 18, 2022
4dfbcae
Merge branch 'main' into robert/update-adr28
alexanderbez Dec 19, 2022
0243f55
Merge remote-tracking branch 'origin/main' into robert/update-adr28
robert-zaremba Dec 19, 2022
800eb1c
improving function docs
robert-zaremba Dec 19, 2022
67d8d8d
Module: fallback to the legacy implementation when no derivation key …
robert-zaremba Dec 19, 2022
99368ca
fix whitespaces
robert-zaremba Dec 19, 2022
c46cc3c
Apply suggestions from code review
robert-zaremba Jan 4, 2023
8354fc5
linter
robert-zaremba Jan 4, 2023
34768e4
update to support NewModuleCredential for Root Module
robert-zaremba Jan 4, 2023
d46bf3c
Merge remote-tracking branch 'origin/main' into robert/update-adr28
robert-zaremba Jan 4, 2023
a2c6398
Merge branch 'main' into robert/update-adr28
robert-zaremba Jan 9, 2023
c6542a7
Update x/auth/types/credentials.go
robert-zaremba Jan 10, 2023
ae14791
Merge branch 'main' into robert/update-adr28
robert-zaremba Jan 12, 2023
8d769f9
Merge branch 'main' into robert/update-adr28
tac0turtle Jan 16, 2023
7f095f1
add changelog and comment
julienrbrt Jan 16, 2023
7bea48a
Merge branch 'main' into robert/update-adr28
julienrbrt Jan 16, 2023
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
67 changes: 36 additions & 31 deletions docs/architecture/adr-028-public-key-addresses.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ As in other parts of the Cosmos SDK, we will use `sha256`.

### Basic Address

We start with defining a base hash algorithm for generating addresses. Notably, it's used for accounts represented by a single key pair. For each public key schema we have to have an associated `typ` string, which we discuss in a section below. `hash` is the cryptographic hash function defined in the previous section.
We start with defining a base algorithm for generating addresses which we will call `Hash`. Notably, it's used for accounts represented by a single key pair. For each public key schema we have to have an associated `typ` string, explained in the next section. `hash` is the cryptographic hash function defined in the previous section.

```go
const A_LEN = 32
Expand All @@ -114,16 +114,13 @@ Motivation: this algorithm keeps the address relatively small (length of the `ty
and it's more secure than [post-hash-prefix-proposal] (which uses the first 20 bytes of a pubkey hash, significantly reducing the address space).
Moreover the cryptographer motivated the choice of adding `typ` in the hash to protect against a switch table attack.

We use the `address.Hash` function for generating addresses for all accounts represented by a single key:
`address.Hash` is a low level function to generate _base_ addresses for a new key types. Example:
robert-zaremba marked this conversation as resolved.
Show resolved Hide resolved

* simple public keys: `address.Hash(keyType, pubkey)`

* aggregated keys (eg: BLS): `address.Hash(keyType, aggregatedPubKey)`
* modules: `address.Hash("module", moduleName)`
* BLS: `address.Hash("bls", pubkey)`

### Composed Addresses

For simple composed accounts (like new naive multisig), we generalize the `address.Hash`. The address is constructed by recursively creating addresses for the sub accounts, sorting the addresses and composing them into a single address. It ensures that the ordering of keys doesn't impact the resulting address.
For simple composed accounts (like a new naive multisig) we generalize the `address.Hash`. The address is constructed by recursively creating addresses for the sub accounts, sorting the addresses and composing them into a single address. It ensures that the ordering of keys doesn't impact the resulting address.

```go
// We don't need a PubKey interface - we need anything which is addressable.
Expand All @@ -140,13 +137,13 @@ func Composed(typ string, subaccounts []Addressable) []byte {

The `typ` parameter should be a schema descriptor, containing all significant attributes with deterministic serialization (eg: utf8 string).
`LengthPrefix` is a function which prepends 1 byte to the address. The value of that byte is the length of the address bits before prepending. The address must be at most 255 bits long.
We are using `LengthPrefix` to eliminate conflicts - it assures, that for 2 lists of addresses: `as = {a1, a2, ..., an}` and `bs = {b1, b2, ..., bm}` such that every `bi` and `ai` is at most 255 long, `concatenate(map(as, \a -> LengthPrefix(a))) = map(bs, \b -> LengthPrefix(b))` iff `as = bs`.
We are using `LengthPrefix` to eliminate conflicts - it assures, that for 2 lists of addresses: `as = {a1, a2, ..., an}` and `bs = {b1, b2, ..., bm}` such that every `bi` and `ai` is at most 255 long, `concatenate(map(as, (a) => LengthPrefix(a))) = map(bs, (b) => LengthPrefix(b))` iff `as = bs`.
julienrbrt marked this conversation as resolved.
Show resolved Hide resolved

Implementation Tip: account implementations should cache addresses.

#### Multisig Addresses

For new multisig public keys, we define the `typ` parameter not based on any encoding scheme (amino or protobuf). This avoids issues with non-determinism in the encoding scheme.
For a new multisig public keys, we define the `typ` parameter not based on any encoding scheme (amino or protobuf). This avoids issues with non-determinism in the encoding scheme.

Example:

Expand Down Expand Up @@ -175,53 +172,61 @@ func (multisig PubKey) Address() {
}
```

#### Module Account Addresses

NOTE: this section is not finalize and it's in active discussion.
### Derived Addresses

In Basic Address section we defined a module account address as:
We must be able to cryptographically derive one address from another one. The derivation process must guarantee hash properties, hence we use the already defined `Hash` function:

```go
address.Hash("module", moduleName)
func Derive(address []byte, derivationKey []byte) []byte {
robert-zaremba marked this conversation as resolved.
Show resolved Hide resolved
return Hash(addres, derivationKey)
}
```

We use `"module"` as a schema type for all module derived addresses. Module accounts can have sub accounts. The derivation process has a defined order: module name, submodule key, subsubmodule key.
Module account addresses are heavily used in the Cosmos SDK so it makes sense to optimize the derivation process: instead of using of using `LengthPrefix` for the module name, we use a null byte (`'\x00'`) as a separator. This works, because null byte is not a part of a valid module name.
### Module Account Addresses

A module account will have `"module"` type. Module accounts can have sub accounts. The submodule account will be created based on module name, and sequence of derivation keys. Typically, the first derivation key should be a class of the derived accounts. The derivation process has a defined order: module name, submodule key, subsubmodule key... An example module account is created using:

```go
func Module(moduleName string, key []byte) []byte{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so currently we are not using this function to generate root module account, instead we are using authtypes.NewModuleAddress(modulenName), right?

Copy link
Member

@julienrbrt julienrbrt Jan 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly, now authtypes.NewModuleAddress(modulenName) falls back to address.Module now, but, the behavior stays the same as authtypes.NewModuleAddress(modulenName): https://github.com/cosmos/cosmos-sdk/pull/14017/files#diff-6cfded83a08be06034f746e83ffa6bbd6c35430cd8ac05015e6bd942c7da1030R73-R76

return Hash("module", []byte(moduleName) + 0 + key)
}
address.Module(moduleName, key)
```

**Example** A lending BTC pool address would be:
An example sub-module account is created using:

```go
btcPool := address.Module("lending", btc.Address()})
groupPolicyAddresses := []byte{1}
address.Module (moduleName, groupPolicyAddresses, policyID)
robert-zaremba marked this conversation as resolved.
Show resolved Hide resolved
```

If we want to create an address for a module account depending on more than one key, we can concatenate them:
The `address.Module` function is using `addres.Hash` with `"module"` as the type argument, and byte representation of the module name concatenated with submodule key. The two last component must be uniquely separated to avoid potential clashes (example: modulename="ab" & submodulekey="bc" will have the same derivation key as modulename="a" & submodulekey="bbc").
robert-zaremba marked this conversation as resolved.
Show resolved Hide resolved
We use a null byte (`'\x00'`) to separate module name from the submodule key. This works, because null byte is not a part of a valid module name. Finally, the sub-submodule accounts are created by applying the `Derive` function recursively.
We could use `Derive` function also in the first step (rather than concatenating module name with zero byte and the submodule key). We decided to do concatenation to avoid one level of derivation and speed up computation.

```go
btcAtomAMM := address.Module("amm", btc.Address() + atom.Address()})
func Module(moduleName string, key []byte, subsubKeys ...[]byte) []byte{
submoduleAddress := Hash("module", []byte(moduleName) + 0 + key)
return fold((a, k) => Derive(a, k), subsubKeys, submoduleAddress)
}
```

#### Derived Addresses

We must be able to cryptographically derive one address from another one. The derivation process must guarantee hash properties, hence we use the already defined `Hash` function:
**Example 1** A lending BTC pool address would be:

```go
func Derive(address []byte, derivationKey []byte) []byte {
return Hash(addres, derivationKey)
}
btcPool := address.Module("lending", btc.Address()})
```

Note: `Module` is a special case of the more general _derived_ address, where we set the `"module"` string for the _from address_.
If we want to create an address for a module account depending on more than one key, we can concatenate them:

```go
btcAtomAMM := address.Module("amm", btc.Address() + atom.Address()})
```

**Example** For a cosmwasm smart-contract address we could use the following construction:
**Example 2** cosmwasm smart-contract address could be constructed by:

```go
smartContractAddr := Derived(Module("cosmwasm", smartContractsNamespace), []{smartContractKey})
smartContractAddr := Module("cosmwasm", smartContractsNamespace, smartContractKey})
// wich equals to:
robert-zaremba marked this conversation as resolved.
Show resolved Hide resolved
Derived(Module("cosmwasm", smartContractsNamespace), []{smartContractKey})
```

### Schema Types
Expand Down
15 changes: 11 additions & 4 deletions types/address/hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ type Addressable interface {
Address() []byte
}

// Hash creates a new address from address type and key
// Hash creates a new address from address type and key.
// The functions should only be used by new types defining thier own address function
robert-zaremba marked this conversation as resolved.
Show resolved Hide resolved
robert-zaremba marked this conversation as resolved.
Show resolved Hide resolved
// (eg public keys).
func Hash(typ string, key []byte) []byte {
hasher := sha256.New()
_, err := hasher.Write(conv.UnsafeStrToBytes(typ))
Expand Down Expand Up @@ -59,10 +61,15 @@ func Compose(typ string, subAddresses []Addressable) ([]byte, error) {
}

// Module is a specialized version of a composed address for modules. Each module account
// is constructed from a module name and module account key.
func Module(moduleName string, key []byte) []byte {
// is constructed from a module name and a sequence of derivation keys (at least one
// derivation key must be provided).
func Module(moduleName string, derivationKey []byte, derivationKeys ...[]byte) []byte {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking to "merge" derivationKey into derivationKeys, but then we would need to panic if someone will provide an empty list.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think as is is great, requires at least 1 derivationKey implicitly which is communicated to the caller.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any reason to require at least one derivation key for modules. This is inconsistent with what we've discussed for ADR 033. There is always the root module address with no derivation keys.

Suggested change
func Module(moduleName string, derivationKey []byte, derivationKeys ...[]byte) []byte {
func Module(moduleName string, derivationKeys ...[]byte) []byte {

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't this conflict with the existing authtypes.NewModuleAddress?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. That uses 20 byte addresses

Copy link
Contributor

@amaury1093 amaury1093 Dec 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My preference is to keep backwards compatibility of existing root module accounts.

func Module(moduleName string, derivationKeys ...[]byte) []byte {
  if len(derivationKeys) == 0 { return authtypes.NewModuleAddress() }
  else { /* do like current code with successive derivations */ }
}

so it's:

  • 20 byte address for root module accounts
  • 32 byte addresses for sub/derived module accounts

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we need to make decision.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

up

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Up again, I'm going to update this PR by implementing #14017 (comment) (which is what @AmauryM seconded: #14017 (comment))

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM

mKey := append([]byte(moduleName), 0)
return Hash("module", append(mKey, key...))
addr := Hash("module", append(mKey, derivationKey...))
for _, k := range derivationKeys {
addr = Derive(addr, k)
}
return addr
}

// Derive derives a new address from the main `address` and a derivation `key`.
Expand Down
11 changes: 10 additions & 1 deletion types/address/hash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,18 @@ func (suite *AddressSuite) TestModule() {
addr2 := Module("myModule2", key)
assert.NotEqual(addr, addr2, "changing module name must change address")

addr3 := Module(modName, []byte{1, 2, 3})
k1 := []byte{1, 2, 3}
addr3 := Module(modName, k1)
assert.NotEqual(addr, addr3, "changing key must change address")
assert.NotEqual(addr2, addr3, "changing key must change address")

addr4 := Module(modName, k1, k1)
assert.Equal(Derive(addr3, k1), addr4)

k2 := []byte{0, 0, 7}
addr5 := Module(modName, k1, k1, k2)
assert.Equal(Derive(addr4, k2), addr5)

robert-zaremba marked this conversation as resolved.
Show resolved Hide resolved
}

func (suite *AddressSuite) TestDerive() {
Expand Down
17 changes: 6 additions & 11 deletions x/auth/types/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,20 @@ const ModuleCredentialType = "ModuleCredential"

var _ cryptotypes.PubKey = &ModuleCredential{}

// NewModuleCredential creates new module credential key.
// At least one derivation key must be provided. Panics otherwise.
func NewModuleCredential(moduleName string, derivationKeys [][]byte) *ModuleCredential {
if len(derivationKeys) == 0 {
robert-zaremba marked this conversation as resolved.
Show resolved Hide resolved
panic("can't create ModuleCredential withouth derivation key")
robert-zaremba marked this conversation as resolved.
Show resolved Hide resolved
}
return &ModuleCredential{
ModuleName: moduleName,
DerivationKeys: derivationKeys,
}
}

func (m *ModuleCredential) Address() cryptotypes.Address {
var addr []byte
for i, dk := range m.DerivationKeys {
if i == 0 {
addr = address.Module(m.ModuleName, dk)
continue
}

addr = address.Derive(addr, dk)
}

return addr
return address.Module(m.ModuleName, m.DerivationKeys[0], m.DerivationKeys[1:]...)
}

func (m *ModuleCredential) Bytes() []byte {
Expand Down
6 changes: 3 additions & 3 deletions x/group/migrations/v2/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const (
)

// Migrate migrates the x/group module state from the consensus version 1 to version 2.
// Specifically, it changes the group policy account to from module account to base account.
// Specifically, it changes the group policy account from module account to base account.
func Migrate(
ctx sdk.Context,
storeKey storetypes.StoreKey,
Expand All @@ -32,11 +32,11 @@ func Migrate(
store := ctx.KVStore(storeKey)
curAccVal := groupPolicySeq.CurVal(store)
groupPolicyAccountDerivationKey := make(map[string][]byte, 0)
policyKey := []byte{GroupPolicyTablePrefix}
for i := uint64(0); i <= curAccVal; i++ {
derivationKey := make([]byte, 8)
binary.BigEndian.PutUint64(derivationKey, i)
parentAcc := address.Module(group.ModuleName, []byte{GroupPolicyTablePrefix})
groupPolicyAcc := sdk.AccAddress(address.Derive(parentAcc, derivationKey))
groupPolicyAcc := sdk.AccAddress(address.Module(group.ModuleName, policyKey, derivationKey))
groupPolicyAccountDerivationKey[groupPolicyAcc.String()] = derivationKey
}

Expand Down