Select Git revision
request_data_graphql.py
-
Vincent Texier authored
except for the request_data_async.py example
Vincent Texier authoredexcept for the request_data_async.py example
block.py 19.29 KiB
import base64
import hashlib
import re
from typing import TypeVar, Type, Optional, List, Sequence
from .block_uid import BlockUID
from .certification import Certification
from .revocation import Revocation
from .identity import Identity
from .document import Document, MalformedDocumentError
from .membership import Membership
from .transaction import Transaction
from ..constants import PUBKEY_REGEX, BLOCK_HASH_REGEX
# required to type hint cls in classmethod
BlockType = TypeVar('BlockType', bound='Block')
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("Issuer: ({pubkey_regex})\n".format(pubkey_regex=PUBKEY_REGEX))
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("PreviousHash: ({block_hash_regex})\n".format(block_hash_regex=BLOCK_HASH_REGEX))
re_previousissuer = re.compile("PreviousIssuer: ({pubkey_regex})\n".format(pubkey_regex=PUBKEY_REGEX))
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]+)\n")
re_parameters_v10 = 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("({pubkey_regex})\n".format(pubkey_regex=PUBKEY_REGEX))
re_certifications = re.compile("Certifications:\n")
re_transactions = re.compile("Transactions:\n")
re_hash = re.compile("InnerHash: ({block_hash_regex})\n".format(block_hash_regex=BLOCK_HASH_REGEX))
re_noonce = 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,
'Noonce': re_noonce,
}
}
def __init__(self,
version: int,
currency: str,
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,
noonce: int,
signature: str
) -> None:
"""
Constructor
:param version: duniter protocol version
:param currency: the block currency
: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 hah
:param noonce: the noonce value of the block
:param signature: the block signature
"""
super().__init__(version, currency, [signature])
documents_versions = 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(
"Block version is too low : {0} < {1}".format(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.noonce = noonce
@property
def blockUID(self) -> BlockUID:
return BlockUID(self.number, self.proof_of_work())
@classmethod
def from_signed_raw(cls: Type[BlockType], signed_raw: str) -> BlockType:
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
if version >= 3 or ud:
unit_base = int(Block.parse_field("UnitBase", lines[n]))
n += 1
issuer = Block.parse_field("Issuer", lines[n])
n += 1
if version >= 3:
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
else:
issuers_frame = None
issuers_frame_var = None
different_issuers_count = None
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:
if version >= 10:
params_match = Block.re_parameters_v10.match(lines[n])
if params_match is None:
raise MalformedDocumentError("Parameters")
parameters = params_match.groups()
else:
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")
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(version, currency, lines[n])
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(version, currency, "IN", lines[n])
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(version, currency, "IN", lines[n])
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(version, currency, "OUT", lines[n])
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(version, currency, lines[n])
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(version, currency,
prev_hash, lines[n])
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("Compact transaction ({0})".format(lines[n]))
tx_version = int(header_data.group(1))
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))
if version > 2:
if tx_version <= 2:
raise MalformedDocumentError("TX document is using wrong version")
sup_lines = 2
else:
sup_lines = 1
tx_max = n + sup_lines + issuers_num * 2 + inputs_num + unlocks_num + outputs_num + has_comment
for i in range(n, tx_max):
tx_lines += lines[n]
n += 1
transaction = Transaction.from_compact(currency, tx_lines)
transactions.append(transaction)
inner_hash = Block.parse_field("InnerHash", lines[n])
n += 1
noonce = int(Block.parse_field("Noonce", lines[n]))
n += 1
signature = Block.parse_field("Signature", lines[n])
return cls(version, currency, 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, noonce, signature)
def raw(self) -> str:
doc = """Version: {version}
Type: Block
Currency: {currency}
Number: {number}
PoWMin: {powmin}
Time: {time}
MedianTime: {mediantime}
""".format(version=self.version,
currency=self.currency,
number=self.number,
powmin=self.powmin,
time=self.time,
mediantime=self.mediantime)
if self.ud:
doc += "UniversalDividend: {0}\n".format(self.ud)
if self.version >= 3 or self.ud:
doc += "UnitBase: {0}\n".format(self.unit_base)
doc += "Issuer: {0}\n".format(self.issuer)
if self.version >= 3:
doc += """IssuersFrame: {0}
IssuersFrameVar: {1}
DifferentIssuersCount: {2}
""".format(self.issuers_frame, self.issuers_frame_var, self.different_issuers_count)
if self.number == 0 and self.parameters is not None:
str_params = ":".join([str(p) for p in self.parameters])
doc += "Parameters: {0}\n".format(str_params)
else:
doc += "PreviousHash: {0}\n\
PreviousIssuer: {1}\n".format(self.prev_hash, self.prev_issuer)
doc += "MembersCount: {0}\n".format(self.members_count)
doc += "Identities:\n"
for identity in self.identities:
doc += "{0}\n".format(identity.inline())
doc += "Joiners:\n"
for joiner in self.joiners:
doc += "{0}\n".format(joiner.inline())
doc += "Actives:\n"
for active in self.actives:
doc += "{0}\n".format(active.inline())
doc += "Leavers:\n"
for leaver in self.leavers:
doc += "{0}\n".format(leaver.inline())
doc += "Revoked:\n"
for revokation in self.revoked:
doc += "{0}\n".format(revokation.inline())
doc += "Excluded:\n"
for exclude in self.excluded:
doc += "{0}\n".format(exclude)
doc += "Certifications:\n"
for cert in self.certifications:
doc += "{0}\n".format(cert.inline())
doc += "Transactions:\n"
for transaction in self.transactions:
doc += "{0}".format(transaction.compact())
doc += "InnerHash: {0}\n".format(self.inner_hash)
doc += "Nonce: {0}\n".format(self.noonce)
return doc
def proof_of_work(self) -> str:
doc_str = """InnerHash: {inner_hash}
Nonce: {nonce}
{signature}
""".format(inner_hash=self.inner_hash, nonce=self.noonce, signature=self.signatures[0])
return hashlib.sha256(doc_str.encode('ascii')).hexdigest().upper()
def computed_inner_hash(self) -> str:
doc = self.signed_raw()
inner_doc = '\n'.join(doc.split('\n')[:-2]) + '\n'
return hashlib.sha256(inner_doc.encode("ascii")).hexdigest().upper()
def sign(self, keys):
"""
Sign the current document.
Warning : current signatures will be replaced with the new ones.
"""
key = keys[0]
signed = self.raw()[-2:]
signing = base64.b64encode(key.signature(bytes(signed, 'ascii')))
self.signatures = [signing.decode("ascii")]
def __eq__(self, other: object) -> bool:
if not isinstance(other, Block):
return NotImplemented
return self.blockUID == other.blockUID
def __lt__(self, other: object) -> bool:
if not isinstance(other, Block):
return False
return self.blockUID < other.blockUID
def __gt__(self, other: object) -> bool:
if not isinstance(other, Block):
return False
return self.blockUID > other.blockUID
def __le__(self, other: object) -> bool:
if not isinstance(other, Block):
return False
return self.blockUID <= other.blockUID
def __ge__(self, other: object) -> bool:
if not isinstance(other, Block):
return False
return self.blockUID >= other.blockUID