Skip to content

Commit

Permalink
Merge pull request #680 from iotaledger/develop
Browse files Browse the repository at this point in the history
0.2.3 release
  • Loading branch information
fijter authored Dec 10, 2021
2 parents 506ed1c + 7cf4eed commit 8cd6ecd
Show file tree
Hide file tree
Showing 711 changed files with 31,936 additions and 23,309 deletions.
2 changes: 0 additions & 2 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
name: Test

on:
push:
branches: [develop]
pull_request:
branches: [develop]

Expand Down
11 changes: 9 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ GIT_COMMIT_SHA := $(shell git rev-list -1 HEAD)
BUILD_TAGS = rocksdb,builtin_static
BUILD_LD_FLAGS = "-X github.com/iotaledger/wasp/packages/wasp.VersionHash=$(GIT_COMMIT_SHA)"

#
# You can override these e.g. as
# make test TEST_PKG=./packages/vm/core/testcore/ TEST_ARG="-v --run TestAccessNodes"
#
TEST_PKG=./...
TEST_ARG=

all: build-lint

build:
Expand All @@ -16,7 +23,7 @@ test-full: install
go test -tags $(BUILD_TAGS),runheavy ./... --timeout 60m --count 1 -failfast

test: install
go test -tags $(BUILD_TAGS) ./... --timeout 30m --count 1 -failfast
go test -tags $(BUILD_TAGS) $(TEST_PKG) --timeout 30m --count 1 -failfast $(TEST_ARG)

test-short:
go test -tags $(BUILD_TAGS) --short --count 1 -failfast ./...
Expand All @@ -39,5 +46,5 @@ docker-build:
--build-arg BUILD_LD_FLAGS='${BUILD_LD_FLAGS}' \
.

.PHONY: all build build-windows build-lint test test-short install install-windows lint gofumpt-list docker-build
.PHONY: all build build-windows build-lint test test-short test-full install install-windows lint gofumpt-list docker-build

3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
introduction](https://blog.iota.org/an-introduction-to-iota-smart-contracts-16ea6f247936)
into ISCP.

The comprehensive overview of design decisions of _IOTA Smart Contracts_ can be found in the
[whitepaper](https://github.com/iotaledger/wasp/raw/master/documentation/ISC_WP_Nov_10_2021.pdf).

## Documentation

The documentation for Wasp and IOTA Smart Contracts can be found on the [IOTA Wiki](https://wiki.iota.org/wasp/overview).
Expand Down
2 changes: 1 addition & 1 deletion client/chainclient/chainclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func (c *Client) PostOffLedgerRequest(
c.nonces[c.KeyPair.PublicKey]++
par.Nonce = c.nonces[c.KeyPair.PublicKey]
}
offledgerReq := request.NewOffLedger(contractHname, entrypoint, par.Args).WithTransfer(par.Transfer)
offledgerReq := request.NewOffLedger(c.ChainID, contractHname, entrypoint, par.Args).WithTransfer(par.Transfer)
offledgerReq.WithNonce(par.Nonce)
offledgerReq.Sign(c.KeyPair)
return offledgerReq, c.WaspClient.PostOffLedgerRequest(c.ChainID, offledgerReq)
Expand Down
2 changes: 1 addition & 1 deletion client/committee_record.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func (c *WaspClient) GetCommitteeRecord(addr ledgerstate.Address) (*registry.Com
// GetCommitteeForChain fetches the CommitteeRecord that manages the given chain
func (c *WaspClient) GetCommitteeForChain(chainID *iscp.ChainID) (*registry.CommitteeRecord, error) {
res := &model.CommitteeRecord{}
if err := c.do(http.MethodGet, routes.GetCommitteeForChain(chainID.Base58()), nil, res); err != nil {
if err := c.do(http.MethodGet, routes.GetCommitteeForChain(chainID.Base58())+"?includeDeactivated=true", nil, res); err != nil {
return nil, err
}
return res.Record(), nil
Expand Down
27 changes: 27 additions & 0 deletions client/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package client

import (
"net/http"

"github.com/iotaledger/wasp/packages/iscp"
"github.com/iotaledger/wasp/packages/webapi/model"
"github.com/iotaledger/wasp/packages/webapi/routes"
)

// GetNodeConnectionMetrics fetches a connection to L1 metrics for all addresses
func (c *WaspClient) GetNodeConnectionMetrics() (*model.NodeConnectionMetrics, error) {
ncm := &model.NodeConnectionMetrics{}
if err := c.do(http.MethodGet, routes.GetChainsNodeConnectionMetrics(), nil, ncm); err != nil {
return nil, err
}
return ncm, nil
}

// GetNodeConnectionMetrics fetches a connection to L1 metrics by address
func (c *WaspClient) GetChainNodeConnectionMetrics(chID *iscp.ChainID) (*model.NodeConnectionMessagesMetrics, error) {
ncmm := &model.NodeConnectionMessagesMetrics{}
if err := c.do(http.MethodGet, routes.GetChainNodeConnectionMetrics(chID.Base58()), nil, ncmm); err != nil {
return nil, err
}
return ncmm, nil
}
19 changes: 19 additions & 0 deletions contracts/native/evm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,22 @@ Finally we start the JSON-RPC server:
```
wasp-cli chain evm jsonrpc
```

## Predictable block time

Some EVM contracts depend on blocks being minted periodically with regular
intervals. ISCP does not support that natively, so by default a new EVM block
is minted every time an ISCP batch is executed that contains at least one EVM
transaction. In other words, by default no EVM blocks will be minted until an
EVM transaction is received.

However, the `evmlight` implementation supports emulating predictable block
times. To enable this feature, just pass the `--block-time n` flag when
deploying the EVM chain with `wasp-cli chain evm deploy`, where `n` is
the desired average amount of seconds between blocks.

Note that this may change the behavior of JSON-RPC functions that query the
EVM state (e.g. `getBalance`), since `evmlight` is not able to store the state
in both the latest minted block and the pending block. These functions will
always return the state computed after accepting the latest transaction (i.e.
the state of the pending block).
2 changes: 1 addition & 1 deletion contracts/native/evm/evmchain/emulator/emulator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,6 @@ func initBenchmark(b *testing.B) (*EVMEmulator, []*types.Transaction, dict.Dict)
InitGenesis(evm.DefaultChainID, db, genesisAlloc, evm.GasLimitDefault, 0)

emu := NewEVMEmulator(db)
defer emu.Close()

contractABI, err := abi.JSON(strings.NewReader(evmtest.StorageContractABI))
require.NoError(b, err)
Expand Down Expand Up @@ -520,6 +519,7 @@ func initBenchmark(b *testing.B) (*EVMEmulator, []*types.Transaction, dict.Dict)
func benchmarkEVMEmulator(b *testing.B, k int) {
// setup: deploy the storage contract and prepare N transactions to send
emu, txs, db := initBenchmark(b)
defer emu.Close()

b.ResetTimer()
for i := 0; i < b.N; i++ {
Expand Down
24 changes: 10 additions & 14 deletions contracts/native/evm/evmchain/impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,32 +43,28 @@ func initialize(ctx iscp.Sandbox) (dict.Dict, error) {
a := assert.NewAssert(ctx.Log())
genesisAlloc, err := evmtypes.DecodeGenesisAlloc(ctx.Params().MustGet(evm.FieldGenesisAlloc))
a.RequireNoError(err)

gasLimit, err := codec.DecodeUint64(ctx.Params().MustGet(evm.FieldGasLimit), evm.GasLimitDefault)
a.RequireNoError(err)

chainID, err := codec.DecodeUint16(ctx.Params().MustGet(evm.FieldChainID), evm.DefaultChainID)
a.RequireNoError(err)
emulator.InitGenesis(
int(chainID),
rawdb.NewDatabase(emulator.NewKVAdapter(ctx.State())), // TODO: use subrealm to avoid collisions with evm management
rawdb.NewDatabase(emulator.NewKVAdapter(evminternal.EVMStateSubrealm(ctx.State()))),
genesisAlloc,
evm.GasLimitDefault,
gasLimit,
timestamp(ctx),
)
evminternal.InitializeManagement(ctx)
return nil, nil
}

func applyTransaction(ctx iscp.Sandbox) (dict.Dict, error) {
a := assert.NewAssert(ctx.Log())

tx := &types.Transaction{}
err := tx.UnmarshalBinary(ctx.Params().MustGet(evm.FieldTransactionData))
a.RequireNoError(err)

return evminternal.RequireGasFee(ctx, tx.Gas(), func() uint64 {
emu := getOrCreateEmulator(ctx)
receipt, err := emu.SendTransaction(tx)
a.RequireNoError(err)
return receipt.GasUsed
}), nil
return evminternal.ApplyTransaction(ctx, func(tx *types.Transaction, _ uint32) (*types.Receipt, error) {
emu := getEmulatorInBlockContext(ctx)
return emu.SendTransaction(tx)
})
}

func getBalance(ctx iscp.SandboxView) (dict.Dict, error) {
Expand Down
9 changes: 5 additions & 4 deletions contracts/native/evm/evmchain/internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/iotaledger/wasp/contracts/native/evm"
"github.com/iotaledger/wasp/contracts/native/evm/evmchain/emulator"
"github.com/iotaledger/wasp/contracts/native/evm/evminternal"
"github.com/iotaledger/wasp/packages/evm/evmtypes"
"github.com/iotaledger/wasp/packages/iscp"
"github.com/iotaledger/wasp/packages/iscp/assert"
Expand All @@ -33,16 +34,16 @@ func isNotFound(err error) bool {
return false
}

// getOrCreateEmulator creates a new emulator instance if this is the first call to applyTransaction
// getEmulatorInBlockContext creates a new emulator instance if this is the first call to applyTransaction
// in the ISCP block; otherwise it returns the previously created instance. The purpose is to
// create a single Ethereum block for each ISCP block.
func getOrCreateEmulator(ctx iscp.Sandbox) *emulator.EVMEmulator {
func getEmulatorInBlockContext(ctx iscp.Sandbox) *emulator.EVMEmulator {
bctx := ctx.BlockContext(createEmulator, commitEthereumBlock)
return bctx.(*emulator.EVMEmulator)
}

func createEmulator(ctx iscp.Sandbox) interface{} {
return emulator.NewEVMEmulator(rawdb.NewDatabase(emulator.NewKVAdapter(ctx.State())), timestamp(ctx))
return emulator.NewEVMEmulator(rawdb.NewDatabase(emulator.NewKVAdapter(evminternal.EVMStateSubrealm(ctx.State()))), timestamp(ctx))
}

// timestamp returns the current timestamp in seconds since epoch
Expand All @@ -59,7 +60,7 @@ func commitEthereumBlock(blockContext interface{}) {

func withEmulatorR(ctx iscp.SandboxView, f func(*emulator.EVMEmulator) (dict.Dict, error)) (dict.Dict, error) {
emu := emulator.NewEVMEmulator(
rawdb.NewDatabase(emulator.NewKVAdapter(buffered.NewBufferedKVStoreAccess(ctx.State()))),
rawdb.NewDatabase(emulator.NewKVAdapter(evminternal.EVMStateSubrealm(buffered.NewBufferedKVStoreAccess(ctx.State())))),
timestamp(ctx),
)
defer emu.Close()
Expand Down
94 changes: 87 additions & 7 deletions contracts/native/evm/evminternal/management.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@
package evminternal

import (
"time"

"github.com/ethereum/go-ethereum/core/types"
"github.com/iotaledger/wasp/contracts/native/evm"
"github.com/iotaledger/wasp/packages/iscp"
"github.com/iotaledger/wasp/packages/iscp/assert"
"github.com/iotaledger/wasp/packages/iscp/colored"
"github.com/iotaledger/wasp/packages/iscp/coreutil"
"github.com/iotaledger/wasp/packages/kv"
"github.com/iotaledger/wasp/packages/kv/codec"
"github.com/iotaledger/wasp/packages/kv/dict"
"github.com/iotaledger/wasp/packages/kv/kvdecoder"
"github.com/iotaledger/wasp/packages/kv/subrealm"
"github.com/iotaledger/wasp/packages/vm/core/accounts"
"github.com/iotaledger/wasp/packages/vm/core/governance"
)
Expand All @@ -20,6 +25,10 @@ const (
keyGasPerIota = "g"
keyEVMOwner = "o"
keyNextEVMOwner = "n"
keyBlockTime = "b"

// keyEVMState is the subrealm prefix for the EVM state
keyEVMState = "s"
)

var ManagementHandlers = []coreutil.ProcessorEntryPoint{
Expand All @@ -29,18 +38,69 @@ var ManagementHandlers = []coreutil.ProcessorEntryPoint{
evm.FuncWithdrawGasFees.WithHandler(withdrawGasFees),
evm.FuncGetOwner.WithHandler(getOwner),
evm.FuncGetGasPerIota.WithHandler(getGasPerIota),
evm.FuncSetBlockTime.WithHandler(setBlockTime),
}

func EVMStateSubrealm(state kv.KVStore) kv.KVStore {
return subrealm.New(state, keyEVMState)
}

func InitializeManagement(ctx iscp.Sandbox) {
ctx.State().Set(keyGasPerIota, codec.EncodeUint64(evm.DefaultGasPerIota))
ctx.State().Set(keyEVMOwner, codec.EncodeAgentID(ctx.ContractCreator()))
}

func requireOwner(ctx iscp.Sandbox) {
func setBlockTime(ctx iscp.Sandbox) (dict.Dict, error) {
requireOwner(ctx)

params := kvdecoder.New(ctx.Params(), ctx.Log())
a := assert.NewAssert(ctx.Log())

blockTime := params.MustGetUint32(evm.FieldBlockTime)
a.Require(blockTime > 0, "blockTime must be > 0")

mustSchedule := !ctx.State().MustHas(keyBlockTime)

ctx.State().Set(keyBlockTime, codec.EncodeUint32(blockTime))
if mustSchedule {
ScheduleNextBlock(ctx)
}
return nil, nil
}

func getBlockTime(state kv.KVStoreReader) uint32 {
bt, _ := codec.DecodeUint32(state.MustGet(keyBlockTime), 0)
return bt
}

func ScheduleNextBlock(ctx iscp.Sandbox) {
requireOwner(ctx, true)

a := assert.NewAssert(ctx.Log())

blockTime := getBlockTime(ctx.State())
a.Require(blockTime > 0, "ScheduleNextBlock: blockTime must be > 0")

ok := ctx.Send(ctx.ChainID().AsAddress(), colored.NewBalancesForIotas(1), &iscp.SendMetadata{
TargetContract: ctx.Contract(),
EntryPoint: evm.FuncMintBlock.Hname(),
}, iscp.SendOptions{
TimeLock: uint32(time.Unix(0, ctx.GetTimestamp()).Unix()) + blockTime,
})
a.Require(ok, "failed to schedule next block")
}

func requireOwner(ctx iscp.Sandbox, allowSelf ...bool) {
contractOwner, err := codec.DecodeAgentID(ctx.State().MustGet(keyEVMOwner))
a := assert.NewAssert(ctx.Log())
a.RequireNoError(err)
a.Require(contractOwner.Equals(ctx.Caller()), "can only be called by the contract owner")

allowed := []*iscp.AgentID{contractOwner}
if len(allowSelf) > 0 && allowSelf[0] {
allowed = append(allowed, iscp.NewAgentID(ctx.ChainID().AsAddress(), ctx.Contract()))
}

a.RequireCaller(ctx, allowed)
}

func setNextOwner(ctx iscp.Sandbox) (dict.Dict, error) {
Expand All @@ -55,7 +115,7 @@ func claimOwnership(ctx iscp.Sandbox) (dict.Dict, error) {

nextOwner, err := codec.DecodeAgentID(ctx.State().MustGet(keyNextEVMOwner))
a.RequireNoError(err)
a.Require(nextOwner.Equals(ctx.Caller()), "Can only be called by the contract owner")
a.RequireCaller(ctx, []*iscp.AgentID{nextOwner})

ctx.State().Set(keyEVMOwner, codec.EncodeAgentID(nextOwner))
return nil, nil
Expand Down Expand Up @@ -102,30 +162,50 @@ func withdrawGasFees(ctx iscp.Sandbox) (dict.Dict, error) {
return nil, nil
}

func RequireGasFee(ctx iscp.Sandbox, txGasLimit uint64, f func() uint64) dict.Dict {
func ApplyTransaction(ctx iscp.Sandbox, apply func(tx *types.Transaction, blockTime uint32) (*types.Receipt, error)) (dict.Dict, error) {
a := assert.NewAssert(ctx.Log())

tx := &types.Transaction{}
err := tx.UnmarshalBinary(ctx.Params().MustGet(evm.FieldTransactionData))
a.RequireNoError(err)

transferredIotas, gasPerIota := takeGasFee(ctx, tx)

blockTime := getBlockTime(ctx.State())
receipt, err := apply(tx, blockTime)
a.RequireNoError(err)

return refundUnusedGasFee(ctx, ctx.Caller(), transferredIotas, gasPerIota, receipt.GasUsed), nil
}

func takeGasFee(ctx iscp.Sandbox, tx *types.Transaction) (uint64, uint64) {
a := assert.NewAssert(ctx.Log())

transferredIotas := ctx.IncomingTransfer().Get(getFeeColor(ctx))
gasPerIota, err := codec.DecodeUint64(ctx.State().MustGet(keyGasPerIota), 0)
a.RequireNoError(err)
txGasLimit := tx.Gas()

a.Require(
transferredIotas >= txGasLimit/gasPerIota,
"transferred tokens (%d) not enough to cover the gas limit set in the transaction (%d at %d gas per iota token)", transferredIotas, txGasLimit, gasPerIota,
)

gasUsed := f()
return transferredIotas, gasPerIota
}

func refundUnusedGasFee(ctx iscp.Sandbox, caller *iscp.AgentID, transferredIotas, gasPerIota, gasUsed uint64) dict.Dict {
iotasGasFee := gasUsed / gasPerIota
if transferredIotas > iotasGasFee {
// refund unspent gas fee to the sender's on-chain account
iotasGasRefund := transferredIotas - iotasGasFee
_, err = ctx.Call(
_, err := ctx.Call(
accounts.Contract.Hname(),
accounts.FuncDeposit.Hname(),
dict.Dict{accounts.ParamAgentID: codec.EncodeAgentID(ctx.Caller())},
dict.Dict{accounts.ParamAgentID: codec.EncodeAgentID(caller)},
colored.NewBalancesForIotas(iotasGasRefund),
)
a := assert.NewAssert(ctx.Log())
a.RequireNoError(err)
}

Expand Down
Loading

0 comments on commit 8cd6ecd

Please sign in to comment.