diff --git a/docs/rpc.md b/docs/rpc.md index be814cd45..31b956316 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -63,7 +63,7 @@ which would yield the response: | `invokefunction` | Yes | | `invokescript` | Yes | | `sendrawtransaction` | Yes | -| `submitblock` | No (#344) | +| `submitblock` | Yes | | `validateaddress` | Yes | ### Unsupported methods diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 8a83bd932..3fbd5cd20 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -52,6 +52,9 @@ var ( // ErrPolicy is returned on attempt to add transaction that doesn't // comply with node's configured policy into the mempool. ErrPolicy = errors.New("not allowed by policy") + // ErrInvalidBlockIndex is returned when trying to add block with index + // other than expected height of the blockchain. + ErrInvalidBlockIndex error = errors.New("invalid block index") ) var ( genAmount = []int{8, 7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} @@ -307,7 +310,7 @@ func (bc *Blockchain) AddBlock(block *block.Block) error { expectedHeight := bc.BlockHeight() + 1 if expectedHeight != block.Index { - return fmt.Errorf("expected block %d, but passed block %d", expectedHeight, block.Index) + return ErrInvalidBlockIndex } headerLen := bc.headerListLen() diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index f8798f164..763ffb6eb 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -391,6 +391,17 @@ func TestCreateBasicChain(t *testing.T) { require.NoError(t, writer.Err) } } + + // Blocks for `submitblock` test. If you are planning to modify test chain from `testblocks.acc`, + // please, update params value of `empty block` and `positive` tests. + var blocks []*block.Block + blocks = append(blocks, bc.newBlock(), bc.newBlock(newMinerTX())) + for _, b := range blocks { + buf := io.NewBufBinWriter() + b.EncodeBinary(buf.BinWriter) + require.NoError(t, buf.Err) + t.Log(hex.EncodeToString(buf.Bytes())) + } } func newNEP5Transfer(sc, from, to util.Uint160, amount int64) *transaction.Transaction { diff --git a/pkg/rpc/response/errors.go b/pkg/rpc/response/errors.go index a0d468136..0a0939322 100644 --- a/pkg/rpc/response/errors.go +++ b/pkg/rpc/response/errors.go @@ -20,6 +20,18 @@ type ( var ( // ErrInvalidParams represents a generic 'invalid parameters' error. ErrInvalidParams = NewInvalidParamsError("", nil) + // ErrAlreadyExists represents SubmitError with code -501 + ErrAlreadyExists = NewSubmitError(-501, "Block or transaction already exists and cannot be sent repeatedly.") + // ErrOutOfMemory represents SubmitError with code -502 + ErrOutOfMemory = NewSubmitError(-502, "The memory pool is full and no more transactions can be sent.") + // ErrUnableToVerify represents SubmitError with code -503 + ErrUnableToVerify = NewSubmitError(-503, "The block cannot be validated.") + // ErrValidationFailed represents SubmitError with code -504 + ErrValidationFailed = NewSubmitError(-504, "Block or transaction validation failed.") + // ErrPolicyFail represents SubmitError with code -505 + ErrPolicyFail = NewSubmitError(-505, "One of the Policy filters failed.") + // ErrUnknown represents SubmitError with code -500 + ErrUnknown = NewSubmitError(-500, "Unknown error.") ) // NewError is an Error constructor that takes Error contents from its @@ -71,6 +83,12 @@ func NewRPCError(message string, data string, cause error) *Error { return NewError(-100, http.StatusUnprocessableEntity, message, data, cause) } +// NewSubmitError creates a new error with +// specified error code and error message +func NewSubmitError(code int64, message string) *Error { + return NewError(code, http.StatusUnprocessableEntity, message, "", nil) +} + // Error implements the error interface. func (e *Error) Error() string { return fmt.Sprintf("%s (%d) - %s - %s", e.Message, e.Code, e.Data, e.Cause) diff --git a/pkg/rpc/server/prometheus.go b/pkg/rpc/server/prometheus.go index 79d3d838f..a7b06f452 100644 --- a/pkg/rpc/server/prometheus.go +++ b/pkg/rpc/server/prometheus.go @@ -187,6 +187,14 @@ var ( }, ) + submitblockCalled = prometheus.NewCounter( + prometheus.CounterOpts{ + Help: "Number of calls to submitblock rpc endpoint", + Name: "submitblock_called", + Namespace: "neogo", + }, + ) + getstorageCalled = prometheus.NewCounter( prometheus.CounterOpts{ Help: "Number of calls to getstorage rpc endpoint", @@ -218,6 +226,7 @@ func init() { gettxoutCalled, getrawtransactionCalled, sendrawtransactionCalled, + submitblockCalled, getstorageCalled, ) } diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index c70d90225..030c031ba 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -12,6 +12,7 @@ import ( "github.com/nspcc-dev/neo-go/config" "github.com/nspcc-dev/neo-go/pkg/core" + "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" @@ -306,6 +307,10 @@ Methods: case "invokescript": results, resultsErr = s.invokescript(reqParams) + case "submitblock": + submitblockCalled.Inc() + results, resultsErr = s.submitBlock(reqParams) + case "sendrawtransaction": sendrawtransactionCalled.Inc() results, resultsErr = s.sendrawtransaction(reqParams) @@ -831,6 +836,34 @@ func (s *Server) runScriptInVM(script []byte) *result.Invoke { return result } +// submitBlock broadcasts a raw block over the NEO network. +func (s *Server) submitBlock(reqParams request.Params) (interface{}, error) { + param, ok := reqParams.ValueWithType(0, request.StringT) + if !ok { + return nil, response.ErrInvalidParams + } + blockBytes, err := param.GetBytesHex() + if err != nil { + return nil, response.ErrInvalidParams + } + b := block.Block{} + r := io.NewBinReaderFromBuf(blockBytes) + b.DecodeBinary(r) + if r.Err != nil { + return nil, response.ErrInvalidParams + } + err = s.chain.AddBlock(&b) + if err != nil { + switch err { + case core.ErrInvalidBlockIndex, core.ErrAlreadyExists: + return nil, response.ErrAlreadyExists + default: + return nil, response.ErrValidationFailed + } + } + return true, nil +} + func (s *Server) sendrawtransaction(reqParams request.Params) (interface{}, error) { var resultsErr error var results interface{} @@ -844,28 +877,24 @@ func (s *Server) sendrawtransaction(reqParams request.Params) (interface{}, erro tx := &transaction.Transaction{} tx.DecodeBinary(r) if r.Err != nil { - err = errors.Wrap(r.Err, "transaction DecodeBinary failed") - } else { - relayReason := s.coreServer.RelayTxn(tx) - switch relayReason { - case network.RelaySucceed: - results = true - case network.RelayAlreadyExists: - err = errors.New("block or transaction already exists and cannot be sent repeatedly") - case network.RelayOutOfMemory: - err = errors.New("the memory pool is full and no more transactions can be sent") - case network.RelayUnableToVerify: - err = errors.New("the block cannot be validated") - case network.RelayInvalid: - err = errors.New("block or transaction validation failed") - case network.RelayPolicyFail: - err = errors.New("one of the Policy filters failed") - default: - err = errors.New("unknown error") - } + return nil, response.ErrInvalidParams } - if err != nil { - resultsErr = response.NewInternalServerError(err.Error(), err) + relayReason := s.coreServer.RelayTxn(tx) + switch relayReason { + case network.RelaySucceed: + results = true + case network.RelayAlreadyExists: + resultsErr = response.ErrAlreadyExists + case network.RelayOutOfMemory: + resultsErr = response.ErrOutOfMemory + case network.RelayUnableToVerify: + resultsErr = response.ErrUnableToVerify + case network.RelayInvalid: + resultsErr = response.ErrValidationFailed + case network.RelayPolicyFail: + resultsErr = response.ErrPolicyFail + default: + resultsErr = response.ErrUnknown } } diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 9636a31a0..06a780c1c 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -767,6 +767,42 @@ var rpcTestCases = map[string][]rpcTestCase{ fail: true, }, }, + "submitblock": { + { + name: "empty block", + params: `["00000000270dd14a8ddb4961cada75e9ec4cce906f9feeaef21b1d6cea5b2f0230baf92e00000000000000000000000000000000000000000000000000000000000000009f0e625ed10000005704000000000000be48d3a3f5d10013ab9ffee489706078714f1ea201fd040140bd2b961ca7df75d6d8896fd4ef487128ddd1ca9feda9cb09da58a6a24607fc71cd572ba072e4ec9d9847c7da6a33fc44dc9a64208fd62851d27d330012bd0bf640ee6e92653789b0d1c87e0cd485c5bbd84194479995930567df9ffec7ab370c965df320134632e53d139a785840cecb16c4fe801f3c6bed17a9dd83460f5f297440fbe5f9d3cc23f9c7b9f10e191eae541fcb71eb03799c09886aa04c0bc43e98c2c91686a5b4fb988c8855f4d217807f01ca9a61bc0d5536eb3149dce7ae66965c407af69085454fb39f0c27cf308219c09efbe902f0a092a065b38864c97529f1dd12fa54c346e8d9482106c7154b3170c150887170c78fe78a5c70e3956880489c8b532102103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e2102a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd622102b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc22103d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee69954ae00"]`, + fail: true, + }, + { + name: "invalid block height", + params: `["000000005fb86f62eafe8e9246bc0d1648e4e5c8389dee9fb7fe03fcc6772ec8c5e4ec2aedb908054ac1409be5f77d5369c6e03490b2f6676d68d0b3370f8159e0fdadf99bc05f5e030000005704000000000000be48d3a3f5d10013ab9ffee489706078714f1ea201fd0401406f299c82b513f59f5bd120317974852c9694c6e10db1ef2f1bb848b1a33e47a08f8dc03ee784166b2060a94cd4e7af88899b39787938f7f2763ea4d2182776ed40f3bafd85214fef38a4836ca97793001ea411f553c51e88781f7b916c59c145bff28314b6e7ea246789422a996fc4937e290a1b40f6b97c5222540f65b0d47aca40d2b3d19203d456428bfdb529e846285052105957385b65388b9a617f6e2d56a64ec41aa73439eafccb52987bb1975c9b67518b053d9e61b445e4a3377dbc206640bd688489bd62adf6bed9d61a73905b9591eb87053c6f0f4dd70f3bee7295541b490caef044b55b6f9f01dc4a05a756a3f2edd06f5adcbe4e984c1e552f9023f08b532102103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e2102a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd622102b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc22103d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee69954ae0100000000000000000000"]`, + fail: true, + }, + { + name: "invalid hex", + params: `["000000005gb86f62eafe8e9246bc0d1648e4e5c8389dee9fb7fe03fcc6772ec8c5e4ec2aedb908054ac1409be5f77d5369c6e03490b2f6676d68d0b3370f8159e0fdadf99bc05f5e030000005704000000000000be48d3a3f5d10013ab9ffee489706078714f1ea201fd0401406f299c82b513f59f5bd120317974852c9694c6e10db1ef2f1bb848b1a33e47a08f8dc03ee784166b2060a94cd4e7af88899b39787938f7f2763ea4d2182776ed40f3bafd85214fef38a4836ca97793001ea411f553c51e88781f7b916c59c145bff28314b6e7ea246789422a996fc4937e290a1b40f6b97c5222540f65b0d47aca40d2b3d19203d456428bfdb529e846285052105957385b65388b9a617f6e2d56a64ec41aa73439eafccb52987bb1975c9b67518b053d9e61b445e4a3377dbc206640bd688489bd62adf6bed9d61a73905b9591eb87053c6f0f4dd70f3bee7295541b490caef044b55b6f9f01dc4a05a756a3f2edd06f5adcbe4e984c1e552f9023f08b532102103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e2102a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd622102b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc22103d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee69954ae0100000000000000000000"]`, + fail: true, + }, + { + name: "invalid block bytes", + params: `["0000000027"]`, + fail: true, + }, + { + name: "no params", + params: `[]`, + fail: true, + }, + { + name: "positive", + // If you are planning to modify test chain from `testblocks.acc`, please, update param value + params: `["00000000270dd14a8ddb4961cada75e9ec4cce906f9feeaef21b1d6cea5b2f0230baf92eedb908054ac1409be5f77d5369c6e03490b2f6676d68d0b3370f8159e0fdadf9b93a615ed10000005704000000000000be48d3a3f5d10013ab9ffee489706078714f1ea201fd0401402dca64bfc20107819171fab7c636427e9634a58f560ff9ef94ff9005a82509dd5e1b054d195201013a8477529d023d794a3ca6efa717f5d7e541e3b81fbabff840b5395429f0da5a8d1023907dca3771c02eca31e520e1acc2851b373a6aedd5bf6ba75a80b0949ee0794156ae48356b98e761143b90ebaa816ca4b0b0f91b38ab40a6feb77e09c0c95c6f2c6571155843f388e1f7cb7ba5af2bc235bac1d57a8c316d817e54895bfdb98b9c34ed9550e7ccff2a06b46ca0f2b99ba720368d9934eb40dbbdc94154cc304296ccda73e4e0257cdc344c4bad4c70f419f59f5d6d862819cead91c55e73e7c30daff2cd38d74eae3e0e49a5226de3060843d0e3b9262dc08b532102103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e2102a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd622102b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc22103d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee69954ae0100000000000000000000"]`, + result: func(e *executor) interface{} { + v := true + return &v + }, + }, + }, "validateaddress": { { name: "positive",