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

Made Validation Middleware Pluggable And Added A Check For POA extraData #756

Merged
merged 3 commits into from
May 2, 2018

Conversation

voith
Copy link
Contributor

@voith voith commented Apr 9, 2018

What was wrong?

see #712

How was it fixed?

A check for POA extra has been added with an error message directing to the docs.

Cute Animal Picture

vulpix full 1774430

@voith
Copy link
Contributor Author

voith commented Apr 9, 2018

This is what I've managed to do till now. However, the exception message is getting lost in the middle for reasons explained below

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~/Projects/web3.py/web3/utils/formatters.py in apply_formatters_to_dict(formatters, value)
     66             try:
---> 67                 yield key, formatters[key](item)
     68             except (TypeError, ValueError) as exc:

~/Python-Env/web3-test/lib/python3.6/site-packages/cytoolz-0.9.0.1-py3.6-macosx-10.12-x86_64.egg/cytoolz/functoolz.pyx in cytoolz.functoolz.Compose.__call__()

~/Python-Env/web3-test/lib/python3.6/site-packages/cytoolz-0.9.0.1-py3.6-macosx-10.12-x86_64.egg/cytoolz/functoolz.pyx in cytoolz.functoolz.curry.__call__()

~/Projects/web3.py/web3/middleware/pythonic.py in check_extradata_length(val, length)
     78             "for more details" % (
---> 79                 result, len(result), length
     80             )

ValueError: The field extraData: HexBytes('0xd783010600846765746887676f312e372e33856c696e75780000000000000000f04a356e34b17926170f92a361fa6785ac5b733e0a01f39913feb6c9e46ae1e22e18ebe956fb1ab268a9c2665964a8c5c640bff3defbb8abed27dba7d334e9e100') is 97 bytes, but should be 32. It is quite likely that you are connected to a POA chain. Refer http://web3py.readthedocs.io/en/latest/middleware.html#geth-style-proof-of-authority for more details

The above exception was the direct cause of the following exception:

ValueError                                Traceback (most recent call last)
<ipython-input-3-1e657b989500> in <module>()
----> 1 w3.eth.getBlock(100000)

~/Projects/web3.py/web3/eth.py in getBlock(self, block_identifier, full_transactions)
    147         return self.web3.manager.request_blocking(
    148             method,
--> 149             [block_identifier, full_transactions],
    150         )
    151 

~/Projects/web3.py/web3/manager.py in request_blocking(self, method, params)
    101         Make a synchronous request using the provider
    102         """
--> 103         response = self._make_request(method, params)
    104 
    105         if "error" in response:

~/Projects/web3.py/web3/manager.py in _make_request(self, method, params)
     84             request_func = provider.request_func(self.web3, tuple(self.middleware_stack))
     85             try:
---> 86                 return request_func(method, params)
     87             except CannotHandleRequest:
     88                 continue

~/Projects/web3.py/web3/middleware/gas_price_strategy.py in middleware(method, params)
     16                     transaction = assoc(transaction, 'gasPrice', generated_gas_price)
     17                     return make_request(method, [transaction])
---> 18         return make_request(method, params)
     19     return middleware

~/Projects/web3.py/web3/middleware/formatting.py in middleware(method, params)
     21                 response = make_request(method, formatted_params)
     22             else:
---> 23                 response = make_request(method, params)
     24 
     25             if 'result' in response and method in result_formatters:

~/Projects/web3.py/web3/middleware/attrdict.py in middleware(method, params)
     16     """
     17     def middleware(method, params):
---> 18         response = make_request(method, params)
     19 
     20         if 'result' in response:

~/Projects/web3.py/web3/middleware/formatting.py in middleware(method, params)
     28                     response,
     29                     'result',
---> 30                     formatter(response['result']),
     31                 )
     32                 return formatted_response

~/Python-Env/web3-test/lib/python3.6/site-packages/cytoolz-0.9.0.1-py3.6-macosx-10.12-x86_64.egg/cytoolz/functoolz.pyx in cytoolz.functoolz.curry.__call__()

~/Projects/web3.py/web3/utils/formatters.py in apply_formatter_if(condition, formatter, value)
     54 def apply_formatter_if(condition, formatter, value):
     55     if condition(value):
---> 56         return formatter(value)
     57     else:
     58         return value

~/Python-Env/web3-test/lib/python3.6/site-packages/cytoolz-0.9.0.1-py3.6-macosx-10.12-x86_64.egg/cytoolz/functoolz.pyx in cytoolz.functoolz.curry.__call__()

~/Python-Env/web3-test/lib/python3.6/site-packages/eth_utils/functional.py in inner(*args, **kwargs)
     20         @functools.wraps(fn)
     21         def inner(*args, **kwargs):
---> 22             return callback(fn(*args, **kwargs))
     23 
     24         return inner

~/Projects/web3.py/web3/utils/formatters.py in apply_formatters_to_dict(formatters, value)
     67                 yield key, formatters[key](item)
     68             except (TypeError, ValueError) as exc:
---> 69                 raise type(exc)("Could not format value %r as field %r" % (item, key)) from exc
     70         else:
     71             yield key, item

ValueError: Could not format value '0xd783010600846765746887676f312e372e33856c696e75780000000000000000f04a356e34b17926170f92a361fa6785ac5b733e0a01f39913feb6c9e46ae1e22e18ebe956fb1ab268a9c2665964a8c5c640bff3defbb8abed27dba7d334e9e100' as field 'extraData'

Since I raised ValueError, the following code hides the main exception

except (TypeError, ValueError) as exc:
raise type(exc)("Could not format value %r as field %r" % (item, key)) from exc

@carver Should I add a new Exception class so that the code mentioned above doesn't hide the original exception?
Does this need a test? Or is the test below sufficient?

def test_long_extra_data(web3):
return_block_with_long_extra_data = construct_fixture_middleware({
'eth_getBlockByNumber': {'extraData': '0x' + 'ff' * 33},
})
web3.middleware_stack.inject(return_block_with_long_extra_data, layer=0)
with pytest.raises(ValueError):
web3.eth.getBlock('latest')

@voith
Copy link
Contributor Author

voith commented Apr 9, 2018

tests are failing because of #753

@voith
Copy link
Contributor Author

voith commented Apr 10, 2018

I changed ValueError to ValidationError .
The current exceptions looks like this:

---------------------------------------------------------------------------
ValidationError                           Traceback (most recent call last)
<ipython-input-3-1e657b989500> in <module>()
----> 1 w3.eth.getBlock(100000)

~/Projects/web3.py/web3/eth.py in getBlock(self, block_identifier, full_transactions)
    147         return self.web3.manager.request_blocking(
    148             method,
--> 149             [block_identifier, full_transactions],
    150         )
    151 

~/Projects/web3.py/web3/manager.py in request_blocking(self, method, params)
    101         Make a synchronous request using the provider
    102         """
--> 103         response = self._make_request(method, params)
    104 
    105         if "error" in response:

~/Projects/web3.py/web3/manager.py in _make_request(self, method, params)
     84             request_func = provider.request_func(self.web3, tuple(self.middleware_stack))
     85             try:
---> 86                 return request_func(method, params)
     87             except CannotHandleRequest:
     88                 continue

~/Projects/web3.py/web3/middleware/gas_price_strategy.py in middleware(method, params)
     16                     transaction = assoc(transaction, 'gasPrice', generated_gas_price)
     17                     return make_request(method, [transaction])
---> 18         return make_request(method, params)
     19     return middleware

~/Projects/web3.py/web3/middleware/formatting.py in middleware(method, params)
     23                 response = make_request(method, formatted_params)
     24             else:
---> 25                 response = make_request(method, params)
     26 
     27             if 'result' in response and method in result_formatters:

~/Projects/web3.py/web3/middleware/attrdict.py in middleware(method, params)
     16     """
     17     def middleware(method, params):
---> 18         response = make_request(method, params)
     19 
     20         if 'result' in response:

~/Projects/web3.py/web3/middleware/formatting.py in middleware(method, params)
     30                     response,
     31                     'result',
---> 32                     formatter(response['result']),
     33                 )
     34                 return formatted_response

~/Python-Env/web3-test/lib/python3.6/site-packages/cytoolz-0.9.0.1-py3.6-macosx-10.12-x86_64.egg/cytoolz/functoolz.pyx in cytoolz.functoolz.curry.__call__()

~/Projects/web3.py/web3/utils/formatters.py in apply_formatter_if(condition, formatter, value)
     54 def apply_formatter_if(condition, formatter, value):
     55     if condition(value):
---> 56         return formatter(value)
     57     else:
     58         return value

~/Python-Env/web3-test/lib/python3.6/site-packages/cytoolz-0.9.0.1-py3.6-macosx-10.12-x86_64.egg/cytoolz/functoolz.pyx in cytoolz.functoolz.curry.__call__()

~/Python-Env/web3-test/lib/python3.6/site-packages/eth_utils/functional.py in inner(*args, **kwargs)
     20         @functools.wraps(fn)
     21         def inner(*args, **kwargs):
---> 22             return callback(fn(*args, **kwargs))
     23 
     24         return inner

~/Projects/web3.py/web3/utils/formatters.py in apply_formatters_to_dict(formatters, value)
     65         if key in formatters:
     66             try:
---> 67                 yield key, formatters[key](item)
     68             except (TypeError, ValueError) as exc:
     69                 raise type(exc)("Could not format value %r as field %r" % (item, key)) from exc

~/Python-Env/web3-test/lib/python3.6/site-packages/cytoolz-0.9.0.1-py3.6-macosx-10.12-x86_64.egg/cytoolz/functoolz.pyx in cytoolz.functoolz.Compose.__call__()

~/Python-Env/web3-test/lib/python3.6/site-packages/cytoolz-0.9.0.1-py3.6-macosx-10.12-x86_64.egg/cytoolz/functoolz.pyx in cytoolz.functoolz.curry.__call__()

~/Projects/web3.py/web3/middleware/pythonic.py in check_extradata_length(val, length)
     80             "http://web3py.readthedocs.io/en/latest/middleware.html#geth-style-proof-of-authority "
     81             "for more details" % (
---> 82                 result, len(result), length
     83             )
     84         )

ValidationError: The field extraData: HexBytes('0xd783010600846765746887676f312e372e33856c696e75780000000000000000f04a356e34b17926170f92a361fa6785ac5b733e0a01f39913feb6c9e46ae1e22e18ebe956fb1ab268a9c2665964a8c5c640bff3defbb8abed27dba7d334e9e100') is 97 bytes, but should be 32. It is quite likely that you are connected to a POA chain. Refer http://web3py.readthedocs.io/en/latest/middleware.html#geth-style-proof-of-authority for more details


I have updated the corresponding test.

While I was looking at ValidationError I found validation middleware. Not sure if the code I added should go there. However, that might take a little more work!

@pipermerriam
Copy link
Member

@voith I do think this belongs in the validation middleware. If you don't mind making that change..

@voith
Copy link
Contributor Author

voith commented Apr 10, 2018

No problem. I'll see how I can add it there!

@carver
Copy link
Collaborator

carver commented Apr 10, 2018

and maybe move the extraData printout to the end of the message:

ValidationError: The field extraData is 97 bytes, but should be 32. It is quite likely that you are connected to a POA chain. Refer http://web3py.readthedocs.io/en/latest/middleware.html#geth-style-proof-of-authority for more details. The full extraData is: HexBytes('0xd783010600846765746887676f312e372e33856c696e75780000000000000000f04a356e34b17926170f92a361fa6785ac5b733e0a01f39913feb6c9e46ae1e22e18ebe956fb1ab268a9c2665964a8c5c640bff3defbb8abed27dba7d334e9e100')

@carver carver mentioned this pull request Apr 12, 2018
@voith
Copy link
Contributor Author

voith commented Apr 16, 2018

update: will work on this today. did not get time over the weekend to work on this

@voith
Copy link
Contributor Author

voith commented Apr 18, 2018

I finally had some time to work on this.

I tried to make the validation middleware configurable like the pythonic middleware so that in future if more parameters need to validated it can be done easily. This is just a POC and I've written the entire code in a single file and blindly copy pasted code(with a few modifications). If the direction is right, this can be further refactored and optimized. @carver @pipermerriam Please check if you like the approach that I've tried here.

@voith
Copy link
Contributor Author

voith commented Apr 18, 2018

test failure is unrelated to this change.

def apply_validator_at_index(validator, at_index, web3, value):
if at_index + 1 > len(value):
raise IndexError(
"Not enough values in iterable to apply formatter. Got: {0}. "
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blindly copy pasted: formatter -> validator

if key in validators:
try:
yield key, validators[key](web3, item)
except (TypeError, ValueError) as exc:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to remove this check as vaildators will raise their own exceptions on failure.



transaction_params_validator = apply_validator_at_index(
compose(transaction_normalizer, TRANSACTION_PARAMS_VALIDATORS),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is wrong. transaction_normalizer here is specific to chainId. This shouldn't be coupled with the logic for pluggable TRANSACTION_PARAMS_VALIDATORS

@voith
Copy link
Contributor Author

voith commented Apr 18, 2018

left some notes for myself to address later.

@curry
@return_arg_type(3)
def apply_validator_at_index(validator, at_index, web3, value):
Copy link
Collaborator

@carver carver Apr 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these copy/pastes from the formatter methods can be removed, and use the formatter version directly (like apply_formatter_at_index instead of defining this method). Bonus points for using the ones from eth-utils. Validators can just be a very specific type of formatter: it either returns the original, or raises an exception.

The whole construct_validator_middleware looks like it can be removed and use construct_formatter_middleware directly.

@voith voith changed the title added a check for POA extraData [WIP]added a check for POA extraData Apr 19, 2018
@voith voith changed the title [WIP]added a check for POA extraData Added a check for POA extraData Apr 20, 2018
@voith
Copy link
Contributor Author

voith commented Apr 20, 2018

@carver
I have tried to incorporate your suggestions. I didn't explain my changes last time as I was sleepy. However I'll explain the changes in my latest commit.
All formatters in the existing codebase are designed to format a single value object. However in the validation middleware, the chainId validation needs a web3 instance as a parameter.

def validate_chain_id(web3, chain_id):

This doesn't conform with the existing formatter utilities and construct_formatting_middleware. So I had to make the following changes:

I have introduced a requests_formattters_with_web3 parameter to handle this scenario in construct_formatting_middleware.
https://github.com/voith/web3.py/blob/b3a30a7374cb5f205d7e96a326533dc8394c7a73/web3/middleware/formatting.py#L16-L17
https://github.com/voith/web3.py/blob/b3a30a7374cb5f205d7e96a326533dc8394c7a73/web3/middleware/formatting.py#L25-L28

This addresses your previous comment for trying to reuse construct_formatting_middleware

I had to introduce two new utilities apply_formatter_with_web3_at_index and apply_formatters_with_web3_to_dict to make the middleware pluggable. With some small tweaks I managed to reuse apply_formatter_at_index and apply_formatters_to_dict from eth-utils and remove the copy/pastes. This again addresses your previous comment.
https://github.com/voith/web3.py/blob/b3a30a7374cb5f205d7e96a326533dc8394c7a73/web3/utils/formatters.py#L168-L180.

The tests for this middleware are already are covered by
tests/core/core/block-utils/test_select_method_for_block_identifier.py and
tests/core/eth-module/test_poa.py

Copy link
Collaborator

@carver carver left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your work on this!

I'm worried that this direction will lead to potentially a lot of new methods to maintain.

I added an idea for another direction, and am happy to hear more suggestions.

@@ -5,20 +5,27 @@

def construct_formatting_middleware(request_formatters=None,
result_formatters=None,
error_formatters=None):
error_formatters=None,
request_formatters_with_web3=None):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking this to its natural conclusion means doubling the number of formatters arguments here, after adding result_formatters_with_web3, and error_formatters_with_web3. Let's look for a way that doesn't pack more complexity into this method.

One approach: I think we could curry the web3-aware formatters with the web3 argument only, and then pass them in as standard formatters. That way we can replace/reusing all that standard formatter middleware. Something like:

middleware/validation.py

# validator-specific implementation:
@curry
def validate_chain_id(w3, chainId):
    if chainId == w3.net.chainId:
        return chainId
    else:
        raise ...

def build_validators_with_web3(w3):
    return dict( 
        request_formatters={
            'eth_sendTransaction': validate_chain_id(w3),
        }
    }

validator_middleware = construct_web3_formatting_middleware(build_validators_with_web3)

middleware/formatting.py

from cytoolz import merge

def construct_formatting_middleware(
        request_formatters=None,
        result_formatters=None,
        error_formatters=None):
     
    def ignore_web3_in_standard_formatters(w3): 
        # This could use **kwargs from construct_formatting_middleware, but then
        # you lose keyword name validation, so this seemed better.
        return dict(
            request_formatters=request_formatters,
            response_formatters=response_formatters,
            error_formatters=error_formatters,
        )
         
    return construct_web3_formatting_middleware(ignore_web3_in_standard_formatters) 

 
def construct_web3_formatting_middleware(web3_formatters_builder)
    def formatter_middleware(make_request, w3):
        formatters = merge(
            {
                'request_formatters': {},
                'result_formatters': {},
                'error_formatters': {},
            },
            web3_formatters_builder(w3),
        ) 
        return apply_formatters(**formatters) 
    return formatter_middleware

@curry
def apply_formatters(method, params, request_formatters, result_formatters, error_formatters):
    if method in request_formatters:
        formatter = request_formatters[method]
        formatted_params = formatter(params)
        response = make_request(method, formatted_params)
    else:
        response = make_request(method, params)

    if 'result' in response and method in result_formatters:
        formatter = result_formatters[method]
        formatted_response = assoc(
            response,
            'result',
            formatter(response['result']),
        )
        return formatted_response
    elif 'error' in response and method in error_formatters:
        formatter = error_formatters[method]
        formatted_response = assoc(
            response,
            'error',
            formatter(response['error']),
        )
        return formatted_response
    else:
        return response

That also means that we can drop the web3 version of all the formatter applicators. One small downside is that most of the formatters would have to change from values to methods, for example:

def transaction_param_formatter(web3):
    return apply_formatters_to_dict({'chainId': validate_chain_id(web3)})

def transaction_args_validator(web3):
    return apply_formatter_at_index(transaction_param_formatter(web3), 0)

etc.


Anyway, I'm not convinced this is the right approach either. Happy to hear other ideas.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very smart way of reusing the existing code.

@@ -27,21 +38,62 @@ def validate_chain_id(web3, chain_id):
)


@curry
def check_extradata_length(val, length):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is kind of straddling generic and specific. Since we have such a specific message we want to give, and don't have other generic uses for it yet, it's fine to just embrace the specificity here, like:

MAX_EXTRADATA_LENGTH = 32


def check_extradata_length(val):
    ...
    if len(result) > MAX_EXTRADATA_LENGTH:
        ...

@curry
def apply_formatter_with_web3_at_index(validator, at_index, web3, value):
_validator = curry(validator)(web3)
return apply_formatter_at_index(_validator, at_index, value)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a lot of different formatter methods. Playing this through to the "end game" implies defining a duplicate of each of them for a web3-enabled version. That seems like it could cause a lot of code bloat in the future.

You did a great job of reusing most of the logic of the original method, though!

@voith
Copy link
Contributor Author

voith commented Apr 23, 2018

Thanks for the detailed review @carver. I'll get back to this in a day or two.

@voith voith force-pushed the issue-712 branch 2 times, most recently from eeb973e to 02a82f2 Compare April 29, 2018 12:58
@voith
Copy link
Contributor Author

voith commented Apr 29, 2018

I added an idea for another direction, and am happy to hear more suggestions.

I don't have a better idea, so I just followed yours.

And sorry for the delay(TBH, limited time). It would have been great if this had made it to v1.2.0

def build_validators_with_web3(w3):
return dict(
request_formatters={
'eth_sendTransaction': chain_id_validator(w3),
Copy link
Contributor Author

@voith voith Apr 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After having a look at https://github.com/ethereum/web3.py/pull/777/files#diff-1e8b0fe1b6f6e15899cb2f41637945bbL36, I'm not sure if I should remove these 3 lines below or If I should comment out line 67.
If I remove the lines below, it won't run the entire transaction_param_validator chain, However It won't leave an example behind on how to use the validation middleware with w3 instance. Perhaps, I should just comment them out with a TODO note

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One approach would be to leave the validator in, but short-circuit it if w3.net.chainId is None. In that case, we assume chainId is unknowable, so we just ignore its value in the transaction, and strip it out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@@ -18,7 +18,6 @@
pytest.param(
lambda web3: 999999999999,
False,
marks=pytest.mark.xfail,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following the approach on short-circuiting if chainId is None, this line should be left in.

@voith
Copy link
Contributor Author

voith commented May 1, 2018

I've split the PR into two commits: One for making the middleware configurable and the other for adding the POA check.

Copy link
Collaborator

@carver carver left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, just a couple tweak suggestions. Let me know which of any of these you want to take on, and we'll get this merged!

def construct_formatting_middleware(
request_formatters=None,
result_formatters=None,
error_formatters=None):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the style-fix!

},
web3_formatters_builder(w3),
)
return apply_formatters(**formatters)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small readability suggestion: consider moving make_request out of the formatters keyword args, like:

        return apply_formatters(make_request=make_request, **formatters)

"The field extraData is %d bytes, but should be %d. "
"It is quite likely that you are connected to a POA chain. "
"Refer "
"http://web3py.readthedocs.io/en/latest/middleware.html#geth-style-proof-of-authority "
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably use stable here instead of latest.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch!!

'chainId': apply_formatter_if(
# Bypass `validate_chain_id` if chainId can't be determined
lambda _: is_not_null(web3.net.chainId),
validate_chain_id(web3)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, this looks like a good solution that can stay in place for the long term 👍

)


extra_data_validator = apply_formatter_if(is_not_null, block_validator)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is intended to be a full block validator, the name extra_data_validator feels a bit off. Rather than come up with yet another name, it could be reasonable to combine this with the earlier block_validator definition:

block_validator = apply_formatter_if(
    is_not_null,
    apply_formatters_to_dict(BLOCK_VALIDATORS),
)

@carver carver merged commit 3321b86 into ethereum:master May 2, 2018
@carver
Copy link
Collaborator

carver commented May 2, 2018

Thanks!

@voith voith deleted the issue-712 branch May 2, 2018 17:15
@voith voith changed the title Added a check for POA extraData Made Validation Middleware Pluggable And Added A Check For POA extraData May 24, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants