Skip to content

Commit c461f29

Browse files
authored
feat: Implement historical pricing for message cost recalculation (#813)
* feat: Implement historical pricing for message cost recalculation * fix: lint * feat: added auth endpoints utils
1 parent 68d9d93 commit c461f29

File tree

10 files changed

+1413
-55
lines changed

10 files changed

+1413
-55
lines changed

deployment/migrations/versions/0033_1c06d0ade60c_calculate_costs_statically.py

Lines changed: 6 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
"""
88

99
from decimal import Decimal
10-
from typing import Dict
1110
from alembic import op
1211
import sqlalchemy as sa
1312
import logging
@@ -17,7 +16,8 @@
1716
from aleph.db.accessors.cost import make_costs_upsert_query
1817
from aleph.db.accessors.messages import get_message_by_item_hash
1918
from aleph.services.cost import _is_confidential_vm, get_detailed_costs, CostComputableContent
20-
from aleph.types.cost import ProductComputeUnit, ProductPrice, ProductPriceOptions, ProductPriceType, ProductPricing
19+
from aleph.services.pricing_utils import build_default_pricing_model
20+
from aleph.types.cost import ProductPriceType
2121
from aleph.types.db_session import DbSession
2222

2323
logger = logging.getLogger("alembic")
@@ -30,48 +30,6 @@
3030
depends_on = None
3131

3232

33-
hardcoded_initial_price: Dict[ProductPriceType, ProductPricing] = {
34-
ProductPriceType.PROGRAM: ProductPricing(
35-
ProductPriceType.PROGRAM,
36-
ProductPrice(
37-
ProductPriceOptions("0.05", "0.000000977"),
38-
ProductPriceOptions("200", "0.011")
39-
),
40-
ProductComputeUnit(1, 2048, 2048)
41-
),
42-
ProductPriceType.PROGRAM_PERSISTENT: ProductPricing(
43-
ProductPriceType.PROGRAM_PERSISTENT,
44-
ProductPrice(
45-
ProductPriceOptions("0.05", "0.000000977"),
46-
ProductPriceOptions("1000", "0.055")
47-
),
48-
ProductComputeUnit(1, 20480, 2048)
49-
),
50-
ProductPriceType.INSTANCE: ProductPricing(
51-
ProductPriceType.INSTANCE,
52-
ProductPrice(
53-
ProductPriceOptions("0.05", "0.000000977"),
54-
ProductPriceOptions("1000", "0.055")
55-
),
56-
ProductComputeUnit(1, 20480, 2048)
57-
),
58-
ProductPriceType.INSTANCE_CONFIDENTIAL: ProductPricing(
59-
ProductPriceType.INSTANCE_CONFIDENTIAL,
60-
ProductPrice(
61-
ProductPriceOptions("0.05", "0.000000977"),
62-
ProductPriceOptions("2000", "0.11")
63-
),
64-
ProductComputeUnit(1, 20480, 2048)
65-
),
66-
ProductPriceType.STORAGE: ProductPricing(
67-
ProductPriceType.STORAGE,
68-
ProductPrice(
69-
ProductPriceOptions("0.333333333"),
70-
)
71-
),
72-
}
73-
74-
7533
def _get_product_instance_type(
7634
content: InstanceContent
7735
) -> ProductPriceType:
@@ -112,12 +70,15 @@ def do_calculate_costs() -> None:
11270

11371
logger.debug("INIT: CALCULATE COSTS FOR: %r", msg_item_hashes)
11472

73+
# Build the initial pricing model from DEFAULT_PRICE_AGGREGATE
74+
initial_pricing_model = build_default_pricing_model()
75+
11576
for item_hash in msg_item_hashes:
11677
message = get_message_by_item_hash(session, item_hash)
11778
if message:
11879
content = message.parsed_content
11980
type = _get_product_price_type(content)
120-
pricing = hardcoded_initial_price[type]
81+
pricing = initial_pricing_model[type]
12182
costs = get_detailed_costs(session, content, message.item_hash, pricing)
12283

12384
if len(costs) > 0:

deployment/scripts/auth_token.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Authentication token generation script for PyAleph.
4+
5+
This script provides functionality to:
6+
1. Generate new ECDSA key pairs
7+
2. Create authentication tokens using private keys
8+
"""
9+
10+
import argparse
11+
import getpass
12+
import sys
13+
from pathlib import Path
14+
15+
# Add src directory to Python path for imports
16+
sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'src'))
17+
18+
from aleph.toolkit.ecdsa import generate_key_pair, create_auth_token, generate_key_pair_from_private_key
19+
20+
21+
def generate_new_keypair():
22+
"""Generate and display a new ECDSA key pair."""
23+
print("Generating new ECDSA key pair...")
24+
private_key, public_key = generate_key_pair()
25+
26+
print(f"\nPrivate Key (keep secure!): {private_key}")
27+
print(f"Public Key: {public_key}")
28+
29+
return private_key, public_key
30+
31+
32+
def read_keypair():
33+
"""Create an authentication token from a private key."""
34+
print("Enter your private key in hex format:")
35+
private_key_hex = getpass.getpass("Private Key: ").strip()
36+
37+
if not private_key_hex:
38+
print("Error: Private key cannot be empty")
39+
return None
40+
41+
private_key, public_key = generate_key_pair_from_private_key(private_key_hex)
42+
43+
# print(f"\nPrivate Key (keep secure!): {private_key}")
44+
# print(f"Public Key: {public_key}")
45+
46+
return private_key, public_key
47+
48+
49+
def main():
50+
parser = argparse.ArgumentParser(
51+
description="PyAleph Authentication Token Generator",
52+
formatter_class=argparse.RawDescriptionHelpFormatter,
53+
epilog="""
54+
Examples:
55+
# Generate a new key pair
56+
python generate_auth_token.py --generate-keys
57+
58+
# Create token from existing private key
59+
python generate_auth_token.py --create-token
60+
61+
# Do both operations
62+
python generate_auth_token.py --generate-keys --create-token
63+
"""
64+
)
65+
66+
parser.add_argument(
67+
'--generate-keys', '-g',
68+
action='store_true',
69+
help='Generate a new ECDSA key pair'
70+
)
71+
72+
parser.add_argument(
73+
'--create-token', '-t',
74+
action='store_true',
75+
help='Create an authentication token from a private key'
76+
)
77+
78+
args = parser.parse_args()
79+
80+
if not args.generate_keys and not args.create_token:
81+
parser.print_help()
82+
return
83+
84+
private_key = None
85+
86+
if args.generate_keys:
87+
private_key, _ = generate_new_keypair()
88+
89+
if args.create_token:
90+
if private_key is None:
91+
private_key, _ = read_keypair()
92+
93+
try:
94+
token = create_auth_token(private_key)
95+
print(f"\nGenerated Token: {token}")
96+
except Exception as e:
97+
print(f"Error creating token: {e}")
98+
99+
100+
if __name__ == "__main__":
101+
main()

src/aleph/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ def get_defaults():
1818
"reference_node_url": None,
1919
# URL of the aleph.im cross-chain indexer.
2020
"indexer_url": "https://multichain.api.aleph.cloud",
21+
"auth": {
22+
# Public key for verifying authentication tokens (compressed secp256k1 format)
23+
"public_key": "0209fe82e08ec3c5c3ee4904fa147a11d49c7130579066c8a452d279d539959389",
24+
# Maximum token age in seconds (default: 5 minutes)
25+
"max_token_age": 300,
26+
},
2127
"balances": {
2228
# Addresses allowed to publish balance updates.
2329
"addresses": [
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"""
2+
Utility functions for pricing model creation and management.
3+
"""
4+
5+
import datetime as dt
6+
from typing import Dict, List, Union
7+
8+
from aleph.db.accessors.aggregates import (
9+
get_aggregate_elements,
10+
merge_aggregate_elements,
11+
)
12+
from aleph.db.models import AggregateElementDb
13+
from aleph.toolkit.constants import (
14+
DEFAULT_PRICE_AGGREGATE,
15+
PRICE_AGGREGATE_KEY,
16+
PRICE_AGGREGATE_OWNER,
17+
)
18+
from aleph.types.cost import ProductPriceType, ProductPricing
19+
from aleph.types.db_session import DbSession
20+
21+
22+
def build_pricing_model_from_aggregate(
23+
aggregate_content: Dict[Union[ProductPriceType, str], dict]
24+
) -> Dict[ProductPriceType, ProductPricing]:
25+
"""
26+
Build a complete pricing model from an aggregate content dictionary.
27+
28+
This function converts the DEFAULT_PRICE_AGGREGATE format or any pricing aggregate
29+
content into a dictionary of ProductPricing objects that can be used by the cost
30+
calculation functions.
31+
32+
Args:
33+
aggregate_content: Dictionary containing pricing information with ProductPriceType as keys
34+
35+
Returns:
36+
Dictionary mapping ProductPriceType to ProductPricing objects
37+
"""
38+
pricing_model: Dict[ProductPriceType, ProductPricing] = {}
39+
40+
for price_type, pricing_data in aggregate_content.items():
41+
try:
42+
price_type = ProductPriceType(price_type)
43+
pricing_model[price_type] = ProductPricing.from_aggregate(
44+
price_type, aggregate_content
45+
)
46+
except (KeyError, ValueError) as e:
47+
# Log the error but continue processing other price types
48+
import logging
49+
50+
logger = logging.getLogger(__name__)
51+
logger.warning(f"Failed to parse pricing for {price_type}: {e}")
52+
53+
return pricing_model
54+
55+
56+
def build_default_pricing_model() -> Dict[ProductPriceType, ProductPricing]:
57+
"""
58+
Build the default pricing model from DEFAULT_PRICE_AGGREGATE constant.
59+
60+
Returns:
61+
Dictionary mapping ProductPriceType to ProductPricing objects
62+
"""
63+
return build_pricing_model_from_aggregate(DEFAULT_PRICE_AGGREGATE)
64+
65+
66+
def get_pricing_aggregate_history(session: DbSession) -> List[AggregateElementDb]:
67+
"""
68+
Get all pricing aggregate updates in chronological order.
69+
70+
Args:
71+
session: Database session
72+
73+
Returns:
74+
List of AggregateElementDb objects ordered by creation_datetime
75+
"""
76+
aggregate_elements = get_aggregate_elements(
77+
session=session, owner=PRICE_AGGREGATE_OWNER, key=PRICE_AGGREGATE_KEY
78+
)
79+
return list(aggregate_elements)
80+
81+
82+
def get_pricing_timeline(
83+
session: DbSession,
84+
) -> List[tuple[dt.datetime, Dict[ProductPriceType, ProductPricing]]]:
85+
"""
86+
Get the complete pricing timeline with timestamps and pricing models.
87+
88+
This function returns a chronologically ordered list of pricing changes,
89+
useful for processing messages in chronological order and applying the
90+
correct pricing at each point in time.
91+
92+
This properly merges aggregate elements up to each point in time to create
93+
the cumulative pricing state, similar to how _update_aggregate works.
94+
95+
Args:
96+
session: Database session
97+
98+
Returns:
99+
List of tuples containing (timestamp, pricing_model)
100+
"""
101+
pricing_elements = get_pricing_aggregate_history(session)
102+
103+
timeline = []
104+
105+
# Add default pricing as the initial state
106+
timeline.append(
107+
(dt.datetime.min.replace(tzinfo=dt.timezone.utc), build_default_pricing_model())
108+
)
109+
110+
# Build cumulative pricing models by merging elements up to each timestamp
111+
elements_so_far = []
112+
for element in pricing_elements:
113+
elements_so_far.append(element)
114+
115+
# Merge all elements up to this point to get the cumulative state
116+
merged_content = merge_aggregate_elements(elements_so_far)
117+
118+
# Build pricing model from the merged content
119+
pricing_model = build_pricing_model_from_aggregate(merged_content)
120+
timeline.append((element.creation_datetime, pricing_model))
121+
122+
return timeline

src/aleph/toolkit/constants.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
from typing import Dict, Union
2+
3+
from aleph.types.cost import ProductPriceType
4+
15
KiB = 1024
26
MiB = 1024 * 1024
37
GiB = 1024 * 1024 * 1024
@@ -8,8 +12,8 @@
812
PRICE_AGGREGATE_OWNER = "0xFba561a84A537fCaa567bb7A2257e7142701ae2A"
913
PRICE_AGGREGATE_KEY = "pricing"
1014
PRICE_PRECISION = 18
11-
DEFAULT_PRICE_AGGREGATE = {
12-
"program": {
15+
DEFAULT_PRICE_AGGREGATE: Dict[Union[ProductPriceType, str], dict] = {
16+
ProductPriceType.PROGRAM: {
1317
"price": {
1418
"storage": {"payg": "0.000000977", "holding": "0.05"},
1519
"compute_unit": {"payg": "0.011", "holding": "200"},
@@ -28,8 +32,8 @@
2832
"memory_mib": 2048,
2933
},
3034
},
31-
"storage": {"price": {"storage": {"holding": "0.333333333"}}},
32-
"instance": {
35+
ProductPriceType.STORAGE: {"price": {"storage": {"holding": "0.333333333"}}},
36+
ProductPriceType.INSTANCE: {
3337
"price": {
3438
"storage": {"payg": "0.000000977", "holding": "0.05"},
3539
"compute_unit": {"payg": "0.055", "holding": "1000"},
@@ -48,8 +52,10 @@
4852
"memory_mib": 2048,
4953
},
5054
},
51-
"web3_hosting": {"price": {"fixed": 50, "storage": {"holding": "0.333333333"}}},
52-
"program_persistent": {
55+
ProductPriceType.WEB3_HOSTING: {
56+
"price": {"fixed": 50, "storage": {"holding": "0.333333333"}}
57+
},
58+
ProductPriceType.PROGRAM_PERSISTENT: {
5359
"price": {
5460
"storage": {"payg": "0.000000977", "holding": "0.05"},
5561
"compute_unit": {"payg": "0.055", "holding": "1000"},
@@ -68,7 +74,7 @@
6874
"memory_mib": 2048,
6975
},
7076
},
71-
"instance_gpu_premium": {
77+
ProductPriceType.INSTANCE_GPU_PREMIUM: {
7278
"price": {
7379
"storage": {"payg": "0.000000977"},
7480
"compute_unit": {"payg": "0.56"},
@@ -93,7 +99,7 @@
9399
"memory_mib": 6144,
94100
},
95101
},
96-
"instance_confidential": {
102+
ProductPriceType.INSTANCE_CONFIDENTIAL: {
97103
"price": {
98104
"storage": {"payg": "0.000000977", "holding": "0.05"},
99105
"compute_unit": {"payg": "0.11", "holding": "2000"},
@@ -112,7 +118,7 @@
112118
"memory_mib": 2048,
113119
},
114120
},
115-
"instance_gpu_standard": {
121+
ProductPriceType.INSTANCE_GPU_STANDARD: {
116122
"price": {
117123
"storage": {"payg": "0.000000977"},
118124
"compute_unit": {"payg": "0.28"},

0 commit comments

Comments
 (0)