Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DA cost estimation to CCIPMessageExecCostUSD18Calculator #275

Merged
merged 11 commits into from
Nov 1, 2024
67 changes: 65 additions & 2 deletions execute/exectypes/costly_messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ import (
cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3"
)

const (
EvmWordBytes = 32
ConstantMessagePartBytes = 10 * 32 // A message consists of 10 abi encoded fields 32B each (after encoding)
0xnogo marked this conversation as resolved.
Show resolved Hide resolved
daMultiplierBase = 10_000 // DA multiplier is in multiples of 0.0001, i.e. 1/daMultiplierBase
)

// CostlyMessageObserver observes messages that are too costly to execute.
type CostlyMessageObserver interface {
// Observe takes a set of messages and returns a slice of message IDs that are too costly to execute.
Expand Down Expand Up @@ -340,11 +346,17 @@ func (c *CCIPMessageExecCostUSD18Calculator) MessageExecCostUSD18(
if feeComponents.ExecutionFee == nil {
return nil, fmt.Errorf("missing execution fee")
}
if feeComponents.DataAvailabilityFee == nil {
return nil, fmt.Errorf("missing data availability fee")
}
daConfig, err := c.ccipReader.GetMedianDataAvailabilityGasConfig(ctx)
if err != nil {
return nil, fmt.Errorf("unable to get data availability gas config: %w", err)
}

for _, msg := range messages {
executionCostUSD18 := c.computeExecutionCostUSD18(feeComponents.ExecutionFee, msg)
// TODO: implement data availability cost
dataAvailabilityCostUSD18 := new(big.Int)
dataAvailabilityCostUSD18 := computeDataAvailabilityCostUSD18(feeComponents.DataAvailabilityFee, daConfig, msg)
totalCostUSD18 := new(big.Int).Add(executionCostUSD18, dataAvailabilityCostUSD18)
messageExecCosts[msg.Header.MessageID] = totalCostUSD18
}
Expand All @@ -364,4 +376,55 @@ func (c *CCIPMessageExecCostUSD18Calculator) computeExecutionCostUSD18(
return cost
}

// computeDataAvailabilityCostUSD18 computes the data availability cost of a message in USD18s.
func computeDataAvailabilityCostUSD18(
dataAvailabilityFee *big.Int,
daConfig cciptypes.DataAvailabilityGasConfig,
message cciptypes.Message,
) plugintypes.USD18 {
if dataAvailabilityFee == nil || dataAvailabilityFee.Cmp(big.NewInt(0)) == 0 {
return big.NewInt(0)
}

messageGas := calculateMessageMaxDAGas(message, daConfig)
return big.NewInt(0).Mul(messageGas, dataAvailabilityFee)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this actually returning fee in USD or fee in the native currency?

there is a pre-existing comment that says executionFee (USD18/gas) so I presume dataAvailabilityFee is denoted in USD/gas as opposed to native/gas, but after reading the code, it seems feeComponents is read straight from the ChainWriter, does chain writer return native/gas (which I thought should be the case and will make this impl incorrect). or actually usd/gas, please double check

Copy link
Contributor

@0xnogo 0xnogo Oct 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like USD/gas: https://github.com/smartcontractkit/chainlink/blob/a1e4f8e960d4b5bf05e353dec3254d13c9a3b4c8/contracts/src/v0.8/ccip/FeeQuoter.sol#L303

function getDestinationChainGasPrice(
    uint64 destChainSelector
  ) external view returns (Internal.TimestampedPackedUint224 memory) {
    return s_usdPerUnitGasByDestChainSelector[destChainSelector];
  }

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still confused, isn't the value fetched from ChainWriter, maybe I missed it, where is it read from FeeQuoter?

}

// calculateMessageMaxDAGas calculates the total DA gas needed for a CCIP message
func calculateMessageMaxDAGas(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DA is quite specific to EVM, I'm not expert of v2 code base, wondering if you want to put it under an EVM folder somewhere?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You right. We will work on making this logic chain abstracted in a second step

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The priority is to unblock the itests right now, so our plan is to merge this, then move this logic to chainlink with the other EVM specific logic

msg cciptypes.Message,
daConfig cciptypes.DataAvailabilityGasConfig,
) *big.Int {
// Calculate token data length
var totalTokenDataLen int
for _, tokenAmount := range msg.TokenAmounts {
0xnogo marked this conversation as resolved.
Show resolved Hide resolved
totalTokenDataLen += len(tokenAmount.SourcePoolAddress) +
0xnogo marked this conversation as resolved.
Show resolved Hide resolved
len(tokenAmount.DestTokenAddress) +
len(tokenAmount.ExtraData) +
EvmWordBytes +
len(tokenAmount.DestExecData)
}

// Calculate total message data length
dataLen := ConstantMessagePartBytes +
len(msg.Data) +
len(msg.Sender) +
len(msg.Receiver) +
len(msg.ExtraArgs) +
len(msg.FeeToken) +
EvmWordBytes*2 + // FeeTokenAmount and FeeValueJuels
0xnogo marked this conversation as resolved.
Show resolved Hide resolved
totalTokenDataLen

// Calculate base gas cost
dataGas := big.NewInt(int64(dataLen))
dataGas = new(big.Int).Mul(dataGas, big.NewInt(int64(daConfig.DestGasPerDataAvailabilityByte)))
dataGas = new(big.Int).Add(dataGas, big.NewInt(int64(daConfig.DestDataAvailabilityOverheadGas)))

// Then apply the multiplier as: (dataGas * daMultiplier) / multiplierBase
dataGas = new(big.Int).Mul(dataGas, big.NewInt(int64(daConfig.DestDataAvailabilityMultiplierBps)))
dataGas = new(big.Int).Div(dataGas, big.NewInt(daMultiplierBase))

return dataGas
}

var _ MessageExecCostUSD18Calculator = &CCIPMessageExecCostUSD18Calculator{}
78 changes: 61 additions & 17 deletions execute/exectypes/costly_messages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,16 +292,18 @@ func TestCCIPMessageFeeE18USDCalculator_MessageFeeE18USD(t *testing.T) {

func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) {
tests := []struct {
name string
messages []ccipocr3.Message
messageGases []uint64
executionFee *big.Int
feeComponentsError error
want map[ccipocr3.Bytes32]plugintypes.USD18
wantErr bool
name string
messages []ccipocr3.Message
messageGases []uint64
executionFee *big.Int
dataAvailabilityFee *big.Int
feeComponentsError error
daGasConfig ccipocr3.DataAvailabilityGasConfig
want map[ccipocr3.Bytes32]plugintypes.USD18
wantErr bool
}{
{
name: "happy path",
name: "happy path, no DA cost",
messages: []ccipocr3.Message{
{
Header: ccipocr3.RampMessageHeader{MessageID: b1},
Expand All @@ -313,16 +315,51 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) {
Header: ccipocr3.RampMessageHeader{MessageID: b3},
},
},
messageGases: []uint64{100, 200, 300},
executionFee: big.NewInt(100),
feeComponentsError: nil,
messageGases: []uint64{100, 200, 300},
executionFee: big.NewInt(100),
dataAvailabilityFee: big.NewInt(0),
feeComponentsError: nil,
daGasConfig: ccipocr3.DataAvailabilityGasConfig{
DestDataAvailabilityOverheadGas: 1,
DestGasPerDataAvailabilityByte: 1,
DestDataAvailabilityMultiplierBps: 1,
},
want: map[ccipocr3.Bytes32]plugintypes.USD18{
b1: plugintypes.NewUSD18(10000),
b2: plugintypes.NewUSD18(20000),
b3: plugintypes.NewUSD18(30000),
},
wantErr: false,
},
{
name: "happy path, with DA cost",
messages: []ccipocr3.Message{
{
Header: ccipocr3.RampMessageHeader{MessageID: b1},
},
{
Header: ccipocr3.RampMessageHeader{MessageID: b2},
},
{
Header: ccipocr3.RampMessageHeader{MessageID: b3},
},
},
messageGases: []uint64{100, 200, 300},
executionFee: big.NewInt(100),
dataAvailabilityFee: big.NewInt(400),
feeComponentsError: nil,
daGasConfig: ccipocr3.DataAvailabilityGasConfig{
DestDataAvailabilityOverheadGas: 1200,
DestGasPerDataAvailabilityByte: 10,
DestDataAvailabilityMultiplierBps: 200,
},
want: map[ccipocr3.Bytes32]plugintypes.USD18{
b1: plugintypes.NewUSD18(50000),
b2: plugintypes.NewUSD18(60000),
b3: plugintypes.NewUSD18(70000),
},
wantErr: false,
},
{
name: "fee components error",
messages: []ccipocr3.Message{
Expand All @@ -336,11 +373,17 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) {
Header: ccipocr3.RampMessageHeader{MessageID: b3},
},
},
messageGases: []uint64{100, 200, 300},
executionFee: big.NewInt(100),
feeComponentsError: fmt.Errorf("error"),
want: map[ccipocr3.Bytes32]plugintypes.USD18{},
wantErr: true,
messageGases: []uint64{100, 200, 300},
executionFee: big.NewInt(100),
dataAvailabilityFee: big.NewInt(0),
feeComponentsError: fmt.Errorf("error"),
daGasConfig: ccipocr3.DataAvailabilityGasConfig{
DestDataAvailabilityOverheadGas: 1,
DestGasPerDataAvailabilityByte: 1,
DestDataAvailabilityMultiplierBps: 1,
},
want: map[ccipocr3.Bytes32]plugintypes.USD18{},
wantErr: true,
},
}

Expand All @@ -352,9 +395,10 @@ func TestCCIPMessageExecCostUSD18Calculator_MessageExecCostUSD18(t *testing.T) {
mockReader := readerpkg_mock.NewMockCCIPReader(t)
feeComponents := types.ChainFeeComponents{
ExecutionFee: tt.executionFee,
DataAvailabilityFee: big.NewInt(0),
DataAvailabilityFee: tt.dataAvailabilityFee,
}
mockReader.EXPECT().GetDestChainFeeComponents(ctx).Return(feeComponents, tt.feeComponentsError)
mockReader.EXPECT().GetMedianDataAvailabilityGasConfig(ctx).Return(tt.daGasConfig, nil).Maybe()

ep := gasmock.NewMockEstimateProvider(t)
if !tt.wantErr {
Expand Down
6 changes: 6 additions & 0 deletions internal/mocks/inmem/ccipreader_inmem.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,5 +170,11 @@ func (r InMemoryCCIPReader) Sync(_ context.Context, _ reader.ContractAddresses)
return nil
}

func (r InMemoryCCIPReader) GetMedianDataAvailabilityGasConfig(
ctx context.Context,
) (cciptypes.DataAvailabilityGasConfig, error) {
return cciptypes.DataAvailabilityGasConfig{}, nil
}

// Interface compatibility check.
var _ reader.CCIPReader = InMemoryCCIPReader{}
56 changes: 56 additions & 0 deletions mocks/pkg/reader/ccip_reader.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

73 changes: 73 additions & 0 deletions pkg/reader/ccip.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"github.com/smartcontractkit/chainlink-common/pkg/types/query"
"github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives"

"github.com/smartcontractkit/chainlink-ccip/internal/plugincommon/consensus"

rmntypes "github.com/smartcontractkit/chainlink-ccip/commit/merkleroot/rmn/types"
typeconv "github.com/smartcontractkit/chainlink-ccip/internal/libs/typeconv"
"github.com/smartcontractkit/chainlink-ccip/internal/plugintypes"
Expand Down Expand Up @@ -1292,5 +1294,76 @@ func (r *ccipChainReader) getRMNRemoteAddress(
return rmnRemoteAddress, nil
}

// Get the DestChainConfig from the FeeQuoter contract on the given chain.
func (r *ccipChainReader) getFeeQuoterDestChainConfig(
ctx context.Context,
chainSelector cciptypes.ChainSelector,
) (cciptypes.FeeQuoterDestChainConfig, error) {
if err := validateExtendedReaderExistence(r.contractReaders, chainSelector); err != nil {
return cciptypes.FeeQuoterDestChainConfig{}, err
}

var destChainConfig cciptypes.FeeQuoterDestChainConfig
srcReader := r.contractReaders[chainSelector]
err := srcReader.ExtendedGetLatestValue(
ctx,
consts.ContractNameFeeQuoter,
consts.MethodNameGetDestChainConfig,
primitives.Unconfirmed,
map[string]any{
"destChainSelector": r.destChain,
},
&destChainConfig,
)

if err != nil {
return cciptypes.FeeQuoterDestChainConfig{}, fmt.Errorf("failed to get dest chain config: %w", err)
}

return destChainConfig, nil
}

// GetMedianDataAvailabilityGasConfig returns the median of the DataAvailabilityGasConfig values from all FeeQuoters
// DA data lives in the FeeQuoter contract on the source chain. To get the config of the destination chain, we need to
// read the FeeQuoter contract on the source chain. As nodes are not required to have all chains configured, we need to
// read all FeeQuoter contracts to get the median.
func (r *ccipChainReader) GetMedianDataAvailabilityGasConfig(
0xnogo marked this conversation as resolved.
Show resolved Hide resolved
ctx context.Context,
) (cciptypes.DataAvailabilityGasConfig, error) {
overheadGasValues := make([]uint32, 0)
gasPerByteValues := make([]uint16, 0)
multiplierBpsValues := make([]uint16, 0)

// TODO: pay attention to performance here, as we are looping through all chains
for chain := range r.contractReaders {
config, err := r.getFeeQuoterDestChainConfig(ctx, chain)
0xnogo marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
continue
}
if config.IsEnabled && config.HasNonEmptyDAGasParams() {
overheadGasValues = append(overheadGasValues, config.DestDataAvailabilityOverheadGas)
gasPerByteValues = append(gasPerByteValues, config.DestGasPerDataAvailabilityByte)
multiplierBpsValues = append(multiplierBpsValues, config.DestDataAvailabilityMultiplierBps)
}
}

if len(overheadGasValues) == 0 {
return cciptypes.DataAvailabilityGasConfig{}, fmt.Errorf("no valid fee quoter destChainConfigs found")
}

// Calculate medians
medianOverheadGas := consensus.Median(overheadGasValues, func(a, b uint32) bool { return a < b })
medianGasPerByte := consensus.Median(gasPerByteValues, func(a, b uint16) bool { return a < b })
medianMultiplierBps := consensus.Median(multiplierBpsValues, func(a, b uint16) bool { return a < b })

daConfig := cciptypes.DataAvailabilityGasConfig{
DestDataAvailabilityOverheadGas: medianOverheadGas,
DestGasPerDataAvailabilityByte: medianGasPerByte,
DestDataAvailabilityMultiplierBps: medianMultiplierBps,
}

return daConfig, nil
}

// Interface compliance check
var _ CCIPReader = (*ccipChainReader)(nil)
3 changes: 3 additions & 0 deletions pkg/reader/ccip_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,7 @@ type CCIPReader interface {
// Sync can be used to perform frequent syncing operations inside the reader implementation.
// Returns a bool indicating whether something was updated.
Sync(ctx context.Context, contracts ContractAddresses) error

// GetMedianDataAvailabilityGasConfig returns the median of the DataAvailabilityGasConfig values from all FeeQuoters
GetMedianDataAvailabilityGasConfig(ctx context.Context) (cciptypes.DataAvailabilityGasConfig, error)
}
Loading
Loading