Astroport.ONE/venv/lib/python3.11/site-packages/duniterpy/documents/block.py

713 lines
24 KiB
Python
Raw Normal View History

2024-03-01 16:15:45 +01:00
# Copyright 2014-2024 Vincent Texier <vit@free.fr>
#
# DuniterPy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# DuniterPy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import base64
import hashlib
import re
from typing import List, Optional, Sequence, Type, TypeVar
from ..constants import BLOCK_HASH_REGEX, G1_CURRENCY_CODENAME, PUBKEY_REGEX
# required to type hint cls in classmethod
from ..key import SigningKey, VerifyingKey
from .block_id import BlockID
from .certification import Certification
from .document import Document, MalformedDocumentError
from .identity import Identity
from .membership import Membership
from .revocation import Revocation
from .transaction import Transaction
BlockType = TypeVar("BlockType", bound="Block")
VERSION = 12
class SignatureException(Exception):
pass
class Block(Document):
"""
The class Block handles Block documents.
.. note:: A block document is specified by the following format :
| Version: VERSION
| Type: Block
| Currency: CURRENCY
| Nonce: NONCE
| Number: BLOCK_NUMBER
| PoWMin: NUMBER_OF_ZEROS
| Time: GENERATED_ON
| MedianTime: MEDIAN_DATE
| UniversalDividend: DIVIDEND_AMOUNT
| Issuer: ISSUER_KEY
| PreviousHash: PREVIOUS_HASH
| PreviousIssuer: PREVIOUS_ISSUER_KEY
| Parameters: PARAMETERS
| MembersCount: WOT_MEM_COUNT
| Identities:
| PUBLIC_KEY:SIGNATURE:TIMESTAMP:USER_ID
| ...
| Joiners:
| PUBLIC_KEY:SIGNATURE:NUMBER:HASH:TIMESTAMP:USER_ID
| ...
| Actives:
| PUBLIC_KEY:SIGNATURE:NUMBER:HASH:TIMESTAMP:USER_ID
| ...
| Leavers:
| PUBLIC_KEY:SIGNATURE:NUMBER:HASH:TIMESTAMP:USER_ID
| ...
| Excluded:
| PUBLIC_KEY
| ...
| Certifications:
| PUBKEY_FROM:PUBKEY_TO:BLOCK_NUMBER:SIGNATURE
| ...
| Transactions:
| COMPACT_TRANSACTION
| ...
| BOTTOM_SIGNATURE
"""
re_type = re.compile("Type: (Block)\n")
re_number = re.compile("Number: ([0-9]+)\n")
re_powmin = re.compile("PoWMin: ([0-9]+)\n")
re_time = re.compile("Time: ([0-9]+)\n")
re_mediantime = re.compile("MedianTime: ([0-9]+)\n")
re_universaldividend = re.compile("UniversalDividend: ([0-9]+)\n")
re_unitbase = re.compile("UnitBase: ([0-9]+)\n")
re_issuer = re.compile(f"Issuer: ({PUBKEY_REGEX})\n")
re_issuers_frame = re.compile("IssuersFrame: ([0-9]+)\n")
re_issuers_frame_var = re.compile("IssuersFrameVar: (0|-?[1-9]\\d{0,18})\n")
re_different_issuers_count = re.compile("DifferentIssuersCount: ([0-9]+)\n")
re_previoushash = re.compile(f"PreviousHash: ({BLOCK_HASH_REGEX})\n")
re_previousissuer = re.compile(f"PreviousIssuer: ({PUBKEY_REGEX})\n")
re_parameters = re.compile(
"Parameters: ([0-9]+\\.[0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+):\
([0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+\\.[0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+\\.[0-9]+):\
([0-9]+):([0-9]+):([0-9]+)\n"
)
re_memberscount = re.compile("MembersCount: ([0-9]+)\n")
re_identities = re.compile("Identities:\n")
re_joiners = re.compile("Joiners:\n")
re_actives = re.compile("Actives:\n")
re_leavers = re.compile("Leavers:\n")
re_revoked = re.compile("Revoked:\n")
re_excluded = re.compile("Excluded:\n")
re_exclusion = re.compile(f"({PUBKEY_REGEX})\n")
re_certifications = re.compile("Certifications:\n")
re_transactions = re.compile("Transactions:\n")
re_hash = re.compile(f"InnerHash: ({BLOCK_HASH_REGEX})\n")
re_nonce = re.compile("Nonce: ([0-9]+)\n")
fields_parsers = {
**Document.fields_parsers,
**{
"Type": re_type,
"Number": re_number,
"PoWMin": re_powmin,
"Time": re_time,
"MedianTime": re_mediantime,
"UD": re_universaldividend,
"UnitBase": re_unitbase,
"Issuer": re_issuer,
"IssuersFrame": re_issuers_frame,
"IssuersFrameVar": re_issuers_frame_var,
"DifferentIssuersCount": re_different_issuers_count,
"PreviousIssuer": re_previousissuer,
"PreviousHash": re_previoushash,
"Parameters": re_parameters,
"MembersCount": re_memberscount,
"Identities": re_identities,
"Joiners": re_joiners,
"Actives": re_actives,
"Leavers": re_leavers,
"Revoked": re_revoked,
"Excluded": re_excluded,
"Certifications": re_certifications,
"Transactions": re_transactions,
"InnerHash": re_hash,
"Nonce": re_nonce,
},
}
def __init__(
self,
number: int,
powmin: int,
time: int,
mediantime: int,
ud: Optional[int],
unit_base: int,
issuer: str,
issuers_frame: int,
issuers_frame_var: int,
different_issuers_count: int,
prev_hash: Optional[str],
prev_issuer: Optional[str],
parameters: Optional[Sequence[str]],
members_count: int,
identities: List[Identity],
joiners: List[Membership],
actives: List[Membership],
leavers: List[Membership],
revokations: List[Revocation],
excluded: List[str],
certifications: List[Certification],
transactions: List[Transaction],
inner_hash: str,
nonce: int,
signing_key: Optional[SigningKey] = None,
version: int = VERSION,
currency: str = G1_CURRENCY_CODENAME,
) -> None:
"""
Constructor
:param number: the number of the block
:param powmin: the powmin value of this block
:param time: the timestamp of this block
:param mediantime: the timestamp of the median time of this block
:param ud: the dividend amount, or None if no dividend present in this block
:param unit_base: the unit_base of the dividend, or None if no dividend present in this block
:param issuer: the pubkey of the issuer of the block
:param issuers_frame:
:param issuers_frame_var:
:param different_issuers_count: the count of issuers
:param prev_hash: the previous block hash
:param prev_issuer: the previous block issuer
:param parameters: the parameters of the currency. Should only be present in block 0.
:param members_count: the number of members found in this block
:param identities: the self certifications declared in this block
:param joiners: the joiners memberships via "IN" documents
:param actives: renewed memberships via "IN" documents
:param leavers: the leavers memberships via "OUT" documents
:param revokations: revokations
:param excluded: members excluded because of missing certifications
:param certifications: certifications documents
:param transactions: transactions documents
:param inner_hash: the block hash
:param nonce: the nonce value of the block
:param signing_key: SigningKey instance to sign the document (default=None)
:param version: Document version (default=block.VERSION)
:param currency: Currency codename (default=constants.CURRENCY_CODENAME_G1)
"""
super().__init__(version, currency)
documents_versions = max( # pylint: disable=nested-min-max
max([1] + [i.version for i in identities]),
max([1] + [m.version for m in actives + leavers + joiners]),
max([1] + [r.version for r in revokations]),
max([1] + [c.version for c in certifications]),
max([1] + [t.version for t in transactions]),
)
if self.version < documents_versions:
raise MalformedDocumentError(
f"Block version is too low: {self.version} < {documents_versions}"
)
self.number = number
self.powmin = powmin
self.time = time
self.mediantime = mediantime
self.ud = ud
self.unit_base = unit_base
self.issuer = issuer
self.issuers_frame = issuers_frame
self.issuers_frame_var = issuers_frame_var
self.different_issuers_count = different_issuers_count
self.prev_hash = prev_hash
self.prev_issuer = prev_issuer
self.parameters = parameters
self.members_count = members_count
self.identities = identities
self.joiners = joiners
self.actives = actives
self.leavers = leavers
self.revoked = revokations
self.excluded = excluded
self.certifications = certifications
self.transactions = transactions
self.inner_hash = inner_hash
self.nonce = nonce
if signing_key is not None:
self.sign(signing_key)
@property
def block_id(self) -> BlockID:
"""
Return Block UID
:return:
"""
return BlockID(self.number, self.proof_of_work())
@classmethod
def from_parsed_json(cls: Type[BlockType], parsed_json_block: dict) -> BlockType:
"""
Return a Block instance from the python dict produced when parsing json
:param parsed_json_block: Block as a Python dict
:return:
"""
b = parsed_json_block # alias for readability
version = b["version"]
currency = b["currency"]
arguments = { # arguments used to create block
"currency": b["currency"],
"number": b["number"],
"powmin": b["powMin"],
"time": b["time"],
"mediantime": b["medianTime"],
"ud": b["dividend"],
"unit_base": b["unitbase"],
"issuer": b["issuer"],
"issuers_frame": b["issuersFrame"],
"issuers_frame_var": b["issuersFrameVar"],
"different_issuers_count": b["issuersCount"],
"prev_hash": b["previousHash"],
"prev_issuer": b["previousIssuer"],
"parameters": b["parameters"],
"members_count": b["membersCount"],
"identities": b["identities"],
"joiners": b["joiners"],
"actives": b["actives"],
"leavers": b["leavers"],
"revokations": b["revoked"],
"excluded": b["excluded"],
"certifications": b["certifications"],
"transactions": b["transactions"],
"inner_hash": b["inner_hash"],
"nonce": b["nonce"],
"version": b["version"],
}
# parameters: Optional[Sequence[str]]
if arguments["parameters"]:
arguments["parameters"] = arguments["parameters"].split(":")
# identities: List[Identity]
arguments["identities"] = [
Identity.from_inline(f"{i}\n", version=version, currency=currency)
for i in arguments["identities"]
]
# joiners: List[Membership]
arguments["joiners"] = [
Membership.from_inline(
f"{i}\n", version=version, currency=currency, membership_type="IN"
)
for i in arguments["joiners"]
]
# actives: List[Membership]
arguments["actives"] = [
Membership.from_inline(
f"{i}\n", version=version, currency=currency, membership_type="IN"
)
for i in arguments["actives"]
]
# leavers: List[Membership]
arguments["leavers"] = [
Membership.from_inline(
f"{i}\n", version=version, currency=currency, membership_type="OUT"
)
for i in arguments["leavers"]
]
# revokations: List[Revocation]
arguments["revokations"] = [
Revocation.from_inline(f"{i}\n", version=version, currency=currency)
for i in arguments["revokations"]
]
# certifications: List[Certification]
arguments["certifications"] = [
Certification.from_inline(
arguments["inner_hash"], f"{i}\n", version=version, currency=currency
)
for i in arguments["certifications"]
]
# transactions: List[Transaction]
arguments["transactions"] = [
Transaction.from_bma_history(i, currency=currency)
for i in arguments["transactions"]
]
block = cls(**arguments)
# return the block with signature
block.signature = b["signature"]
return block
@classmethod
def from_signed_raw(cls: Type[BlockType], signed_raw: str) -> BlockType:
"""
Create Block instance from signed raw format string
:param signed_raw: Signed raw format string
:return:
"""
lines = signed_raw.splitlines(True)
n = 0
version = int(Block.parse_field("Version", lines[n]))
n += 1
Block.parse_field("Type", lines[n])
n += 1
currency = Block.parse_field("Currency", lines[n])
n += 1
number = int(Block.parse_field("Number", lines[n]))
n += 1
powmin = int(Block.parse_field("PoWMin", lines[n]))
n += 1
time = int(Block.parse_field("Time", lines[n]))
n += 1
mediantime = int(Block.parse_field("MedianTime", lines[n]))
n += 1
ud_match = Block.re_universaldividend.match(lines[n])
ud = None
unit_base = 0
if ud_match is not None:
ud = int(Block.parse_field("UD", lines[n]))
n += 1
unit_base = int(Block.parse_field("UnitBase", lines[n]))
n += 1
issuer = Block.parse_field("Issuer", lines[n])
n += 1
issuers_frame = Block.parse_field("IssuersFrame", lines[n])
n += 1
issuers_frame_var = Block.parse_field("IssuersFrameVar", lines[n])
n += 1
different_issuers_count = Block.parse_field("DifferentIssuersCount", lines[n])
n += 1
prev_hash = None
prev_issuer = None
if number > 0:
prev_hash = str(Block.parse_field("PreviousHash", lines[n]))
n += 1
prev_issuer = str(Block.parse_field("PreviousIssuer", lines[n]))
n += 1
parameters = None
if number == 0:
try:
params_match = Block.re_parameters.match(lines[n])
if params_match is None:
raise MalformedDocumentError("Parameters")
parameters = params_match.groups()
n += 1
except AttributeError:
raise MalformedDocumentError("Parameters") from AttributeError
members_count = int(Block.parse_field("MembersCount", lines[n]))
n += 1
identities = []
joiners = []
actives = []
leavers = []
revoked = []
excluded = []
certifications = []
transactions = []
if Block.re_identities.match(lines[n]) is not None:
n += 1
while Block.re_joiners.match(lines[n]) is None:
selfcert = Identity.from_inline(
lines[n], version=version, currency=currency
)
identities.append(selfcert)
n += 1
if Block.re_joiners.match(lines[n]):
n += 1
while Block.re_actives.match(lines[n]) is None:
membership = Membership.from_inline(
lines[n], version=version, currency=currency, membership_type="IN"
)
joiners.append(membership)
n += 1
if Block.re_actives.match(lines[n]):
n += 1
while Block.re_leavers.match(lines[n]) is None:
membership = Membership.from_inline(
lines[n], version=version, currency=currency, membership_type="IN"
)
actives.append(membership)
n += 1
if Block.re_leavers.match(lines[n]):
n += 1
while Block.re_revoked.match(lines[n]) is None:
membership = Membership.from_inline(
lines[n], version=version, currency=currency, membership_type="OUT"
)
leavers.append(membership)
n += 1
if Block.re_revoked.match(lines[n]):
n += 1
while Block.re_excluded.match(lines[n]) is None:
revokation = Revocation.from_inline(
lines[n], version=version, currency=currency
)
revoked.append(revokation)
n += 1
if Block.re_excluded.match(lines[n]):
n += 1
while Block.re_certifications.match(lines[n]) is None:
exclusion_match = Block.re_exclusion.match(lines[n])
if exclusion_match is not None:
exclusion = exclusion_match.group(1)
excluded.append(exclusion)
n += 1
if Block.re_certifications.match(lines[n]):
n += 1
while Block.re_transactions.match(lines[n]) is None:
certification = Certification.from_inline(
prev_hash, lines[n], version=version, currency=currency
)
certifications.append(certification)
n += 1
if Block.re_transactions.match(lines[n]):
n += 1
while not Block.re_hash.match(lines[n]):
tx_lines = ""
header_data = Transaction.re_header.match(lines[n])
if header_data is None:
raise MalformedDocumentError(f"Compact transaction ({lines[n]})")
issuers_num = int(header_data.group(2))
inputs_num = int(header_data.group(3))
unlocks_num = int(header_data.group(4))
outputs_num = int(header_data.group(5))
has_comment = int(header_data.group(6))
sup_lines = 2
tx_max = (
n
+ sup_lines
+ issuers_num * 2
+ inputs_num
+ unlocks_num
+ outputs_num
+ has_comment
)
for index in range(n, tx_max):
tx_lines += lines[index]
n += tx_max - n
transaction = Transaction.from_compact(tx_lines, currency=currency)
transactions.append(transaction)
inner_hash = Block.parse_field("InnerHash", lines[n])
n += 1
nonce = int(Block.parse_field("Nonce", lines[n]))
n += 1
signature = Block.parse_field("Signature", lines[n])
block = cls(
number,
powmin,
time,
mediantime,
ud,
unit_base,
issuer,
issuers_frame,
issuers_frame_var,
different_issuers_count,
prev_hash,
prev_issuer,
parameters,
members_count,
identities,
joiners,
actives,
leavers,
revoked,
excluded,
certifications,
transactions,
inner_hash,
nonce,
version=version,
currency=currency,
)
# return block with signature
block.signature = signature
return block
def raw(self) -> str:
"""
Return Block in raw format
:return:
"""
doc = f"Version: {self.version}\n\
Type: Block\n\
Currency: {self.currency}\n\
Number: {self.number}\n\
PoWMin: {self.powmin}\n\
Time: {self.time}\n\
MedianTime: {self.mediantime}\n"
if self.ud:
doc += f"UniversalDividend: {self.ud}\n"
doc += f"UnitBase: {self.unit_base}\n"
doc += f"Issuer: {self.issuer}\n"
doc += f"IssuersFrame: {self.issuers_frame}\n\
IssuersFrameVar: {self.issuers_frame_var}\n\
DifferentIssuersCount: {self.different_issuers_count}\n"
if self.number == 0 and self.parameters is not None:
str_params = ":".join([str(p) for p in self.parameters])
doc += f"Parameters: {str_params}\n"
else:
doc += f"PreviousHash: {self.prev_hash}\n\
PreviousIssuer: {self.prev_issuer}\n"
doc += f"MembersCount: {self.members_count}\n"
doc += "Identities:\n"
for identity in self.identities:
doc += f"{identity.inline()}\n"
doc += "Joiners:\n"
for joiner in self.joiners:
doc += f"{joiner.inline()}\n"
doc += "Actives:\n"
for active in self.actives:
doc += f"{active.inline()}\n"
doc += "Leavers:\n"
for leaver in self.leavers:
doc += f"{leaver.inline()}\n"
doc += "Revoked:\n"
for revokation in self.revoked:
doc += f"{revokation.inline()}\n"
doc += "Excluded:\n"
for exclude in self.excluded:
doc += f"{exclude}\n"
doc += "Certifications:\n"
for cert in self.certifications:
doc += f"{cert.inline()}\n"
doc += "Transactions:\n"
for transaction in self.transactions:
doc += f"{transaction.compact()}"
doc += f"InnerHash: {self.inner_hash}\n"
doc += f"Nonce: {self.nonce}\n"
return doc
def proof_of_work(self) -> str:
"""
Return Proof of Work hash for the Block
:return:
"""
doc_str = (
f"InnerHash: {self.inner_hash}\nNonce: {self.nonce}\n{self.signature}\n"
)
return hashlib.sha256(doc_str.encode("ascii")).hexdigest().upper()
def computed_inner_hash(self) -> str:
"""
Return inner hash of the Block
:return:
"""
doc = self.raw()
inner_doc = "\n".join(doc.split("\n")[:-3]) + "\n"
return hashlib.sha256(inner_doc.encode("ascii")).hexdigest().upper()
def sign(self, key: SigningKey) -> None:
"""
Sign the current document with key
:param key: Libnacl SigningKey instance
:return:
"""
string_to_sign = f"InnerHash: {self.inner_hash}\nNonce: {self.nonce}\n"
signature = base64.b64encode(key.signature(bytes(string_to_sign, "ascii")))
self.signature = signature.decode("ascii")
def __eq__(self, other: object) -> bool:
if not isinstance(other, Block):
return NotImplemented
return self.block_id == other.block_id
def __lt__(self, other: object) -> bool:
if not isinstance(other, Block):
return False
return self.block_id < other.block_id
def __gt__(self, other: object) -> bool:
if not isinstance(other, Block):
return False
return self.block_id > other.block_id
def __le__(self, other: object) -> bool:
if not isinstance(other, Block):
return False
return self.block_id <= other.block_id
def __ge__(self, other: object) -> bool:
if not isinstance(other, Block):
return False
return self.block_id >= other.block_id
def check_signature(self, pubkey: str):
"""
Check if the signature is from pubkey
:param pubkey: Base58 public key
:return:
"""
if self.signature is None:
raise SignatureException("Signature is None, can not verify signature")
content_to_verify = f"InnerHash: {self.inner_hash}\nNonce: {self.nonce}\n"
verifying_key = VerifyingKey(pubkey)
return verifying_key.check_signature(content_to_verify, self.signature)