Skip to content

Commit 4cf032c

Browse files
authored
Merkle log support (#259)
* Merkle log support Confirmation status is now an enumerated type. Confirmation logic tests for both CONFIRMED(simple_hash) or COMMITTED (merkle_log). Added suitable unit ahd func tests. Fixes AB#9103
1 parent 31c23e7 commit 4cf032c

14 files changed

Lines changed: 401 additions & 76 deletions

Taskfile.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ tasks:
1010
desc: Generate about.py
1111
cmds:
1212
- ./scripts/builder.sh ./scripts/version.sh
13-
status:
14-
- test -s archivist/about.py
1513

1614
audit:
1715
desc: Audit the code
@@ -54,7 +52,7 @@ tasks:
5452
- task: check-pyright
5553

5654
check-pyright:
57-
desc: Execute pyright conditinally - currently does not work in 3.12 (https://github.com/ekalinin/nodeenv/issues/341)
55+
desc: Execute pyright conditionally - currently does not work in 3.12 (https://github.com/ekalinin/nodeenv/issues/341)
5856
cmds:
5957
- |
6058
if [ "{{.PYVERSION}}" != "3.12" ]; then

archivist/confirmation_status.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Archivist Confirmation Status
2+
3+
Enumerated type that allows user to select the confirmation status option when
4+
creating an asset.
5+
6+
"""
7+
8+
# pylint: disable=unused-private-member
9+
10+
from enum import Enum
11+
12+
13+
class ConfirmationStatus(Enum):
14+
"""Enumerate confirmation status options"""
15+
16+
UNSPECIFIED = 0
17+
# not yet committed
18+
PENDING = 1
19+
CONFIRMED = 2
20+
# permanent failure
21+
FAILED = 3
22+
# forestrie, "its in the db"
23+
STORED = 4
24+
# forestrie, "you can know if its changed"
25+
COMMITTED = 5
26+
# forestrie, "You easily prove it was publicly available to all"
27+
UNEQUIVOCAL = 6

archivist/confirmer.py

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,8 @@
1515
from .events import Event, _EventsPublic, _EventsRestricted
1616

1717

18-
from .constants import (
19-
CONFIRMATION_CONFIRMED,
20-
CONFIRMATION_FAILED,
21-
CONFIRMATION_PENDING,
22-
CONFIRMATION_STATUS,
23-
)
18+
from .confirmation_status import ConfirmationStatus
19+
from .constants import CONFIRMATION_STATUS
2420
from .errors import ArchivistUnconfirmedError
2521
from .utils import backoff_handler
2622

@@ -54,25 +50,29 @@ def __on_giveup_confirmation(details: "Details"):
5450

5551
@overload
5652
def _wait_for_confirmation(
57-
self: "_AssetsRestricted", identity: str
53+
self: "_AssetsRestricted",
54+
identity: str,
5855
) -> "Asset": ... # pragma: no cover
5956

6057

6158
@overload
6259
def _wait_for_confirmation(
63-
self: "_AssetsPublic", identity: str
60+
self: "_AssetsPublic",
61+
identity: str,
6462
) -> "Asset": ... # pragma: no cover
6563

6664

6765
@overload
6866
def _wait_for_confirmation(
69-
self: "_EventsRestricted", identity: str
67+
self: "_EventsRestricted",
68+
identity: str,
7069
) -> "Event": ... # pragma: no cover
7170

7271

7372
@overload
7473
def _wait_for_confirmation(
75-
self: "_EventsPublic", identity: str
74+
self: "_EventsPublic",
75+
identity: str,
7676
) -> "Event": ... # pragma: no cover
7777

7878

@@ -94,12 +94,20 @@ def _wait_for_confirmation(self: Managers, identity: str) -> ReturnTypes:
9494
f"cannot confirm {identity} as confirmation_status is not present"
9595
)
9696

97-
if entity[CONFIRMATION_STATUS] == CONFIRMATION_FAILED:
97+
if entity[CONFIRMATION_STATUS] == ConfirmationStatus.FAILED.name:
9898
raise ArchivistUnconfirmedError(
9999
f"confirmation for {identity} FAILED - this is unusable"
100100
)
101101

102-
if entity[CONFIRMATION_STATUS] == CONFIRMATION_CONFIRMED:
102+
# Simple hash
103+
if entity[CONFIRMATION_STATUS] == ConfirmationStatus.CONFIRMED.name:
104+
return entity
105+
106+
# merkle_log
107+
if (
108+
ConfirmationStatus[entity[CONFIRMATION_STATUS]].value
109+
>= ConfirmationStatus.COMMITTED.value
110+
):
103111
return entity
104112

105113
return None # pyright: ignore
@@ -122,21 +130,24 @@ def __on_giveup_confirmed(details: "Details"):
122130
on_giveup=__on_giveup_confirmed,
123131
)
124132
def _wait_for_confirmed(
125-
self: PrivateManagers, *, props: "dict[str, Any]|None" = None, **kwargs: Any
133+
self: PrivateManagers,
134+
*,
135+
props: "dict[str, Any]|None" = None,
136+
**kwargs: Any,
126137
) -> bool:
127138
"""Return False until all entities are confirmed"""
128139

129140
# look for unconfirmed entities
130141
newprops = deepcopy(props) if props else {}
131-
newprops[CONFIRMATION_STATUS] = CONFIRMATION_PENDING
142+
newprops[CONFIRMATION_STATUS] = ConfirmationStatus.PENDING.name
132143

133144
LOGGER.debug("Count unconfirmed entities %s", newprops)
134145
count = self.count(props=newprops, **kwargs)
135146

136147
if count == 0:
137148
# did any fail
138149
newprops = deepcopy(props) if props else {}
139-
newprops[CONFIRMATION_STATUS] = CONFIRMATION_FAILED
150+
newprops[CONFIRMATION_STATUS] = ConfirmationStatus.FAILED.name
140151
count = self.count(props=newprops, **kwargs)
141152
if count > 0:
142153
raise ArchivistUnconfirmedError(f"There are {count} FAILED entities")

archivist/constants.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,9 @@
1818
HEADERS_TOTAL_COUNT = "X-Total-Count"
1919
HEADERS_RETRY_AFTER = "Archivist-Rate-Limit-Reset"
2020

21+
PROOF_MECHANISM = "proof_mechanism"
22+
2123
CONFIRMATION_STATUS = "confirmation_status"
22-
CONFIRMATION_PENDING = "PENDING"
23-
CONFIRMATION_FAILED = "FAILED"
24-
CONFIRMATION_CONFIRMED = "CONFIRMED"
2524

2625
APPIDP_SUBPATH = "iam/v1"
2726
APPIDP_LABEL = "appidp"

archivist/subjects_confirmer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
if TYPE_CHECKING:
1010
from .subjects import Subject, _SubjectsClient
1111

12+
from .confirmation_status import ConfirmationStatus
1213
from .constants import (
13-
CONFIRMATION_CONFIRMED,
1414
CONFIRMATION_STATUS,
1515
)
1616
from .errors import ArchivistUnconfirmedError
@@ -48,7 +48,7 @@ def _wait_for_confirmation(self: "_SubjectsClient", identity: str) -> "Subject":
4848
if CONFIRMATION_STATUS not in subject:
4949
return None # pyright: ignore
5050

51-
if subject[CONFIRMATION_STATUS] == CONFIRMATION_CONFIRMED:
51+
if subject[CONFIRMATION_STATUS] == ConfirmationStatus.CONFIRMED.name:
5252
return subject
5353

5454
return None # pyright: ignore

functests/execassets.py

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@
3333
"some_custom_attribute": "value",
3434
}
3535

36+
SIMPLE_HASH = {
37+
"proof_mechanism": ProofMechanism.SIMPLE_HASH.name,
38+
}
39+
MERKLE_LOG = {
40+
"proof_mechanism": ProofMechanism.MERKLE_LOG.name,
41+
}
42+
3643
ASSET_NAME = "Telephone with 2 attachments - one bad or not scanned 2022-03-01"
3744
REQUEST_EXISTS_ATTACHMENTS_SIMPLE_HASH = {
3845
"selector": [
@@ -121,6 +128,10 @@ def setUp(self):
121128
self.attrs = deepcopy(ATTRS)
122129
self.traffic_light = deepcopy(ATTRS)
123130
self.traffic_light["arc_display_type"] = "Traffic light with violation camera"
131+
self.traffic_light_merkle_log = deepcopy(ATTRS)
132+
self.traffic_light_merkle_log["arc_display_type"] = (
133+
"Traffic light with violation camera (merkle_log)"
134+
)
124135

125136
def tearDown(self):
126137
self.arch.close()
@@ -132,6 +143,7 @@ def test_asset_create_simple_hash(self):
132143
"""
133144
Test asset creation uses simple hash proof mechanism
134145
"""
146+
# default is simple hash so it is unspecified
135147
asset = self.arch.assets.create(
136148
attrs=self.traffic_light,
137149
confirm=True,
@@ -145,7 +157,25 @@ def test_asset_create_simple_hash(self):
145157
tenancy = self.arch.tenancies.publicinfo(asset["tenant_identity"])
146158
LOGGER.debug("tenancy %s", json_dumps(tenancy, sort_keys=True, indent=4))
147159

148-
def test_asset_create_with_fixtures(self):
160+
def test_asset_create_merkle_log(self):
161+
"""
162+
Test asset creation uses merkle_log proof mechanism
163+
"""
164+
asset = self.arch.assets.create(
165+
props=MERKLE_LOG,
166+
attrs=self.traffic_light_merkle_log,
167+
confirm=True,
168+
)
169+
LOGGER.debug("asset %s", json_dumps(asset, sort_keys=True, indent=4))
170+
self.assertEqual(
171+
asset["proof_mechanism"],
172+
ProofMechanism.MERKLE_LOG.name,
173+
msg="Incorrect asset proof mechanism",
174+
)
175+
tenancy = self.arch.tenancies.publicinfo(asset["tenant_identity"])
176+
LOGGER.debug("tenancy %s", json_dumps(tenancy, sort_keys=True, indent=4))
177+
178+
def test_asset_create_with_fixtures_simple_hash(self):
149179
"""
150180
Test creation with fixtures
151181
"""
@@ -196,6 +226,59 @@ def test_asset_create_with_fixtures(self):
196226
msg="Incorrect number of fancy_traffic_lights",
197227
)
198228

229+
def test_asset_create_with_fixtures_merkle_log(self):
230+
"""
231+
Test creation with fixtures
232+
"""
233+
# creates simple_hash endpoint
234+
simple_hash = copy(self.arch)
235+
simple_hash.fixtures = {
236+
"assets": {
237+
"proof_mechanism": ProofMechanism.MERKLE_LOG.name,
238+
},
239+
}
240+
241+
# create traffic lights endpoint from simple_hash
242+
traffic_lights = copy(simple_hash)
243+
traffic_lights.fixtures = {
244+
"assets": {
245+
"attributes": {
246+
"arc_display_type": "Traffic light with violation camera (merkle_log)",
247+
"arc_namespace": f"functests {uuid4()}",
248+
},
249+
},
250+
}
251+
traffic_lights.assets.create(
252+
props=MERKLE_LOG,
253+
attrs=self.attrs,
254+
confirm=True,
255+
)
256+
self.assertEqual(
257+
traffic_lights.assets.count(),
258+
1,
259+
msg="Incorrect number of traffic_lights",
260+
)
261+
262+
# create fancy traffic lights endpoint from traffic lights
263+
fancy_traffic_lights = copy(traffic_lights)
264+
fancy_traffic_lights.fixtures = {
265+
"assets": {
266+
"attributes": {
267+
"arc_namespace1": f"functests {uuid4()}",
268+
},
269+
},
270+
}
271+
fancy_traffic_lights.assets.create(
272+
props=MERKLE_LOG,
273+
attrs=self.attrs,
274+
confirm=True,
275+
)
276+
self.assertEqual(
277+
fancy_traffic_lights.assets.count(),
278+
1,
279+
msg="Incorrect number of fancy_traffic_lights",
280+
)
281+
199282
def test_asset_create_event_merkle_log(self):
200283
"""
201284
Test list

scripts/builder.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ docker run \
3131
-e DATATRAILS_AUTHTOKEN_FILENAME_2 \
3232
-e DATATRAILS_BLOB_IDENTITY \
3333
-e DATATRAILS_APPREG_CLIENT \
34+
-e DATATRAILS_APPREG_CLIENT_FILENAME \
3435
-e DATATRAILS_APPREG_SECRET \
3536
-e DATATRAILS_APPREG_SECRET_FILENAME \
3637
-e DATATRAILS_LOGLEVEL \

scripts/functests.sh

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,35 @@ then
77
echo "DATATRAILS_URL is undefined"
88
exit 1
99
fi
10+
if [ -n "${DATATRAILS_APPREG_CLIENT_FILENAME}" ]
11+
then
12+
if [ -s "${DATATRAILS_APPREG_CLIENT_FILENAME}" ]
13+
then
14+
export DATATRAILS_APPREG_CLIENT=$(cat ${DATATRAILS_APPREG_CLIENT_FILENAME})
15+
fi
16+
fi
1017
if [ -n "${DATATRAILS_APPREG_CLIENT}" ]
1118
then
1219
if [ -n "${DATATRAILS_APPREG_SECRET_FILENAME}" ]
1320
then
14-
if [ ! -s "${DATATRAILS_APPREG_SECRET_FILENAME}" ]
21+
if [ -s "${DATATRAILS_APPREG_SECRET_FILENAME}" ]
1522
then
16-
echo "${DATATRAILS_APPREG_SECRET_FILENAME} does not exist"
17-
exit 1
23+
export DATATRAILS_APPREG_SECRET=$(cat ${DATATRAILS_APPREG_SECRET_FILENAME})
1824
fi
19-
elif [ -z "${DATATRAILS_APPREG_SECRET}" ]
20-
then
21-
echo "Both DATATRAILS_APPREG_SECRET_FILENAME"
22-
echo "and DATATRAILS_APPREG_SECRET are undefined"
23-
exit 1
2425
fi
25-
else
26-
if [ -n "${DATATRAILS_AUTHTOKEN_FILENAME}" ]
27-
then
28-
if [ ! -s "${DATATRAILS_AUTHTOKEN_FILENAME}" ]
29-
then
30-
echo "${DATATRAILS_AUTHTOKEN_FILENAME} does not exist"
31-
exit 1
32-
fi
33-
elif [ -z "${DATATRAILS_AUTHTOKEN}" ]
26+
fi
27+
if [ -n "${DATATRAILS_AUTHTOKEN_FILENAME}" ]
28+
then
29+
if [ -s "${DATATRAILS_AUTHTOKEN_FILENAME}" ]
3430
then
35-
echo "Both DATATRAILS_AUTHTOKEN_FILENAME"
36-
echo "and DATATRAILS_AUTHTOKEN are undefined"
37-
exit 1
31+
export DATATRAILS_AUTHTOKEN=$(cat ${DATATRAILS_AUTHTOKEN_FILENAME})
3832
fi
3933
fi
34+
if [ -z "${DATATRAILS_AUTHTOKEN}" -a -z "${DATATRAILS_APPREG_CLIENT}" -a -z "${DATATRAILS_APPREG_SECRET}" ]
35+
then
36+
echo "No credentials found, DATATRAILS_AUTHTOKEN, DATATRAILS_APPREG_CLIENT, DATATRAILS_APPREG_SECRET"
37+
exit 1
38+
fi
4039

4140
python3 --version
4241

unittests/testaccess_policies.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from archivist.errors import ArchivistBadRequestError
1818

1919
from .mock_response import MockResponse
20-
from .testassets import RESPONSE as ASSET
20+
from .testassets import RESPONSE_SIMPLE_HASH as ASSET
2121

2222
# pylint: disable=missing-docstring
2323
# pylint: disable=protected-access

0 commit comments

Comments
 (0)