Skip to content

Commit 1471202

Browse files
ashcherbakovandkononykhin
authored andcommitted
INDY-877: Protocol version support (#407)
* add protocolVersion parameter to Request Signed-off-by: ashcherbakov <[email protected]> * set current protocol version for all client Requests Signed-off-by: ashcherbakov <[email protected]> * remove deprecated method Signed-off-by: ashcherbakov <[email protected]> * static code validation Signed-off-by: ashcherbakov <[email protected]> * fix tests Signed-off-by: ashcherbakov <[email protected]>
1 parent 5b8db99 commit 1471202

16 files changed

+437
-53
lines changed

common/serializers/serialization.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
proof_nodes_serializer = Base64Serializer()
1818

1919

20+
# TODO: separate data, metadata and signature, so that we don't need to have topLevelKeysToIgnore
2021
def serialize_msg_for_signing(msg: Mapping, topLevelKeysToIgnore=None):
2122
"""
2223
Serialize a message for signing.

plenum/common/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# inter-node communication
22
from enum import IntEnum, unique
33

4+
from plenum.common.plenum_protocol_version import PlenumProtocolVersion
45
from plenum.common.roles import Roles
56
from plenum.common.transactions import PlenumTransactions
67

@@ -154,3 +155,5 @@ class LedgerState(IntEnum):
154155
PLUGIN_BASE_DIR_PATH = "PluginBaseDirPath"
155156
POOL_LEDGER_ID = 0
156157
DOMAIN_LEDGER_ID = 1
158+
159+
CURRENT_PROTOCOL_VERSION = PlenumProtocolVersion.STATE_PROOF_SUPPORT.value

plenum/common/messages/client_request.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
TARGET_NYM, VERKEY, ROLE, NODE, NYM, GET_TXN, VALIDATOR, BLS_KEY
33
from plenum.common.messages.fields import NetworkIpAddressField, NetworkPortField, IterableField, \
44
ChooseField, ConstantField, DestNodeField, VerkeyField, DestNymField, RoleField, TxnSeqNoField, IdentifierField, \
5-
NonNegativeNumberField, SignatureField, LimitedLengthStringField
5+
NonNegativeNumberField, SignatureField, LimitedLengthStringField, ProtocolVersionField
66
from plenum.common.messages.message_base import MessageValidator
77
from plenum.common.types import OPERATION, f
88
from plenum.config import ALIAS_FIELD_LIMIT, DIGEST_FIELD_LIMIT, SIGNATURE_FIELD_LIMIT, BLS_KEY_LIMIT
@@ -108,4 +108,5 @@ def __init__(self, operation_schema_is_strict, *args, **kwargs):
108108
(OPERATION, ClientOperationField()),
109109
(f.SIG.nm, SignatureField(max_length=SIGNATURE_FIELD_LIMIT, optional=True)),
110110
(f.DIGEST.nm, LimitedLengthStringField(max_length=DIGEST_FIELD_LIMIT, optional=True)),
111+
(f.PROTOCOL_VERSION.nm, ProtocolVersionField(optional=True)),
111112
)

plenum/common/messages/fields.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import base58
77
from plenum.common.constants import DOMAIN_LEDGER_ID, POOL_LEDGER_ID
8+
from plenum.common.plenum_protocol_version import PlenumProtocolVersion
89
from plenum.config import BLS_MULTI_SIG_LIMIT
910

1011

@@ -124,12 +125,13 @@ def _specific_validation(self, val):
124125
class SignatureField(LimitedLengthStringField):
125126
_base_types = (str, type(None))
126127

127-
# TODO do nothing because EmptySignature should be raised somehow
128-
129128
def _specific_validation(self, val):
130-
if val and len(val) > 0:
131-
return super()._specific_validation(val)
132-
return
129+
if val is None:
130+
# TODO do nothing because EmptySignature should be raised somehow
131+
return
132+
if len(val) == 0:
133+
return "signature can not be empty"
134+
return super()._specific_validation(val)
133135

134136

135137
class RoleField(FieldBase):
@@ -523,3 +525,13 @@ def _specific_validation(self, val):
523525
return err
524526
if len(participants) == 0:
525527
return "multi-signature participants list is empty"
528+
529+
530+
class ProtocolVersionField(FieldBase):
531+
_base_types = (int, type(None))
532+
533+
def _specific_validation(self, val):
534+
if val is None:
535+
return
536+
if not PlenumProtocolVersion.has_value(val):
537+
return 'Unknown protocol version value {}'.format(val)

plenum/common/messages/message_base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class MessageValidator(FieldValidator):
1212

1313
schema = ()
1414
optional = False
15+
schema_is_strict = True
1516

1617
def __init__(self, schema_is_strict=True):
1718
self.schema_is_strict = schema_is_strict
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from enum import Enum, unique
2+
3+
4+
@unique
5+
class PlenumProtocolVersion(Enum):
6+
# These numeric constants CANNOT be changed once they have been used
7+
STATE_PROOF_SUPPORT = 1
8+
9+
def __str__(self):
10+
return self.name
11+
12+
@staticmethod
13+
def has_value(value):
14+
try:
15+
PlenumProtocolVersion(value)
16+
return True
17+
except ValueError:
18+
return False

plenum/common/request.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,41 @@
22
from typing import Mapping, NamedTuple
33

44
from common.serializers.serialization import serialize_msg_for_signing
5-
from plenum.common.constants import REQDIGEST, REQKEY, FORCE
5+
from plenum.common.constants import REQDIGEST, REQKEY, FORCE, CURRENT_PROTOCOL_VERSION
66
from plenum.common.messages.client_request import ClientMessageValidator
77
from plenum.common.types import f, OPERATION
88
from stp_core.types import Identifier
99

1010

1111
class Request:
1212
def __init__(self,
13-
identifier: Identifier=None,
14-
reqId: int=None,
15-
operation: Mapping=None,
16-
signature: str=None):
13+
identifier: Identifier = None,
14+
reqId: int = None,
15+
operation: Mapping = None,
16+
signature: str = None,
17+
protocolVersion: int = CURRENT_PROTOCOL_VERSION):
1718
self.identifier = identifier
1819
self.reqId = reqId
1920
self.operation = operation
20-
self.digest = self.getDigest()
21+
self.protocolVersion = protocolVersion
2122
self.signature = signature
23+
# TODO: getDigest must be called after all initialization above! refactor it
24+
self.digest = self.getDigest()
2225

2326
@property
2427
def as_dict(self):
25-
return {
28+
# TODO: as of now, the keys below must be equal to the class fields name (see SafeRequest)
29+
dct = {
2630
f.IDENTIFIER.nm: self.identifier,
2731
f.REQ_ID.nm: self.reqId,
28-
OPERATION: self.operation,
29-
f.SIG.nm: self.signature
32+
OPERATION: self.operation
3033
}
34+
if self.signature is not None:
35+
dct[f.SIG.nm] = self.signature
36+
if self.protocolVersion is not None:
37+
dct[f.PROTOCOL_VERSION.nm] = self.protocolVersion
38+
39+
return dct
3140

3241
def __eq__(self, other):
3342
return self.as_dict == other.as_dict
@@ -51,11 +60,15 @@ def __getstate__(self):
5160

5261
@property
5362
def signingState(self):
54-
return {
63+
# TODO: separate data, metadata and signature, so that we don't need to have this kind of messages
64+
dct = {
5565
f.IDENTIFIER.nm: self.identifier,
5666
f.REQ_ID.nm: self.reqId,
5767
OPERATION: self.operation
5868
}
69+
if self.protocolVersion is not None:
70+
dct[f.PROTOCOL_VERSION.nm] = self.protocolVersion
71+
return dct
5972

6073
def __setstate__(self, state):
6174
self.__dict__.update(state)
@@ -91,7 +104,6 @@ class ReqKey(NamedTuple(REQKEY, [f.IDENTIFIER, f.REQ_ID])):
91104

92105

93106
class SafeRequest(Request, ClientMessageValidator):
94-
95107
def __init__(self, **kwargs):
96108
self.validate(kwargs)
97109
super().__init__(**kwargs)

plenum/common/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class f: # provides a namespace for reusable field constants
3737
IS_STABLE = Field('isStable', bool)
3838
MSGS = Field('messages', List[Mapping])
3939
SIG = Field('signature', Optional[str])
40+
PROTOCOL_VERSION = Field('protocolVersion', int)
4041
SUSP_CODE = Field('suspicionCode', int)
4142
ELECTION_DATA = Field('electionData', Any)
4243
TXN_ID = Field('txnId', str)

plenum/req_handler/__init__.py

Whitespace-only changes.

plenum/test/client/test_client.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from plenum.common.keygen_utils import initRemoteKeys
33

44
from stp_core.loop.eventually import eventually
5-
from plenum.common.exceptions import EmptySignature
5+
from plenum.common.exceptions import MissingSignature
66
from plenum.common.exceptions import NotConnectedToAny
77
from stp_core.common.log import getlogger
88
from plenum.common.constants import OP_FIELD_NAME, REPLY, REQACK
@@ -107,15 +107,15 @@ async def go(ctx):
107107
params = n.spylog.getLastParams(Node.handleInvalidClientMsg)
108108
ex = params['ex']
109109
msg, _ = params['wrappedMsg']
110-
assert isinstance(ex, EmptySignature)
110+
assert isinstance(ex, MissingSignature)
111111
assert msg.get(f.IDENTIFIER.nm) == request.identifier
112112

113113
params = n.spylog.getLastParams(Node.discard)
114114
reason = params["reason"]
115115
(msg, frm) = params["msg"]
116116
assert msg == request.as_dict
117117
assert msg.get(f.IDENTIFIER.nm) == request.identifier
118-
assert "EmptySignature" in reason
118+
assert "MissingSignature" in reason
119119

120120
pool.run(go)
121121

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from plenum.common.constants import CURRENT_PROTOCOL_VERSION
2+
from plenum.test.helper import sendRandomRequests, waitForSufficientRepliesForRequests, signed_random_requests, \
3+
send_signed_requests, checkReqNackWithReason
4+
from plenum.test.pool_transactions.conftest import looper, clientAndWallet1, \
5+
client1, wallet1, client1Connected
6+
from stp_core.loop.eventually import eventually
7+
8+
9+
def test_request_no_protocol_version(tconf, looper, txnPoolNodeSet,
10+
client1, client1Connected,
11+
wallet1):
12+
reqs = signed_random_requests(wallet1, 2)
13+
for req in reqs:
14+
req.protocolVersion = None
15+
send_signed_requests(client1, reqs)
16+
waitForSufficientRepliesForRequests(looper, client1, requests=reqs)
17+
18+
19+
def test_version_set_by_default(tconf, looper, txnPoolNodeSet,
20+
client1, client1Connected,
21+
wallet1):
22+
reqs = signed_random_requests(wallet1, 1)
23+
assert reqs[0].protocolVersion
24+
send_signed_requests(client1, reqs)
25+
waitForSufficientRepliesForRequests(looper, client1, requests=reqs)
26+
27+
28+
def test_request_with_correct_version(tconf, looper,
29+
txnPoolNodeSet, client1, client1Connected,
30+
wallet1):
31+
reqs = signed_random_requests(wallet1, 2)
32+
for req in reqs:
33+
req.protocolVersion = CURRENT_PROTOCOL_VERSION
34+
send_signed_requests(client1, reqs)
35+
waitForSufficientRepliesForRequests(looper, client1, requests=reqs)
36+
37+
38+
def test_request_with_invalid_version(tconf, looper, txnPoolNodeSet,
39+
client1, client1Connected,
40+
wallet1):
41+
reqs = signed_random_requests(wallet1, 2)
42+
for req in reqs:
43+
req.protocolVersion = -1
44+
send_signed_requests(client1, reqs)
45+
for node in txnPoolNodeSet:
46+
looper.run(eventually(checkReqNackWithReason, client1,
47+
'Unknown protocol version value -1',
48+
node.clientstack.name, retryWait=1))

plenum/test/helper.py

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -73,39 +73,6 @@ def check_sufficient_replies_received(client: Client,
7373
.format(full_request_id))
7474

7575

76-
def checkSufficientRepliesReceived(receivedMsgs: Iterable,
77-
reqId: int,
78-
fValue: int):
79-
"""
80-
Checks number of replies for request with specified id in given inbox and
81-
if this number is lower than number of malicious nodes (fValue) -
82-
raises exception
83-
84-
If you do not need response ponder on using
85-
waitForSufficientRepliesForRequests instead
86-
87-
:returns: response for request
88-
"""
89-
90-
receivedReplies = getRepliesFromClientInbox(inbox=receivedMsgs,
91-
reqId=reqId)
92-
logger.debug("received replies for reqId {}: {}".
93-
format(reqId, receivedReplies))
94-
assert len(receivedReplies) > fValue, "Received {} replies but expected " \
95-
"at-least {} for reqId {}". \
96-
format(len(receivedReplies), fValue + 1, reqId)
97-
result = checkIfMoreThanFSameItems([reply[f.RESULT.nm] for reply in
98-
receivedReplies], fValue)
99-
assert result, "reqId {}: found less than in {} same replies".format(
100-
reqId, fValue)
101-
102-
assert all([r[f.RESULT.nm][f.REQ_ID.nm] == reqId for r in receivedReplies]
103-
), "not all replies have got reqId {}".format(reqId)
104-
105-
return result
106-
# TODO add test case for what happens when replies don't have the same data
107-
108-
10976
def waitForSufficientRepliesForRequests(looper,
11077
client,
11178
*, # To force usage of names
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from plenum.common.messages.fields import ProtocolVersionField
2+
from plenum.common.plenum_protocol_version import PlenumProtocolVersion
3+
4+
validator = ProtocolVersionField()
5+
6+
7+
def test_valid():
8+
assert not validator.validate(1)
9+
assert not validator.validate(PlenumProtocolVersion.STATE_PROOF_SUPPORT.value)
10+
assert not validator.validate(None) # version can be None (for backward compatibility)
11+
12+
13+
def test_invalid():
14+
assert validator.validate(2)
15+
assert validator.validate("1")
16+
assert validator.validate("")
17+
assert validator.validate(0)
18+
assert validator.validate(1.0)
19+
assert validator.validate(0.1)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from collections import OrderedDict
2+
3+
from plenum.common.messages.client_request import ClientMessageValidator, ClientOperationField
4+
from plenum.common.messages.fields import LimitedLengthStringField, SignatureField, NonNegativeNumberField, \
5+
IdentifierField, ProtocolVersionField
6+
7+
EXPECTED_ORDERED_FIELDS = OrderedDict([
8+
("identifier", IdentifierField),
9+
("reqId", NonNegativeNumberField),
10+
("operation", ClientOperationField),
11+
("signature", SignatureField),
12+
("digest", LimitedLengthStringField),
13+
("protocolVersion", ProtocolVersionField),
14+
])
15+
16+
17+
def test_has_expected_fields():
18+
actual_field_names = OrderedDict(ClientMessageValidator.schema).keys()
19+
assert list(actual_field_names) == list(EXPECTED_ORDERED_FIELDS.keys())
20+
21+
22+
def test_has_expected_validators():
23+
schema = dict(ClientMessageValidator.schema)
24+
for field, validator in EXPECTED_ORDERED_FIELDS.items():
25+
assert isinstance(schema[field], validator)

0 commit comments

Comments
 (0)