Skip to content
This repository has been archived by the owner on May 13, 2022. It is now read-only.

Commit

Permalink
Implement cross-engine call dispatch for eWASM
Browse files Browse the repository at this point in the history
- Implement the eWASM `call` syscall to support calling contracts

- Wire up WASM VM as a Disptacher so it can call EVM and Natives
(including Precompiles) as well as other WASM contracts.

- To support 128-bit wide (and 256-bit wide EVM!) values use big.Int for
Value and Gas. Note the Account model currently still uses uint64 so
outside of the execution machinery we still throw an error if a balance
would overflow uint64. This paves the way for relaxing this requirement
and making account balances etc.

- exec.CallData `Value` and `Gas` are now the big-endian big.Int bytes
rather than uint64 to support the above.

- EVM and WASM now share calling and some other code. EVM now more
strictly throws errors that are not ExecutionReverted becuase it
probably always should have and makes it more consistent with WASM.

WASM uses panics to transmit its errors. If we have errors that should
not halt execution (AKA 'non-trapping') we should probably explicitly encode
this fact into the coded errors and act accordingly and consistenly in
WASM and EVM

Signed-off-by: Silas Davis <[email protected]>
  • Loading branch information
Silas Davis authored and seanyoung committed Feb 4, 2021
1 parent 86b3b7b commit 0afe540
Show file tree
Hide file tree
Showing 41 changed files with 1,205 additions and 894 deletions.
18 changes: 18 additions & 0 deletions binary/integer.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,24 @@ func SignExtend(x *big.Int, n uint) *big.Int {
}
}

// Reverse bytes in-place
func reverse(bs []byte) []byte {
n := len(bs) - 1
for i := 0; i < len(bs)/2; i++ {
bs[i], bs[n-i] = bs[n-i], bs[i]
}
return bs
}

// Note: this function destructively reverses its input slice - pass a copy if the original is used elsewhere
func BigIntFromLittleEndianBytes(bs []byte) *big.Int {
return new(big.Int).SetBytes(reverse(bs))
}

func BigIntToLittleEndianBytes(x *big.Int) []byte {
return reverse(x.Bytes())
}

func andMask(n uint) *big.Int {
x := new(big.Int)
return x.Sub(x.Lsh(big1, n), big1)
Expand Down
7 changes: 7 additions & 0 deletions binary/integer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ func TestSignExtend(t *testing.T) {
"0000 0000 0000 0000 0000 0000 0000 0000 0001 0000 1000 0000 1101 0011 1001 0000")
}

func TestLittleEndian(t *testing.T) {
x, ok := new(big.Int).SetString("234890234579042368982348972347234789897", 10)
require.True(t, ok)
y := BigIntFromLittleEndianBytes(BigIntToLittleEndianBytes(x))
require.Equal(t, x.Cmp(y), 0)
}

func assertSignExtend(t *testing.T, extendedBits int, embeddedBits uint, inputString, expectedString string) bool {
input := intFromString(t, extendedBits, inputString)
expected := intFromString(t, extendedBits, expectedString)
Expand Down
4 changes: 3 additions & 1 deletion execution/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package execution
import (
"fmt"

"github.com/hyperledger/burrow/execution/engine"

"github.com/hyperledger/burrow/execution/evm"
)

Expand Down Expand Up @@ -45,7 +47,7 @@ func VMOptions(vmOptions evm.Options) func(*executor) {
func (ec *ExecutionConfig) ExecutionOptions() ([]Option, error) {
var exeOptions []Option
vmOptions := evm.Options{
MemoryProvider: evm.DefaultDynamicMemoryProvider,
MemoryProvider: engine.DefaultDynamicMemoryProvider,
CallStackMaxDepth: ec.CallStackMaxDepth,
DataStackInitialCapacity: ec.DataStackInitialCapacity,
DataStackMaxDepth: ec.DataStackMaxDepth,
Expand Down
23 changes: 13 additions & 10 deletions execution/contexts/call_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package contexts

import (
"fmt"
"math/big"

"github.com/hyperledger/burrow/acm"
"github.com/hyperledger/burrow/acm/acmstate"
Expand All @@ -10,7 +11,6 @@ import (
"github.com/hyperledger/burrow/execution/errors"
"github.com/hyperledger/burrow/execution/evm"
"github.com/hyperledger/burrow/execution/exec"
"github.com/hyperledger/burrow/execution/native"
"github.com/hyperledger/burrow/execution/wasm"
"github.com/hyperledger/burrow/logging"
"github.com/hyperledger/burrow/logging/structure"
Expand Down Expand Up @@ -135,7 +135,7 @@ func (ctx *CallContext) Deliver(inAcc, outAcc *acm.Account, value uint64) error
callee = crypto.NewContractAddress(caller, ctx.txe.TxHash)
code = ctx.tx.Data
wcode = ctx.tx.WASM
err := native.CreateAccount(txCache, callee)
err := engine.CreateAccount(txCache, callee)
if err != nil {
return err
}
Expand All @@ -144,7 +144,7 @@ func (ctx *CallContext) Deliver(inAcc, outAcc *acm.Account, value uint64) error
"init_code", code)

// store abis
err = native.UpdateContractMeta(txCache, metaCache, callee, ctx.tx.ContractMeta)
err = engine.UpdateContractMeta(txCache, metaCache, callee, ctx.tx.ContractMeta)
if err != nil {
return err
}
Expand Down Expand Up @@ -184,19 +184,21 @@ func (ctx *CallContext) Deliver(inAcc, outAcc *acm.Account, value uint64) error
var ret []byte
var err error
txHash := ctx.txe.Envelope.Tx.Hash()
gas := ctx.tx.GasLimit
gas := new(big.Int).SetUint64(ctx.tx.GasLimit)

params := engine.CallParams{
Origin: caller,
Caller: caller,
Callee: callee,
Input: ctx.tx.Data,
Value: value,
Gas: &gas,
Value: *new(big.Int).SetUint64(value),
Gas: gas,
}

if len(wcode) != 0 {
ret, err = wasm.RunWASM(txCache, params, wcode)
// TODO: accept options
vm := wasm.Default()
ret, err = vm.Execute(txCache, ctx.Blockchain, ctx.txe, params, wcode)
if err != nil {
// Failure. Charge the gas fee. The 'value' was otherwise not transferred.
ctx.Logger.InfoMsg("Error on WASM execution",
Expand All @@ -206,7 +208,7 @@ func (ctx *CallContext) Deliver(inAcc, outAcc *acm.Account, value uint64) error
} else {
ctx.Logger.TraceMsg("Successful execution")
if createContract {
err := native.InitWASMCode(txCache, callee, ret)
err := engine.InitWASMCode(txCache, callee, ret)
if err != nil {
return err
}
Expand All @@ -233,7 +235,7 @@ func (ctx *CallContext) Deliver(inAcc, outAcc *acm.Account, value uint64) error
} else {
ctx.Logger.TraceMsg("Successful execution")
if createContract {
err := native.InitEVMCode(txCache, callee, ret)
err := engine.InitEVMCode(txCache, callee, ret)
if err != nil {
return err
}
Expand All @@ -245,7 +247,8 @@ func (ctx *CallContext) Deliver(inAcc, outAcc *acm.Account, value uint64) error
}
ctx.CallEvents(err)
}
ctx.txe.Return(ret, ctx.tx.GasLimit-gas)
// Gas starts life as a uint64 and should only been reduced (used up) over a transaction so .Uint64() is safe
ctx.txe.Return(ret, ctx.tx.GasLimit-gas.Uint64())
// Create a receipt from the ret and whether it erred.
ctx.Logger.TraceMsg("VM Call complete",
"caller", caller,
Expand Down
46 changes: 17 additions & 29 deletions execution/native/account.go → execution/engine/account.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package native
package engine

import (
"bytes"
"math/big"

"github.com/hyperledger/burrow/acm"
"github.com/hyperledger/burrow/acm/acmstate"
Expand All @@ -12,23 +13,6 @@ import (
"golang.org/x/crypto/sha3"
)

func CreateAccount(st acmstate.ReaderWriter, address crypto.Address) error {
acc, err := st.GetAccount(address)
if err != nil {
return err
}
if acc != nil {
if acc.NativeName != "" {
return errors.Errorf(errors.Codes.ReservedAddress,
"cannot create account at %v because that address is reserved for a native contract '%s'",
address, acc.NativeName)
}
return errors.Errorf(errors.Codes.DuplicateAddress,
"tried to create an account at an address that already exists: %v", address)
}
return st.UpdateAccount(&acm.Account{Address: address})
}

func InitEVMCode(st acmstate.ReaderWriter, address crypto.Address, code []byte) error {
return initEVMCode(st, address, nil, code)
}
Expand All @@ -38,7 +22,7 @@ func InitChildCode(st acmstate.ReaderWriter, address crypto.Address, parent cryp
}

func initEVMCode(st acmstate.ReaderWriter, address crypto.Address, parent *crypto.Address, code []byte) error {
acc, err := mustAccount(st, address)
acc, err := MustAccount(st, address)
if err != nil {
return err
}
Expand Down Expand Up @@ -116,7 +100,7 @@ func codehashPermitted(codehash []byte, metamap []*acm.ContractMeta) bool {
}

func InitWASMCode(st acmstate.ReaderWriter, address crypto.Address, code []byte) error {
acc, err := mustAccount(st, address)
acc, err := MustAccount(st, address)
if err != nil {
return err
}
Expand All @@ -133,33 +117,37 @@ func InitWASMCode(st acmstate.ReaderWriter, address crypto.Address, code []byte)
return st.UpdateAccount(acc)
}

func Transfer(st acmstate.ReaderWriter, from, to crypto.Address, amount uint64) error {
if amount == 0 {
// TODO: consider pushing big.Int usage all the way to account balance
func Transfer(st acmstate.ReaderWriter, from, to crypto.Address, amount *big.Int) error {
if !amount.IsInt64() {
return errors.Errorf(errors.Codes.IntegerOverflow, "transfer amount %v overflows int64", amount)
}
if amount.Sign() == 0 {
return nil
}
acc, err := mustAccount(st, from)
acc, err := MustAccount(st, from)
if err != nil {
return err
}
if acc.Balance < amount {
if new(big.Int).SetUint64(acc.Balance).Cmp(amount) < 0 {
return errors.Codes.InsufficientBalance
}
err = UpdateAccount(st, from, func(account *acm.Account) error {
return account.SubtractFromBalance(amount)
return account.SubtractFromBalance(amount.Uint64())
})
if err != nil {
return err
}
return UpdateAccount(st, to, func(account *acm.Account) error {
return account.AddToBalance(amount)
return account.AddToBalance(amount.Uint64())
})
}

func UpdateContractMeta(st acmstate.ReaderWriter, metaSt acmstate.MetadataWriter, address crypto.Address, payloadMeta []*payload.ContractMeta) error {
if len(payloadMeta) == 0 {
return nil
}
acc, err := mustAccount(st, address)
acc, err := MustAccount(st, address)
if err != nil {
return err
}
Expand Down Expand Up @@ -194,7 +182,7 @@ func RemoveAccount(st acmstate.ReaderWriter, address crypto.Address) error {
}

func UpdateAccount(st acmstate.ReaderWriter, address crypto.Address, updater func(acc *acm.Account) error) error {
acc, err := mustAccount(st, address)
acc, err := MustAccount(st, address)
if err != nil {
return err
}
Expand All @@ -205,7 +193,7 @@ func UpdateAccount(st acmstate.ReaderWriter, address crypto.Address, updater fun
return st.UpdateAccount(acc)
}

func mustAccount(st acmstate.Reader, address crypto.Address) (*acm.Account, error) {
func MustAccount(st acmstate.Reader, address crypto.Address) (*acm.Account, error) {
acc, err := st.GetAccount(address)
if err != nil {
return nil, err
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package native
package engine

import (
"testing"

"github.com/hyperledger/burrow/acm"
"github.com/hyperledger/burrow/acm/acmstate"
"github.com/hyperledger/burrow/crypto"
"github.com/hyperledger/burrow/execution/engine"
"github.com/hyperledger/burrow/execution/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand All @@ -30,7 +29,7 @@ func TestState_CreateAccount(t *testing.T) {

func TestState_Sync(t *testing.T) {
backend := acmstate.NewCache(acmstate.NewMemoryState())
st := engine.NewCallFrame(backend)
st := NewCallFrame(backend)
address := AddressFromName("frogs")

err := CreateAccount(st, address)
Expand All @@ -46,7 +45,7 @@ func TestState_Sync(t *testing.T) {
}

func TestState_NewCache(t *testing.T) {
st := engine.NewCallFrame(acmstate.NewMemoryState())
st := NewCallFrame(acmstate.NewMemoryState())
address := AddressFromName("frogs")

cache, err := st.NewFrame()
Expand All @@ -64,7 +63,7 @@ func TestState_NewCache(t *testing.T) {
err = cache.Sync()
require.NoError(t, err)

acc, err = mustAccount(cache, address)
acc, err = MustAccount(cache, address)
require.NoError(t, err)
assert.Equal(t, amt, acc.Balance)

Expand Down
98 changes: 98 additions & 0 deletions execution/engine/accounts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package engine

import (
"fmt"

"github.com/hyperledger/burrow/acm"
"github.com/hyperledger/burrow/acm/acmstate"
"github.com/hyperledger/burrow/crypto"
"github.com/hyperledger/burrow/execution/errors"
"github.com/hyperledger/burrow/permission"
)

type Maybe interface {
PushError(err error) bool
Error() error
}

func GetAccount(st acmstate.Reader, m Maybe, address crypto.Address) *acm.Account {
acc, err := st.GetAccount(address)
if err != nil {
m.PushError(err)
return nil
}
return acc
}

// Guaranteed to return a non-nil account, if the account does not exist returns a pointer to the zero-value of Account
// and pushes an error.
func MustGetAccount(st acmstate.Reader, m Maybe, address crypto.Address) *acm.Account {
acc := GetAccount(st, m, address)
if acc == nil {
m.PushError(errors.Errorf(errors.Codes.NonExistentAccount, "account %v does not exist", address))
return &acm.Account{}
}
return acc
}

func EnsurePermission(callFrame *CallFrame, address crypto.Address, perm permission.PermFlag) error {
hasPermission, err := HasPermission(callFrame, address, perm)
if err != nil {
return err
} else if !hasPermission {
return errors.PermissionDenied{
Address: address,
Perm: perm,
}
}
return nil
}

// CONTRACT: it is the duty of the contract writer to call known permissions
// we do not convey if a permission is not set
// (unlike in state/execution, where we guarantee HasPermission is called
// on known permissions and panics else)
// If the perm is not defined in the acc nor set by default in GlobalPermissions,
// this function returns false.
func HasPermission(st acmstate.Reader, address crypto.Address, perm permission.PermFlag) (bool, error) {
acc, err := st.GetAccount(address)
if err != nil {
return false, err
}
if acc == nil {
return false, fmt.Errorf("account %v does not exist", address)
}
globalPerms, err := acmstate.GlobalAccountPermissions(st)
if err != nil {
return false, err
}
perms := acc.Permissions.Base.Compose(globalPerms.Base)
value, err := perms.Get(perm)
if err != nil {
return false, err
}
return value, nil
}

func CreateAccount(st acmstate.ReaderWriter, address crypto.Address) error {
acc, err := st.GetAccount(address)
if err != nil {
return err
}
if acc != nil {
if acc.NativeName != "" {
return errors.Errorf(errors.Codes.ReservedAddress,
"cannot create account at %v because that address is reserved for a native contract '%s'",
address, acc.NativeName)
}
return errors.Errorf(errors.Codes.DuplicateAddress,
"tried to create an account at an address that already exists: %v", address)
}
return st.UpdateAccount(&acm.Account{Address: address})
}

func AddressFromName(name string) (address crypto.Address) {
hash := crypto.Keccak256([]byte(name))
copy(address[:], hash[len(hash)-crypto.AddressLength:])
return
}
Loading

0 comments on commit 0afe540

Please sign in to comment.