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
62 changes: 60 additions & 2 deletions execute/exectypes/costly_messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ import (
cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3"
)

const (
EvmWordBytes = 32
0xnogo marked this conversation as resolved.
Show resolved Hide resolved
MessageFixedBytesPerToken = 32 * ((2 * 3) + 3)
ConstantMessagePartBytes = 32 * 14 // A message consists of 14 abi encoded fields 32B each (after encoding)
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 +347,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 +377,49 @@ 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 += MessageFixedBytesPerToken +
len(tokenAmount.ExtraData) +
len(tokenAmount.DestExecData)
}

// Calculate total message data length
dataLen := ConstantMessagePartBytes +
len(msg.Data) +
len(msg.Sender) +
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{}
198 changes: 181 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,148 @@ 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(55200), // 10_000 (exec) + 45_200 (da)
b2: plugintypes.NewUSD18(65200), // 20_000 (exec) + 45_200 (da)
b3: plugintypes.NewUSD18(75200), // 30_000 (exec) + 45_200 (da)
},
wantErr: false,
},
{
name: "message with token amounts affects DA gas calculation",
messages: []ccipocr3.Message{
{
Header: ccipocr3.RampMessageHeader{MessageID: b1},
TokenAmounts: []ccipocr3.RampTokenAmount{
{
SourcePoolAddress: []byte("source_pool"),
DestTokenAddress: []byte("dest_token"),
ExtraData: []byte("extra"),
DestExecData: []byte("exec_data"),
Amount: ccipocr3.NewBigInt(big.NewInt(1)),
},
},
Data: []byte("some_data"),
Sender: []byte("sender"),
Receiver: []byte("receiver"),
ExtraArgs: []byte("extra_args"),
},
},
messageGases: []uint64{100},
executionFee: big.NewInt(100),
dataAvailabilityFee: big.NewInt(400),
feeComponentsError: nil,
daGasConfig: ccipocr3.DataAvailabilityGasConfig{
DestDataAvailabilityOverheadGas: 1000,
DestGasPerDataAvailabilityByte: 10,
DestDataAvailabilityMultiplierBps: 200,
},
want: map[ccipocr3.Bytes32]plugintypes.USD18{
b1: plugintypes.NewUSD18(79200), // 10_000 (exec) + 69_200 (da)
},
wantErr: false,
},
{
name: "zero DA multiplier results in only overhead gas",
messages: []ccipocr3.Message{
{
Header: ccipocr3.RampMessageHeader{MessageID: b1},
Data: []byte("some_data"),
},
},
messageGases: []uint64{100},
executionFee: big.NewInt(100),
dataAvailabilityFee: big.NewInt(400),
feeComponentsError: nil,
daGasConfig: ccipocr3.DataAvailabilityGasConfig{
DestDataAvailabilityOverheadGas: 1000,
DestGasPerDataAvailabilityByte: 10,
DestDataAvailabilityMultiplierBps: 0, // Zero multiplier
},
want: map[ccipocr3.Bytes32]plugintypes.USD18{
b1: plugintypes.NewUSD18(10000), // Only exec cost, DA cost is 0
},
wantErr: false,
},
{
name: "large message with multiple tokens",
messages: []ccipocr3.Message{
{
Header: ccipocr3.RampMessageHeader{MessageID: b1},
TokenAmounts: []ccipocr3.RampTokenAmount{
{
SourcePoolAddress: make([]byte, 100), // Large token data
DestTokenAddress: make([]byte, 100),
ExtraData: make([]byte, 100),
DestExecData: make([]byte, 100),
Amount: ccipocr3.NewBigInt(big.NewInt(1)),
},
{
SourcePoolAddress: make([]byte, 100), // Second token
DestTokenAddress: make([]byte, 100),
ExtraData: make([]byte, 100),
DestExecData: make([]byte, 100),
Amount: ccipocr3.NewBigInt(big.NewInt(1)),
},
},
Data: make([]byte, 1000), // Large message data
Sender: make([]byte, 100),
Receiver: make([]byte, 100),
ExtraArgs: make([]byte, 100),
},
},
messageGases: []uint64{100},
executionFee: big.NewInt(100),
dataAvailabilityFee: big.NewInt(400),
feeComponentsError: nil,
daGasConfig: ccipocr3.DataAvailabilityGasConfig{
DestDataAvailabilityOverheadGas: 1000,
DestGasPerDataAvailabilityByte: 10,
DestDataAvailabilityMultiplierBps: 200,
},
want: map[ccipocr3.Bytes32]plugintypes.USD18{
b1: plugintypes.NewUSD18(219600), // 10_000 (exec) + 218_600 (da)
},
wantErr: false,
},
{
name: "fee components error",
messages: []ccipocr3.Message{
Expand All @@ -336,11 +470,38 @@ 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: nil,
wantErr: true,
},
{
name: "minimal message - only constant parts",
messages: []ccipocr3.Message{
{
Header: ccipocr3.RampMessageHeader{MessageID: b1},
},
},
messageGases: []uint64{100},
executionFee: big.NewInt(100),
dataAvailabilityFee: big.NewInt(400),
feeComponentsError: nil,
daGasConfig: ccipocr3.DataAvailabilityGasConfig{
DestDataAvailabilityOverheadGas: 1000,
DestGasPerDataAvailabilityByte: 10,
DestDataAvailabilityMultiplierBps: 200,
},
want: map[ccipocr3.Bytes32]plugintypes.USD18{
b1: plugintypes.NewUSD18(53600), // 10_000 (exec) + 43_600 (da)
},
wantErr: false,
},
}

Expand All @@ -352,9 +513,12 @@ 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)
if !tt.wantErr {
mockReader.EXPECT().GetMedianDataAvailabilityGasConfig(ctx).Return(tt.daGasConfig, nil)
}

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.

Loading
Loading