Repository: hyperledger/aries-cloudagent-python

Issue:
present proof v2 does not support proof request without restrictions
Using the same flow in present proof v1 and v2 with indy credentials will give different results. If a proof request has no restrictions it will succeed in v1 and fail in v2. 

I think it should be possible to request a proof without restrictions?

The proof request looks like this for v1:

```json
{
    "connection_id": "{{connectionId}}",
    "proof_request": {
        "name": "Proof Request",
        "version": "1.0.0",
        "requested_attributes": {
            "name": {
                "name": "name",
                "restrictions": []
            }
        },
        "requested_predicates": {}
    }
}
```

and like this for v2:

```json
{
    "connection_id": "{{connectionId}}",
    "presentation_request": {
        "indy": {
            "name": "Proof Request",
            "version": "1.0.0",
            "requested_attributes": {
                "name": {
                    "name": "name",
                    "restrictions": []
                }
            },
            "requested_predicates": {}
        }
    }
}
```

V2 will log errors when validating the received presentation (so no errors returned in the api).

The error:

```
Traceback (most recent call last):
  File "/Users/timo/Developer/Work/Projects/AFJ/aries-cloudagent-python/aries_cloudagent/core/dispatcher.py", line 197, in handle_message
    await handler(context, responder)
  File "/Users/timo/Developer/Work/Projects/AFJ/aries-cloudagent-python/aries_cloudagent/protocols/present_proof/v2_0/handlers/pres_handler.py", line 40, in handle
    pres_ex_record = await pres_manager.receive_pres(
  File "/Users/timo/Developer/Work/Projects/AFJ/aries-cloudagent-python/aries_cloudagent/protocols/present_proof/v2_0/manager.py", line 329, in receive_pres
    receive_pres_return = await pres_format.handler(
  File "/Users/timo/Developer/Work/Projects/AFJ/aries-cloudagent-python/aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py", line 292, in receive_pres
    _check_proof_vs_proposal()
  File "/Users/timo/Developer/Work/Projects/AFJ/aries-cloudagent-python/aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py", line 198, in _check_proof_vs_proposal
    raise V20PresFormatHandlerError(
aries_cloudagent.protocols.present_proof.v2_0.formats.handler.V20PresFormatHandlerError: Presented attribute name does not satisfy proof request restrictions []
```

See here for a full reproduction flow, I can give access if you want to easily be able to run the flow to reproduce: https://www.postman.com/timoanimo/workspace/timo/flow/6270013353dfac45e5b56995


Discussion:
@andrewwhitehead -- this is a weird one.  Can you please take a look?
Assigned this to @shaangill025 for now, but if anyone wanted to learn about the details of how protocols are implemented, this would be a fun investigation...

Resolving patch:
diff --git a/aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py b/aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py
--- a/aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py
+++ b/aries_cloudagent/protocols/present_proof/v2_0/formats/indy/handler.py
@@ -199,7 +199,10 @@ def _check_proof_vs_proposal():
                     f"attr::{name}::value": proof_value,
                 }
 
-                if not any(r.items() <= criteria.items() for r in req_restrictions):
+                if (
+                    not any(r.items() <= criteria.items() for r in req_restrictions)
+                    and len(req_restrictions) != 0
+                ):
                     raise V20PresFormatHandlerError(
                         f"Presented attribute {reft} does not satisfy proof request "
                         f"restrictions {req_restrictions}"
@@ -234,7 +237,10 @@ def _check_proof_vs_proposal():
                     },
                 }
 
-                if not any(r.items() <= criteria.items() for r in req_restrictions):
+                if (
+                    not any(r.items() <= criteria.items() for r in req_restrictions)
+                    and len(req_restrictions) != 0
+                ):
                     raise V20PresFormatHandlerError(
                         f"Presented attr group {reft} does not satisfy proof request "
                         f"restrictions {req_restrictions}"
@@ -287,7 +293,10 @@ def _check_proof_vs_proposal():
                     "issuer_did": cred_def_id.split(":")[-5],
                 }
 
-                if not any(r.items() <= criteria.items() for r in req_restrictions):
+                if (
+                    not any(r.items() <= criteria.items() for r in req_restrictions)
+                    and len(req_restrictions) != 0
+                ):
                     raise V20PresFormatHandlerError(
                         f"Presented predicate {reft} does not satisfy proof request "
                         f"restrictions {req_restrictions}"


Test patch:
diff --git a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_manager.py b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_manager.py
--- a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_manager.py
+++ b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_manager.py
@@ -1400,6 +1400,164 @@ async def test_receive_pres_receive_pred_value_mismatch_punt_to_indy(self):
             save_ex.assert_called_once()
             assert px_rec_out.state == (V20PresExRecord.STATE_PRESENTATION_RECEIVED)
 
+    async def test_receive_pres_indy_no_predicate_restrictions(self):
+        connection_record = async_mock.MagicMock(connection_id=CONN_ID)
+        indy_proof_req = {
+            "name": PROOF_REQ_NAME,
+            "version": PROOF_REQ_VERSION,
+            "nonce": PROOF_REQ_NONCE,
+            "requested_attributes": {
+                "0_player_uuid": {
+                    "name": "player",
+                    "restrictions": [{"cred_def_id": CD_ID}],
+                    "non_revoked": {"from": NOW, "to": NOW},
+                },
+                "0_screencapture_uuid": {
+                    "name": "screenCapture",
+                    "restrictions": [{"cred_def_id": CD_ID}],
+                    "non_revoked": {"from": NOW, "to": NOW},
+                },
+            },
+            "requested_predicates": {
+                "0_highscore_GE_uuid": {
+                    "name": "highScore",
+                    "p_type": ">=",
+                    "p_value": 1000000,
+                    "restrictions": [],
+                    "non_revoked": {"from": NOW, "to": NOW},
+                }
+            },
+        }
+        pres_request = V20PresRequest(
+            formats=[
+                V20PresFormat(
+                    attach_id="indy",
+                    format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][
+                        V20PresFormat.Format.INDY.api
+                    ],
+                )
+            ],
+            request_presentations_attach=[
+                AttachDecorator.data_base64(indy_proof_req, ident="indy")
+            ],
+        )
+        pres = V20Pres(
+            formats=[
+                V20PresFormat(
+                    attach_id="indy",
+                    format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api],
+                )
+            ],
+            presentations_attach=[
+                AttachDecorator.data_base64(INDY_PROOF, ident="indy")
+            ],
+        )
+        pres.assign_thread_id("thread-id")
+
+        px_rec_dummy = V20PresExRecord(
+            pres_request=pres_request.serialize(),
+        )
+
+        # cover by_format property
+        by_format = px_rec_dummy.by_format
+
+        assert by_format.get("pres_request").get("indy") == indy_proof_req
+
+        with async_mock.patch.object(
+            V20PresExRecord, "save", autospec=True
+        ) as save_ex, async_mock.patch.object(
+            V20PresExRecord, "retrieve_by_tag_filter", autospec=True
+        ) as retrieve_ex, async_mock.patch.object(
+            self.profile,
+            "session",
+            async_mock.MagicMock(return_value=self.profile.session()),
+        ) as session:
+            retrieve_ex.side_effect = [px_rec_dummy]
+            px_rec_out = await self.manager.receive_pres(pres, connection_record, None)
+            retrieve_ex.assert_called_once_with(
+                session.return_value,
+                {"thread_id": "thread-id"},
+                {"role": V20PresExRecord.ROLE_VERIFIER, "connection_id": CONN_ID},
+            )
+            save_ex.assert_called_once()
+            assert px_rec_out.state == (V20PresExRecord.STATE_PRESENTATION_RECEIVED)
+
+    async def test_receive_pres_indy_no_attr_restrictions(self):
+        connection_record = async_mock.MagicMock(connection_id=CONN_ID)
+        indy_proof_req = {
+            "name": PROOF_REQ_NAME,
+            "version": PROOF_REQ_VERSION,
+            "nonce": PROOF_REQ_NONCE,
+            "requested_attributes": {
+                "0_player_uuid": {
+                    "name": "player",
+                    "restrictions": [],
+                    "non_revoked": {"from": NOW, "to": NOW},
+                }
+            },
+            "requested_predicates": {},
+        }
+        proof = deepcopy(INDY_PROOF)
+        proof["requested_proof"]["revealed_attrs"] = {
+            "0_player_uuid": {
+                "sub_proof_index": 0,
+                "raw": "Richie Knucklez",
+                "encoded": "516439982",
+            }
+        }
+        proof["requested_proof"]["predicates"] = {}
+        pres_request = V20PresRequest(
+            formats=[
+                V20PresFormat(
+                    attach_id="indy",
+                    format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][
+                        V20PresFormat.Format.INDY.api
+                    ],
+                )
+            ],
+            request_presentations_attach=[
+                AttachDecorator.data_base64(indy_proof_req, ident="indy")
+            ],
+        )
+        pres = V20Pres(
+            formats=[
+                V20PresFormat(
+                    attach_id="indy",
+                    format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api],
+                )
+            ],
+            presentations_attach=[AttachDecorator.data_base64(proof, ident="indy")],
+        )
+        pres.assign_thread_id("thread-id")
+
+        px_rec_dummy = V20PresExRecord(
+            pres_request=pres_request.serialize(),
+        )
+
+        # cover by_format property
+        by_format = px_rec_dummy.by_format
+
+        assert by_format.get("pres_request").get("indy") == indy_proof_req
+
+        with async_mock.patch.object(
+            V20PresExRecord, "save", autospec=True
+        ) as save_ex, async_mock.patch.object(
+            V20PresExRecord, "retrieve_by_tag_filter", autospec=True
+        ) as retrieve_ex, async_mock.patch.object(
+            self.profile,
+            "session",
+            async_mock.MagicMock(return_value=self.profile.session()),
+        ) as session:
+            retrieve_ex.side_effect = [px_rec_dummy]
+            px_rec_out = await self.manager.receive_pres(pres, connection_record, None)
+            retrieve_ex.assert_called_once_with(
+                session.return_value,
+                {"thread_id": "thread-id"},
+                {"role": V20PresExRecord.ROLE_VERIFIER, "connection_id": CONN_ID},
+            )
+            save_ex.assert_called_once()
+            assert px_rec_out.state == (V20PresExRecord.STATE_PRESENTATION_RECEIVED)
+
     async def test_receive_pres_bait_and_switch_attr_name(self):
         connection_record = async_mock.MagicMock(connection_id=CONN_ID)
         indy_proof_req = deepcopy(INDY_PROOF_REQ_NAME)