diff --git a/onionr/api.py b/onionr/api.py index 48587f8e..3962fb86 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -81,8 +81,8 @@ class API: logger.debug('Your web password (KEEP SECRET): ' + logger.colors.underline + self.clientToken) if not debug and not self._developmentMode: - hostNums = [random.randint(1, 255), random.randint(1, 255), random.randint(1, 255)] - self.host = '127.' + str(hostNums[0]) + '.' + str(hostNums[1]) + '.' + str(hostNums[2]) + hostOctets = [127, random.randint(0x02, 0xFF), random.randint(0x02, 0xFF), random.randint(0x02, 0xFF)] + self.host = '.'.join(hostOctets) else: self.host = '127.0.0.1' diff --git a/onionr/core.py b/onionr/core.py index ee5d6170..c6aff9f9 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -655,7 +655,7 @@ class Core: conn.close() return True - def insertBlock(self, data, header='txt', sign=False): + def insertBlock(self, data, header='txt', sign=False, metadata = {}): ''' Inserts a block into the network ''' @@ -688,7 +688,11 @@ class Core: data = data.encode() retData = '' - metadata = {'type': header, 'powHash': powHash, 'powToken': powToken} + + metadata['type'] = header + metadata['powHash'] = powHash + metadata['powToken'] = powToken + sig = {} metadata = json.dumps(metadata) diff --git a/onionr/onionrblockapi.py b/onionr/onionrblockapi.py index 3a8da265..f48800a6 100644 --- a/onionr/onionrblockapi.py +++ b/onionr/onionrblockapi.py @@ -57,6 +57,7 @@ class Block: self.signature = None self.signedData = None self.blockFile = None + self.parent = None self.bheader = {} self.bmetadata = {} @@ -110,6 +111,7 @@ class Block: 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'))) self.btype = self.getMetadata('type') self.powHash = self.getMetadata('powHash') self.powToken = self.getMetadata('powToken') @@ -162,7 +164,7 @@ class Block: blockFile.write(self.getRaw().encode()) self.update() else: - self.hash = self.getCore().insertBlock(self.getContent(), header = self.getType(), sign = sign) + self.hash = self.getCore().insertBlock(self.getContent(), header = self.getType(), sign = sign, metadata = self.getMetadata()) self.update() return self.getHash() else: @@ -226,8 +228,7 @@ class Block: if not key is None: return self.getHeader()[key] - else: - return self.bheader + return self.bheader def getMetadata(self, key = None): ''' @@ -242,8 +243,7 @@ class Block: if not key is None: return self.getMetadata()[key] - else: - return self.bmetadata + return self.bmetadata def getContent(self): ''' @@ -254,6 +254,16 @@ class Block: ''' return str(self.bcontent) + + def getParent(self): + ''' + Returns the Block's parent Block, or None + + Outputs: + - (Block): the Block's parent + ''' + + return self.parent def getDate(self): ''' @@ -345,11 +355,31 @@ class Block: - btype (str): the type of block to be set to Outputs: - - (Block): the block instance + - (Block): the Block instance ''' 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 def setContent(self, bcontent): ''' @@ -359,13 +389,31 @@ class Block: - bcontent (str): the contents to be set to Outputs: - - (Block): the block instance + - (Block): the Block instance ''' self.bcontent = str(bcontent) 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 - # static + # static functions def getBlocks(type = None, signer = None, signed = None, reverse = False, core = None): ''' @@ -421,6 +469,66 @@ class Block: return list() + 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) + def exists(hash): ''' Checks if a block is saved to file or not diff --git a/onionr/tests.py b/onionr/tests.py index ac4ff36b..348e2068 100755 --- a/onionr/tests.py +++ b/onionr/tests.py @@ -119,7 +119,7 @@ class OnionrTests(unittest.TestCase): def testBlockAPI(self): logger.debug('-'*26 + '\n') - logger.info('Running BlockAPI test...') + logger.info('Running BlockAPI test #1...') content = 'Onionr test block' @@ -133,7 +133,33 @@ class OnionrTests(unittest.TestCase): if not block.getContent() == content: logger.warn('Test block content is invalid! (%s != %s)' % (block.getContent(), content)) self.assertTrue(False) + + logger.debug('-'*26 + '\n') + logger.info('Running BlockAPI test #2...') + + original_content = 'onionr' + contents = [original_content[i:i+2] for i in range(0, len(original_content), 2)] + contents.reverse() + + blocks = list() + parent = None + + for content in contents: + block = Block('test', content) + block.setParent(parent) + parent = block + print('block "%s": %s' % (content, block.save())) + blocks.append(block) + child = blocks[-1] + + merged = Block.merge(child) + + print('merged blocks: %s' % merged) + + if merged != original_content: + self.assertTrue(False) + self.assertTrue(True) def testBitcoinNode(self): @@ -253,6 +279,6 @@ class OnionrTests(unittest.TestCase): else: self.assertTrue(False) else: - self.assertTrue(False) + self.assertTrue(False) # <- annoying :( unittest.main()