rpcsrv: refactor findstoragehistoric handler to avoid DoS attack

Do not retrieve the whole set of storage items when trying to find
the ones from the specified start. Use DAO's Seek interface
implemented over MPT TrieStore to retrieve only the necessary items.

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
This commit is contained in:
Anna Shaleva 2023-08-24 14:23:42 +03:00
parent c7d5ee0898
commit 7b64b693bd
3 changed files with 53 additions and 33 deletions

View file

@ -195,6 +195,7 @@ type StateRoot interface {
CurrentLocalStateRoot() util.Uint256 CurrentLocalStateRoot() util.Uint256
CurrentValidatedHeight() uint32 CurrentValidatedHeight() uint32
FindStates(root util.Uint256, prefix, start []byte, max int) ([]storage.KeyValue, error) FindStates(root util.Uint256, prefix, start []byte, max int) ([]storage.KeyValue, error)
SeekStates(root util.Uint256, prefix []byte, f func(k, v []byte) bool)
GetState(root util.Uint256, key []byte) ([]byte, error) GetState(root util.Uint256, key []byte) ([]byte, error)
GetStateProof(root util.Uint256, key []byte) ([][]byte, error) GetStateProof(root util.Uint256, key []byte) ([][]byte, error)
GetStateRoot(height uint32) (*state.MPTRoot, error) GetStateRoot(height uint32) (*state.MPTRoot, error)

View file

@ -93,6 +93,32 @@ func (s *Module) FindStates(root util.Uint256, prefix, start []byte, max int) ([
return tr.Find(prefix, start, max) return tr.Find(prefix, start, max)
} }
// SeekStates traverses over contract storage with the state based on the
// specified root. `prefix` is expected to consist of contract ID and the desired
// storage items prefix. `cont` is called for every matching key-value pair;
// the resulting key does not include contract ID and the desired storage item
// prefix (they are stripped to match the Blockchain's SeekStorage behaviour.
// The result includes item with the key that equals to the `prefix` (if
// such item is found in the storage). Traversal process is stopped when `false`
// is returned from `cont`.
func (s *Module) SeekStates(root util.Uint256, prefix []byte, cont func(k, v []byte) bool) {
// Allow accessing old values, it's RO thing.
store := mpt.NewTrieStore(root, s.mode&^mpt.ModeGCFlag, storage.NewMemCachedStore(s.Store))
// Tiny hack to satisfy TrieStore with the given prefix. This
// storage.STStorage prefix is a stub that will be stripped by the
// TrieStore.Seek while performing MPT traversal and isn't actually relevant
// here.
key := make([]byte, len(prefix)+1)
key[0] = byte(storage.STStorage)
copy(key[1:], prefix)
store.Seek(storage.SeekRange{Prefix: key}, func(k, v []byte) bool {
// Cut the prefix to match the Blockchain's SeekStorage behaviour.
return cont(k[len(key):], v)
})
}
// GetStateProof returns proof of having key in the MPT with the specified root. // GetStateProof returns proof of having key in the MPT with the specified root.
func (s *Module) GetStateProof(root util.Uint256, key []byte) ([][]byte, error) { func (s *Module) GetStateProof(root util.Uint256, key []byte) ([][]byte, error) {
// Allow accessing old values, it's RO thing. // Allow accessing old values, it's RO thing.

View file

@ -99,7 +99,6 @@ type (
GetValidators() ([]*keys.PublicKey, error) GetValidators() ([]*keys.PublicKey, error)
HeaderHeight() uint32 HeaderHeight() uint32
InitVerificationContext(ic *interop.Context, hash util.Uint160, witness *transaction.Witness) error InitVerificationContext(ic *interop.Context, hash util.Uint160, witness *transaction.Witness) error
SeekStorage(id int32, prefix []byte, cont func(k, v []byte) bool)
SubscribeForBlocks(ch chan *block.Block) SubscribeForBlocks(ch chan *block.Block)
SubscribeForExecutions(ch chan *state.AppExecResult) SubscribeForExecutions(ch chan *state.AppExecResult)
SubscribeForNotifications(ch chan *state.ContainedNotificationEvent) SubscribeForNotifications(ch chan *state.ContainedNotificationEvent)
@ -111,6 +110,13 @@ type (
VerifyTx(*transaction.Transaction) error VerifyTx(*transaction.Transaction) error
VerifyWitness(util.Uint160, hash.Hashable, *transaction.Witness, int64) (int64, error) VerifyWitness(util.Uint160, hash.Hashable, *transaction.Witness, int64) (int64, error)
mempool.Feer // fee interface mempool.Feer // fee interface
ContractStorageSeeker
}
// ContractStorageSeeker is the interface `findstorage*` handlers need to be able to
// seek over contract storage.
ContractStorageSeeker interface {
SeekStorage(id int32, prefix []byte, cont func(k, v []byte) bool)
} }
// OracleHandler is the interface oracle service needs to provide for the Server. // OracleHandler is the interface oracle service needs to provide for the Server.
@ -1689,12 +1695,16 @@ func (s *Server) findStorage(reqParams params.Params) (any, *neorpc.Error) {
if respErr != nil { if respErr != nil {
return nil, respErr return nil, respErr
} }
return s.findStorageInternal(id, prefix, start, take, s.chain)
}
func (s *Server) findStorageInternal(id int32, prefix []byte, start, take int, seeker ContractStorageSeeker) (any, *neorpc.Error) {
var ( var (
i int i int
end = start + take end = start + take
res = new(result.FindStorage) res = new(result.FindStorage)
) )
s.chain.SeekStorage(id, prefix, func(k, v []byte) bool { seeker.SeekStorage(id, prefix, func(k, v []byte) bool {
if i < start { if i < start {
i++ i++
return true return true
@ -1727,38 +1737,21 @@ func (s *Server) findStorageHistoric(reqParams params.Params) (any, *neorpc.Erro
return nil, respErr return nil, respErr
} }
var ( return s.findStorageInternal(id, prefix, start, take, mptStorageSeeker{
end = start + take root: root,
res = new(result.FindStorage) module: s.chain.GetStateModule(),
pKey = makeStorageKey(id, prefix) })
) }
// @roman-khimov, retrieving only the necessary part of the contract storage
// requires an mpt Billet refactoring, we can do it in a separate issue, create?
kvs, err := s.chain.GetStateModule().FindStates(root, pKey, nil, end+1) // +1 to define result truncation
if err != nil && !errors.Is(err, mpt.ErrNotFound) {
return nil, neorpc.NewInternalServerError(fmt.Sprintf("failed to find state items: %s", err))
}
if len(kvs) == end+1 {
res.Truncated = true
kvs = kvs[:len(kvs)-1]
}
if start >= len(kvs) {
kvs = nil
} else {
kvs = kvs[start:]
}
if len(kvs) != 0 { // keep consistency with `findstorage` response // mptStorageSeeker is an auxiliary structure that implements ContractStorageSeeker interface.
res.Results = make([]result.KeyValue, len(kvs)) type mptStorageSeeker struct {
for i := range res.Results { root util.Uint256
res.Results[i] = result.KeyValue{ module core.StateRoot
Key: kvs[i].Key[4:], // Cut contract ID as it is done in C#. }
Value: kvs[i].Value,
} func (s mptStorageSeeker) SeekStorage(id int32, prefix []byte, cont func(k, v []byte) bool) {
} key := makeStorageKey(id, prefix)
} s.module.SeekStates(s.root, key, cont)
res.Next = start + len(res.Results)
return res, nil
} }
func (s *Server) getFindStorageParams(reqParams params.Params, root ...util.Uint256) (int32, []byte, int, int, *neorpc.Error) { func (s *Server) getFindStorageParams(reqParams params.Params, root ...util.Uint256) (int32, []byte, int, int, *neorpc.Error) {