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

[chain] Remove State Access from Auth #669

Merged
merged 23 commits into from
Jan 20, 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
168 changes: 48 additions & 120 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ If malleable signatures are used, it would be trivial for an attacker to generat
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()`)._
_It is up to each `Auth` module to limit the computational complexity of `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
Expand Down Expand Up @@ -662,14 +662,7 @@ You can view what this looks like in the `tokenvm` by clicking this
### Action
```golang
type Action interface {
// GetTypeID uniquely identifies each supported [Action]. We use IDs to avoid
// reflection.
GetTypeID() uint8

// ValidRange is the timestamp range (in ms) that this [Action] is considered valid.
//
// -1 means no start/end
ValidRange(Rules) (start int64, end int64)
Object

// MaxComputeUnits is the maximum amount of compute a given [Action] could use. This is
// used to determine whether the [Action] can be included in a given block and to compute
Expand All @@ -679,9 +672,10 @@ type Action interface {
// users don't need to have a large balance to call an [Action] (must prepay fee before execution).
MaxComputeUnits(Rules) uint64

// OutputsWarpMessage indicates whether an [Action] will produce a warp message. The max size
// of any warp message is [MaxOutgoingWarpChunks].
OutputsWarpMessage() bool
// StateKeysMaxChunks is used to estimate the fee a transaction should pay. It includes the max
// chunks each state key could use without requiring the state keys to actually be provided (may
// not be known until execution).
StateKeysMaxChunks() []uint16

// StateKeys is a full enumeration of all database keys that could be touched during execution
// of an [Action]. This is used to prefetch state and will be used to parallelize execution (making
Expand All @@ -691,12 +685,7 @@ type Action interface {
// key (formatted as a big-endian uint16). This is used to automatically calculate storage usage.
//
// If any key is removed and then re-created, this will count as a creation instead of a modification.
StateKeys(auth Auth, txID ids.ID) []string

// StateKeysMaxChunks is used to estimate the fee a transaction should pay. It includes the max
// chunks each state key could use without requiring the state keys to actually be provided (may
// not be known until execution).
StateKeysMaxChunks() []uint16
StateKeys(actor codec.Address, txID ids.ID) []string

// Execute actually runs the [Action]. Any state changes that the [Action] performs should
// be done here.
Expand All @@ -711,17 +700,14 @@ type Action interface {
r Rules,
mu state.Mutable,
timestamp int64,
auth Auth,
actor codec.Address,
txID ids.ID,
warpVerified bool,
) (success bool, computeUnits uint64, output []byte, warpMessage *warp.UnsignedMessage, err error)

// Marshal encodes an [Action] as bytes.
Marshal(p *codec.Packer)

// Size is the number of bytes it takes to represent this [Action]. This is used to preallocate
// memory during encoding and to charge bandwidth fees.
Size() int
// OutputsWarpMessage indicates whether an [Action] will produce a warp message. The max size
// of any warp message is [MaxOutgoingWarpChunks].
OutputsWarpMessage() bool
}
```

Expand Down Expand Up @@ -755,107 +741,41 @@ and optionally a `WarpMessage` (which Subnet Validators will sign).
### Auth
```golang
type Auth interface {
// GetTypeID uniquely identifies each supported [Auth]. We use IDs to avoid
// reflection.
GetTypeID() uint8
Object

// ValidRange is the timestamp range (in ms) that this [Auth] is considered valid.
//
// -1 means no start/end
ValidRange(Rules) (start int64, end int64)

// MaxComputeUnits is the maximum amount of compute a given [Auth] could use. This is
// used to determine whether the [Auth] can be included in a given block and to compute
// ComputeUnits is the amount of compute required to call [Verify]. This is
// used to determine whether [Auth] can be included in a given block and to compute
// the required fee to execute.
//
// Developers should make every effort to bound this as tightly to the actual max so that
// users don't need to have a large balance to call an [Auth] (must prepay fee before execution).
//
// MaxComputeUnits should take into account [AsyncVerify], [CanDeduct], [Deduct], and [Refund]
MaxComputeUnits(Rules) uint64

// StateKeys is a full enumeration of all database keys that could be touched during execution
// of an [Auth]. This is used to prefetch state and will be used to parallelize execution (making
// an execution tree is trivial).
//
// All keys specified must be suffixed with the number of chunks that could ever be read from that
// key (formatted as a big-endian uint16). This is used to automatically calculate storage usage.
StateKeys() []string

// AsyncVerify should perform any verification that can be run concurrently. It may not be run by the time
// [Verify] is invoked but will be checked before a [Transaction] is considered successful.
//
// AsyncVerify is typically used to perform cryptographic operations.
AsyncVerify(msg []byte) error
ComputeUnits(Rules) uint64

// Verify performs any checks against state required to determine if [Auth] is valid.
//
// This could be used, for example, to determine that the public key used to sign a transaction
// is registered as the signer for an account. This could also be used to pull a [Program] from disk.
Verify(
ctx context.Context,
r Rules,
im state.Immutable,
action Action,
) (computeUnits uint64, err error)
// Verify is run concurrently during transaction verification. It may not be run by the time
// a transaction is executed but will be checked before a [Transaction] is considered successful.
// Verify is typically used to perform cryptographic operations.
Verify(ctx context.Context, msg []byte) error

// Actor is the subject of [Action].
// Actor is the subject of the [Action] signed.
//
// To avoid collisions with other [Auth] modules, this must be prefixed
// by the [TypeID].
Actor() codec.Address

// Sponsor is the fee payer of [Auth].
// Sponsor is the fee payer of the [Action] signed.
//
// If the [Actor] is not the same as [Sponsor], it is likely that the [Actor] signature
// is wrapped by the [Sponsor] signature. It is important that the [Actor], in this case,
// signs the [Sponsor] address or else their transaction could be replayed.
//
// TODO: add a standard sponsor wrapper auth (but this does not need to be handled natively)
//
// 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

// Deduct removes [amount] from [Auth] during transaction execution to pay fees.
Deduct(ctx context.Context, mu state.Mutable, amount uint64) error

// Refund returns [amount] to [Auth] after transaction execution if any fees were
// not used.
//
// Refund will return an error if it attempts to create any new keys. It can only
// modify or remove existing keys.
//
// Refund is only invoked if [amount] > 0.
Refund(ctx context.Context, mu state.Mutable, amount uint64) error

// Marshal encodes an [Auth] as bytes.
Marshal(p *codec.Packer)

// Size is the number of bytes it takes to represent this [Auth]. This is used to preallocate
// memory during encoding and to charge bandwidth fees.
Size() int
}
```

`Auth` shares many similarities with `Action` (recall that authentication is
abstract and defined by the `hypervm`) but adds the notion of some abstract
"payer" that must pay fees for the operations that occur in an `Action`. Any
fees that are not consumed can be returned to said "payer" if specified in the
corresponding `Action` that was authenticated.

The `Auth` mechanism is arguably the most powerful core module of the
`hypersdk` because it lets the builder create arbitrary authentication rules
that align with their goals. The `indexvm`, for example, allows users to rotate
their keys and to enable others to perform specific actions on their behalf. It also
lets accounts natively pay for the fees of other accounts. These features are particularly
useful for server-based accounts that want to implement a periodic key rotation
scheme without losing the history of their rating activity on-chain (which
determines their reputation).

You can view what direct (simple account signature) `Auth` looks like
[here](https://github.com/ava-labs/indexvm/blob/main/auth/direct.go) and what
delegate (acting on behalf of another account) `Auth` looks like
[here](https://github.com/ava-labs/indexvm/blob/main/auth/delegate.go). The
`indexvm` provides an ["authorize" `Action`](https://github.com/ava-labs/indexvm/blob/main/actions/authorize.go)
that an account owner can call to perform any ACL modifications.
that align with their goals.

### Rules
```golang
Expand All @@ -879,17 +799,25 @@ type Rules interface {
GetWarpComputeUnitsPerSigner() uint64
GetOutgoingWarpComputeUnits() uint64

// Controllers must manage the max key length and max value length
GetColdStorageKeyReadUnits() uint64
GetColdStorageValueReadUnits() uint64 // per chunk
GetWarmStorageKeyReadUnits() uint64
GetWarmStorageValueReadUnits() uint64 // per chunk
GetStorageKeyCreateUnits() uint64
GetStorageValueCreateUnits() uint64 // per chunk
GetColdStorageKeyModificationUnits() uint64
GetColdStorageValueModificationUnits() uint64 // per chunk
GetWarmStorageKeyModificationUnits() uint64
GetWarmStorageValueModificationUnits() uint64 // per chunk
// Invariants:
// * Controllers must manage the max key length and max value length (max network
// limit is ~2MB)
// * Creating a new key involves first allocating and then writing
// * Keys are only charged once per transaction (even if used multiple times), it is
// up to the controller to ensure multiple usage has some compute cost
//
// Interesting Scenarios:
// * If a key is created and then modified during a transaction, the second
// read will be a read of 0 chunks (reads are based on disk contents before exec)
// * If a key is removed and then re-created with the same value during a transaction,
// it doesn't count as a modification (returning to the current value on-disk is a no-op)
GetSponsorStateKeysMaxChunks() []uint16
GetStorageKeyReadUnits() uint64
GetStorageValueReadUnits() uint64 // per chunk
GetStorageKeyAllocateUnits() uint64
GetStorageValueAllocateUnits() uint64 // per chunk
GetStorageKeyWriteUnits() uint64
GetStorageValueWriteUnits() uint64 // per chunk

GetWarpConfig(sourceChainID ids.ID) (bool, uint64, uint64)

Expand Down
4 changes: 3 additions & 1 deletion chain/auth_batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
package chain

import (
"context"

"github.com/ava-labs/avalanchego/utils/logging"
"github.com/ava-labs/hypersdk/workers"
)
Expand Down Expand Up @@ -50,7 +52,7 @@ func (a *AuthBatch) Add(digest []byte, auth Auth) {
// processing.
bv, ok := a.bvs[auth.GetTypeID()]
if !ok {
a.job.Go(func() error { return auth.AsyncVerify(digest) })
a.job.Go(func() error { return auth.Verify(context.TODO(), digest) })
return
}
bv.items <- &authBatchObject{digest, auth}
Expand Down
4 changes: 2 additions & 2 deletions chain/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ func (b *StatelessBlock) populateTxs(ctx context.Context) error {

// Setup signature verification job
_, sigVerifySpan := b.vm.Tracer().Start(ctx, "StatelessBlock.verifySignatures")
job, err := b.vm.SignatureWorkers().NewJob(len(b.Txs))
job, err := b.vm.AuthVerifiers().NewJob(len(b.Txs))
if err != nil {
return err
}
Expand All @@ -187,7 +187,7 @@ func (b *StatelessBlock) populateTxs(ctx context.Context) error {
b.txsSet.Add(tx.ID())

// Verify signature async
if b.vm.GetVerifySignatures() {
if b.vm.GetVerifyAuth() {
txDigest, err := tx.Digest()
if err != nil {
return err
Expand Down
4 changes: 1 addition & 3 deletions chain/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,8 +273,7 @@ func BuildBlock(

// Execute block
tsv := ts.NewView(stateKeys, storage)
authCUs, err := tx.PreExecute(ctx, feeManager, sm, r, tsv, nextTime)
if err != nil {
if err := tx.PreExecute(ctx, feeManager, sm, r, tsv, nextTime); err != nil {
// We don't need to rollback [tsv] here because it will never
// be committed.
if HandlePreExecute(log, err) {
Expand Down Expand Up @@ -338,7 +337,6 @@ func BuildBlock(
result, err := tx.Execute(
ctx,
feeManager,
authCUs,
reads,
sm,
r,
Expand Down
Loading
Loading