Skip to content

Commit

Permalink
[chain] Address Refactor (ava-labs#571)
Browse files Browse the repository at this point in the history
* break apart useful utils

* work on generalizing address

* test invalid checksum

* add tests for bech32 encoding

* add check for invalid checksum

* add utils tests

* remove shared utilities from crypto packages

* remove HRP error from crypto

* update secp256r1 auth

* work on refactoring packing

* generalize storage + revert address refactor

* update ed25519 storage

* unmarshaling works

* update transfer action

* add to auth registry

* remove panic

* vm compiles

* integration compiles

* add TODO to fix bug

* add more TODOs

* fix circular dependency

* morpheus compiles

* integration passing

* sending between secp256r1 and ed25519

* cleanup auth/helpers

* remove unnecessary TODOs

* use address terminology byte representation

* remove unnecessary helper

* remove balance key logging

* remove ShortBytes from crypto

* rename address functions

* e2e compiles

* update short bytes trim

* handler compiles

* add MustBech32

* cli updated for LoadBytes

* morephus-cli builds

* update address in genesis

* separate log levels

* e2e passing

* remove short bytes

* update address parsing

* link BIP-173

* add PrefixID func

* add scope checks to chain for address

* update func name

* codec tests passing

* update morepheus vm auth handling

* remove address verification

* remove hrp from genesis

* fix genesis

* morepheusvm compiles

* cleaning up CLI

* remove ed25519 from keystorage

* remove ed25519 from key

* remove all references of ed25519 from cli

* integration tests passing

* e2e compiles

* implement key handling

* support generic key spam

* e2e morpheusvm test passing

* fix load test

* fix lint

* use Sponsor

* update mocks

* remove payer from morpheusvm

* remove ed25519 from tokenvm storage

* auth updated

* remove all ed25519 references from actions

* tokenvm compiles

* change to Address from AddressBytes

* transform AddressBytes to Address

* more replacements

* more conversion

* morpheus-cli lint

* fix lint

* fix handler

* remove utils from action

* remove tutils from explorer

* cli compiles

* cleanup faucet address usage

* token-feed compiles

* tokenvm integration passing

* fix load test

* tokenvm e2e compiles

* e2e tests work

* remove utils reference

* lint on cli.PrivateKey

* fix load test size

* print key type in morpheus-cli

* fix accounts

* migrate morephus-cli to use WS

* add missing comment

* handle createClient err

* remove extra break

* readme cleanup

* account abstraction on README
  • Loading branch information
patrick-ogrady authored Nov 6, 2023
1 parent f735d94 commit 80c1e5e
Show file tree
Hide file tree
Showing 105 changed files with 1,898 additions and 1,588 deletions.
81 changes: 52 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,47 @@ _The number of blocks that the `hypersdk` stores on-disk, the `AcceptedBlockWind
to an arbitrary depth (or set to `MaxInt` to keep all blocks). To limit disk IO used to serve blocks over
the P2P network, `hypervms` can configure `AcceptedBlockWindowCache` to store recent blocks in memory._

### WASM-Based Programs
In the `hypersdk`, [smart contracts](https://ethereum.org/en/developers/docs/smart-contracts/)
(e.g. programs that run on blockchains) are referred to simply as `programs`. `Programs`
are [WASM-based](https://webassembly.org/) binaries that can be invoked during block
execution to perform arbitrary state transitions. This is a more flexible, yet less performant,
alternative to defining all `Auth` and/or `Actions` that can be invoked in the `hypervm` in the
`hypervm's` code (like the `tokenvm`).

Because the `hypersdk` can execute arbitrary WASM, any language (Rust, C, C++, Zig, etc.) that can
be compiled to WASM can be used to write `programs`. You can view a collection of
Rust-based `programs` [here](https://github.com/ava-labs/hypersdk/tree/main/x/programs/rust/examples).

### Account Abstraction
The `hypersdk` provides out-of-the-box support for arbitrary transaction authorization logic.
Each `hypersdk` transaction includes an `Auth` object that implements an
`Actor` function (identity that participates in an `Action`) and a `Sponsor` function (identity
that pays fees). These two identities could be the same (if using a simple signature
verification `Auth` module) but may be different (if using a "gas relayer" `Auth` module).

`Auth` modules may be hardcoded, like in
[`morpheusvm`](https://github.com/ava-labs/hypersdk/tree/main/examples/morpheusvm/auth) and
[`tokenvm`](https://github.com/ava-labs/hypersdk/tree/main/examples/tokenvm/auth), or execute
a `program` (i.e. a custom deployed multi-sig). To allow for easy interaction between different
`Auth` modules (and to ensure `Auth` modules can't interfere with each other), the
`hypersdk` employs a standard, 33-byte addressing scheme: `<typeID><ids.ID>`. Transaction
verification ensures that any `Actor` and `Sponsor` returned by an `Auth` module
must have the same `<typeID>` as the module generating an address. The 32-byte hash (`<ids.ID>`)
is used to uniquely identify accounts within an `Auth` scheme. For `programs`, this
will likely be the `txID` when the `program` was deployed and will be the hash
of the public key for pure cryptographic primitives (the indirect benefit of this
is that account public keys are obfuscated until used).

_Because transaction IDs are used to prevent replay, it is critical that any signatures used
in `Auth` are [not malleable](https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki).
If malleable signatures are used, it would be trivial for an attacker to generate additional, valid
transactions from an existing transaction and submit it to the network (duplicating whatever `Action` was
specified by the sender)._

_It is up to each `Auth` module to limit the computational complexity of `Auth.AsyncVerify()`
and `Auth.Verify()` to prevent a DoS (invalid `Auth` will not charge `Auth.Sponsor()`)._

### Optimized Block Execution Out-of-the-Box
The `hypersdk` is primarily about an obsession with hyper-speed and
hyper-scalability (and making it easy for developers to achieve both by
Expand Down Expand Up @@ -192,18 +233,6 @@ capability for any `Auth` module that implements the `AuthBatchVerifier` interfa
even parallelizing batch computation for systems that only use a single-thread to
verify a batch.

### WASM-Based Programs
In the `hypersdk`, [smart contracts](https://ethereum.org/en/developers/docs/smart-contracts/)
(e.g. programs that run on blockchains) are referred to simply as `programs`. `Programs`
are [WASM-based](https://webassembly.org/) binaries that can be invoked during block
execution to perform arbitrary state transitions. This is a more flexible, yet less performant,
alternative to defining all `Auth` and/or `Actions` that can be invoked in the `hypervm` in the
`hypervm's` code (like the `tokenvm`).

Because the `hypersdk` can execute arbitrary WASM, any language (Rust, C, C++, Zig, etc.) that can
be compiled to WASM can be used to write `programs`. You can view a collection of
Rust-based `programs` [here](https://github.com/ava-labs/hypersdk/tree/main/x/programs/rust/examples).

### Multidimensional Fee Pricing
Instead of mapping transaction resource usage to a one-dimensional unit (i.e. "gas"
or "fuel"), the `hypersdk` utilizes five independently parameterized unit dimensions
Expand Down Expand Up @@ -371,14 +400,6 @@ is executed which modifies the same value, the net cost for modifying the key
to the `hypervm` (and to the entire network) is much cheaper than modifying a
new key.

### Account Abstraction
The `hypersdk` makes no assumptions about how `Actions` (the primitive for
interactions with any `hyperchain`, as explained below) are verified. Rather,
`hypervms` provide the `hypersdk` with a registry of supported `Auth` modules
that can be used to validate each type of transaction. These `Auth` modules can
perform simple things like signature verification or complex tasks like
executing a WASM blob.

### Nonce-less and Expiring Transactions
`hypersdk` transactions don't use [nonces](https://help.myetherwallet.com/en/articles/5461509-what-is-a-nonce)
to protect against replay attack like many other account-based blockchains. This means users
Expand All @@ -397,12 +418,6 @@ for a single account and ensure they are ordered) and makes the network layer
more efficient (we can gossip any valid transaction to any node instead of just
the transactions for each account that can be executed at the moment).

_Because transaction IDs are used to prevent replay, it is critical that any signatures used
in `Auth` are [not malleable](https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki).
If malleable signatures are used, it would be trivial for an attacker to generate additional, valid
transactions from an existing transaction and submit it to the network (duplicating whatever `Action` was
specified by the sender)._

### Avalanche Warp Messaging Support
`hypersdk` provides support for Avalanche Warp Messaging (AWM) out-of-the-box. AWM enables any
Avalanche Subnet to send arbitrary messages to any other Avalanche Subnet in just a few
Expand Down Expand Up @@ -803,9 +818,17 @@ type Auth interface {
action Action,
) (computeUnits uint64, err error)

// Payer is the owner of [Auth]. It is used by the mempool to ensure that there aren't too many transactions
// from a single Payer.
Payer() []byte
// Actor is the subject of [Action].
//
// To avoid collisions with other [Auth] modules, this must be prefixed
// by the [TypeID].
Actor() codec.Address

// Sponsor is the fee payer of [Auth].
//
// To avoid collisions with other [Auth] modules, this must be prefixed
// by the [TypeID].
Sponsor() codec.Address

// CanDeduct returns an error if [amount] cannot be paid by [Auth].
CanDeduct(ctx context.Context, im state.Immutable, amount uint64) error
Expand Down
14 changes: 11 additions & 3 deletions chain/dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,9 +286,17 @@ type Auth interface {
action Action,
) (computeUnits uint64, err error)

// Payer is the owner of [Auth]. It is used by the mempool to ensure that there aren't too many transactions
// from a single Payer.
Payer() []byte
// Actor is the subject of [Action].
//
// To avoid collisions with other [Auth] modules, this must be prefixed
// by the [TypeID].
Actor() codec.Address

// Sponsor is the fee payer of [Auth].
//
// To avoid collisions with other [Auth] modules, this must be prefixed
// by the [TypeID].
Sponsor() codec.Address

// CanDeduct returns an error if [amount] cannot be paid by [Auth].
CanDeduct(ctx context.Context, im state.Immutable, amount uint64) error
Expand Down
2 changes: 2 additions & 0 deletions chain/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ var (
ErrAuthNotActivated = errors.New("auth not activated")
ErrAuthFailed = errors.New("auth failed")
ErrMisalignedTime = errors.New("misaligned time")
ErrInvalidActor = errors.New("invalid actor")
ErrInvalidSponsor = errors.New("invalid sponsor")

// Execution Correctness
ErrInvalidBalance = errors.New("invalid balance")
Expand Down
42 changes: 28 additions & 14 deletions chain/mock_auth.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 14 additions & 3 deletions chain/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ func (t *Transaction) Sign(

func (t *Transaction) AuthAsyncVerify() func() error {
return func() error {
// It is up to [t.Auth] to limit the computational
// complexity of [t.Auth.AsyncVerify] and [t.Auth.Verify] to prevent
// a DoS (invalid Auth will not charge [t.Auth.Sponsor()].
return t.Auth.AsyncVerify(t.digest)
}
}
Expand Down Expand Up @@ -313,6 +316,9 @@ func (t *Transaction) PreExecute(
if end >= 0 && timestamp > end {
return 0, ErrAuthNotActivated
}
// It is up to [t.Auth] to limit the computational
// complexity of [t.Auth.AsyncVerify] and [t.Auth.Verify] to prevent
// a DoS (invalid Auth will not charge [t.Auth.Sponsor()].
authCUs, err := t.Auth.Verify(ctx, r, im, t.Action)
if err != nil {
return 0, fmt.Errorf("%w: %v", ErrAuthFailed, err) //nolint:errorlint
Expand Down Expand Up @@ -569,9 +575,8 @@ func (t *Transaction) Execute(
}, nil
}

// Used by mempool
func (t *Transaction) Payer() string {
return string(t.Auth.Payer())
func (t *Transaction) Sponsor() codec.Address {
return t.Auth.Sponsor()
}

func (t *Transaction) Marshal(p *codec.Packer) error {
Expand Down Expand Up @@ -692,6 +697,12 @@ func UnmarshalTx(
if err != nil {
return nil, fmt.Errorf("%w: could not unmarshal auth", err)
}
if actorType := auth.Actor()[0]; actorType != authType {
return nil, fmt.Errorf("%w: actorType (%d) did not match authType (%d)", ErrInvalidActor, actorType, authType)
}
if sponsorType := auth.Sponsor()[0]; sponsorType != authType {
return nil, fmt.Errorf("%w: sponsorType (%d) did not match authType (%d)", ErrInvalidSponsor, sponsorType, authType)
}
warpExpected := actionWarp || authWarp
if !warpExpected && warpMessage != nil {
return nil, ErrUnexpectedWarpMessage
Expand Down
6 changes: 3 additions & 3 deletions cli/dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
package cli

import (
"github.com/ava-labs/hypersdk/crypto/ed25519"
"github.com/ava-labs/hypersdk/codec"
)

type Controller interface {
DatabasePath() string
Symbol() string
Decimals() uint8
Address(ed25519.PublicKey) string
ParseAddress(string) (ed25519.PublicKey, error)
Address(codec.Address) string
ParseAddress(string) (codec.Address, error)
}
51 changes: 6 additions & 45 deletions cli/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,50 +7,11 @@ import (
"context"

"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/hypersdk/crypto/ed25519"
"github.com/ava-labs/hypersdk/codec"
"github.com/ava-labs/hypersdk/rpc"
"github.com/ava-labs/hypersdk/utils"
)

func (h *Handler) GenerateKey() error {
// TODO: encrypt key
priv, err := ed25519.GeneratePrivateKey()
if err != nil {
return err
}
if err := h.StoreKey(priv); err != nil {
return err
}
publicKey := priv.PublicKey()
if err := h.StoreDefaultKey(publicKey); err != nil {
return err
}
utils.Outf(
"{{green}}created address:{{/}} %s",
h.c.Address(publicKey),
)
return nil
}

func (h *Handler) ImportKey(keyPath string) error {
priv, err := ed25519.LoadKey(keyPath)
if err != nil {
return err
}
if err := h.StoreKey(priv); err != nil {
return err
}
publicKey := priv.PublicKey()
if err := h.StoreDefaultKey(publicKey); err != nil {
return err
}
utils.Outf(
"{{green}}imported address:{{/}} %s",
h.c.Address(publicKey),
)
return nil
}

func (h *Handler) SetKey(lookupBalance func(int, string, string, uint32, ids.ID) error) error {
keys, err := h.GetKeys()
if err != nil {
Expand All @@ -75,7 +36,7 @@ func (h *Handler) SetKey(lookupBalance func(int, string, string, uint32, ids.ID)
}
utils.Outf("{{cyan}}stored keys:{{/}} %d\n", len(keys))
for i := 0; i < len(keys); i++ {
if err := lookupBalance(i, h.c.Address(keys[i].PublicKey()), uris[0], networkID, chainID); err != nil {
if err := lookupBalance(i, h.c.Address(keys[i].Address), uris[0], networkID, chainID); err != nil {
return err
}
}
Expand All @@ -86,11 +47,11 @@ func (h *Handler) SetKey(lookupBalance func(int, string, string, uint32, ids.ID)
return err
}
key := keys[keyIndex]
return h.StoreDefaultKey(key.PublicKey())
return h.StoreDefaultKey(key.Address)
}

func (h *Handler) Balance(checkAllChains bool, promptAsset bool, printBalance func(ed25519.PublicKey, string, uint32, ids.ID, ids.ID) error) error {
priv, err := h.GetDefaultKey(true)
func (h *Handler) Balance(checkAllChains bool, promptAsset bool, printBalance func(codec.Address, string, uint32, ids.ID, ids.ID) error) error {
addr, _, err := h.GetDefaultKey(true)
if err != nil {
return err
}
Expand All @@ -117,7 +78,7 @@ func (h *Handler) Balance(checkAllChains bool, promptAsset bool, printBalance fu
if err != nil {
return err
}
if err := printBalance(priv.PublicKey(), uri, networkID, chainID, assetID); err != nil {
if err := printBalance(addr, uri, networkID, chainID, assetID); err != nil {
return err
}
}
Expand Down
6 changes: 3 additions & 3 deletions cli/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import (
"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/avalanchego/utils/set"
"github.com/ava-labs/hypersdk/chain"
"github.com/ava-labs/hypersdk/crypto/ed25519"
"github.com/ava-labs/hypersdk/codec"
"github.com/ava-labs/hypersdk/utils"
"github.com/manifoldco/promptui"
)

func (h *Handler) PromptAddress(label string) (ed25519.PublicKey, error) {
func (h *Handler) PromptAddress(label string) (codec.Address, error) {
promptText := promptui.Prompt{
Label: label,
Validate: func(input string) error {
Expand All @@ -29,7 +29,7 @@ func (h *Handler) PromptAddress(label string) (ed25519.PublicKey, error) {
}
recipient, err := promptText.Run()
if err != nil {
return ed25519.EmptyPublicKey, err
return codec.EmptyAddress, err
}
recipient = strings.TrimSpace(recipient)
return h.c.ParseAddress(recipient)
Expand Down
Loading

0 comments on commit 80c1e5e

Please sign in to comment.