diff --git a/vms/platformvm/client.go b/vms/platformvm/client.go index a631176f5e46..d8880d5b204e 100644 --- a/vms/platformvm/client.go +++ b/vms/platformvm/client.go @@ -16,6 +16,7 @@ import ( "github.com/ava-labs/avalanchego/utils/formatting/address" "github.com/ava-labs/avalanchego/utils/json" "github.com/ava-labs/avalanchego/utils/rpc" + "github.com/ava-labs/avalanchego/vms/components/fee" "github.com/ava-labs/avalanchego/vms/platformvm/fx" "github.com/ava-labs/avalanchego/vms/platformvm/status" "github.com/ava-labs/avalanchego/vms/secp256k1fx" @@ -123,6 +124,10 @@ type Client interface { GetBlock(ctx context.Context, blockID ids.ID, options ...rpc.Option) ([]byte, error) // GetBlockByHeight returns the block at the given [height]. GetBlockByHeight(ctx context.Context, height uint64, options ...rpc.Option) ([]byte, error) + // GetFeeConfig returns the dynamic fee config of the chain. + GetFeeConfig(ctx context.Context, options ...rpc.Option) (*fee.Config, error) + // GetFeeState returns the current fee state of the chain. + GetFeeState(ctx context.Context, options ...rpc.Option) (fee.State, fee.GasPrice, time.Time, error) } // Client implementation for interacting with the P Chain endpoint @@ -517,6 +522,18 @@ func (c *client) GetBlockByHeight(ctx context.Context, height uint64, options .. return formatting.Decode(res.Encoding, res.Block) } +func (c *client) GetFeeConfig(ctx context.Context, options ...rpc.Option) (*fee.Config, error) { + res := &fee.Config{} + err := c.requester.SendRequest(ctx, "platform.getFeeConfig", struct{}{}, res, options...) + return res, err +} + +func (c *client) GetFeeState(ctx context.Context, options ...rpc.Option) (fee.State, fee.GasPrice, time.Time, error) { + res := &GetFeeStateReply{} + err := c.requester.SendRequest(ctx, "platform.getFeeState", struct{}{}, res, options...) + return res.State, res.Price, res.Time, err +} + func AwaitTxAccepted( c Client, ctx context.Context, diff --git a/vms/platformvm/service.go b/vms/platformvm/service.go index 0299192b6677..285fd530cc16 100644 --- a/vms/platformvm/service.go +++ b/vms/platformvm/service.go @@ -28,6 +28,7 @@ import ( "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/components/fee" "github.com/ava-labs/avalanchego/vms/components/keystore" "github.com/ava-labs/avalanchego/vms/platformvm/fx" "github.com/ava-labs/avalanchego/vms/platformvm/reward" @@ -1828,6 +1829,48 @@ func (s *Service) GetBlockByHeight(_ *http.Request, args *api.GetBlockByHeightAr return err } +// GetFeeConfig returns the dynamic fee config of the chain. +func (s *Service) GetFeeConfig(_ *http.Request, _ *struct{}, reply *fee.Config) error { + s.vm.ctx.Log.Debug("API called", + zap.String("service", "platform"), + zap.String("method", "getFeeConfig"), + ) + + // TODO: Remove after Etna is activated. + now := time.Now() + if !s.vm.Config.UpgradeConfig.IsEtnaActivated(now) { + return nil + } + + *reply = s.vm.DynamicFeeConfig + return nil +} + +type GetFeeStateReply struct { + fee.State + Price fee.GasPrice `json:"price"` + Time time.Time `json:"timestamp"` +} + +// GetFeeState returns the current fee state of the chain. +func (s *Service) GetFeeState(_ *http.Request, _ *struct{}, reply *GetFeeStateReply) error { + s.vm.ctx.Log.Debug("API called", + zap.String("service", "platform"), + zap.String("method", "getFeeState"), + ) + + s.vm.ctx.Lock.Lock() + defer s.vm.ctx.Lock.Unlock() + + reply.State = s.vm.state.GetFeeState() + reply.Price = s.vm.DynamicFeeConfig.MinGasPrice.MulExp( + reply.State.Excess, + s.vm.DynamicFeeConfig.ExcessConversionConstant, + ) + reply.Time = s.vm.state.GetTimestamp() + return nil +} + func (s *Service) getAPIUptime(staker *state.Staker) (*avajson.Float32, error) { // Only report uptimes that we have been actively tracking. if constants.PrimaryNetworkID != staker.SubnetID && !s.vm.TrackedSubnets.Contains(staker.SubnetID) { diff --git a/vms/platformvm/service_test.go b/vms/platformvm/service_test.go index a3b2e743c9bc..a1f429510538 100644 --- a/vms/platformvm/service_test.go +++ b/vms/platformvm/service_test.go @@ -33,6 +33,7 @@ import ( "github.com/ava-labs/avalanchego/utils/formatting" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/components/fee" "github.com/ava-labs/avalanchego/vms/platformvm/block" "github.com/ava-labs/avalanchego/vms/platformvm/signer" "github.com/ava-labs/avalanchego/vms/platformvm/state" @@ -1107,3 +1108,67 @@ func TestServiceGetSubnets(t *testing.T) { }, }, response.Subnets) } + +func TestGetFeeConfig(t *testing.T) { + tests := []struct { + name string + etnaTime time.Time + expected fee.Config + }{ + { + name: "pre-etna", + etnaTime: time.Now().Add(time.Hour), + expected: fee.Config{}, + }, + { + name: "post-etna", + etnaTime: time.Now().Add(-time.Hour), + expected: defaultDynamicFeeConfig, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + service, _, _ := defaultService(t) + service.vm.Config.UpgradeConfig.EtnaTime = test.etnaTime + + var reply fee.Config + require.NoError(service.GetFeeConfig(nil, nil, &reply)) + require.Equal(test.expected, reply) + }) + } +} + +func FuzzGetFeeState(f *testing.F) { + f.Fuzz(func(t *testing.T, capacity, excess uint64) { + require := require.New(t) + + service, _, _ := defaultService(t) + + var ( + expectedState = fee.State{ + Capacity: fee.Gas(capacity), + Excess: fee.Gas(excess), + } + expectedTime = time.Now() + expectedReply = GetFeeStateReply{ + State: expectedState, + Price: defaultDynamicFeeConfig.MinGasPrice.MulExp( + expectedState.Excess, + defaultDynamicFeeConfig.ExcessConversionConstant, + ), + Time: expectedTime, + } + ) + + service.vm.ctx.Lock.Lock() + service.vm.state.SetFeeState(expectedState) + service.vm.state.SetTimestamp(expectedTime) + service.vm.ctx.Lock.Unlock() + + var reply GetFeeStateReply + require.NoError(service.GetFeeState(nil, nil, &reply)) + require.Equal(expectedReply, reply) + }) +} diff --git a/vms/platformvm/vm_test.go b/vms/platformvm/vm_test.go index 08d51ce4f135..939112c03242 100644 --- a/vms/platformvm/vm_test.go +++ b/vms/platformvm/vm_test.go @@ -59,7 +59,6 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/signer" "github.com/ava-labs/avalanchego/vms/platformvm/status" "github.com/ava-labs/avalanchego/vms/platformvm/txs" - "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" "github.com/ava-labs/avalanchego/vms/platformvm/txs/txstest" "github.com/ava-labs/avalanchego/vms/secp256k1fx" @@ -68,9 +67,11 @@ import ( smeng "github.com/ava-labs/avalanchego/snow/engine/snowman" snowgetter "github.com/ava-labs/avalanchego/snow/engine/snowman/getter" timetracker "github.com/ava-labs/avalanchego/snow/networking/tracker" + feecomponent "github.com/ava-labs/avalanchego/vms/components/fee" blockbuilder "github.com/ava-labs/avalanchego/vms/platformvm/block/builder" blockexecutor "github.com/ava-labs/avalanchego/vms/platformvm/block/executor" txexecutor "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" + txfee "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" walletbuilder "github.com/ava-labs/avalanchego/wallet/chain/p/builder" walletsigner "github.com/ava-labs/avalanchego/wallet/chain/p/signer" walletcommon "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" @@ -123,6 +124,26 @@ var ( defaultMaxValidatorStake = 100 * defaultMinValidatorStake defaultBalance = 2 * defaultMaxValidatorStake // amount all genesis validators have in defaultVM + defaultStaticFeeConfig = txfee.StaticConfig{ + TxFee: defaultTxFee, + CreateSubnetTxFee: 100 * defaultTxFee, + TransformSubnetTxFee: 100 * defaultTxFee, + CreateBlockchainTxFee: 100 * defaultTxFee, + } + defaultDynamicFeeConfig = feecomponent.Config{ + Weights: feecomponent.Dimensions{ + feecomponent.Bandwidth: 1, + feecomponent.DBRead: 1, + feecomponent.DBWrite: 1, + feecomponent.Compute: 1, + }, + MaxGasCapacity: 10_000, + MaxGasPerSecond: 1_000, + TargetGasPerSecond: 500, + MinGasPrice: 1, + ExcessConversionConstant: 5_000, + } + // subnet that exists at genesis in defaultVM // Its controlKeys are keys[0], keys[1], keys[2] // Its threshold is 2 @@ -248,18 +269,14 @@ func defaultVM(t *testing.T, f fork) (*VM, *txstest.WalletFactory, database.Data UptimeLockedCalculator: uptime.NewLockedCalculator(), SybilProtectionEnabled: true, Validators: validators.NewManager(), - StaticFeeConfig: fee.StaticConfig{ - TxFee: defaultTxFee, - CreateSubnetTxFee: 100 * defaultTxFee, - TransformSubnetTxFee: 100 * defaultTxFee, - CreateBlockchainTxFee: 100 * defaultTxFee, - }, - MinValidatorStake: defaultMinValidatorStake, - MaxValidatorStake: defaultMaxValidatorStake, - MinDelegatorStake: defaultMinDelegatorStake, - MinStakeDuration: defaultMinStakingDuration, - MaxStakeDuration: defaultMaxStakingDuration, - RewardConfig: defaultRewardConfig, + StaticFeeConfig: defaultStaticFeeConfig, + DynamicFeeConfig: defaultDynamicFeeConfig, + MinValidatorStake: defaultMinValidatorStake, + MaxValidatorStake: defaultMaxValidatorStake, + MinDelegatorStake: defaultMinDelegatorStake, + MinStakeDuration: defaultMinStakingDuration, + MaxStakeDuration: defaultMaxStakingDuration, + RewardConfig: defaultRewardConfig, UpgradeConfig: upgrade.Config{ ApricotPhase3Time: apricotPhase3Time, ApricotPhase5Time: apricotPhase5Time,