Neo: Strict enum value checking

Created on 12 Nov 2019  路  14Comments  路  Source: neo-project/neo

Summary
Enum's in C# are not strict when it comes to casting. e.g.

```C#
public enum VMState : byte
{
NONE = 0,

    HALT = 1 << 0,
    FAULT = 1 << 1,
    BREAK = 1 << 2,
}

VMState state = (VMState) 8; <- works fine

This can lead to various issues or performance degradation; 
* In NEO2 a contract was successfully deployed with an invalid `ContractParameterType` (#228). The impact for this specific case was low but could be worse.  
* Objects can pass deserialization with invalid data ([example](https://github.com/neo-project/neo/blob/f1d643430966b9cc13f66ecb4ef2cfb462ccee85/neo/Ledger/TransactionState.cs#L32)) leading to unnecessary resource consumption, where we could have thrown an exception, aborted early and save resources.
* The VM can run longer than necessary with invalid data ([interop layer example that could abort early](https://github.com/neo-project/neo/blob/f1d643430966b9cc13f66ecb4ef2cfb462ccee85/neo/SmartContract/InteropService.cs#L576))

**Do you have any solution you want to propose?**
I'd like to suggest we create some helper method that does strict Enum value checking and throws an exception if it is out of bounds.

So the above could look something like
```C#
VMState state = Helper.StrictEnumFrom<VMState>(8); <- raises FormatException

Where in the software does this update applies to?
Everywhere

I'm interested to hear thoughts on the idea

discussion

Most helpful comment

Don't mind my C# benchmark skills 馃槄

```C#
private static void func1(int i)
{
VMState state = EnumHelper.StrictFrom(i);
}

    private static void func2(int i)
    {
        VMState value = (VMState)i; 
        if (!Enum.IsDefined(typeof(VMState), value))
            throw new FormatException();
    }
```C#
            var watch = new Stopwatch();
            List<int> indice = new List<int> { 0, 1, 2, 4 };
            // clean up
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();

            watch.Start();
            for (int i = 0; i < 5000; i++)
            {
                func2(indice[i%4]);
            }
            watch.Stop();
            Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);

3 runs of the above code
func1: 35.639 ms, 33.4553 ms, 33.4517 ms
func2: 74.522 ms, 76.22 ms, 66.9185 ms
So the hashset is ~2x faster

All 14 comments

The implementation could be something along the lines of this (not working, but for showing the idea)
```C#
public static class EnumHelper
{
public static T StrictEnumFrom(int value) where T : Enum
{
T state = (T)value;
if (!EnumValueCache.DefinedValues.Contains(state))
throw new FormatException();
return state;
}

    private static class EnumValueCache<T> where T : Enum
    {
        public static readonly HashSet<T> DefinedValues = new HashSet<T>((T[])Enum.GetValues(typeof(T)));
    }
}

```

Enum.IsDefined() is apparently slow (according to SO posts), so the cache should help avoid a performance hit every time we call Enum.GetValues for the type.

I like it, only when the enum is not a flag.

Sometimes we do this check

https://github.com/neo-project/neo/blob/af85f3c8b223767c55e962720d052aeec106ef88/neo/Network/P2P/Payloads/InvPayload.cs#L39-L41

It is great that we at least do it sometimes, but I hope we can get to some coding rule where we say "Every non-flag enum in NEO should be strict" and then to make it easy have some helper function to take care of it.

@shargon is there any reason why we couldn't make an equivalent helper for Flag enums? I still kind of expect that an enum also does not accept any invalid flags. For example I expect this to fail if GetBigInteger() returns any value that is not 0 or 1 (=the valid possible Flags)
https://github.com/neo-project/neo/blob/f1d643430966b9cc13f66ecb4ef2cfb462ccee85/neo/SmartContract/InteropService.cs#L576

PS: If you happen to know a solution for T state = (T)value; not compiling let me know. T state = (T)(object)value; allows it to compile, but then it still fails at runtime so 馃し鈥嶁檪

If is fast, we can do it.. otherwise what could be the problem with flag, every time you will check the expected flag, so if is not included you won't do anything

Could you create the PR for see easy your problem?

If is fast, we can do it.. otherwise what could be the problem with flag, every time you will check the expected flag, so if is not included you won't do anything

I guess the only benefit would be saving computational resources by aborting VM execution on invalid data.

Could you create the PR for see easy your problem?

I can't really create a PR yet because the helper function doesn't work (see the last 2 lines of https://github.com/neo-project/neo/issues/1231#issuecomment-552848602)
Otherwise the PR would just become applying

C# if (!Enum.IsDefined(typeof(object_type), Type)) throw new FormatException();
to all locations that are not using it but should.

Try with

public static class EnumHelper
    {
        public static T StrictEnumFrom<T>(int value) where T : Enum
        {
            T state = (T)Enum.ToObject(typeof(T), value);
            if (!EnumValueCache<T>.DefinedValues.Contains(state))
                throw new FormatException();
            return state;
        }

        public static T StrictEnumFrom<T>(byte value) where T : Enum
        {
            T state = (T)Enum.ToObject(typeof(T), value);
            if (!EnumValueCache<T>.DefinedValues.Contains(state))
                throw new FormatException();
            return state;
        }

        private static class EnumValueCache<T> where T : Enum
        {
            public static readonly HashSet<T> DefinedValues = new HashSet<T>((T[])Enum.GetValues(typeof(T)));
        }
    }

Please do some benchmarks with Enum.IsDefined(typeof(T), value); instead of your hashset

Don't mind my C# benchmark skills 馃槄

```C#
private static void func1(int i)
{
VMState state = EnumHelper.StrictFrom(i);
}

    private static void func2(int i)
    {
        VMState value = (VMState)i; 
        if (!Enum.IsDefined(typeof(VMState), value))
            throw new FormatException();
    }
```C#
            var watch = new Stopwatch();
            List<int> indice = new List<int> { 0, 1, 2, 4 };
            // clean up
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();

            watch.Start();
            for (int i = 0; i < 5000; i++)
            {
                func2(indice[i%4]);
            }
            watch.Stop();
            Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);

3 runs of the above code
func1: 35.639 ms, 33.4553 ms, 33.4517 ms
func2: 74.522 ms, 76.22 ms, 66.9185 ms
So the hashset is ~2x faster

Any update @ixje ?
I think that it's a good idea

I'll try to PR this week. First have to finish some other task.

@shargon I wanted to implement it in NEO but I'm getting the error:

Feature 'enum generic type constraints' is not available in C# 7.0. Please use language version 7.3 or greater

So this means the whole NEO project needs to be upgraded to use C# language version 7.3 or greater. This sounds like a big change, so before I continue I'd like to hear if that is acceptable.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

vncoelho picture vncoelho  路  3Comments

canesin picture canesin  路  3Comments

roman-khimov picture roman-khimov  路  3Comments

shargon picture shargon  路  4Comments

igormcoelho picture igormcoelho  路  4Comments