diff --git a/dot/rpc/http_test.go b/dot/rpc/http_test.go index 8decb021c7..0caf138f47 100644 --- a/dot/rpc/http_test.go +++ b/dot/rpc/http_test.go @@ -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" @@ -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" @@ -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) diff --git a/dot/rpc/modules/rpc.go b/dot/rpc/modules/rpc.go index 1a01adabbd..c491bc14bd 100644 --- a/dot/rpc/modules/rpc.go +++ b/dot/rpc/modules/rpc.go @@ -12,6 +12,7 @@ var ( UnsafeMethods = []string{ "system_addReservedPeer", "system_removeReservedPeer", + "system_dryRun", "author_submitExtrinsic", "author_removeExtrinsic", "author_insertKey", diff --git a/dot/rpc/modules/system.go b/dot/rpc/modules/system.go index 2f5685de78..e0c6c4f293 100644 --- a/dot/rpc/modules/system.go +++ b/dot/rpc/modules/system.go @@ -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" @@ -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") } @@ -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 +} diff --git a/dot/rpc/modules/system_integration_test.go b/dot/rpc/modules/system_integration_test.go index bc2d4e8398..6957aefc47 100644 --- a/dot/rpc/modules/system_integration_test.go +++ b/dot/rpc/modules/system_integration_test.go @@ -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" @@ -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" @@ -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) @@ -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) @@ -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 { @@ -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 +} diff --git a/dot/services_integration_test.go b/dot/services_integration_test.go index ec1d7e2092..5bcf3c3d3c 100644 --- a/dot/services_integration_test.go +++ b/dot/services_integration_test.go @@ -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) diff --git a/lib/babe/build.go b/lib/babe/build.go index 42d3db33e6..d2a95e22b7 100644 --- a/lib/babe/build.go +++ b/lib/babe/build.go @@ -194,7 +194,7 @@ func (b *BlockBuilder) buildBlockExtrinsics(slot Slot, rt ExtrinsicHandler) []*t continue } - err = determineErr(ret) + err = DetermineErr(ret) if err != nil { logger.Warnf("error when applying extrinsic %s: %s", extrinsic, err) @@ -287,7 +287,7 @@ func buildBlockInherents(slot Slot, rt ExtrinsicHandler, parent *types.Header) ( } if !bytes.Equal(ret, []byte{0, 0}) { - errTxt := determineErr(ret) + errTxt := DetermineErr(ret) return nil, fmt.Errorf("error applying inherent: %s", errTxt) } } diff --git a/lib/babe/build_integration_test.go b/lib/babe/build_integration_test.go index bfe2ff4a26..2c3e5da134 100644 --- a/lib/babe/build_integration_test.go +++ b/lib/babe/build_integration_test.go @@ -257,7 +257,7 @@ func TestBuildAndApplyExtrinsic_InvalidPayment(t *testing.T) { header := types.NewHeader(genesisHeader.Hash(), common.Hash{}, common.Hash{}, 1, types.NewDigest()) bestBlockHash := babeService.blockState.BestBlockHash() - rt, err := babeService.blockState.GetRuntime(bestBlockHash) + rt, err := babeService.blockState.GetRuntime(ะน) require.NoError(t, err) err = rt.InitializeBlock(header) @@ -308,7 +308,7 @@ func TestBuildAndApplyExtrinsic_InvalidPayment(t *testing.T) { res, err := rt.ApplyExtrinsic(extEnc.Bytes()) require.NoError(t, err) - err = determineErr(res) + err = DetermineErr(res) _, ok := err.(*TransactionValidityError) require.True(t, ok) require.Equal(t, "transaction validity error: invalid payment", err.Error()) diff --git a/lib/babe/errors.go b/lib/babe/errors.go index 26b35a49a6..3e407d7a4a 100644 --- a/lib/babe/errors.go +++ b/lib/babe/errors.go @@ -504,7 +504,7 @@ func (mvdt unknown) ValueAt(index uint) (value any, err error) { return nil, scale.ErrUnknownVaryingDataTypeValue } -func determineErr(res []byte) error { +func DetermineErr(res []byte) error { okRes := scale.NewResult(nil, dispatchError{}) errRes := scale.NewResult(invalid{}, unknown{}) result := scale.NewResult(okRes, errRes) diff --git a/lib/babe/errors_test.go b/lib/babe/errors_test.go index 2ef6cfe4e4..fc974851ea 100644 --- a/lib/babe/errors_test.go +++ b/lib/babe/errors_test.go @@ -69,7 +69,7 @@ func TestApplyExtrinsicErrors(t *testing.T) { for _, c := range testCases { t.Run(c.name, func(t *testing.T) { - err := determineErr(c.test) + err := DetermineErr(c.test) if c.expected == "" { require.NoError(t, err) return