From 7baf6cf68de880035b6c7b427c848d5187963450 Mon Sep 17 00:00:00 2001 From: elmora-do <2745076+elmora-do@users.noreply.github.com> Date: Tue, 5 Jun 2018 19:15:36 -0500 Subject: [PATCH] Add $PAC (#474) * Add $PAC * Refactor and improve masternode notifications * Update DASH/$PAC RPC documentation These RPC commands were documented: masternode.announce.broadcast masternode.subscribe masternode.list masternode.info --- docs/protocol-methods.rst | 103 +++++++++++++++++++++++ lib/coins.py | 45 ++++++++++ server/controller.py | 2 + server/session.py | 122 +++++++++++++++++++++++++--- tests/blocks/pac_mainnet_27676.json | 17 ++++ tests/blocks/pac_testnet_16275.json | 15 ++++ 6 files changed, 293 insertions(+), 11 deletions(-) create mode 100644 tests/blocks/pac_mainnet_27676.json create mode 100644 tests/blocks/pac_testnet_16275.json diff --git a/docs/protocol-methods.rst b/docs/protocol-methods.rst index 697ee2a..494c443 100644 --- a/docs/protocol-methods.rst +++ b/docs/protocol-methods.rst @@ -1055,3 +1055,106 @@ Identify the client to the server and negotiate the protocol version. ["ElectrumX 1.2.1", "1.2"] "ElectrumX 1.2.1" + +masternode.announce.broadcast +-------------- + +Pass through the masternode announce message to be broadcast by the daemon. + +Whenever a masternode comes online or a client is syncing, they will send this message which describes the masternode entry and how to validate messages from it. + +**Signature** + + .. function:: masternode.announce.broadcast(signmnb) + + * *signmnb* + + Signed masternode broadcast message. + +**Result** + + True if the message was broadcasted succesfully otherwise False. + +masternode.subscribe +-------------- + +Returns the status of masternode. + +**Signature** + + .. function:: masternode.subscribe(collateral) + + * *collateral* + + A masternode collateral is a transaction with a specific amount of coins, it's also known as a masternode identifier. + + i.e. for DASH the required amount is 1,000 DASH or for $PAC is 500,000 $PAC. + +**Result** + + As this is a subcription, the client will receive a notification when the masternode status changes. + + The status depends on the server the masternode is hosted, the internet connection, the offline time and even the collateral amount, so this subscription notice these changes to the user. + +**Example Results**:: + + {'method': 'masternode.subscribe', u'jsonrpc': u'2.0', u'result': u'ENABLED', 'params': ['8c59133e714797650cf69043d05e409bbf45670eed7c4e4a386e52c46f1b5e24-0'], u'id': 19} + +masternode.list +-------------- + +Returns the list of masternodes. + +**Signature** + + .. function:: masternode.list(payees) + + * *payees* + + An array of masternode payee addresses. + +**Result** + + An array with the masternodes information. + +**Examples**:: + + masternode.list("['PDFHmjKLvSGdnWgDJSJX49Rrh0SJtRANcE', + 'PDFHmjKLvSGdnWgDJSJX49Rrh0SJtRANcF']") + +**Example Results**:: + + [ + { + "vin": "9d298c00dae8b491d6801f50cab2e0037852cb556c5619ddb07c50421x9a31ab", + "status": "ENABLED", + "protocol": 70213, + "payee": "PDFHmjKLvSGdnWgDJSJX49Rrh0SJtRANcE", + "lastseen": "2018-04-01 12:34", + "activeseconds": 1258000, + "lastpaidtime": "2018-03-10 12:29", + "lastpaidblock": 1234, + "ip": "1.0.0.1", + "paymentposition": 184, + "inselection": true, + "balance": 510350 + }, + { + "vin": "9d298c00dae8b491d6801f50cab2e0037852cb556c5619ddb07c50421x9a31ac", + "status": "ENABLED", + "protocol": 70213, + "payee": "PDFHmjKLvSGdnWgDJSJX49Rrh0SJtRANcF", + "lastseen": "2018-04-01 12:34", + "activeseconds": 1258000, + "lastpaidtime": "2018-03-15 05:29", + "lastpaidblock": 1234, + "ip": "1.0.0.2", + "paymentposition": 3333, + "inselection": false, + "balance": 520700 + }, + ..., + ..., + ..., + ... + ] \ No newline at end of file diff --git a/lib/coins.py b/lib/coins.py index cae1054..6e32e96 100644 --- a/lib/coins.py +++ b/lib/coins.py @@ -1722,3 +1722,48 @@ class Xuez(Coin): 'nonce': nonce, 'nAccumulatorCheckpoint': hash_to_str(header[80:112]), } + +class Pac(Coin): + NAME = "PAC" + SHORTNAME = "PAC" + NET = "mainnet" + XPUB_VERBYTES = bytes.fromhex("0488B21E") + XPRV_VERBYTES = bytes.fromhex("0488ADE4") + GENESIS_HASH = ('00000354655ff039a51273fe61d3b493' + 'bd2897fe6c16f732dbc4ae19f04b789e') + P2PKH_VERBYTE = bytes.fromhex("37") + P2SH_VERBYTES = [bytes.fromhex("0A")] + WIF_BYTE = bytes.fromhex("CC") + TX_COUNT_HEIGHT = 14939 + TX_COUNT = 23708 + TX_PER_BLOCK = 2 + RPC_PORT = 7111 + PEERS = [ + 'electrum.paccoin.io s t', + 'electro-pac.paccoin.io s t' + ] + SESSIONCLS = DashElectrumX + DAEMON = daemon.DashDaemon + ESTIMATE_FEE = 0.00001 + RELAY_FEE = 0.00001 + + @classmethod + def header_hash(cls, header): + '''Given a header return the hash.''' + import x11_hash + return x11_hash.getPoWHash(header) + +class PacTestnet(Pac): + SHORTNAME = "tPAC" + NET = "testnet" + XPUB_VERBYTES = bytes.fromhex("043587CF") + XPRV_VERBYTES = bytes.fromhex("04358394") + GENESIS_HASH = ('00000da63bd9478b655ef6bf1bf76cd9' + 'af05202ab68643f9091e049b2b5280ed') + P2PKH_VERBYTE = bytes.fromhex("78") + P2SH_VERBYTES = [bytes.fromhex("0E")] + WIF_BYTE = bytes.fromhex("EF") + TX_COUNT_HEIGHT = 16275 + TX_COUNT = 16275 + TX_PER_BLOCK = 1 + RPC_PORT = 17111 diff --git a/server/controller.py b/server/controller.py index 0b862ae..351e777 100644 --- a/server/controller.py +++ b/server/controller.py @@ -83,6 +83,8 @@ class Controller(ServerBase): self.history_cache = pylru.lrucache(256) self.header_cache = pylru.lrucache(8) self.cache_height = 0 + self.cache_mn_height = 0 + self.mn_cache = pylru.lrucache(256) env.max_send = max(350000, env.max_send) # Set up the RPC request handlers cmds = ('add_peer daemon_url disconnect getinfo groups log peers reorg ' diff --git a/server/session.py b/server/session.py index 74fbbf7..78d330f 100644 --- a/server/session.py +++ b/server/session.py @@ -10,6 +10,7 @@ import codecs import itertools import time +import datetime from functools import partial from aiorpcx import ServerSession, JSONRPCAutoDetect, RPCError @@ -495,22 +496,28 @@ class DashElectrumX(ElectrumX): 'masternode.announce.broadcast': self.masternode_announce_broadcast, 'masternode.subscribe': self.masternode_subscribe, + 'masternode.list': self.masternode_list }) + async def notify_masternodes_async(self): + for masternode in self.mns: + status = await self.daemon.masternode_list(['status', masternode]) + self.send_notification('masternode.subscribe', + [masternode, status.get(masternode)]) + def notify(self, height, touched): '''Notify the client about changes in masternode list.''' result = super().notify(height, touched) - - for masternode in self.mns: - status = self.daemon.masternode_list(['status', masternode]) - self.send_notification('masternode.subscribe', - [masternode, status.get(masternode)]) + self.controller.create_task(self.notify_masternodes_async()) return result + # Masternode command handlers async def masternode_announce_broadcast(self, signmnb): '''Pass through the masternode announce message to be broadcast - by the daemon.''' + by the daemon. + + signmnb: signed masternode broadcast message.''' try: return await self.daemon.masternode_broadcast(['relay', signmnb]) except DaemonError as e: @@ -520,10 +527,103 @@ class DashElectrumX(ElectrumX): raise RPCError(BAD_REQUEST, 'the masternode broadcast was ' f'rejected.\n\n{message}\n[{signmnb}]') - async def masternode_subscribe(self, vin): - '''Returns the status of masternode.''' - result = await self.daemon.masternode_list(['status', vin]) + async def masternode_subscribe(self, collateral): + '''Returns the status of masternode. + + collateral: masternode collateral. + ''' + result = await self.daemon.masternode_list(['status', collateral]) if result is not None: - self.mns.add(vin) - return result.get(vin) + self.mns.add(collateral) + return result.get(collateral) return None + + async def masternode_list(self, payees): + ''' + Returns the list of masternodes. + + payees: a list of masternode payee addresses. + ''' + result = [] + + def get_masternode_payment_queue(mns): + ''' + Returns the calculated position in the payment queue for all the valid + masterernodes in the given mns list. + + mns: a list of masternodes information. + ''' + now = int(datetime.datetime.utcnow().strftime("%s")) + mn_queue=[] + + # Only ENABLED masternodes are considered for the list. + for line in mns: + mnstat = mns[line].split() + if mnstat[0] == 'ENABLED': + # if last paid time == 0 + if int(mnstat[5]) == 0: + # use active seconds + mnstat.append(int(mnstat[4])) + else: + # now minus last paid + delta = now - int(mnstat[5]) + # if > active seconds, use active seconds + if delta >= int(mnstat[4]): + mnstat.append(int(mnstat[4])) + # use active seconds + else: + mnstat.append(delta) + mn_queue.append(mnstat) + mn_queue = sorted(mn_queue, key=lambda x: x[8], reverse=True) + return mn_queue + + def get_payment_position(payment_queue, address): + ''' + Returns the position of the payment list for the given address. + + payment_queue: position in the payment queue for the masternode. + address: masternode payee address. + ''' + position = -1 + for pos, mn in enumerate(payment_queue, start=1): + if mn[2] == address: + position = pos + break + return position + + # Accordingly with the masternode payment queue, a custom list with + # the masternode information including the payment position is returned. + if self.controller.cache_mn_height != self.height() or not self.controller.mn_cache: + self.controller.cache_mn_height = self.height() + self.controller.mn_cache.clear() + full_mn_list = await self.daemon.masternode_list(['full']) + mn_payment_queue = get_masternode_payment_queue(full_mn_list) + mn_payment_count = len(mn_payment_queue) + mn_list = [] + for key, value in full_mn_list.items(): + mn_data = value.split() + mn_info = {} + mn_info['vin'] = key + mn_info['status'] = mn_data[0] + mn_info['protocol'] = mn_data[1] + mn_info['payee'] = mn_data[2] + mn_info['lastseen'] = mn_data[3] + mn_info['activeseconds'] = mn_data[4] + mn_info['lastpaidtime'] = mn_data[5] + mn_info['lastpaidblock'] = mn_data[6] + mn_info['ip'] = mn_data[7] + mn_info['paymentposition'] = get_payment_position(mn_payment_queue, mn_info['payee']) + mn_info['inselection'] = mn_info['paymentposition'] < mn_payment_count // 10 + balance = await self.controller.address_get_balance(mn_info['payee']) + mn_info['balance'] = sum(balance.values()) / self.controller.coin.VALUE_PER_COIN + mn_list.append(mn_info) + self.controller.mn_cache = mn_list + + # If payees is an empty list the whole masternode list is returned + if payees: + result = [mn for mn in self.controller.mn_cache + for address in payees if mn['payee'] == address] + else: + result = self.controller.mn_cache + + return result diff --git a/tests/blocks/pac_mainnet_27676.json b/tests/blocks/pac_mainnet_27676.json new file mode 100644 index 0000000..723ef71 --- /dev/null +++ b/tests/blocks/pac_mainnet_27676.json @@ -0,0 +1,17 @@ +{ + "hash": "00000000000001439cf3d3b09f067a600d10cbe8b532e14413c904ae17bccd49", + "confirmations": 39839, + "size": 456, + "height": 27676, + "merkleroot": "cffe89f39de2f101d6e2509b1f5121609c369c8e4ac0152252cb0cc18c613603", + "tx": [ + "d4426bc2597439f509e685848dbc5272b2d52e882e2b6cf4910971062e130dd6", + "7f8761bdd82d6cf4b976008c533ae56c2f6c8f49b67597067dc156871fc40a4f" + ], + "time": 1520767916, + "nonce": 2372120537, + "bits": "1a03d789", + "previousblockhash": "00000000000003b32ed577c591f6e60eb7b534f61ebdcc9f5fd28d2386e8f391", + "nextblockhash": "000000000000017f3d0fe299f7a52cee98a46be48c853ff8197fd78340cbfcd8", + "block": "0000002091f3e886238dd25f9fccbd1ef634b5b70ee6f691c577d52eb3030000000000000336618cc10ccb522215c04a8e9c369c6021511f9b50e2d601f1e29df389fecfac13a55a89d7031ad9af638d0201000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1f021c6c04ac13a55a083ffffbbdb14c01000d2f6e6f64655374726174756d2f0000000002631e0b75700000001976a91489982fec237dfd38a42de5900c95b6deed54dad988ac7f027c96900000001976a9143b39725cff6ae6228194e15ad817976dbbc581e588ac00000000010000000178d66e9ffd71fc5edf9ac6bbffecbcf57d26c090e157b48f231fed89002dd217000000006a47304402205d1b6d24615cb924c38686e87319ec4282434368f6bc0881a94f8d22f980ca18022038b83b016e19c6fbccedef66c5eb73147119cb1cd93dacfdd3d42f647575ed30012102171a4f5763f91281a4ee4775d2e01d81f5c1ccddf2a05662b9682f84bf09ac93feffffff0260bf570b000000001976a91489dd6e460e00695b55b710d62b2e1fb815d44b6588ac1e6bb32d350000001976a91495f95b4a02171deca1046cf5c505aa3ea57216da88ac196c0000" +} \ No newline at end of file diff --git a/tests/blocks/pac_testnet_16275.json b/tests/blocks/pac_testnet_16275.json new file mode 100644 index 0000000..38eb98d --- /dev/null +++ b/tests/blocks/pac_testnet_16275.json @@ -0,0 +1,15 @@ +{ + "hash": "000003527433edc9fdf28d848c5fa3279ff8a016afc67d47a080534461871aa2", + "size": 215, + "height": 16275, + "merkleroot": "699bda4bad8fc13eff818ffa1a2aa1d19a992d8436ec67e86dd8ce038130e6e5", + "tx": [ + "699bda4bad8fc13eff818ffa1a2aa1d19a992d8436ec67e86dd8ce038130e6e5" + ], + "time": 1525276230, + "nonce": 2816146, + "bits": "1e042d0c", + "previousblockhash": "000002bc2882394ca3852cda708311e500c8a9df717255a1336cc350b07550f0", + "nextblockhash": "000003eec7b324a19edbfc0aa539549dc02bd24e3de9d31e8c85fd3da68f1962", + "block": "00000020f05075b050c36c33a1557271dfa9c800e5118370da2c85a34c398228bc020000e5e6308103ced86de867ec36842d999ad1a12a1afa8f81ff3ec18fad4bda9b6946dee95a0c2d041e92f82a000101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0502933f0101ffffffff020072b8101000000023210336524995b3bb2c2f300b3dd45e920f27f1cfc3c4f2e5a84e65af0fddc3bab00eac00aecefaf00000001976a914c2f707ddbb9c3ca5b2c3458cabc0d24baab66b8b88ac00000000" +} \ No newline at end of file