def test_get_etas(self, mcd: DssDeployment, simpledb: SimpleDatabase, our_address: Address, guy_address: Address): print_out("test_get_etas") block = mcd.web3.eth.blockNumber yays = simpledb.get_yays(0, block) etas = simpledb.get_etas(yays, block) verify([], etas, 0)
def initial_query(self): """ Updates a locally stored database with the DS-Chief state since its last update. If a local database is not found, create one and query the DS-Chief state since its deployment. """ self.logger.info('') self.logger.info( 'Querying DS-Chief state since last update ( !! Could take up to 15 minutes !! )' ) self.database = SimpleDatabase(self.web3, self.deployment_block, self.arguments.network, self.dss) result = self.database.create() self.logger.info(result)
def test_initial_query(self, mcd: DssDeployment, simpledb: SimpleDatabase, our_address: Address, guy_address: Address): print_out("test_initial_query") simpledb.create() yays = simpledb.db.get(doc_id=2)["yays"] etas = simpledb.db.get(doc_id=3)["upcoming_etas"] verify([ our_address.address, guy_address.address, pytest.global_spell.address.address ], yays, 3) verify([], etas, 0)
def test_etas_update(self, mcd: DssDeployment, simpledb: SimpleDatabase, our_address: Address, guy_address: Address): print_out("test_etas_update") assert mcd.ds_chief.lift( pytest.global_spell.address).transact(from_address=our_address) assert pytest.global_spell.schedule().transact( from_address=our_address) block = mcd.web3.eth.blockNumber # Although pause.delay is 0, uddate_db_etas also catches etas that can be casted on the next block simpledb.update_db_etas(block) etas = simpledb.db.get(doc_id=3)['upcoming_etas'] verify([pytest.global_spell.address.address], etas, 1)
def test_unpack_slate(self, mcd: DssDeployment, simpledb: SimpleDatabase, our_address: Address, guy_address: Address): print_out("test_unpack_slate") # unpack the first etch etches = mcd.ds_chief.past_etch(3) yays = simpledb.unpack_slate(etches[0].slate, 3) verify([our_address.address, guy_address.address], yays, 2)
def test_check_eta_receipt(self, mcd: DssDeployment, keeper: ChiefKeeper, simpledb: SimpleDatabase, our_address: Address): print_out("test_check_eta_receipt") # clear out anything that came before keeper.check_hat() keeper.check_eta() # Give 1000 MKR to our_address amount = Wad.from_number(5000) mint_mkr(mcd.mkr, our_address, amount) assert mcd.mkr.balance_of(our_address) == amount # Lock MKR in DS-Chief assert mcd.mkr.approve( mcd.ds_chief.address).transact(from_address=our_address) assert mcd.ds_chief.lock(amount).transact(from_address=our_address) # Deploy spell spell = DSSBadSpell.deploy(mcd.web3) # Vote 5000 mkr on the spell assert mcd.ds_chief.vote_yays([spell.address.address ]).transact(from_address=our_address) keeper.check_hat() block = mcd.web3.eth.blockNumber simpledb.update_db_etas(block) hat = mcd.ds_chief.get_hat() etas = keeper.database.db.get(doc_id=3)['upcoming_etas'] verify([hat.address], etas, 1) keeper.check_eta() # Confirm that the spell was casted and that the database was updated # For the DSSBadSpell, the cast() call in non-conformant. Usually # cast() will flip done to true, but in this broken spell it's modified # to not set done to true so we can test this bug and prevent # regressions. assert DSSBadSpell(mcd.web3, Address(hat)).done() == False etas = keeper.database.db.get(doc_id=3)['upcoming_etas'] verify([], etas, 0)
def test_get_yays(self, mcd: DssDeployment, simpledb: SimpleDatabase, our_address: Address, guy_address: Address): print_out("test_get_yays") yays = simpledb.get_yays(0, mcd.web3.eth.blockNumber) verify([ our_address.address, guy_address.address, pytest.global_spell.address.address ], yays, 3)
def test_yays_update(self, mcd: DssDeployment, simpledb: SimpleDatabase, our_address: Address, guy_address: Address): print_out("test_yays_update") # Vote 1000 mkr on our address assert mcd.ds_chief.vote_yays([our_address.address ]).transact(from_address=our_address) block = mcd.web3.eth.blockNumber # Updated vote should not delete yays that have had approval history simpledb.update_db_yays(block) yays = simpledb.db.get(doc_id=2)["yays"] DBblockNumber = simpledb.db.get( doc_id=1)["last_block_checked_for_yays"] verify([ our_address.address, guy_address.address, pytest.global_spell.address.address ], yays, 3) assert DBblockNumber == block
class ChiefKeeper: """Keeper that lifts the hat and streamlines executive actions""" logger = logging.getLogger('chief-keeper') def __init__(self, args: list, **kwargs): """Pass in arguements assign necessary variables/objects and instantiate other Classes""" parser = argparse.ArgumentParser("chief-keeper") parser.add_argument( "--rpc-host", type=str, default="https://localhost:8545", help="JSON-RPC host:port (default: 'localhost:8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument( "--network", type=str, required=True, help= "Network that you're running the Keeper on (options, 'mainnet', 'kovan', 'testnet')" ) parser.add_argument( "--eth-from", type=str, required=True, help= "Ethereum address from which to send transactions; checksummed (e.g. '0x12AebC')" ) parser.add_argument( "--eth-key", type=str, nargs='*', help= "Ethereum private key(s) to use (e.g. 'key_file=/path/to/keystore.json,pass_file=/path/to/passphrase.txt')" ) parser.add_argument( "--dss-deployment-file", type=str, required=False, help= "Json description of all the system addresses (e.g. /Full/Path/To/configFile.json)" ) parser.add_argument( "--chief-deployment-block", type=int, required=False, default=0, help= " Block that the Chief from dss-deployment-file was deployed at (e.g. 8836668" ) parser.add_argument( "--max-errors", type=int, default=100, help= "Maximum number of allowed errors before the keeper terminates (default: 100)" ) parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") parser.add_argument("--ethgasstation-api-key", type=str, default=None, help="ethgasstation API key") parser.add_argument("--gas-initial-multiplier", type=str, default=1.0, help="ethgasstation API key") parser.add_argument("--gas-reactive-multiplier", type=str, default=2.25, help="gas strategy tuning") parser.add_argument("--gas-maximum", type=str, default=5000, help="gas strategy tuning") parser.set_defaults(cageFacilitated=False) self.arguments = parser.parse_args(args) self.web3: Web3 = kwargs['web3'] if 'web3' in kwargs else web3_via_http( endpoint_uri=self.arguments.rpc_host, timeout=self.arguments.rpc_timeout, http_pool_size=100) self.web3.eth.defaultAccount = self.arguments.eth_from register_keys(self.web3, self.arguments.eth_key) self.our_address = Address(self.arguments.eth_from) if self.arguments.dss_deployment_file: self.dss = DssDeployment.from_json( web3=self.web3, conf=open(self.arguments.dss_deployment_file, "r").read()) else: self.dss = DssDeployment.from_network( web3=self.web3, network=self.arguments.network) self.deployment_block = self.arguments.chief_deployment_block self.max_errors = self.arguments.max_errors self.errors = 0 self.confirmations = 0 # Create dynamic gas strategy if self.arguments.ethgasstation_api_key: self.gas_price = DynamicGasPrice(self.arguments, self.web3) else: self.gas_price = DefaultGasPrice() logging.basicConfig( format='%(asctime)-15s %(levelname)-8s %(message)s', level=(logging.DEBUG if self.arguments.debug else logging.INFO)) def main(self): """ Initialize the lifecycle and enter into the Keeper Lifecycle controller. Each function supplied by the lifecycle will accept a callback function that will be executed. The lifecycle.on_block() function will enter into an infinite loop, but will gracefully shutdown if it recieves a SIGINT/SIGTERM signal. """ with Lifecycle(self.web3) as lifecycle: self.lifecycle = lifecycle lifecycle.on_startup(self.check_deployment) lifecycle.on_block(self.process_block) def check_deployment(self): self.logger.info('') self.logger.info('Please confirm the deployment details') self.logger.info( f'Keeper Balance: {self.web3.eth.getBalance(self.our_address.address) / (10**18)} ETH' ) self.logger.info(f'DS-Chief: {self.dss.ds_chief.address}') self.logger.info(f'DS-Pause: {self.dss.pause.address}') self.logger.info('') self.initial_query() def initial_query(self): """ Updates a locally stored database with the DS-Chief state since its last update. If a local database is not found, create one and query the DS-Chief state since its deployment. """ self.logger.info('') self.logger.info( 'Querying DS-Chief state since last update ( !! Could take up to 15 minutes !! )' ) self.database = SimpleDatabase(self.web3, self.deployment_block, self.arguments.network, self.dss) result = self.database.create() self.logger.info(result) def process_block(self): """ Callback called on each new block. If too many errors, terminate the keeper. This is the entrypoint to the Keeper's monitoring logic """ if self.errors >= self.max_errors: self.lifecycle.terminate() else: self.check_hat() self.check_eta() def check_hat(self): """ Ensures the Hat is on the proposal (spell, EOA, multisig, etc) with the most approval. First, the local database is updated with proposal addresses (yays) that have been `etched` in DSChief between the last block reviewed and the most recent block receieved. Next, it simply traverses through each address, checking if its approval has surpased the current Hat. If it has, it will `lift` the hat. If the current or new hat hasn't been casted nor plotted in the pause, it will `schedule` the spell """ blockNumber = self.web3.eth.blockNumber self.logger.info(f'Checking Hat on block {blockNumber}') self.database.update_db_yays(blockNumber) yays = self.database.db.get(doc_id=2)["yays"] hat = self.dss.ds_chief.get_hat().address hatApprovals = self.dss.ds_chief.get_approvals(hat) contender, highestApprovals = hat, hatApprovals for yay in yays: contenderApprovals = self.dss.ds_chief.get_approvals(yay) if contenderApprovals > highestApprovals: contender = yay highestApprovals = contenderApprovals if contender != hat: self.logger.info(f'Lifting hat') self.logger.info(f'Old hat ({hat}) with Approvals {hatApprovals}') self.logger.info( f'New hat ({contender}) with Approvals {highestApprovals}') self.dss.ds_chief.lift( Address(contender)).transact(gas_price=self.gas_price) else: self.logger.info( f'Current hat ({hat}) with Approvals {hatApprovals}') # Read the hat; either is equivalent to the contender or old hat hatNew = self.dss.ds_chief.get_hat().address if hatNew != hat: self.logger.info(f'Confirmed ({contender}) now has the hat') spell = DSSSpell(self.web3, Address(hatNew)) if is_contract_at( self.web3, Address(hatNew)) else None # Schedules spells that haven't been scheduled nor casted if spell is not None: # Functional with DSSSpells but not DSSpells (not compatiable with DSPause) if spell.done() == False and self.database.get_eta_inUnix( spell) == 0: self.logger.info(f'Scheduling spell ({yay})') spell.schedule().transact(gas_price=self.gas_price) else: self.logger.warning( f'Spell is an EOA or 0x0, so keeper will not attempt to call schedule()' ) def check_eta(self): """ Cast spells that meet their schedule. First, the local database is updated with spells that have been scheduled between the last block reviewed and the most recent block receieved. Next, it simply traverses through each spell address, checking if its schedule has been reached/passed. If it has, it attempts to `cast` the spell. """ blockNumber = self.web3.eth.blockNumber now = self.web3.eth.getBlock(blockNumber).timestamp self.logger.info(f'Checking scheduled spells on block {blockNumber}') self.database.update_db_etas(blockNumber) etas = self.database.db.get(doc_id=3)["upcoming_etas"] yays = list(etas.keys()) for yay in yays: if etas[yay] <= now: spell = DSSSpell(self.web3, Address(yay)) if is_contract_at( self.web3, Address(yay)) else None if spell is not None: if spell.done() == False: self.logger.info( f'Casting spell ({spell.address.address})') receipt = spell.cast().transact( gas_price=self.gas_price) if receipt is None or receipt.successful == True: del etas[yay] else: del etas[yay] else: self.logger.warning( f'Spell is an EOA or 0x0, so keeper will not attempt to call cast()' ) del etas[yay] self.database.db.update({'upcoming_etas': etas}, doc_ids=[3])
def simpledb(web3: Web3, mcd: DssDeployment) -> SimpleDatabase: simpledb = SimpleDatabase(web3, 0, "testnet", mcd) assert isinstance(simpledb, SimpleDatabase) return simpledb