Skip to content

Commit

Permalink
Aerial: Initial offline table based gas strategy (#107)
Browse files Browse the repository at this point in the history
  • Loading branch information
ejfitzgerald authored Mar 11, 2022
1 parent 3cb23c5 commit 345c9d4
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 29 deletions.
8 changes: 6 additions & 2 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]
cosmpy = {editable = true, extras = ["dev", "test"], path = "."}
cosmpy = { editable = true, extras = ["dev", "test"], path = "." }

[packages]
cosmpy = {editable = true, extras = [], path = "."}
cosmpy = { editable = true, extras = [], path = "." }

[pipenv]
allow_prereleases = true

[scripts]
fmt = "./scripts/run-fmt.sh"
checks = "./scripts/run-checks.sh"
36 changes: 27 additions & 9 deletions cosmpy/aerial/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import time
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Optional
from typing import Optional, Tuple

import certifi
import grpc
Expand All @@ -35,6 +35,7 @@
OutOfGasError,
QueryTimeoutError,
)
from cosmpy.aerial.gas import GasStrategy, OfflineMessageTableStrategy
from cosmpy.aerial.tx import SigningCfg, Transaction
from cosmpy.aerial.tx_helpers import MessageLog, SubmittedTx, TxResponse
from cosmpy.aerial.urls import Protocol, parse_url
Expand Down Expand Up @@ -78,9 +79,10 @@ class Account:
class LedgerClient:
def __init__(self, cfg: NetworkConfig):
cfg.validate()

self._network_config = cfg

self._gas_strategy: GasStrategy = OfflineMessageTableStrategy.default_table()

parsed_url = parse_url(cfg.url)

if parsed_url.protocol == Protocol.GRPC:
Expand Down Expand Up @@ -112,6 +114,16 @@ def __init__(self, cfg: NetworkConfig):
def network_config(self) -> NetworkConfig:
return self._network_config

@property
def gas_strategy(self) -> GasStrategy:
return self._gas_strategy

@gas_strategy.setter
def gas_strategy(self, strategy: GasStrategy):
if not isinstance(strategy, GasStrategy):
raise RuntimeError("Invalid strategy must implement GasStrategy interface")
self._gas_strategy = strategy

def query_account(self, address: Address) -> Account:
request = QueryAccountRequest(address=str(address))
response = self.auth.Account(request)
Expand Down Expand Up @@ -153,17 +165,15 @@ def send_tokens(
# query the account information for the sender
account = self.query_account(sender.address())

# estimate the fee required for this transaction
gas_limit = (
gas_limit or 100000
) # TODO: Need to interface to the simulation engine
fee = self.estimate_fee_from_gas(gas_limit)

# build up the store transaction
tx = Transaction()
tx.add_message(
create_bank_send_msg(sender.address(), destination, amount, denom)
)

# estimate the fee required for this transaction
gas_limit, fee = self.estimate_gas_and_fee_for_tx(tx)

tx.seal(
SigningCfg.direct(sender.public_key(), account.sequence),
fee=fee,
Expand All @@ -176,9 +186,17 @@ def send_tokens(
# broadcast the store transaction
return self.broadcast_tx(tx)

def estimate_fee_from_gas(self, gas_limit: int):
def estimate_gas_for_tx(self, tx: Transaction) -> int:
return self._gas_strategy.estimate_gas(tx)

def estimate_fee_from_gas(self, gas_limit: int) -> str:
return f"{gas_limit * self.network_config.fee_minimum_gas_price}{self.network_config.fee_denomination}"

def estimate_gas_and_fee_for_tx(self, tx: Transaction) -> Tuple[int, str]:
gas_estimate = self.estimate_gas_for_tx(tx)
fee = self.estimate_fee_from_gas(gas_estimate)
return gas_estimate, fee

def wait_for_query_tx(
self,
tx_hash: str,
Expand Down
30 changes: 12 additions & 18 deletions cosmpy/aerial/contract/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,13 @@ def store(self, sender: Wallet, gas_limit: Optional[int] = None) -> int:
# query the account information for the sender
account = self._client.query_account(sender.address())

# estimate the fee required for this transaction
gas_limit = (
gas_limit or 2000000
) # TODO: Need to interface to the simulation engine
fee = self._client.estimate_fee_from_gas(gas_limit)

# build up the store transaction
tx = Transaction()
tx.add_message(create_cosmwasm_store_code_msg(self._path, sender.address()))

# estimate the fee required for this transaction
gas_limit, fee = self._client.estimate_gas_and_fee_for_tx(tx)

tx.seal(
SigningCfg.direct(sender.public_key(), account.sequence),
fee=fee,
Expand Down Expand Up @@ -121,12 +119,6 @@ def instantiate(
account = self._client.query_account(sender.address())
label = label or _generate_label(self._digest)

# estimate the fee required for this transaction
gas_limit = (
gas_limit or 2000000
) # TODO: Need to interface to the simulation engine
fee = self._client.estimate_fee_from_gas(gas_limit)

# build up the store transaction
tx = Transaction()
tx.add_message(
Expand All @@ -139,6 +131,10 @@ def instantiate(
funds=funds,
)
)

# estimate the fee required for this transaction
gas_limit, fee = self._client.estimate_gas_and_fee_for_tx(tx)

tx.seal(
SigningCfg.direct(sender.public_key(), account.sequence),
fee=fee,
Expand Down Expand Up @@ -202,19 +198,17 @@ def execute(
# query the account information for the sender
account = self._client.query_account(sender.address())

# estimate the fee required for this transaction
gas_limit = (
gas_limit or 2000000
) # TODO: Need to interface to the simulation engine
fee = self._client.estimate_fee_from_gas(gas_limit)

# build up the store transaction
tx = Transaction()
tx.add_message(
create_cosmwasm_execute_msg(
sender.address(), self._address, args, funds=funds
)
)

# estimate the fee required for this transaction
gas_limit, fee = self._client.estimate_gas_and_fee_for_tx(tx)

tx.seal(
SigningCfg.direct(sender.public_key(), account.sequence),
fee=fee,
Expand Down
69 changes: 69 additions & 0 deletions cosmpy/aerial/gas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
# ------------------------------------------------------------------------------
#
# Copyright 2018-2021 Fetch.AI Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# ------------------------------------------------------------------------------
from abc import ABC, abstractmethod
from typing import Dict, Optional

from cosmpy.aerial.tx import Transaction


class GasStrategy(ABC):
@abstractmethod
def estimate_gas(self, tx: Transaction) -> int:
pass # pragma: no cover

@abstractmethod
def block_gas_limit(self) -> int:
pass # pragma: no cover


class OfflineMessageTableStrategy(GasStrategy):
DEFAULT_FALLBACK_GAS_LIMIT = 400_000
DEFAULT_BLOCK_LIMIT = 2_000_000

@staticmethod
def default_table() -> "OfflineMessageTableStrategy":
strategy = OfflineMessageTableStrategy()
strategy.update_entry("cosmos.bank.v1beta1.MsgSend", 100_000)
strategy.update_entry("cosmwasm.wasm.v1.MsgStoreCode", 2_000_000)
strategy.update_entry("cosmwasm.wasm.v1.MsgInstantiateContract", 250_000)
strategy.update_entry("cosmwasm.wasm.v1.MsgExecuteContract", 400_000)
return strategy

def __init__(
self,
fallback_gas_limit: Optional[int] = None,
block_limit: Optional[int] = None,
):
self._table: Dict[str, int] = {}
self._block_limit = block_limit or self.DEFAULT_BLOCK_LIMIT
self._fallback_gas_limit = fallback_gas_limit or self.DEFAULT_FALLBACK_GAS_LIMIT

def update_entry(self, transaction_type: str, gas_limit: int):
self._table[str(transaction_type)] = int(gas_limit)

def estimate_gas(self, tx: Transaction) -> int:
gas_estimate = 0
for msg in tx.msgs:
gas_estimate += self._table.get(
msg.DESCRIPTOR.full_name, self._fallback_gas_limit
)
return min(self.block_gas_limit(), gas_estimate)

def block_gas_limit(self) -> int:
return self._block_limit
48 changes: 48 additions & 0 deletions scripts/run-checks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/bin/bash
set -e

echo Running black-check ...
tox -e black-check
echo Running black-check ... complete

echo Running isort-check ...
tox -e isort-check
echo Running isort-check ... complete

echo Running flake8 ...
tox -e flake8
echo Running flake8 ... complete

echo Running vulture ...
tox -e vulture
echo Running vulture ... complete

echo Running bandit ...
tox -e bandit
echo Running bandit ... complete

echo Running safety ...
tox -e safety
echo Running safety ... complete

echo Running mypy ...
tox -e mypy
echo Running mypy ... complete

echo Running liccheck ...
tox -e liccheck
echo Running liccheck ... complete

echo Running copyright-check ...
tox -e copyright-check
echo Running copyright-check ... complete

echo Running test ...
tox -e test
echo Running test ... complete

echo Running coverage-report ...
tox -e coverage-report
echo Running coverage-report ... complete

echo 'Great Success - all done!'
8 changes: 8 additions & 0 deletions scripts/run-fmt.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/bash
set -e

echo Running Black...
tox -e black

echo Running Black...
tox -e isort
54 changes: 54 additions & 0 deletions tests/unit/test_aerial/gas_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
# ------------------------------------------------------------------------------
#
# Copyright 2018-2021 Fetch.AI Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# ------------------------------------------------------------------------------
import pytest

from cosmpy.aerial.gas import GasStrategy, OfflineMessageTableStrategy
from cosmpy.aerial.tx import Transaction
from cosmpy.protos.cosmos.bank.v1beta1.tx_pb2 import MsgSend
from cosmpy.protos.cosmwasm.wasm.v1.tx_pb2 import (
MsgExecuteContract,
MsgInstantiateContract,
MsgStoreCode,
)


@pytest.mark.parametrize(
"input_msgs,expected_gas_estimate",
[
([MsgSend()], 100_000),
([MsgStoreCode()], 2_000_000),
([MsgInstantiateContract()], 250_000),
([MsgExecuteContract()], 400_000),
([MsgSend(), MsgSend()], 200_000),
([MsgSend(), MsgStoreCode()], 2_000_000), # hits block limit
([MsgSend(), MsgInstantiateContract()], 350_000),
([MsgInstantiateContract(), MsgExecuteContract()], 650_000),
],
)
def test_table_gas_estimation(input_msgs, expected_gas_estimate):
# build up the TX
tx = Transaction()
for input_msg in input_msgs:
tx.add_message(input_msg)

# estimate the gas for the this test transaction
strategy: GasStrategy = OfflineMessageTableStrategy.default_table()
gas_estimate = strategy.estimate_gas(tx)

assert gas_estimate == expected_gas_estimate

0 comments on commit 345c9d4

Please sign in to comment.