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

P1sar/feat/system dry run rpc #4042

Draft
wants to merge 2 commits into
base: development
Choose a base branch
from
Draft
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
62 changes: 60 additions & 2 deletions dot/rpc/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import (
"io"
"net"
"net/http"
"net/url"
"testing"
"time"

rtstorage "github.com/ChainSafe/gossamer/lib/runtime/storage"
wazero_runtime "github.com/ChainSafe/gossamer/lib/runtime/wazero"
"github.com/gorilla/websocket"
"github.com/libp2p/go-libp2p/core/peer"

"github.com/ChainSafe/gossamer/dot/core"
Expand All @@ -28,6 +28,8 @@ import (
"github.com/ChainSafe/gossamer/lib/crypto/sr25519"
"github.com/ChainSafe/gossamer/lib/keystore"
"github.com/ChainSafe/gossamer/lib/runtime"
rtstorage "github.com/ChainSafe/gossamer/lib/runtime/storage"
wazero_runtime "github.com/ChainSafe/gossamer/lib/runtime/wazero"
"github.com/btcsuite/btcutil/base58"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
Expand Down Expand Up @@ -97,6 +99,62 @@ func TestUnsafeRPCProtection(t *testing.T) {
}
}

func TestUnsafeWSProtection(t *testing.T) {
cfg := &HTTPServerConfig{
Modules: []string{"system", "author", "chain", "state", "rpc", "grandpa", "dev", "syncstate"},
RPCPort: 7878,
WSExternal: true,
WSPort: 9546,
RPCAPI: NewService(),
RPCUnsafeExternal: false,
RPCUnsafe: false,
WSUnsafeExternal: false,
}

s := NewHTTPServer(cfg)
err := s.Start()
require.NoError(t, err)

time.Sleep(time.Second)
defer s.Stop()

for _, unsafe := range modules.UnsafeMethods {
t.Run(fmt.Sprintf("Unsafe method %s should not be reachable", unsafe), func(t *testing.T) {
data := []byte(fmt.Sprintf(`{"jsonrpc":"2.0","method":"%s","params":[],"id":1}`, unsafe))

buf := new(bytes.Buffer)
_, err = buf.Write(data)
require.NoError(t, err)

const addr = "localhost:9546"
u := url.URL{Scheme: "ws", Host: addr, Path: "/"}

c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
defer c.Close()

err = c.WriteMessage(websocket.TextMessage, data)
require.NoError(t, err)

_, message, err := c.ReadMessage()
require.NoError(t, err)

expected := fmt.Sprintf(`{`+
`"error":{`+
`"code":-32000,`+
`"data":null,`+
`"message":"unsafe rpc method %s cannot be reachable"`+
`},`+
`"id":1,`+
`"jsonrpc":"2.0"`+
`}`+"\n",
unsafe,
)

require.Equal(t, expected, string(message))
})
}
}

func TestRPCUnsafeExpose(t *testing.T) {
ctrl := gomock.NewController(t)

Expand Down
1 change: 1 addition & 0 deletions dot/rpc/modules/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var (
UnsafeMethods = []string{
"system_addReservedPeer",
"system_removeReservedPeer",
"system_dryRun",
"author_submitExtrinsic",
"author_removeExtrinsic",
"author_insertKey",
Expand Down
40 changes: 40 additions & 0 deletions dot/rpc/modules/system.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ package modules
import (
"bytes"
"errors"
"fmt"
"math/big"
"net/http"
"strings"

"github.com/ChainSafe/gossamer/dot/types"
"github.com/ChainSafe/gossamer/lib/common"
"github.com/ChainSafe/gossamer/lib/crypto"
"github.com/ChainSafe/gossamer/pkg/scale"
Expand Down Expand Up @@ -161,6 +163,7 @@ func (sm *SystemModule) NodeRoles(r *http.Request, req *EmptyRequest, res *[]int

// AccountNextIndex Returns the next valid index (aka. nonce) for given account.
func (sm *SystemModule) AccountNextIndex(r *http.Request, req *StringRequest, res *U64Response) error {
// I do not understand how this params should be parsed. eg. func (gm *GrandpaModule) ProveFinality should accept 3 paras according to RPC documentation
if req == nil || req.String == "" {
return errors.New("account address must be valid")
}
Expand Down Expand Up @@ -289,3 +292,40 @@ func (sm *SystemModule) RemoveReservedPeer(r *http.Request, req *StringRequest,

return sm.networkAPI.RemoveReservedPeers(req.String)
}

// DryRunRequest request struct
type DryRunRequest struct {
Extrinsic []byte
Bhash *common.Hash
}

// DryRun Dry run an extrinsic. Returns a SCALE encoded ApplyExtrinsicResult.
// Params:
// - HEX - The raw, SCALE encoded extrinsic.
// - HASH - The block hash indicating the state. Null implies the current state.
//
// unsafe: This method is only active with appropriate flags
// interface: api.rpc.system.dryRun
// jsonrpc: system_dryRun
// Response: The SCALE encoded ApplyExtrinsicResult.
func (sm *SystemModule) DryRun(_ *http.Request, req *DryRunRequest, result *[]byte) error {

var block common.Hash
if req.Bhash == nil {
block = sm.blockAPI.BestBlockHash()
} else {
block = *req.Bhash
}

// TODO: add different runtime execution flow based on runtime version <6
runtime, err := sm.blockAPI.GetRuntime(block)
if err != nil {
return fmt.Errorf("GetRuntime error: %w", err)
}
extrResult, err := runtime.ApplyExtrinsic(types.NewExtrinsic(req.Extrinsic))
if err != nil {
return fmt.Errorf("ApplyExtrinsic error: %w", err)
}
*result = extrResult
return nil
}
176 changes: 173 additions & 3 deletions dot/rpc/modules/system_integration_test.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
// Copyright 2021 ChainSafe Systems (ON)
// SPDX-License-Identifier: LGPL-3.0-only

//go:build integration

package modules

import (
"errors"
"fmt"
ctypes "github.com/centrifuge/go-substrate-rpc-client/v4/types"
"math/big"
"os"
"path/filepath"
"testing"
"time"

"github.com/btcsuite/btcd/btcutil/base58"
"github.com/centrifuge/go-substrate-rpc-client/v4/signature"
"github.com/multiformats/go-multiaddr"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
Expand All @@ -23,10 +24,14 @@ import (
"github.com/ChainSafe/gossamer/dot/rpc/modules/mocks"
"github.com/ChainSafe/gossamer/dot/state"
"github.com/ChainSafe/gossamer/dot/types"
"github.com/ChainSafe/gossamer/internal/database"
"github.com/ChainSafe/gossamer/internal/log"
"github.com/ChainSafe/gossamer/lib/babe"
"github.com/ChainSafe/gossamer/lib/common"
"github.com/ChainSafe/gossamer/lib/genesis"
"github.com/ChainSafe/gossamer/lib/keystore"
"github.com/ChainSafe/gossamer/lib/runtime"
rtstorage "github.com/ChainSafe/gossamer/lib/runtime/storage"
wazero_runtime "github.com/ChainSafe/gossamer/lib/runtime/wazero"
"github.com/ChainSafe/gossamer/lib/transaction"
"github.com/ChainSafe/gossamer/pkg/scale"
Expand All @@ -42,6 +47,98 @@ var (
testPeers []common.PeerInfo
)

// test data
var (
sampleBodyBytes = *types.NewBody([]types.Extrinsic{[]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}})
// sampleBodyString is string conversion of sampleBodyBytes
sampleBodyString = []string{"0x2800010203040506070809"}
)

func loadTestBlocks(t *testing.T, gh common.Hash, bs *state.BlockState, rt runtime.Instance) {
digest := types.NewDigest()
prd, err := types.NewBabeSecondaryPlainPreDigest(0, 1).ToPreRuntimeDigest()
require.NoError(t, err)
err = digest.Add(*prd)
require.NoError(t, err)

header1 := &types.Header{
Number: 1,
Digest: digest,
ParentHash: gh,
StateRoot: trie.EmptyHash,
}

block1 := &types.Block{
Header: *header1,
Body: sampleBodyBytes,
}

err = bs.AddBlock(block1)
require.NoError(t, err)
bs.StoreRuntime(header1.Hash(), rt)

header2 := &types.Header{
Number: 2,
Digest: digest,
ParentHash: header1.Hash(),
StateRoot: trie.EmptyHash,
}

block2 := &types.Block{
Header: *header2,
Body: sampleBodyBytes,
}

err = bs.AddBlock(block2)
require.NoError(t, err)
bs.StoreRuntime(header2.Hash(), rt)
}

func newTestStateService(t *testing.T) *state.Service {
testDatadirPath := t.TempDir()

ctrl := gomock.NewController(t)
telemetryMock := NewMockTelemetry(ctrl)
telemetryMock.EXPECT().SendMessage(gomock.Any()).AnyTimes()

config := state.Config{
Path: testDatadirPath,
LogLevel: log.Info,
Telemetry: telemetryMock,
}
stateSrvc := state.NewService(config)
stateSrvc.UseMemDB()

gen, genesisTrie, genesisHeader := newWestendLocalGenesisWithTrieAndHeader(t)

err := stateSrvc.Initialise(&gen, &genesisHeader, &genesisTrie)
require.NoError(t, err)

err = stateSrvc.Start()
require.NoError(t, err)

var rtCfg wazero_runtime.Config

rtCfg.Storage = rtstorage.NewTrieState(&genesisTrie)

if stateSrvc != nil {
rtCfg.NodeStorage.BaseDB = stateSrvc.Base
} else {
rtCfg.NodeStorage.BaseDB, err = database.LoadDatabase(filepath.Join(testDatadirPath, "offline_storage"), false)
require.NoError(t, err)
}

rt, err := wazero_runtime.NewRuntimeFromGenesis(rtCfg)
require.NoError(t, err)

loadTestBlocks(t, genesisHeader.Hash(), stateSrvc.Block, rt)

t.Cleanup(func() {
stateSrvc.Stop()
})
return stateSrvc
}

func newNetworkService(t *testing.T) *network.Service {
ctrl := gomock.NewController(t)

Expand Down Expand Up @@ -294,6 +391,8 @@ func setupSystemModule(t *testing.T) *SystemModule {
// setup service
net := newNetworkService(t)
chain := newTestStateService(t)

block := chain.Block
// init storage with test data
ts, err := chain.Storage.TrieState(nil)
require.NoError(t, err)
Expand Down Expand Up @@ -348,7 +447,7 @@ func setupSystemModule(t *testing.T) *SystemModule {
AnyTimes()

txQueue := state.NewTransactionState(telemetryMock)
return NewSystemModule(net, nil, core, chain.Storage, txQueue, nil, nil)
return NewSystemModule(net, nil, core, chain.Storage, txQueue, block, nil)
}

func newCoreService(t *testing.T, srvc *state.Service) *core.Service {
Expand Down Expand Up @@ -552,3 +651,74 @@ func TestAddReservedPeer(t *testing.T) {
require.Error(t, sysModule.RemoveReservedPeer(nil, &StringRequest{String: " "}, nil))
})
}

func TestSystemModule_DryRun_HappyPass(t *testing.T) {
sys := setupSystemModule(t)

runtimeInstance, err := sys.blockAPI.GetRuntime(sys.blockAPI.BestBlockHash())
require.NoError(t, err)

_, _, genesisHeader := newWestendLocalGenesisWithTrieAndHeader(t)

keyRing, err := keystore.NewSr25519Keyring()

require.NoError(t, err)

charlie, err := ctypes.NewMultiAddressFromHexAccountID(
keyRing.KeyCharlie.Public().Hex())
require.NoError(t, err)

extHex := runtime.NewTestExtrinsic(t, runtimeInstance, genesisHeader.Hash(), sys.blockAPI.BestBlockHash(),
0, signature.TestKeyringPairAlice, "Balances.transfer",
charlie, ctypes.NewUCompactFromUInt(12345))

//NewTestExtrinsic returns hex encoded value so we decode it
req := &DryRunRequest{
Extrinsic: common.MustHexToBytes(extHex),
}

var callResponse []byte

err = sys.DryRun(nil, req, &callResponse)

fmt.Printf("TRACE: bestBlock hash%x", sys.blockAPI.BestBlockHash())

require.NoError(t, err)

// Result is Hexed and umrashalled SCALE
err = babe.DetermineErr(callResponse)
// TODO: currently happens error: "transaction validity error: ancient birth block"
require.NoError(t, err, "An error was determined in response")
}

func TestSystemModule_DryRun_MalformedExtrinsic(t *testing.T) {
sys := setupSystemModule(t)

testCallArguments := []byte{0xab, 0xcd}

runtimeInstance, err := sys.blockAPI.GetRuntime(sys.blockAPI.BestBlockHash())
require.NoError(t, err)

_, _, genesisHeader := newWestendLocalGenesisWithTrieAndHeader(t)

extHex := runtime.NewTestExtrinsic(t, runtimeInstance, genesisHeader.Hash(), sys.blockAPI.BestBlockHash(),
100, signature.TestKeyringPairAlice, "System.remark", testCallArguments)

req := &DryRunRequest{
Extrinsic: common.MustHexToBytes(extHex),
}

var callResponse []byte

err = sys.DryRun(nil, req, &callResponse)
require.NoError(t, err)

// Result is Hexed and umrashalled SCALE

err = babe.DetermineErr(callResponse)
require.NoError(t, err, "An error was determined in response")
}

func TestSystemModule_DryRun_Pass_WithBlockPassed(t *testing.T) {
// TODO
}
3 changes: 2 additions & 1 deletion dot/services_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,8 @@ func TestNewWebSocketServer(t *testing.T) {
expected: []byte(`{"jsonrpc":"2.0","result":1,"id":3}` + "\n")},
{
call: []byte(`{"jsonrpc":"2.0","method":"state_subscribeStorage","params":[],"id":4}`),
expected: []byte(`{"jsonrpc":"2.0","result":2,"id":4}` + "\n")},
expected: []byte(`{"jsonrpc":"2.0","result":2,"id":4}` + "\n"),
},
}

config := DefaultTestWestendDevConfig(t)
Expand Down
Loading