Skip to content

Commit bbbaf30

Browse files
Mr-Leshiystevenj
andauthored
feat(cat-gateway): Bumped cat-libs, include Catalyst Signed Docs time based validation (#2146)
* add signed doc time based validation * fix spelling * wip * wip * remove usage of uuid7 lib, replace it with the custom impl * Update catalyst-gateway/bin/src/settings/signed_doc.rs Co-authored-by: Steven Johnson <[email protected]> --------- Co-authored-by: Steven Johnson <[email protected]>
1 parent dc4fd99 commit bbbaf30

File tree

8 files changed

+127
-47
lines changed

8 files changed

+127
-47
lines changed

catalyst-gateway/bin/Cargo.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ repository.workspace = true
1515
workspace = true
1616

1717
[dependencies]
18-
cardano-chain-follower = { version = "0.0.8", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250401-01" }
19-
rbac-registration = { version = "0.0.4", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250401-01" }
20-
catalyst-types = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250401-01" }
21-
cardano-blockchain-types = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250401-01" }
22-
catalyst-signed-doc = { version = "0.0.4", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250401-01" }
18+
cardano-chain-follower = { version = "0.0.8", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250406-00" }
19+
rbac-registration = { version = "0.0.4", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250406-00" }
20+
catalyst-types = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250406-00" }
21+
cardano-blockchain-types = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250406-00" }
22+
catalyst-signed-doc = { version = "0.0.4", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250406-00" }
2323

2424
pallas = { version = "0.30.1", git = "https://github.com/input-output-hk/catalyst-pallas.git", rev = "9b5183c8b90b90fe2cc319d986e933e9518957b3" }
2525
pallas-traverse = { version = "0.30.1", git = "https://github.com/input-output-hk/catalyst-pallas.git", rev = "9b5183c8b90b90fe2cc319d986e933e9518957b3" }

catalyst-gateway/bin/src/service/api/documents/get_document.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use super::templates::get_doc_static_template;
77
use crate::{
88
db::event::{error::NotFoundError, signed_docs::FullSignedDoc},
99
service::common::{responses::WithErrorResponses, types::payload::cbor::Cbor},
10+
settings::Settings,
1011
};
1112

1213
/// Endpoint responses.
@@ -71,4 +72,14 @@ impl catalyst_signed_doc::providers::CatalystSignedDocumentProvider for DocProvi
7172
Err(err) => Err(err),
7273
}
7374
}
75+
76+
fn future_threshold(&self) -> Option<std::time::Duration> {
77+
let signed_doc_cfg = Settings::signed_doc_cfg();
78+
Some(signed_doc_cfg.future_threshold())
79+
}
80+
81+
fn past_threshold(&self) -> Option<std::time::Duration> {
82+
let signed_doc_cfg = Settings::signed_doc_cfg();
83+
Some(signed_doc_cfg.past_threshold())
84+
}
7485
}

catalyst-gateway/bin/src/settings/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use crate::{
2323

2424
pub(crate) mod cassandra_db;
2525
pub(crate) mod chain_follower;
26+
pub(crate) mod signed_doc;
2627
mod str_env_var;
2728

2829
/// Default address to start service on, '0.0.0.0:3030'.
@@ -136,6 +137,9 @@ struct EnvVars {
136137
/// The Chain Follower configuration
137138
chain_follower: chain_follower::EnvVars,
138139

140+
/// The Catalyst Signed Documents configuration
141+
signed_doc: signed_doc::EnvVars,
142+
139143
/// Internal API Access API Key
140144
internal_api_key: Option<StringEnvVar>,
141145

@@ -211,6 +215,7 @@ static ENV_VARS: LazyLock<EnvVars> = LazyLock::new(|| {
211215
cassandra_db::VOLATILE_NAMESPACE_DEFAULT,
212216
),
213217
chain_follower: chain_follower::EnvVars::new(),
218+
signed_doc: signed_doc::EnvVars::new(),
214219
internal_api_key: StringEnvVar::new_optional("INTERNAL_API_KEY", true),
215220
check_config_tick: StringEnvVar::new_as_duration(
216221
"CHECK_CONFIG_TICK",
@@ -307,6 +312,11 @@ impl Settings {
307312
ENV_VARS.chain_follower.clone()
308313
}
309314

315+
/// Get the configuration of the Catalyst Signed Documents.
316+
pub(crate) fn signed_doc_cfg() -> signed_doc::EnvVars {
317+
ENV_VARS.signed_doc.clone()
318+
}
319+
310320
/// Chain Follower network (The Blockchain network we are configured to use).
311321
/// Note: Catalyst Gateway can ONLY follow one network at a time.
312322
pub(crate) fn cardano_network() -> Network {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//! Command line and environment variable settings for the Catalyst Signed Docs
2+
3+
use std::time::Duration;
4+
5+
use super::str_env_var::StringEnvVar;
6+
7+
/// Default number value of `future_threshold`, 30 seconds.
8+
const DEFAULT_FUTURE_THRESHOLD: Duration = Duration::from_secs(30);
9+
10+
/// Default number value of `past_threshold`, 10 minutes.
11+
const DEFAULT_PAST_THRESHOLD: Duration = Duration::from_secs(60 * 10);
12+
13+
/// Configuration for the Catalyst Signed Documents validation configuration.
14+
#[derive(Clone)]
15+
pub(crate) struct EnvVars {
16+
/// The Catalyst Signed Document threshold, document cannot be too far in the future.
17+
future_threshold: Duration,
18+
19+
/// The Catalyst Signed Document threshold, document cannot be too far behind.
20+
past_threshold: Duration,
21+
}
22+
23+
impl EnvVars {
24+
/// Create a config for Catalyst Signed Document validation configuration.
25+
pub(super) fn new() -> Self {
26+
let future_threshold =
27+
StringEnvVar::new_as_duration("SIGNED_DOC_FUTURE_THRESHOLD", DEFAULT_FUTURE_THRESHOLD);
28+
29+
let past_threshold =
30+
StringEnvVar::new_as_duration("SIGNED_DOC_PAST_THRESHOLD", DEFAULT_PAST_THRESHOLD);
31+
32+
Self {
33+
future_threshold,
34+
past_threshold,
35+
}
36+
}
37+
38+
/// The Catalyst Signed Document threshold, document cannot be too far in the future
39+
/// (in seconds).
40+
pub(crate) fn future_threshold(&self) -> Duration {
41+
self.future_threshold
42+
}
43+
44+
/// The Catalyst Signed Document threshold, document cannot be too far behind
45+
/// (in seconds).
46+
pub(crate) fn past_threshold(&self) -> Duration {
47+
self.past_threshold
48+
}
49+
}

catalyst-gateway/tests/api_tests/integration/test_signed_doc.py

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import pytest
22
from loguru import logger
3-
from utils import health, signed_doc
3+
from utils import health, signed_doc, uuid_v7
44
from api.v1 import document
55
import os
66
import json
77
from typing import Dict, Any, List
8-
from uuid_extensions import uuid7str
98
import copy
109
from utils.auth_token import rbac_auth_token_factory
1110

@@ -70,7 +69,7 @@ def comment_templates() -> List[str]:
7069
def proposal_doc_factory(proposal_templates, rbac_auth_token_factory):
7170
def __proposal_doc_factory() -> SignedDocument:
7271
rbac_auth_token = rbac_auth_token_factory()
73-
proposal_doc_id = uuid7str()
72+
proposal_doc_id = uuid_v7.uuid_v7()
7473
proposal_metadata_json = {
7574
"id": proposal_doc_id,
7675
"ver": proposal_doc_id,
@@ -99,11 +98,13 @@ def __proposal_doc_factory() -> SignedDocument:
9998

10099
# return a Comment document which is already published to the cat-gateway
101100
@pytest.fixture
102-
def comment_doc_factory(proposal_doc_factory, comment_templates, rbac_auth_token_factory) -> SignedDocument:
101+
def comment_doc_factory(
102+
proposal_doc_factory, comment_templates, rbac_auth_token_factory
103+
) -> SignedDocument:
103104
def __comment_doc_factory() -> SignedDocument:
104105
rbac_auth_token = rbac_auth_token_factory()
105106
proposal_doc = proposal_doc_factory()
106-
comment_doc_id = uuid7str()
107+
comment_doc_id = uuid_v7.uuid_v7()
107108
comment_metadata_json = {
108109
"id": comment_doc_id,
109110
"ver": comment_doc_id,
@@ -136,7 +137,7 @@ def submission_action_factory(
136137
def __submission_action_factory() -> SignedDocument:
137138
rbac_auth_token = rbac_auth_token_factory()
138139
proposal_doc = proposal_doc_factory()
139-
submission_action_id = uuid7str()
140+
submission_action_id = uuid_v7.uuid_v7()
140141
sub_action_metadata_json = {
141142
"id": submission_action_id,
142143
"ver": submission_action_id,
@@ -190,7 +191,9 @@ def test_proposal_doc(proposal_doc_factory, rbac_auth_token_factory):
190191
), f"Failed to get document: {resp.status_code} - {resp.text}"
191192

192193
# Post a signed document with filter ID
193-
resp = document.post("/index", filter={"id": {"eq": proposal_doc_id}}, token=rbac_auth_token)
194+
resp = document.post(
195+
"/index", filter={"id": {"eq": proposal_doc_id}}, token=rbac_auth_token
196+
)
194197
assert (
195198
resp.status_code == 200
196199
), f"Failed to post document: {resp.status_code} - {resp.text}"
@@ -205,7 +208,7 @@ def test_proposal_doc(proposal_doc_factory, rbac_auth_token_factory):
205208

206209
# Put a signed document with same ID, but different version and different content
207210
new_doc = proposal_doc.copy()
208-
new_doc.metadata["ver"] = uuid7str()
211+
new_doc.metadata["ver"] = uuid_v7.uuid_v7()
209212
new_doc.content["setup"]["title"]["title"] = "another title"
210213
resp = document.put(data=new_doc.hex(), token=rbac_auth_token)
211214
assert (
@@ -214,15 +217,15 @@ def test_proposal_doc(proposal_doc_factory, rbac_auth_token_factory):
214217

215218
# Put a proposal document with the not known template field
216219
invalid_doc = proposal_doc.copy()
217-
invalid_doc.metadata["template"] = {"id": uuid7str()}
220+
invalid_doc.metadata["template"] = {"id": uuid_v7.uuid_v7()}
218221
resp = document.put(data=invalid_doc.hex(), token=rbac_auth_token)
219222
assert (
220223
resp.status_code == 422
221224
), f"Publish document, expected 422 Unprocessable Content: {resp.status_code} - {resp.text}"
222225

223226
# Put a proposal document with empty content
224227
invalid_doc = proposal_doc.copy()
225-
invalid_doc.metadata["ver"] = uuid7str()
228+
invalid_doc.metadata["ver"] = uuid_v7.uuid_v7()
226229
invalid_doc.content = {}
227230
resp = document.put(data=invalid_doc.hex(), token=rbac_auth_token)
228231
assert (
@@ -250,14 +253,16 @@ def test_comment_doc(comment_doc_factory, rbac_auth_token_factory):
250253
), f"Failed to get document: {resp.status_code} - {resp.text}"
251254

252255
# Post a signed document with filter ID
253-
resp = document.post("/index", filter={"id": {"eq": comment_doc_id}}, token=rbac_auth_token)
256+
resp = document.post(
257+
"/index", filter={"id": {"eq": comment_doc_id}}, token=rbac_auth_token
258+
)
254259
assert (
255260
resp.status_code == 200
256261
), f"Failed to post document: {resp.status_code} - {resp.text}"
257262

258263
# Put a comment document with empty content
259264
invalid_doc = comment_doc.copy()
260-
invalid_doc.metadata["ver"] = uuid7str()
265+
invalid_doc.metadata["ver"] = uuid_v7.uuid_v7()
261266
invalid_doc.content = {}
262267
resp = document.put(data=invalid_doc.hex(), token=rbac_auth_token)
263268
assert (
@@ -266,7 +271,7 @@ def test_comment_doc(comment_doc_factory, rbac_auth_token_factory):
266271

267272
# Put a comment document referencing to the not known proposal
268273
invalid_doc = comment_doc.copy()
269-
invalid_doc.metadata["ref"] = {"id": uuid7str()}
274+
invalid_doc.metadata["ref"] = {"id": uuid_v7.uuid_v7()}
270275
resp = document.put(data=invalid_doc.hex(), token=rbac_auth_token)
271276
assert (
272277
resp.status_code == 422
@@ -293,7 +298,9 @@ def test_submission_action(submission_action_factory, rbac_auth_token_factory):
293298
), f"Failed to get document: {resp.status_code} - {resp.text}"
294299

295300
# Post a signed document with filter ID
296-
resp = document.post("/index", filter={"id": {"eq": submission_action_id}}, token=rbac_auth_token)
301+
resp = document.post(
302+
"/index", filter={"id": {"eq": submission_action_id}}, token=rbac_auth_token
303+
)
297304
assert (
298305
resp.status_code == 200
299306
), f"Failed to post document: {resp.status_code} - {resp.text}"
@@ -308,7 +315,7 @@ def test_submission_action(submission_action_factory, rbac_auth_token_factory):
308315

309316
# Put a submission action document referencing an unknown proposal
310317
invalid_doc = submission_action.copy()
311-
invalid_doc.metadata["ref"] = {"id": uuid7str()}
318+
invalid_doc.metadata["ref"] = {"id": uuid_v7.uuid_v7()}
312319
resp = document.put(data=invalid_doc.hex(), token=rbac_auth_token)
313320
assert (
314321
resp.status_code == 422
@@ -325,7 +332,7 @@ def test_document_index_endpoint(proposal_doc_factory, rbac_auth_token_factory):
325332
for _ in range(total_amount - 1):
326333
doc = first_proposal.copy()
327334
# keep the same id, but different version
328-
doc.metadata["ver"] = uuid7str()
335+
doc.metadata["ver"] = uuid_v7.uuid_v7()
329336
resp = document.put(data=doc.hex(), token=rbac_auth_token)
330337
assert (
331338
resp.status_code == 201
@@ -335,9 +342,7 @@ def test_document_index_endpoint(proposal_doc_factory, rbac_auth_token_factory):
335342
page = 0
336343
filter = {"id": {"eq": first_proposal.metadata["id"]}}
337344
resp = document.post(
338-
f"/index?limit={limit}&page={page}",
339-
filter=filter,
340-
token=rbac_auth_token
345+
f"/index?limit={limit}&page={page}", filter=filter, token=rbac_auth_token
341346
)
342347
assert (
343348
resp.status_code == 200
@@ -350,9 +355,7 @@ def test_document_index_endpoint(proposal_doc_factory, rbac_auth_token_factory):
350355

351356
page += 1
352357
resp = document.post(
353-
f"/index?limit={limit}&page={page}",
354-
filter=filter,
355-
token=rbac_auth_token
358+
f"/index?limit={limit}&page={page}", filter=filter, token=rbac_auth_token
356359
)
357360
assert (
358361
resp.status_code == 200
@@ -364,9 +367,7 @@ def test_document_index_endpoint(proposal_doc_factory, rbac_auth_token_factory):
364367
assert data["page"]["remaining"] == total_amount - 1 - page
365368

366369
resp = document.post(
367-
f"/index?limit={total_amount}",
368-
filter=filter,
369-
token=rbac_auth_token
370+
f"/index?limit={total_amount}", filter=filter, token=rbac_auth_token
370371
)
371372
assert (
372373
resp.status_code == 200
@@ -378,9 +379,7 @@ def test_document_index_endpoint(proposal_doc_factory, rbac_auth_token_factory):
378379

379380
# Pagination out of range
380381
resp = document.post(
381-
"/index?page=92233720368547759",
382-
filter={},
383-
token=rbac_auth_token
382+
"/index?page=92233720368547759", filter={}, token=rbac_auth_token
384383
)
385384
assert (
386385
resp.status_code == 412

catalyst-gateway/tests/api_tests/poetry.lock

Lines changed: 2 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

catalyst-gateway/tests/api_tests/pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ requests = "^2.32.3"
2020
pytest = "^8.0.0"
2121
python-bitcoinlib = "^0.12.2"
2222
pytest-cov = "^5.0.0"
23-
uuid7 = "^0.1.0"
2423
cryptography = "^44.0.2"
2524

2625
[build-system]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import time
2+
import os
3+
4+
5+
def uuid_v7() -> str:
6+
# random bytes
7+
value = bytearray(os.urandom(16))
8+
# current timestamp in ms
9+
timestamp = int(time.time() * 1000)
10+
11+
# timestamp
12+
value[0] = (timestamp >> 40) & 0xFF
13+
value[1] = (timestamp >> 32) & 0xFF
14+
value[2] = (timestamp >> 24) & 0xFF
15+
value[3] = (timestamp >> 16) & 0xFF
16+
value[4] = (timestamp >> 8) & 0xFF
17+
value[5] = timestamp & 0xFF
18+
19+
# version and variant
20+
value[6] = (value[6] & 0x0F) | 0x70
21+
value[8] = (value[8] & 0x3F) | 0x80
22+
23+
return f"{value[:4].hex()}-{value[4:6].hex()}-{value[6:8].hex()}-{value[8:10].hex()}-{value[10:].hex()}"

0 commit comments

Comments
 (0)