Onionr/onionr/onionrblockapi.py

606 lines
18 KiB
Python
Raw Normal View History

2018-05-15 06:43:29 +00:00
'''
Onionr - P2P Microblogging Platform & Social network.
This class contains the OnionrBlocks class which is a class for working with Onionr blocks
'''
'''
This program 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.
This program 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 <https://www.gnu.org/licenses/>.
'''
2018-05-16 01:47:58 +00:00
import core as onionrcore, logger
import json, os, datetime
2018-05-15 06:43:29 +00:00
class Block:
def __init__(self, hash = None, core = None):
2018-05-16 02:12:23 +00:00
'''
Initializes Onionr
Inputs:
- hash (str): the hash of the block to be imported, if any
- core (Core/str):
- if (Core): this is the Core instance to be used, don't create a new one
- if (str): treat `core` as the block content, and instead, treat `hash` as the block type
Outputs:
- (Block): the new Block instance
'''
2018-05-16 01:47:58 +00:00
# input from arguments
if (type(hash) == str) and (type(core) == str):
self.btype = hash
self.bcontent = core
self.hash = None
self.core = None
else:
self.btype = ''
self.bcontent = ''
self.hash = hash
self.core = core
2018-05-15 06:43:29 +00:00
2018-05-16 01:47:58 +00:00
# initialize variables
self.valid = True
self.raw = None
self.powHash = None
self.powToken = None
self.signed = False
self.signature = None
self.signedData = None
2018-05-16 03:08:42 +00:00
self.blockFile = None
self.parent = None
2018-05-16 01:47:58 +00:00
self.bheader = {}
self.bmetadata = {}
# handle arguments
2018-05-15 06:43:29 +00:00
if self.getCore() is None:
self.core = onionrcore.Core()
if not self.getHash() is None:
self.update()
# logic
def update(self, data = None, file = None):
'''
Loads data from a block in to the current object.
Inputs:
- data (str):
- if None: will load from file by hash
- else: will load from `data` string
- file (str):
- if None: will load from file specified in this parameter
- else: will load from wherever block is stored by hash
Outputs:
- (bool): indicates whether or not the operation was successful
'''
2018-05-16 01:47:58 +00:00
try:
# import from string
blockdata = data
# import from file
if blockdata is None:
filelocation = file
if filelocation is None:
if self.getHash() is None:
return False
filelocation = 'data/blocks/%s.dat' % self.getHash()
2018-05-19 22:22:35 +00:00
with open(filelocation, 'rb') as f:
blockdata = f.read().decode('utf-8')
2018-05-16 01:47:58 +00:00
self.blockFile = filelocation
else:
self.blockFile = None
# parse block
self.raw = str(blockdata)
self.bheader = json.loads(self.getRaw()[:self.getRaw().index('\n')])
self.bcontent = self.getRaw()[self.getRaw().index('\n') + 1:]
self.bmetadata = json.loads(self.getHeader('meta'))
self.parent = (None if not 'parent' in self.getMetadata() else Block(self.getMetadata('parent')))
2018-05-16 01:47:58 +00:00
self.btype = self.getMetadata('type')
self.powHash = self.getMetadata('powHash')
self.powToken = self.getMetadata('powToken')
self.signed = ('sig' in self.getHeader() and self.getHeader('sig') != '')
self.signature = (None if not self.isSigned() else self.getHeader('sig'))
self.signedData = (None if not self.isSigned() else self.getHeader('meta') + '\n' + self.getContent())
self.date = self.getCore().getBlockDate(self.getHash())
if not self.getDate() is None:
self.date = datetime.datetime.fromtimestamp(self.getDate())
self.valid = True
return True
except Exception as e:
logger.error('Failed to update block data.', error = e, timestamp = False)
self.valid = False
2018-05-15 06:43:29 +00:00
return False
def delete(self):
2018-05-16 02:12:23 +00:00
'''
Deletes the block's file and records, if they exist
Outputs:
- (bool): whether or not the operation was successful
'''
2018-05-16 01:47:58 +00:00
if self.exists():
os.remove(self.getBlockFile())
2018-05-16 02:12:23 +00:00
removeBlock(self.getHash())
2018-05-16 01:47:58 +00:00
return True
2018-05-15 06:43:29 +00:00
return False
2018-05-16 02:12:23 +00:00
def save(self, sign = False, recreate = True):
'''
Saves a block to file and imports it into Onionr
Inputs:
- sign (bool): whether or not to sign the block before saving
- recreate (bool): if the block already exists, whether or not to recreate the block and save under a new hash
Outputs:
- (bool): whether or not the operation was successful
'''
2018-05-16 01:47:58 +00:00
try:
if self.isValid() is True:
2018-05-16 02:12:23 +00:00
if (not self.getBlockFile() is None) and (recreate is True):
2018-05-16 01:47:58 +00:00
with open(self.getBlockFile(), 'wb') as blockFile:
blockFile.write(self.getRaw().encode())
self.update()
else:
self.hash = self.getCore().insertBlock(self.getContent(), header = self.getType(), sign = sign, metadata = self.getMetadata())
2018-05-16 01:47:58 +00:00
self.update()
2018-05-19 22:21:35 +00:00
return self.getHash()
2018-05-16 01:47:58 +00:00
else:
logger.warn('Not writing block; it is invalid.')
except Exception as e:
logger.error('Failed to save block.', error = e, timestamp = False)
return False
2018-05-15 06:43:29 +00:00
# getters
def getHash(self):
2018-05-16 02:12:23 +00:00
'''
Returns the hash of the block if saved to file
Outputs:
- (str): the hash of the block, or None
'''
2018-05-15 06:43:29 +00:00
return self.hash
def getCore(self):
2018-05-16 02:12:23 +00:00
'''
Returns the Core instance being used by the Block
Outputs:
- (Core): the Core instance
'''
2018-05-15 06:43:29 +00:00
return self.core
def getType(self):
2018-05-16 02:12:23 +00:00
'''
Returns the type of the block
Outputs:
- (str): the type of the block
'''
2018-05-15 06:43:29 +00:00
return self.btype
2018-05-16 01:47:58 +00:00
def getRaw(self):
2018-05-16 02:12:23 +00:00
'''
Returns the raw contents of the block, if saved to file
Outputs:
- (str): the raw contents of the block, or None
'''
2018-05-16 01:47:58 +00:00
return str(self.raw)
def getHeader(self, key = None):
2018-05-16 02:12:23 +00:00
'''
Returns the header information
Inputs:
- key (str): only returns the value of the key in the header
Outputs:
- (dict/str): either the whole header as a dict, or one value
'''
2018-05-16 01:47:58 +00:00
if not key is None:
return self.getHeader()[key]
return self.bheader
2018-05-16 01:47:58 +00:00
def getMetadata(self, key = None):
2018-05-16 02:12:23 +00:00
'''
Returns the metadata information
Inputs:
- key (str): only returns the value of the key in the metadata
Outputs:
- (dict/str): either the whole metadata as a dict, or one value
'''
2018-05-16 01:47:58 +00:00
if not key is None:
return self.getMetadata()[key]
return self.bmetadata
2018-05-15 06:43:29 +00:00
def getContent(self):
2018-05-16 02:12:23 +00:00
'''
Returns the contents of the block
Outputs:
- (str): the contents of the block
'''
2018-05-16 01:47:58 +00:00
return str(self.bcontent)
def getParent(self):
'''
Returns the Block's parent Block, or None
Outputs:
- (Block): the Block's parent
'''
return self.parent
2018-05-15 06:43:29 +00:00
def getDate(self):
2018-05-16 02:12:23 +00:00
'''
Returns the date that the block was received, if loaded from file
Outputs:
- (datetime): the date that the block was received
'''
2018-05-15 06:43:29 +00:00
return self.date
2018-05-16 01:47:58 +00:00
def getBlockFile(self):
2018-05-16 02:12:23 +00:00
'''
Returns the location of the block file if it is saved
Outputs:
- (str): the location of the block file, or None
'''
2018-05-16 01:47:58 +00:00
return self.blockFile
2018-05-15 06:43:29 +00:00
def isValid(self):
2018-05-16 02:12:23 +00:00
'''
Checks if the block is valid
Outputs:
- (bool): whether or not the block is valid
'''
2018-05-15 06:43:29 +00:00
return self.valid
def isSigned(self):
2018-05-16 02:12:23 +00:00
'''
Checks if the block was signed
Outputs:
- (bool): whether or not the block is signed
'''
2018-05-15 06:43:29 +00:00
return self.signed
2018-05-16 01:47:58 +00:00
def getSignature(self):
2018-05-16 02:12:23 +00:00
'''
Returns the base64-encoded signature
Outputs:
- (str): the signature, or None
'''
2018-05-16 01:47:58 +00:00
return self.signature
def getSignedData(self):
2018-05-16 02:12:23 +00:00
'''
Returns the data that was signed
Outputs:
- (str): the data that was signed, or None
'''
2018-05-16 01:47:58 +00:00
return self.signedData
def isSigner(self, signer, encodedData = True):
2018-05-16 02:12:23 +00:00
'''
Checks if the block was signed by the signer inputted
Inputs:
- signer (str): the public key of the signer to check against
- encodedData (bool): whether or not the `signer` argument is base64 encoded
Outputs:
- (bool): whether or not the signer of the block is the signer inputted
'''
2018-05-16 01:47:58 +00:00
try:
if (not self.isSigned()) or (not self.getCore()._utils.validatePubKey(signer)):
return False
return bool(self.getCore()._crypto.edVerify(self.getSignedData(), signer, self.getSignature(), encodedData = encodedData))
except:
return False
2018-05-15 06:43:29 +00:00
# setters
def setType(self, btype):
2018-05-16 02:12:23 +00:00
'''
Sets the type of the block
Inputs:
- btype (str): the type of block to be set to
Outputs:
- (Block): the Block instance
2018-05-16 02:12:23 +00:00
'''
2018-05-15 06:43:29 +00:00
self.btype = btype
return self
def setMetadata(self, key, val):
'''
Sets a custom metadata value
Metadata should not store block-specific data structures.
Inputs:
- key (str): the key
- val: the value (type is irrelevant)
Outputs:
- (Block): the Block instance
'''
if key == 'parent' and (not val is None) and (not val == self.getParent().getHash()):
self.setParent(val)
else:
self.bmetadata[key] = val
return self
2018-05-15 06:43:29 +00:00
def setContent(self, bcontent):
2018-05-16 02:12:23 +00:00
'''
Sets the contents of the block
Inputs:
- bcontent (str): the contents to be set to
Outputs:
- (Block): the Block instance
2018-05-16 02:12:23 +00:00
'''
2018-05-16 01:47:58 +00:00
self.bcontent = str(bcontent)
2018-05-15 06:43:29 +00:00
return self
def setParent(self, parent):
'''
Sets the Block's parent
Inputs:
- parent (Block/str): the Block's parent, to be stored in metadata
Outputs:
- (Block): the Block instance
'''
if type(parent) == str:
parent = Block(parent, core = self.getCore())
self.parent = parent
self.setMetadata('parent', (None if parent is None else self.getParent().getHash()))
return self
2018-05-15 06:43:29 +00:00
# static functions
2018-05-15 06:43:29 +00:00
2018-05-16 02:12:23 +00:00
def getBlocks(type = None, signer = None, signed = None, reverse = False, core = None):
'''
Returns a list of Block objects based on supplied filters
Inputs:
- type (str): filters by block type
- signer (str/list): filters by signer (one in the list has to be a signer)
- signed (bool): filters out by whether or not the block is signed
- reverse (bool): reverses the list if True
- core (Core): lets you optionally supply a core instance so one doesn't need to be started
Outputs:
- (list): a list of Block objects that match the input
'''
2018-05-15 06:43:29 +00:00
2018-05-16 01:47:58 +00:00
try:
core = (core if not core is None else onionrcore.Core())
relevant_blocks = list()
blocks = (core.getBlockList() if type is None else core.getBlocksByType(type))
for block in blocks:
if Block.exists(block):
2018-05-16 02:12:23 +00:00
block = Block(block, core = core)
2018-05-16 01:47:58 +00:00
relevant = True
if (not signed is None) and (block.isSigned() != bool(signed)):
relevant = False
if not signer is None:
if isinstance(signer, (str,)):
signer = [signer]
isSigner = False
for key in signer:
if block.isSigner(key):
isSigner = True
break
if not isSigner:
relevant = False
if relevant:
relevant_blocks.append(block)
2018-05-16 02:12:23 +00:00
if bool(reverse):
relevant_blocks.reverse()
2018-05-16 01:47:58 +00:00
return relevant_blocks
except Exception as e:
logger.debug(('Failed to get blocks: %s' % str(e)) + logger.parse_error())
return list()
2018-05-15 06:43:29 +00:00
def merge(child, file = None, maximumFollows = 32, core = None):
'''
Follows a child Block to its root parent Block, merging content
Inputs:
- child (str/Block): the child Block to be followed
- file (str/file): the file to write the content to, instead of returning it
- maximumFollows (int): the maximum number of Blocks to follow
'''
# validate data and instantiate Core
core = (core if not core is None else onionrcore.Core())
maximumFollows = max(0, maximumFollows)
# type conversions
if type(child) == str:
child = Block(child)
if (not file is None) and (type(file) == str):
file = open(file, 'ab')
# only store hashes to avoid intensive memory usage
blocks = [child.getHash()]
# generate a list of parent Blocks
while True:
# end if the maximum number of follows has been exceeded
if len(blocks) - 1 >= maximumFollows:
break
block = Block(blocks[-1], core = core).getParent()
# end if there is no parent Block
if block is None:
break
# end if the Block is pointing to a previously parsed Block
if block.getHash() in blocks:
break
# end if the block is not valid
if not block.isValid():
break
blocks.append(block.getHash())
buffer = ''
# combine block contents
for hash in blocks:
block = Block(hash, core = core)
contents = block.getContent()
if file is None:
buffer += contents
else:
file.write(contents)
return (None if not file is None else buffer)
2018-06-04 16:29:04 +00:00
def create(data = None, chunksize = 4999000, file = None, type = 'chunk', sign = True):
'''
Creates a chain of blocks to store larger amounts of data
The chunksize is set to 4999000 because it provides the least amount of PoW for the most amount of data.
TODO: Add docs
'''
blocks = list()
# initial datatype checks
if data is None and file is None:
return blocks
elif not (file is None or (isinstance(file, str) and os.path.exists(file))):
return blocks
elif isinstance(file, str):
file = open(file, 'rb')
if isinstance(data, str):
data = str(data)
if not file is None:
while True:
# read chunksize bytes from the file
content = file.read(chunksize)
# if it is the end of the file, exit
if not content:
break
# create block
block = Block()
block.setType(type)
block.setContent(content)
block.setParent((blocks[-1] if len(blocks) != 0 else None))
hash = block.save(sign = sign)
# remember the hash in cache
blocks.append(hash)
elif not data is None:
for content in [data[n:n + chunksize] for n in range(0, len(data), chunksize)]:
# create block
block = Block()
block.setType(type)
block.setContent(content)
block.setParent((blocks[-1] if len(blocks) != 0 else None))
hash = block.save(sign = sign)
# remember the hash in cache
blocks.append(hash)
return blocks
2018-05-15 06:43:29 +00:00
def exists(hash):
2018-05-16 02:12:23 +00:00
'''
Checks if a block is saved to file or not
Inputs:
- hash (str/Block):
- if (Block): check if this block is saved to file
- if (str): check if a block by this hash is in file
Outputs:
- (bool): whether or not the block file exists
'''
2018-05-16 01:47:58 +00:00
if hash is None:
return False
elif type(hash) == Block:
blockfile = hash.getBlockFile()
else:
blockfile = 'data/blocks/%s.dat' % hash
return os.path.exists(blockfile) and os.path.isfile(blockfile)