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

Default Contract Manager Implementation #1687

Merged
merged 9 commits into from
Oct 25, 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
26 changes: 17 additions & 9 deletions x/contracts/runtime/call_context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/ava-labs/hypersdk/codec"
"github.com/ava-labs/hypersdk/x/contracts/test"
)

func TestCallContext(t *testing.T) {
Expand All @@ -22,11 +23,14 @@ func TestCallContext(t *testing.T) {
contractID := ids.GenerateTestID()
contractAccount := codec.CreateAddress(0, contractID)
stringedID := string(contractID[:])
contractManager := NewContractStateManager(test.NewTestDB(), []byte{})
err := contractManager.SetAccountContract(ctx, contractAccount, ContractID(stringedID))
require.NoError(err)
testStateManager := &TestStateManager{
ContractsMap: map[string][]byte{},
AccountMap: map[codec.Address]string{contractAccount: stringedID},
ContractManager: contractManager,
}
err := testStateManager.CompileAndSetContract(ContractID(stringedID), "call_contract")

err = testStateManager.CompileAndSetContract(ContractID(stringedID), "call_contract")
require.NoError(err)

r := NewRuntime(
Expand Down Expand Up @@ -75,12 +79,13 @@ func TestCallContextPreventOverwrite(t *testing.T) {
contract1Address := codec.CreateAddress(1, contract1ID)
stringedID0 := string(contract0ID[:])

contractManager := NewContractStateManager(test.NewTestDB(), []byte{})
err := contractManager.SetAccountContract(ctx, contract0Address, ContractID(stringedID0))
require.NoError(err)
testStateManager := &TestStateManager{
ContractsMap: map[string][]byte{},
AccountMap: map[codec.Address]string{contract0Address: stringedID0},
ContractManager: contractManager,
}

err := testStateManager.CompileAndSetContract(ContractID(stringedID0), "call_contract")
err = testStateManager.CompileAndSetContract(ContractID(stringedID0), "call_contract")
require.NoError(err)

r := NewRuntime(
Expand All @@ -94,10 +99,13 @@ func TestCallContextPreventOverwrite(t *testing.T) {
})

stringedID1 := string(contract1ID[:])
contractManager1 := NewContractStateManager(test.NewTestDB(), []byte{})
err = contractManager.SetAccountContract(ctx, contract1Address, ContractID(stringedID1))
require.NoError(err)
testStateManager1 := &TestStateManager{
ContractsMap: map[string][]byte{},
AccountMap: map[codec.Address]string{contract1Address: stringedID1},
ContractManager: contractManager1,
}

err = testStateManager1.CompileAndSetContract(ContractID(stringedID1), "call_contract")
require.NoError(err)

Expand Down
177 changes: 177 additions & 0 deletions x/contracts/runtime/manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Copyright (C) 2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package runtime

import (
"context"
"crypto/sha256"
"errors"

"github.com/ava-labs/avalanchego/database"
"github.com/ava-labs/avalanchego/ids"

"github.com/ava-labs/hypersdk/codec"
"github.com/ava-labs/hypersdk/state"
)

var (
_ ContractManager = &ContractStateManager{}
Copy link
Contributor

Choose a reason for hiding this comment

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

What's this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

type check

ErrUnknownAccount = errors.New("unknown account")
contractKeyBytes = []byte("contract")
)

const (
// Global directory of contractIDs to contractBytes
contractPrefix = 0x0

// Prefix for all contract state spaces
accountPrefix = 0x1
// Associated data for an account, such as the contractID
accountDataPrefix = 0x0
// State space associated with an account
accountStatePrefix = 0x1
)

// ContractStateManager is an out of the box implementation of the ContractManager interface.
// The contract state manager is responsible for managing all state keys associated with contracts.
type ContractStateManager struct {
db state.Mutable
}

// NewContractStateManager returns a new ContractStateManager instance.
// [prefix] must be unique to ensures the contract's state space
// remains isolated from other state spaces in [db].
func NewContractStateManager(
db state.Mutable,
prefix []byte,
) *ContractStateManager {
prefixedState := newPrefixStateMutable(prefix, db)

return &ContractStateManager{
db: prefixedState,
}
}

// GetContractState returns a mutable state instance associated with [account].
func (p *ContractStateManager) GetContractState(account codec.Address) state.Mutable {
return newAccountPrefixedMutable(account, p.db)
}

// GetAccountContract grabs the associated id with [account]. The ID is the key mapping to the contractbytes
// Errors if there is no found account or an error fetching
func (p *ContractStateManager) GetAccountContract(ctx context.Context, account codec.Address) (ContractID, error) {
contractID, exists, err := p.getAccountContract(ctx, account)
if err != nil {
return ids.Empty[:], err
}
if !exists {
return ids.Empty[:], ErrUnknownAccount
}
return contractID[:], nil
}

// [contractID] -> [contractBytes]
func (p *ContractStateManager) GetContractBytes(ctx context.Context, contractID ContractID) ([]byte, error) {
// TODO: take fee out of balance?
contractBytes, err := p.db.GetValue(ctx, contractKey(contractID))
if err != nil {
return []byte{}, ErrUnknownAccount
}

return contractBytes, nil
}

func (p *ContractStateManager) NewAccountWithContract(ctx context.Context, contractID ContractID, accountCreationData []byte) (codec.Address, error) {
newID := sha256.Sum256(append(contractID, accountCreationData...))
newAccount := codec.CreateAddress(0, newID)
return newAccount, p.SetAccountContract(ctx, newAccount, contractID)
}

func (p *ContractStateManager) SetAccountContract(ctx context.Context, account codec.Address, contractID ContractID) error {
return p.db.Insert(ctx, accountDataKey(account[:], contractKeyBytes), contractID)
}

// setContract stores [contract] at [contractID]
func (p *ContractStateManager) SetContractBytes(
ctx context.Context,
contractID ContractID,
contract []byte,
) error {
return p.db.Insert(ctx, contractKey(contractID[:]), contract)
}

func contractKey(key []byte) (k []byte) {
k = make([]byte, 0, 1+len(key))
k = append(k, contractPrefix)
k = append(k, key...)
return
}

// Creates a key an account balance key
func accountDataKey(account []byte, key []byte) (k []byte) {
// accountPrefix + account + accountDataPrefix + key
k = make([]byte, 0, 2+len(account)+len(key))
k = append(k, accountPrefix)
k = append(k, account...)
k = append(k, accountDataPrefix)
k = append(k, key...)
return
}

func accountContractKey(account []byte) []byte {
return accountDataKey(account, contractKeyBytes)
}

func (p *ContractStateManager) getAccountContract(ctx context.Context, account codec.Address) (ids.ID, bool, error) {
v, err := p.db.GetValue(ctx, accountContractKey(account[:]))
if errors.Is(err, database.ErrNotFound) {
return ids.Empty, false, nil
}
if err != nil {
return ids.Empty, false, err
}
return ids.ID(v[:ids.IDLen]), true, nil
}

// prefixed state
type prefixedStateMutable struct {
inner state.Mutable
prefix []byte
}

func newPrefixStateMutable(prefix []byte, inner state.Mutable) *prefixedStateMutable {
return &prefixedStateMutable{inner: inner, prefix: prefix}
}

func (s *prefixedStateMutable) prefixKey(key []byte) (k []byte) {
k = make([]byte, len(s.prefix)+len(key))
copy(k, s.prefix)
copy(k[len(s.prefix):], key)
return
}

func (s *prefixedStateMutable) GetValue(ctx context.Context, key []byte) (value []byte, err error) {
return s.inner.GetValue(ctx, s.prefixKey(key))
}

func (s *prefixedStateMutable) Insert(ctx context.Context, key []byte, value []byte) error {
return s.inner.Insert(ctx, s.prefixKey(key), value)
}

func (s *prefixedStateMutable) Remove(ctx context.Context, key []byte) error {
return s.inner.Remove(ctx, s.prefixKey(key))
}

func newAccountPrefixedMutable(account codec.Address, mutable state.Mutable) state.Mutable {
return &prefixedStateMutable{inner: mutable, prefix: accountStateKey(account[:])}
}

// [accountPrefix] + [account] + [accountStatePrefix] = state space associated with a contract
func accountStateKey(key []byte) (k []byte) {
k = make([]byte, 2+len(key))
k[0] = accountPrefix
copy(k[1:], key)
k[len(k)-1] = accountStatePrefix
return
}
2 changes: 2 additions & 0 deletions x/contracts/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ type ContractManager interface {
NewAccountWithContract(ctx context.Context, contractID ContractID, accountCreationData []byte) (codec.Address, error)
// SetAccountContract associates the given contract ID with the given account.
SetAccountContract(ctx context.Context, account codec.Address, contractID ContractID) error
// SetContractBytes stores the compiled WASM bytes of the contract with the given ID.
SetContractBytes(ctx context.Context, contractID ContractID, contractBytes []byte) error
}

func NewRuntime(
Expand Down
48 changes: 15 additions & 33 deletions x/contracts/runtime/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,16 @@ import (
)

type TestStateManager struct {
ContractsMap map[string][]byte
AccountMap map[codec.Address]string
Balances map[codec.Address]uint64
Mu state.Mutable
ContractManager *ContractStateManager
Balances map[codec.Address]uint64
}

func (t TestStateManager) GetAccountContract(_ context.Context, account codec.Address) (ContractID, error) {
if contractID, ok := t.AccountMap[account]; ok {
return ContractID(contractID), nil
}
return ids.Empty[:], nil
func (t TestStateManager) GetAccountContract(ctx context.Context, account codec.Address) (ContractID, error) {
return t.ContractManager.GetAccountContract(ctx, account)
}

func (t TestStateManager) GetContractBytes(_ context.Context, contractID ContractID) ([]byte, error) {
contractBytes, ok := t.ContractsMap[string(contractID)]
if !ok {
return nil, errors.New("couldn't find contract")
}

return contractBytes, nil
func (t TestStateManager) GetContractBytes(ctx context.Context, contractID ContractID) ([]byte, error) {
return t.ContractManager.GetContractBytes(ctx, contractID)
}

func compileContract(contractName string) ([]byte, error) {
Expand All @@ -59,28 +49,24 @@ func compileContract(contractName string) ([]byte, error) {
return contractBytes, nil
}

func (t TestStateManager) SetContractBytes(contractID ContractID, contractBytes []byte) {
t.ContractsMap[string(contractID)] = contractBytes
func (t TestStateManager) SetContractBytes(ctx context.Context, contractID ContractID, contractBytes []byte) error {
return t.ContractManager.SetContractBytes(ctx, contractID, contractBytes)
}

func (t TestStateManager) CompileAndSetContract(contractID ContractID, contractName string) error {
contractBytes, err := compileContract(contractName)
if err != nil {
return err
}
t.SetContractBytes(contractID, contractBytes)
return nil
return t.SetContractBytes(context.Background(), contractID, contractBytes)
}

func (t TestStateManager) NewAccountWithContract(_ context.Context, contractID ContractID, _ []byte) (codec.Address, error) {
account := codec.CreateAddress(0, ids.GenerateTestID())
t.AccountMap[account] = string(contractID)
return account, nil
return t.ContractManager.NewAccountWithContract(context.Background(), contractID, []byte{})
}

func (t TestStateManager) SetAccountContract(_ context.Context, account codec.Address, contractID ContractID) error {
t.AccountMap[account] = string(contractID)
return nil
return t.ContractManager.SetAccountContract(context.Background(), account, contractID)
}

func (t TestStateManager) GetBalance(_ context.Context, address codec.Address) (uint64, error) {
Expand All @@ -104,7 +90,7 @@ func (t TestStateManager) TransferBalance(ctx context.Context, from codec.Addres
}

func (t TestStateManager) GetContractState(address codec.Address) state.Mutable {
return &prefixedState{address: address, inner: t.Mu}
return t.ContractManager.GetContractState(address)
}

var _ state.Mutable = (*prefixedState)(nil)
Expand Down Expand Up @@ -197,9 +183,7 @@ func (t *testRuntime) AddContract(contractID ContractID, account codec.Address,
if err != nil {
return err
}

t.StateManager.(TestStateManager).AccountMap[account] = string(contractID)
return nil
return t.StateManager.(TestStateManager).SetAccountContract(t.Context, account, contractID)
}

func (t *testRuntime) CallContract(contract codec.Address, function string, params ...interface{}) ([]byte, error) {
Expand All @@ -220,10 +204,8 @@ func newTestRuntime(ctx context.Context) *testRuntime {
NewConfig(),
logging.NoLog{}).WithDefaults(CallInfo{Fuel: 1000000000}),
StateManager: TestStateManager{
ContractsMap: map[string][]byte{},
AccountMap: map[codec.Address]string{},
Balances: map[codec.Address]uint64{},
Mu: test.NewTestDB(),
ContractManager: NewContractStateManager(test.NewTestDB(), []byte{}),
Balances: map[codec.Address]uint64{},
},
}
}
Expand Down
5 changes: 3 additions & 2 deletions x/contracts/simulator/ffi/ffi.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,15 @@ func CreateContract(db *C.Mutable, path *C.char) C.CreateContractResponse {
}
}

contractID, err := generateRandomID()
id, err := generateRandomID()
if err != nil {
return C.CreateContractResponse{
error: C.CString(err.Error()),
}
}

err = contractManager.SetContract(context.TODO(), contractID, contractBytes)
contractID := runtime.ContractID(id[:])
err = contractManager.SetContractBytes(context.TODO(), contractID, contractBytes)
if err != nil {
errmsg := "contract creation failed: " + err.Error()
return C.CreateContractResponse{
Expand Down
Loading
Loading