Skip to content

Commit af3e90f

Browse files
feat: add gas estimation to client (#66)
* Add gas estimation to client * Add estimation logic * Remove leftover code * Resolve linter * Remove leftover comment * Add ratios
1 parent 12fe054 commit af3e90f

13 files changed

+240
-44
lines changed

internal/client/client.go

+37
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ import (
55

66
"github.com/gnolang/gno/gno.land/pkg/gnoland"
77
"github.com/gnolang/gno/tm2/pkg/amino"
8+
abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types"
89
"github.com/gnolang/gno/tm2/pkg/bft/rpc/client"
910
core_types "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types"
1011
"github.com/gnolang/gno/tm2/pkg/std"
1112
"github.com/gnolang/supernova/internal/common"
1213
)
1314

15+
const simulatePath = ".app/simulate"
16+
1417
type Client struct {
1518
conn *client.RPCClient
1619
}
@@ -133,3 +136,37 @@ func (h *Client) GetBlockGasLimit(height int64) (int64, error) {
133136

134137
return consensusParams.ConsensusParams.Block.MaxGas, nil
135138
}
139+
140+
func (h *Client) EstimateGas(tx *std.Tx) (int64, error) {
141+
// Prepare the transaction.
142+
// The transaction needs to be amino-binary encoded
143+
// in order to be estimated
144+
encodedTx, err := amino.Marshal(tx)
145+
if err != nil {
146+
return 0, fmt.Errorf("unable to marshal tx: %w", err)
147+
}
148+
149+
// Perform the simulation query
150+
resp, err := h.conn.ABCIQuery(simulatePath, encodedTx)
151+
if err != nil {
152+
return 0, fmt.Errorf("unable to perform ABCI query: %w", err)
153+
}
154+
155+
// Extract the query response
156+
if err = resp.Response.Error; err != nil {
157+
return 0, fmt.Errorf("error encountered during ABCI query: %w", err)
158+
}
159+
160+
var deliverTx abci.ResponseDeliverTx
161+
if err = amino.Unmarshal(resp.Response.Value, &deliverTx); err != nil {
162+
return 0, fmt.Errorf("unable to unmarshal gas estimation response: %w", err)
163+
}
164+
165+
if err = deliverTx.Error; err != nil {
166+
return 0, fmt.Errorf("error encountered during gas estimation: %w", err)
167+
}
168+
169+
// Return the actual value returned by the node
170+
// for executing the transaction
171+
return deliverTx.GasUsed, nil
172+
}

internal/common/common.go

+20-16
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,27 @@ import "github.com/gnolang/gno/tm2/pkg/std"
44

55
const Denomination = "ugnot"
66

7-
// TODO support estimating gas params
8-
// These are constants for now,
9-
// but should be fetched as estimations
10-
// from the Tendermint node once this functionality
11-
// is available.
12-
//
13-
// Each package call / deployment
14-
// costs a fixed 1 GNOT
15-
// https://github.com/gnolang/gno/issues/649
16-
var (
17-
DefaultGasFee = std.Coin{
7+
// DefaultGasPrice represents the gno.land chain's
8+
// default minimum gas price ratio, which is 0.001ugnot/gas
9+
var DefaultGasPrice = std.GasPrice{
10+
Gas: 1000,
11+
Price: std.Coin{
1812
Denom: Denomination,
1913
Amount: 1,
20-
}
14+
},
15+
}
2116

22-
InitialTxCost = std.Coin{
23-
Denom: Denomination,
24-
Amount: 1000000, // 1 GNOT
17+
// CalculateFeeInRatio calculates the minimum gas fee that should be specified
18+
// in a transaction, given the gas wanted (of the tx) and the reference gas ratio
19+
func CalculateFeeInRatio(gasWanted int64, reference std.GasPrice) std.Fee {
20+
// required amount = ceil((gas wanted * reference.Price.Amount) / reference.Gas)
21+
requiredAmount := (gasWanted*reference.Price.Amount + reference.Gas - 1) / reference.Gas
22+
23+
return std.Fee{
24+
GasWanted: gasWanted,
25+
GasFee: std.Coin{
26+
Denom: reference.Price.Denom,
27+
Amount: requiredAmount,
28+
},
2529
}
26-
)
30+
}

internal/distributor/distributor.go

+14-13
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ var errInsufficientFunds = errors.New("insufficient distributor funds")
1919
type Client interface {
2020
GetAccount(address string) (*gnoland.GnoAccount, error)
2121
BroadcastTransaction(tx *std.Tx) error
22+
EstimateGas(tx *std.Tx) (int64, error)
2223
}
2324

2425
// Distributor is the process
@@ -66,13 +67,13 @@ func calculateRuntimeCosts(totalTx int64) std.Coin {
6667
// NOTE: Since there is no gas estimation support yet, this value
6768
// is fixed, but it will change in the future once pricing estimations
6869
// are added
69-
baseTxCost := common.DefaultGasFee.Add(common.InitialTxCost)
70+
baseTxCost := common.CalculateFeeInRatio(1_000_000, common.DefaultGasPrice)
7071

7172
// Each account should have enough funds
7273
// to execute the entire run
7374
subAccountCost := std.Coin{
7475
Denom: common.Denomination,
75-
Amount: totalTx * baseTxCost.Amount,
76+
Amount: totalTx * baseTxCost.GasFee.Amount,
7677
}
7778

7879
return subAccountCost
@@ -146,12 +147,15 @@ func (d *Distributor) fundAccounts(
146147
return nil, fmt.Errorf("unable to fetch distributor account, %w", err)
147148
}
148149

149-
distributorBalance := distributor.Coins
150-
fundableIndex := 0
150+
var (
151+
distributorBalance = distributor.Coins
152+
fundableIndex = 0
153+
defaultFee = common.CalculateFeeInRatio(100_000, common.DefaultGasPrice)
154+
)
151155

152156
for _, account := range shortAccounts {
153-
// The transfer cost is the single run cost (missing balance) + 1ugnot fee (fixed)
154-
transferCost := std.NewCoins(common.DefaultGasFee.Add(account.missingFunds))
157+
// The transfer cost is the single run cost (missing balance) + approximate transfer cost
158+
transferCost := std.NewCoins(defaultFee.GasFee.Add(account.missingFunds))
155159

156160
if distributorBalance.IsAllLT(transferCost) {
157161
// Distributor does not have any more funds
@@ -176,13 +180,10 @@ func (d *Distributor) fundAccounts(
176180
return nil, errInsufficientFunds
177181
}
178182

179-
var (
180-
// Locally keep track of the nonce, so
181-
// there is no need to re-fetch the account again
182-
// before signing a future tx
183-
nonce = distributor.Sequence
184-
defaultFee = std.NewFee(100000, common.DefaultGasFee)
185-
)
183+
// Locally keep track of the nonce, so
184+
// there is no need to re-fetch the account again
185+
// before signing a future tx
186+
nonce := distributor.Sequence
186187

187188
fmt.Printf("Funding %d accounts...\n", len(shortAccounts))
188189
bar := progressbar.Default(int64(len(shortAccounts)), "funding short accounts")

internal/distributor/distributor_test.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -148,12 +148,14 @@ func TestDistributor_Distribute(t *testing.T) {
148148
}
149149

150150
if acc.Equals(accounts[0]) {
151+
sendCost := common.CalculateFeeInRatio(100_000, common.DefaultGasPrice)
152+
151153
return &gnoland.GnoAccount{
152154
BaseAccount: *std.NewBaseAccount(
153155
acc.PubKey().Address(),
154156
std.NewCoins(std.Coin{
155157
Denom: common.Denomination,
156-
Amount: int64(numTx) * common.DefaultGasFee.Add(singleCost).Amount,
158+
Amount: int64(numTx) * sendCost.GasFee.Add(singleCost).Amount,
157159
}),
158160
nil,
159161
0,

internal/distributor/mock_test.go

+10
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import (
88
type (
99
broadcastTransactionDelegate func(*std.Tx) error
1010
getAccountDelegate func(string) (*gnoland.GnoAccount, error)
11+
estimateGasDelegate func(*std.Tx) (int64, error)
1112
)
1213

1314
type mockClient struct {
1415
broadcastTransactionFn broadcastTransactionDelegate
1516
getAccountFn getAccountDelegate
17+
estimateGasFn estimateGasDelegate
1618
}
1719

1820
func (m *mockClient) BroadcastTransaction(tx *std.Tx) error {
@@ -30,3 +32,11 @@ func (m *mockClient) GetAccount(address string) (*gnoland.GnoAccount, error) {
3032

3133
return nil, nil
3234
}
35+
36+
func (m *mockClient) EstimateGas(tx *std.Tx) (int64, error) {
37+
if m.estimateGasFn != nil {
38+
return m.estimateGasFn(tx)
39+
}
40+
41+
return 0, nil
42+
}

internal/pipeline.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ func (p *Pipeline) Execute() error {
110110
runAccounts,
111111
p.cfg.Transactions,
112112
p.cfg.ChainID,
113+
p.cli.EstimateGas,
113114
)
114115
if err != nil {
115116
return fmt.Errorf("unable to construct transactions, %w", err)
@@ -205,7 +206,12 @@ func prepareRuntime(
205206
}
206207

207208
// Get the predeploy transactions
208-
predeployTxs, err := txRuntime.Initialize(deployer, deployerKey, chainID)
209+
predeployTxs, err := txRuntime.Initialize(
210+
deployer,
211+
deployerKey,
212+
chainID,
213+
cli.EstimateGas,
214+
)
209215
if err != nil {
210216
return fmt.Errorf("unable to initialize runtime, %w", err)
211217
}

internal/runtime/helper.go

+52-1
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ import (
55

66
"github.com/gnolang/gno/tm2/pkg/crypto"
77
"github.com/gnolang/gno/tm2/pkg/std"
8+
"github.com/gnolang/supernova/internal/common"
89
"github.com/gnolang/supernova/internal/signer"
910
"github.com/schollz/progressbar/v3"
1011
)
1112

13+
const gasBuffer = 10_000 // 10k gas
14+
1215
// msgFn defines the transaction message constructor
1316
type msgFn func(creator std.Account, index int) std.Msg
1417

@@ -20,6 +23,7 @@ func constructTransactions(
2023
transactions uint64,
2124
chainID string,
2225
getMsg msgFn,
26+
estimateFn EstimateGasFn,
2327
) ([]*std.Tx, error) {
2428
var (
2529
txs = make([]*std.Tx, transactions)
@@ -30,6 +34,53 @@ func constructTransactions(
3034
nonceMap = make(map[uint64]uint64) // accountNumber -> nonce
3135
)
3236

37+
fmt.Printf("\n⏳ Estimating Gas ⏳\n")
38+
39+
// Estimate the fee for the transaction batch
40+
txFee := common.CalculateFeeInRatio(
41+
1_000_000,
42+
common.DefaultGasPrice,
43+
)
44+
45+
// Construct the first tx
46+
var (
47+
creator = accounts[0]
48+
creatorKey = keys[0]
49+
)
50+
51+
tx := &std.Tx{
52+
Msgs: []std.Msg{getMsg(creator, 0)},
53+
Fee: txFee,
54+
}
55+
56+
// Sign the transaction
57+
cfg := signer.SignCfg{
58+
ChainID: chainID,
59+
AccountNumber: creator.GetAccountNumber(),
60+
Sequence: creator.GetSequence(),
61+
}
62+
63+
if err := signer.SignTx(tx, creatorKey, cfg); err != nil {
64+
return nil, fmt.Errorf("unable to sign transaction, %w", err)
65+
}
66+
67+
gasWanted, err := estimateFn(tx)
68+
if err != nil {
69+
return nil, fmt.Errorf("unable to estimate gas, %w", err)
70+
}
71+
72+
// Clear the old signatures, because they need
73+
// to be regenerated
74+
clear(tx.Signatures)
75+
76+
// Use the estimated gas limit
77+
txFee = common.CalculateFeeInRatio(gasWanted+gasBuffer, common.DefaultGasPrice) // 10k gas buffer
78+
79+
if err = signer.SignTx(tx, creatorKey, cfg); err != nil {
80+
return nil, fmt.Errorf("unable to sign transaction, %w", err)
81+
}
82+
83+
fmt.Printf("\nEstimated Gas for 1 run tx: %d \n", tx.Fee.GasWanted)
3384
fmt.Printf("\n🔨 Constructing Transactions 🔨\n\n")
3485

3586
bar := progressbar.Default(int64(transactions), "constructing txs")
@@ -44,7 +95,7 @@ func constructTransactions(
4495

4596
tx := &std.Tx{
4697
Msgs: []std.Msg{getMsg(creator, i)},
47-
Fee: defaultDeployTxFee,
98+
Fee: txFee,
4899
}
49100

50101
// Fetch the next account nonce

internal/runtime/helper_test.go

+16-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/gnolang/gno/gno.land/pkg/gnoland"
77
"github.com/gnolang/gno/gno.land/pkg/sdk/vm"
88
"github.com/gnolang/gno/tm2/pkg/std"
9+
"github.com/gnolang/supernova/internal/common"
910
testutils "github.com/gnolang/supernova/internal/testing"
1011
"github.com/stretchr/testify/assert"
1112
"github.com/stretchr/testify/require"
@@ -49,15 +50,28 @@ func TestHelper_ConstructTransactions(t *testing.T) {
4950
}
5051
)
5152

52-
txs, err := constructTransactions(accountKeys, accounts, transactions, "dummy", getMsgFn)
53+
txs, err := constructTransactions(
54+
accountKeys,
55+
accounts,
56+
transactions,
57+
"dummy",
58+
getMsgFn,
59+
func(_ *std.Tx) (int64, error) {
60+
return 1_000_000, nil
61+
},
62+
)
5363
require.NoError(t, err)
5464

5565
assert.Len(t, txs, int(transactions))
5666

5767
// Make sure the constructed transactions are valid
5868
for _, tx := range txs {
5969
// Make sure the fee is valid
60-
assert.Equal(t, defaultDeployTxFee, tx.Fee)
70+
assert.Equal(
71+
t,
72+
common.CalculateFeeInRatio(1_000_000+gasBuffer, common.DefaultGasPrice),
73+
tx.Fee,
74+
)
6175

6276
// Make sure the message is valid
6377
if len(tx.Msgs) != 1 {

internal/runtime/package_deployment.go

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ func (c *packageDeployment) Initialize(
2020
_ std.Account,
2121
_ crypto.PrivKey,
2222
_ string,
23+
_ EstimateGasFn,
2324
) ([]*std.Tx, error) {
2425
// No extra setup needed for this runtime type
2526
return nil, nil
@@ -30,6 +31,7 @@ func (c *packageDeployment) ConstructTransactions(
3031
accounts []std.Account,
3132
transactions uint64,
3233
chainID string,
34+
estimateFn EstimateGasFn,
3335
) ([]*std.Tx, error) {
3436
var (
3537
timestamp = time.Now().Unix()
@@ -65,5 +67,6 @@ func (c *packageDeployment) ConstructTransactions(
6567
transactions,
6668
chainID,
6769
getMsgFn,
70+
estimateFn,
6871
)
6972
}

0 commit comments

Comments
 (0)