Skip to content

Commit e9f6c5a

Browse files
committed
add txmass calc endpoint
1 parent be1b61a commit e9f6c5a

File tree

4 files changed

+186
-1
lines changed

4 files changed

+186
-1
lines changed

endpoints/get_transaction_mass.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from fastapi import HTTPException
2+
from pydantic import BaseModel
3+
4+
from endpoints.get_transactions import search_for_transactions, TxSearch
5+
from endpoints.spectred_requests.submit_transaction_request import SubmitTxModel
6+
from helper.mass_calculation_compute import calc_compute_mass
7+
from helper.mass_calculation_storage import calc_storage_mass
8+
from server import app
9+
10+
11+
class TxMass(BaseModel):
12+
mass: int
13+
storage_mass: int
14+
compute_mass: int
15+
16+
17+
def _get_amount_from_tx_output_index(txs, tx_id, output_index: int):
18+
for tx in txs:
19+
if tx["transaction_id"] == tx_id:
20+
for output in tx["outputs"]:
21+
if output.index == output_index:
22+
return output.amount
23+
24+
25+
@app.post(
26+
"/transactions/mass",
27+
response_model=TxMass,
28+
tags=["Spectre transactions"],
29+
response_model_exclude_unset=True,
30+
)
31+
async def calculate_transaction_mass(tx: SubmitTxModel):
32+
"""
33+
This function calculates and returns the mass of a transaction, which is essential for determining the minimum fee. The mass calculation takes into account the storage mass as defined in KIP-0009.
34+
35+
Note: Be aware that if the transaction has a very low output amount or a high number of outputs, the mass can become significantly large.
36+
"""
37+
previous_outpoints = [input.previousOutpoint for input in tx.inputs]
38+
39+
txs = list(
40+
await search_for_transactions(
41+
TxSearch(
42+
transactionIds=list([x.transactionId for x in previous_outpoints])
43+
),
44+
"",
45+
False,
46+
)
47+
)
48+
49+
if len(txs) != len(previous_outpoints):
50+
raise HTTPException(
51+
status_code=404, detail="Previous outpoint(s) not found in database."
52+
)
53+
54+
tx_input_amounts = [
55+
_get_amount_from_tx_output_index(
56+
txs,
57+
previous_outpoint.transactionId,
58+
previous_outpoint.index,
59+
)
60+
for previous_outpoint in previous_outpoints
61+
]
62+
63+
tx_output_amounts = [output.amount for output in tx.outputs]
64+
65+
storage_mass = calc_storage_mass(tx_input_amounts, tx_output_amounts)
66+
compute_mass = calc_compute_mass(tx.dict())
67+
68+
return TxMass(
69+
mass=max(storage_mass, compute_mass),
70+
storage_mass=storage_mass,
71+
compute_mass=compute_mass,
72+
)

helper/mass_calculation_compute.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# encoding: utf-8
2+
3+
MASS_PER_TX_BYTE = 1
4+
MASS_PER_SCRIPT_PUB_KEY_BYTE = 10
5+
MASS_PER_SIG_OP = 1000
6+
7+
MAXIMUM_STANDARD_TRANSACTION_MASS = 100_000
8+
9+
10+
def outpoint_size(tx_input_outpoint):
11+
"""
12+
size for outpoint
13+
"""
14+
size = 0
15+
size += 32 # one HASH_SIZE constant
16+
size += 4 # Index
17+
return size
18+
19+
20+
def tx_output_serialized(tx_output):
21+
"""
22+
size for each output
23+
"""
24+
size = 0
25+
size += 8 # value
26+
size += 2 # scriptpubkey version
27+
size += 8 # length of script pub key
28+
size += len(tx_output["scriptPublicKey"]["scriptPublicKey"]) / 2
29+
return size
30+
31+
32+
def tx_input_serialized(tx_input):
33+
"""
34+
size for each input
35+
"""
36+
size = 0
37+
size += outpoint_size(tx_input) # previous outpoint size
38+
size += 8 # length of signature script
39+
size += len(tx_input["signatureScript"]) / 2
40+
size += 8 # sequence
41+
return size
42+
43+
44+
def tx_serialized_size(tx):
45+
size = 0
46+
size += 2 # 2 bytes for tx version
47+
size += 8 # count of inputs
48+
size += sum([tx_input_serialized(x) for x in tx["inputs"]])
49+
size += 8 # count of outputs
50+
size += sum([tx_output_serialized(x) for x in tx["outputs"]])
51+
size += 8 # lock time
52+
size += 20 # subnetwork id size
53+
size += 8 # gas
54+
size += 32 # hash size payload hash
55+
size += 8 # length of the payload
56+
size += len(tx.get("payload", "")) / 2 # length of payload
57+
58+
return size
59+
60+
61+
def calc_compute_mass(tx):
62+
"""
63+
Calculate mass for a TX-OBJECT
64+
:param tx:
65+
:return:
66+
"""
67+
if tx["subnetworkId"] == "0100000000000000000000000000000000000000":
68+
return 0
69+
70+
# get the size of serialized tx
71+
size = tx_serialized_size(tx)
72+
73+
# calc mass for serialized tx
74+
mass = size * MASS_PER_TX_BYTE
75+
76+
# count ALL outputs (script public key version + script public key)
77+
total_script_public_key_sum = sum(
78+
[2 + (len(x["scriptPublicKey"]["scriptPublicKey"]) / 2) for x in tx["outputs"]]
79+
)
80+
81+
# calc sum
82+
total_script_public_key_mass = (
83+
MASS_PER_SCRIPT_PUB_KEY_BYTE * total_script_public_key_sum
84+
)
85+
86+
# calc mass for all inputs with sigOpCount
87+
total_sigops_mass = MASS_PER_SIG_OP * sum([x["sigOpCount"] for x in tx["inputs"]])
88+
return int(mass + total_script_public_key_mass + total_sigops_mass)

helper/mass_calculation_storage.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# encoding: utf-8
2+
3+
# Storage mass constant
4+
C = 10**12
5+
6+
7+
def _negative_mass(inputs, outputs_num):
8+
"""
9+
Calculates the negative component of the storage mass formula. Note that there is no dependency on output
10+
values but only on their count. The code is designed to maximize precision and to avoid intermediate overflows.
11+
"""
12+
inputs_num = len(inputs)
13+
if outputs_num == 1 or inputs_num == 1 or (outputs_num == 2 and inputs_num == 2):
14+
return sum(C // v for v in inputs)
15+
return inputs_num * (C // (sum(inputs) // inputs_num))
16+
17+
18+
def calc_storage_mass(inputs, outputs):
19+
"""
20+
Calculates the storage mass for the provided input/output collections
21+
"""
22+
N = _negative_mass(inputs, outputs_num=len(outputs))
23+
P = sum(C // o for o in outputs)
24+
return max(P - N, 0)

main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from endpoints.get_hashrate import get_hashrate
2323
from endpoints.get_health import health_state
2424
from endpoints.get_marketcap import get_marketcap
25+
from endpoints.get_transaction_mass import calculate_transaction_mass
2526
from endpoints.get_transactions import get_transaction
2627
from endpoints.get_virtual_chain_blue_score import (
2728
get_virtual_selected_parent_blue_score,
@@ -39,7 +40,7 @@
3940
f"{get_spectred_info}, {get_network}, {get_fee_estimate}, {get_marketcap}, {get_hashrate}, {get_blockreward}"
4041
f"{get_halving} {health_state} {get_transaction}"
4142
f"{get_virtual_selected_parent_blue_score} {get_transactions_for_address}"
42-
f"{submit_a_new_transaction} {get_price}"
43+
f"{submit_a_new_transaction} {calculate_transaction_mass} {get_price}"
4344
)
4445

4546
if os.getenv("VSPC_REQUEST") == "true":

0 commit comments

Comments
 (0)