mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-13 23:43:47 +00:00
feat(API): consider L1 fee in eth_call and eth_estimateGas (#248)
* consider L1 fee in eth_call and eth_estimateGas * address comments * consider l1fee in trace call * nit * fix bugs and add tests * address comments * bump version
This commit is contained in:
parent
a74b35e86f
commit
b986f1ea7b
5 changed files with 183 additions and 16 deletions
|
|
@ -23,6 +23,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"math/big"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
@ -41,6 +42,7 @@ import (
|
||||||
"github.com/scroll-tech/go-ethereum/log"
|
"github.com/scroll-tech/go-ethereum/log"
|
||||||
"github.com/scroll-tech/go-ethereum/params"
|
"github.com/scroll-tech/go-ethereum/params"
|
||||||
"github.com/scroll-tech/go-ethereum/rlp"
|
"github.com/scroll-tech/go-ethereum/rlp"
|
||||||
|
"github.com/scroll-tech/go-ethereum/rollup/fees"
|
||||||
"github.com/scroll-tech/go-ethereum/rpc"
|
"github.com/scroll-tech/go-ethereum/rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -897,6 +899,16 @@ func (api *API) traceTx(ctx context.Context, message core.Message, txctx *Contex
|
||||||
// Run the transaction with tracing enabled.
|
// Run the transaction with tracing enabled.
|
||||||
vmenv := vm.NewEVM(vmctx, txContext, statedb, api.backend.ChainConfig(), vm.Config{Debug: true, Tracer: tracer, NoBaseFee: true})
|
vmenv := vm.NewEVM(vmctx, txContext, statedb, api.backend.ChainConfig(), vm.Config{Debug: true, Tracer: tracer, NoBaseFee: true})
|
||||||
|
|
||||||
|
// If gasPrice is 0, make sure that the account has sufficient balance to cover `l1Fee`.
|
||||||
|
if api.backend.ChainConfig().UsingScroll && message.GasPrice().Cmp(big.NewInt(0)) == 0 {
|
||||||
|
l1Fee, err := fees.CalculateL1MsgFee(message, vmenv.StateDB)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
statedb.AddBalance(message.From(), l1Fee)
|
||||||
|
}
|
||||||
|
|
||||||
// Call Prepare to clear out the statedb access list
|
// Call Prepare to clear out the statedb access list
|
||||||
statedb.Prepare(txctx.TxHash, txctx.TxIndex)
|
statedb.Prepare(txctx.TxHash, txctx.TxIndex)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ import (
|
||||||
"github.com/scroll-tech/go-ethereum/eth/ethconfig"
|
"github.com/scroll-tech/go-ethereum/eth/ethconfig"
|
||||||
"github.com/scroll-tech/go-ethereum/node"
|
"github.com/scroll-tech/go-ethereum/node"
|
||||||
"github.com/scroll-tech/go-ethereum/params"
|
"github.com/scroll-tech/go-ethereum/params"
|
||||||
|
"github.com/scroll-tech/go-ethereum/rollup/rcfg"
|
||||||
"github.com/scroll-tech/go-ethereum/rpc"
|
"github.com/scroll-tech/go-ethereum/rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -182,14 +183,26 @@ func TestToFilterArg(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
|
testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
|
||||||
testAddr = crypto.PubkeyToAddress(testKey.PublicKey)
|
testAddr = crypto.PubkeyToAddress(testKey.PublicKey)
|
||||||
testBalance = big.NewInt(2e15)
|
emptyAccountKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f292")
|
||||||
|
emptyAddr = crypto.PubkeyToAddress(emptyAccountKey.PublicKey)
|
||||||
|
testBalance = big.NewInt(2e15)
|
||||||
)
|
)
|
||||||
|
|
||||||
var genesis = &core.Genesis{
|
var genesis = &core.Genesis{
|
||||||
Config: params.AllEthashProtocolChanges,
|
Config: params.AllEthashProtocolChanges,
|
||||||
Alloc: core.GenesisAlloc{testAddr: {Balance: testBalance}},
|
Alloc: core.GenesisAlloc{
|
||||||
|
testAddr: {Balance: testBalance},
|
||||||
|
rcfg.L1GasPriceOracleAddress: {
|
||||||
|
Balance: big.NewInt(0),
|
||||||
|
Storage: map[common.Hash]common.Hash{
|
||||||
|
rcfg.L1BaseFeeSlot: common.BigToHash(big.NewInt(10000)),
|
||||||
|
rcfg.OverheadSlot: common.BigToHash(big.NewInt(10000)),
|
||||||
|
rcfg.ScalarSlot: common.BigToHash(big.NewInt(10000)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
ExtraData: []byte("test genesis"),
|
ExtraData: []byte("test genesis"),
|
||||||
Timestamp: 9000,
|
Timestamp: 9000,
|
||||||
BaseFee: big.NewInt(params.InitialBaseFee),
|
BaseFee: big.NewInt(params.InitialBaseFee),
|
||||||
|
|
@ -285,6 +298,9 @@ func TestEthClient(t *testing.T) {
|
||||||
"CallContract": {
|
"CallContract": {
|
||||||
func(t *testing.T) { testCallContract(t, client) },
|
func(t *testing.T) { testCallContract(t, client) },
|
||||||
},
|
},
|
||||||
|
"EstimateGas": {
|
||||||
|
func(t *testing.T) { testEstimateGas(t, client) },
|
||||||
|
},
|
||||||
"AtFunctions": {
|
"AtFunctions": {
|
||||||
func(t *testing.T) { testAtFunctions(t, client) },
|
func(t *testing.T) { testAtFunctions(t, client) },
|
||||||
},
|
},
|
||||||
|
|
@ -534,6 +550,22 @@ func testCallContract(t *testing.T, client *rpc.Client) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testEstimateGas(t *testing.T, client *rpc.Client) {
|
||||||
|
ec := NewClient(client)
|
||||||
|
|
||||||
|
// EstimateGas
|
||||||
|
msg := ethereum.CallMsg{
|
||||||
|
From: emptyAddr,
|
||||||
|
To: &common.Address{},
|
||||||
|
GasPrice: big.NewInt(1000000000),
|
||||||
|
}
|
||||||
|
_, err := ec.EstimateGas(context.Background(), msg)
|
||||||
|
|
||||||
|
if err == nil || err.Error() != "insufficient funds for l1 fee" {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testAtFunctions(t *testing.T, client *rpc.Client) {
|
func testAtFunctions(t *testing.T, client *rpc.Client) {
|
||||||
ec := NewClient(client)
|
ec := NewClient(client)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,13 +34,16 @@ import (
|
||||||
"github.com/scroll-tech/go-ethereum/ethclient"
|
"github.com/scroll-tech/go-ethereum/ethclient"
|
||||||
"github.com/scroll-tech/go-ethereum/node"
|
"github.com/scroll-tech/go-ethereum/node"
|
||||||
"github.com/scroll-tech/go-ethereum/params"
|
"github.com/scroll-tech/go-ethereum/params"
|
||||||
|
"github.com/scroll-tech/go-ethereum/rollup/rcfg"
|
||||||
"github.com/scroll-tech/go-ethereum/rpc"
|
"github.com/scroll-tech/go-ethereum/rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
|
testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
|
||||||
testAddr = crypto.PubkeyToAddress(testKey.PublicKey)
|
testAddr = crypto.PubkeyToAddress(testKey.PublicKey)
|
||||||
testBalance = big.NewInt(2e15)
|
emptyAccountKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f292")
|
||||||
|
emptyAddr = crypto.PubkeyToAddress(emptyAccountKey.PublicKey)
|
||||||
|
testBalance = big.NewInt(2e15)
|
||||||
)
|
)
|
||||||
|
|
||||||
func newTestBackend(t *testing.T) (*node.Node, []*types.Block) {
|
func newTestBackend(t *testing.T) (*node.Node, []*types.Block) {
|
||||||
|
|
@ -72,8 +75,18 @@ func generateTestChain() (*core.Genesis, []*types.Block) {
|
||||||
db := rawdb.NewMemoryDatabase()
|
db := rawdb.NewMemoryDatabase()
|
||||||
config := params.AllEthashProtocolChanges
|
config := params.AllEthashProtocolChanges
|
||||||
genesis := &core.Genesis{
|
genesis := &core.Genesis{
|
||||||
Config: config,
|
Config: config,
|
||||||
Alloc: core.GenesisAlloc{testAddr: {Balance: testBalance}},
|
Alloc: core.GenesisAlloc{
|
||||||
|
testAddr: {Balance: testBalance},
|
||||||
|
rcfg.L1GasPriceOracleAddress: {
|
||||||
|
Balance: big.NewInt(0),
|
||||||
|
Storage: map[common.Hash]common.Hash{
|
||||||
|
rcfg.L1BaseFeeSlot: common.BigToHash(big.NewInt(10000)),
|
||||||
|
rcfg.OverheadSlot: common.BigToHash(big.NewInt(10000)),
|
||||||
|
rcfg.ScalarSlot: common.BigToHash(big.NewInt(10000)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
ExtraData: []byte("test genesis"),
|
ExtraData: []byte("test genesis"),
|
||||||
Timestamp: 9000,
|
Timestamp: 9000,
|
||||||
}
|
}
|
||||||
|
|
@ -126,6 +139,9 @@ func TestGethClient(t *testing.T) {
|
||||||
}, {
|
}, {
|
||||||
"TestCallContract",
|
"TestCallContract",
|
||||||
func(t *testing.T) { testCallContract(t, client) },
|
func(t *testing.T) { testCallContract(t, client) },
|
||||||
|
}, {
|
||||||
|
"TestCallContractNoGas",
|
||||||
|
func(t *testing.T) { testCallContractNoGas(t, client) },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
@ -306,3 +322,20 @@ func testCallContract(t *testing.T, client *rpc.Client) {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testCallContractNoGas(t *testing.T, client *rpc.Client) {
|
||||||
|
ec := New(client)
|
||||||
|
msg := ethereum.CallMsg{
|
||||||
|
From: emptyAddr, // 0 balance
|
||||||
|
To: &common.Address{},
|
||||||
|
Gas: 21000,
|
||||||
|
GasPrice: big.NewInt(0), // gas price 0
|
||||||
|
Value: big.NewInt(0),
|
||||||
|
}
|
||||||
|
|
||||||
|
// this would fail with `insufficient funds for gas * price + value`
|
||||||
|
// before we started considering l1fee for 0 gas calls.
|
||||||
|
if _, err := ec.CallContract(context.Background(), msg, big.NewInt(0), nil); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ import (
|
||||||
"github.com/scroll-tech/go-ethereum/p2p"
|
"github.com/scroll-tech/go-ethereum/p2p"
|
||||||
"github.com/scroll-tech/go-ethereum/params"
|
"github.com/scroll-tech/go-ethereum/params"
|
||||||
"github.com/scroll-tech/go-ethereum/rlp"
|
"github.com/scroll-tech/go-ethereum/rlp"
|
||||||
|
"github.com/scroll-tech/go-ethereum/rollup/fees"
|
||||||
"github.com/scroll-tech/go-ethereum/rpc"
|
"github.com/scroll-tech/go-ethereum/rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -848,11 +849,12 @@ func (s *PublicBlockChainAPI) GetStorageAt(ctx context.Context, address common.A
|
||||||
// if statDiff is set, all diff will be applied first and then execute the call
|
// if statDiff is set, all diff will be applied first and then execute the call
|
||||||
// message.
|
// message.
|
||||||
type OverrideAccount struct {
|
type OverrideAccount struct {
|
||||||
Nonce *hexutil.Uint64 `json:"nonce"`
|
Nonce *hexutil.Uint64 `json:"nonce"`
|
||||||
Code *hexutil.Bytes `json:"code"`
|
Code *hexutil.Bytes `json:"code"`
|
||||||
Balance **hexutil.Big `json:"balance"`
|
Balance **hexutil.Big `json:"balance"`
|
||||||
State *map[common.Hash]common.Hash `json:"state"`
|
BalanceAdd **hexutil.Big `json:"balanceAdd"`
|
||||||
StateDiff *map[common.Hash]common.Hash `json:"stateDiff"`
|
State *map[common.Hash]common.Hash `json:"state"`
|
||||||
|
StateDiff *map[common.Hash]common.Hash `json:"stateDiff"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// StateOverride is the collection of overridden accounts.
|
// StateOverride is the collection of overridden accounts.
|
||||||
|
|
@ -873,9 +875,16 @@ func (diff *StateOverride) Apply(state *state.StateDB) error {
|
||||||
state.SetCode(addr, *account.Code)
|
state.SetCode(addr, *account.Code)
|
||||||
}
|
}
|
||||||
// Override account balance.
|
// Override account balance.
|
||||||
|
if account.Balance != nil && account.BalanceAdd != nil {
|
||||||
|
return fmt.Errorf("account %s has both 'balance' and 'balanceAdd'", addr.Hex())
|
||||||
|
}
|
||||||
if account.Balance != nil {
|
if account.Balance != nil {
|
||||||
state.SetBalance(addr, (*big.Int)(*account.Balance))
|
state.SetBalance(addr, (*big.Int)(*account.Balance))
|
||||||
}
|
}
|
||||||
|
if account.BalanceAdd != nil {
|
||||||
|
balance := big.NewInt(0).Add(state.GetBalance(addr), (*big.Int)(*account.BalanceAdd))
|
||||||
|
state.SetBalance(addr, balance)
|
||||||
|
}
|
||||||
if account.State != nil && account.StateDiff != nil {
|
if account.State != nil && account.StateDiff != nil {
|
||||||
return fmt.Errorf("account %s has both 'state' and 'stateDiff'", addr.Hex())
|
return fmt.Errorf("account %s has both 'state' and 'stateDiff'", addr.Hex())
|
||||||
}
|
}
|
||||||
|
|
@ -893,6 +902,54 @@ func (diff *StateOverride) Apply(state *state.StateDB) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newRPCBalance(balance *big.Int) **hexutil.Big {
|
||||||
|
rpcBalance := (*hexutil.Big)(balance)
|
||||||
|
return &rpcBalance
|
||||||
|
}
|
||||||
|
|
||||||
|
func CalculateL1MsgFee(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, timeout time.Duration, globalGasCap uint64, config *params.ChainConfig) (*big.Int, error) {
|
||||||
|
if !config.UsingScroll {
|
||||||
|
return big.NewInt(0), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
|
||||||
|
if state == nil || err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := overrides.Apply(state); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Setup context so it may be cancelled the call has completed
|
||||||
|
// or, in case of unmetered gas, setup a context with a timeout.
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
if timeout > 0 {
|
||||||
|
ctx, cancel = context.WithTimeout(ctx, timeout)
|
||||||
|
} else {
|
||||||
|
ctx, cancel = context.WithCancel(ctx)
|
||||||
|
}
|
||||||
|
// Make sure the context is cancelled when the call has completed
|
||||||
|
// this makes sure resources are cleaned up.
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Get a new instance of the EVM.
|
||||||
|
msg, err := args.ToMessage(globalGasCap, header.BaseFee)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
evm, _, err := b.GetEVM(ctx, msg, state, header, &vm.Config{NoBaseFee: true})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Wait for the context to be done and cancel the evm. Even if the
|
||||||
|
// EVM has finished, cancelling may be done (repeatedly)
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
evm.Cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return fees.CalculateL1MsgFee(msg, evm.StateDB)
|
||||||
|
}
|
||||||
|
|
||||||
func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, error) {
|
func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, error) {
|
||||||
defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now())
|
defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now())
|
||||||
|
|
||||||
|
|
@ -985,6 +1042,26 @@ func (e *revertError) ErrorData() interface{} {
|
||||||
// Note, this function doesn't make and changes in the state/blockchain and is
|
// Note, this function doesn't make and changes in the state/blockchain and is
|
||||||
// useful to execute and retrieve values.
|
// useful to execute and retrieve values.
|
||||||
func (s *PublicBlockChainAPI) Call(ctx context.Context, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride) (hexutil.Bytes, error) {
|
func (s *PublicBlockChainAPI) Call(ctx context.Context, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride) (hexutil.Bytes, error) {
|
||||||
|
// If gasPrice is 0 and no state override is set, make sure
|
||||||
|
// that the account has sufficient balance to cover `l1Fee`.
|
||||||
|
isGasPriceZero := args.GasPrice == nil || args.GasPrice.ToInt().Cmp(big.NewInt(0)) == 0
|
||||||
|
|
||||||
|
if overrides == nil {
|
||||||
|
overrides = &StateOverride{}
|
||||||
|
}
|
||||||
|
_, isOverrideSet := (*overrides)[args.from()]
|
||||||
|
|
||||||
|
if isGasPriceZero && !isOverrideSet {
|
||||||
|
l1Fee, err := CalculateL1MsgFee(ctx, s.b, args, blockNrOrHash, overrides, s.b.RPCEVMTimeout(), s.b.RPCGasCap(), s.b.ChainConfig())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
(*overrides)[args.from()] = OverrideAccount{
|
||||||
|
BalanceAdd: newRPCBalance(l1Fee),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result, err := DoCall(ctx, s.b, args, blockNrOrHash, overrides, s.b.RPCEVMTimeout(), s.b.RPCGasCap())
|
result, err := DoCall(ctx, s.b, args, blockNrOrHash, overrides, s.b.RPCEVMTimeout(), s.b.RPCGasCap())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -1040,12 +1117,25 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr
|
||||||
}
|
}
|
||||||
balance := state.GetBalance(*args.From) // from can't be nil
|
balance := state.GetBalance(*args.From) // from can't be nil
|
||||||
available := new(big.Int).Set(balance)
|
available := new(big.Int).Set(balance)
|
||||||
|
|
||||||
|
// account for tx value
|
||||||
if args.Value != nil {
|
if args.Value != nil {
|
||||||
if args.Value.ToInt().Cmp(available) >= 0 {
|
if args.Value.ToInt().Cmp(available) >= 0 {
|
||||||
return 0, errors.New("insufficient funds for transfer")
|
return 0, errors.New("insufficient funds for transfer")
|
||||||
}
|
}
|
||||||
available.Sub(available, args.Value.ToInt())
|
available.Sub(available, args.Value.ToInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// account for l1 fee
|
||||||
|
l1Fee, err := CalculateL1MsgFee(ctx, b, args, blockNrOrHash, nil, 0, gasCap, b.ChainConfig())
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if l1Fee.Cmp(available) >= 0 {
|
||||||
|
return 0, errors.New("insufficient funds for l1 fee")
|
||||||
|
}
|
||||||
|
available.Sub(available, l1Fee)
|
||||||
|
|
||||||
allowance := new(big.Int).Div(available, feeCap)
|
allowance := new(big.Int).Div(available, feeCap)
|
||||||
|
|
||||||
// If the allowance is larger than maximum uint64, skip checking
|
// If the allowance is larger than maximum uint64, skip checking
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import (
|
||||||
const (
|
const (
|
||||||
VersionMajor = 3 // Major version component of the current release
|
VersionMajor = 3 // Major version component of the current release
|
||||||
VersionMinor = 1 // Minor version component of the current release
|
VersionMinor = 1 // Minor version component of the current release
|
||||||
VersionPatch = 1 // Patch version component of the current release
|
VersionPatch = 2 // Patch version component of the current release
|
||||||
VersionMeta = "alpha" // Version metadata to append to the version string
|
VersionMeta = "alpha" // Version metadata to append to the version string
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue