Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions pkg/ccip/codec/addresscodec.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package codec

import (
"encoding/base64"
"encoding/binary"
"encoding/hex"
"errors"
Expand Down Expand Up @@ -61,6 +62,28 @@ func NewAddressCodec() ccipocr3.ChainSpecificAddressCodec {
return addressCodec{}
}

// AddressBytesToStringWithBurning converts a byte slice representing a TON address into its string representation.
// It first attempts to parse the bytes with `4 byte workchain (int32) + 32 byte data` TON address
// If that fails, we expect user-friendly format (36 bytes): 1 byte flags + 1 byte workchain + 32 bytes data + 2 bytes CRC16, by validating the format and CRC16 checksum.
// If parsing fails (invalid format, length, or CRC16), returns the zero address to indicate funds should be burned.
// This is only used in the hot path in plugin (executecodec.go and msghasher.go) where we want to avoid errors and just burn funds if the address is invalid
func AddressBytesToStringWithBurning(bytes []byte) *address.Address {
Comment on lines +65 to +70
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

AddressBytesToStringWithBurning returns *address.Address, but both the function name and the doc comment say it converts to a “string representation”. This mismatch is misleading for callers; either rename it (e.g., AddressBytesToAddressWithBurning) and adjust the comment, or change the return type to a string to match the name/documentation.

Suggested change
// AddressBytesToStringWithBurning converts a byte slice representing a TON address into its string representation.
// It first attempts to parse the bytes with `4 byte workchain (int32) + 32 byte data` TON address
// If that fails, we expect user-friendly format (36 bytes): 1 byte flags + 1 byte workchain + 32 bytes data + 2 bytes CRC16, by validating the format and CRC16 checksum.
// If parsing fails (invalid format, length, or CRC16), returns the zero address to indicate funds should be burned.
// This is only used in the hot path in plugin (executecodec.go and msghasher.go) where we want to avoid errors and just burn funds if the address is invalid
func AddressBytesToStringWithBurning(bytes []byte) *address.Address {
// AddressBytesToAddressWithBurning converts a byte slice representing a TON address into a *address.Address.
// It first attempts to parse the bytes with `4 byte workchain (int32) + 32 byte data` TON address.
// If that fails, it treats the input as user-friendly format (36 bytes): 1 byte flags + 1 byte workchain + 32 bytes data + 2 bytes CRC16, validating the format and CRC16 checksum.
// If parsing fails (invalid format, length, or CRC16), it returns the zero address to indicate funds should be burned.
// This is only used in the hot path in plugin (executecodec.go and msghasher.go) where we want to avoid errors and just burn funds if the address is invalid.
func AddressBytesToAddressWithBurning(bytes []byte) *address.Address {

Copilot uses AI. Check for mistakes.
// First try parsing as raw format (4 byte workchain + 32 byte data)
if addr, err := AddressBytesToTONAddress(bytes); err == nil {
return addr
}

// user-friendly address encoding Validation with CRC16 checksum
addrStr := base64.RawURLEncoding.EncodeToString(bytes)
addr, err := address.ParseAddr(addrStr)
if err != nil {
// Checksum failed or invalid address format - return zero address to mark funds as burned
return tvm.ZeroAddress
}
Comment on lines +70 to +82
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

AddressBytesToStringWithBurning base64-encodes whatever byte slice it receives before calling address.ParseAddr. Since this is used in a hot path, consider short-circuiting invalid lengths (e.g., if len(bytes) != tvm.AddressLength) and returning tvm.ZeroAddress immediately to avoid potentially large allocations/CPU if an unexpectedly large Receiver/DestTokenAddress is provided.

Copilot uses AI. Check for mistakes.

return addr
}

// AddressBytesToString converts a byte slice representing a TON address into its string representation, only supporting standard TON addresses.
func (a addressCodec) AddressBytesToString(bytes []byte) (string, error) {
if len(bytes) != tvm.AddressLength {
Expand Down
76 changes: 76 additions & 0 deletions pkg/ccip/codec/addresscodec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,82 @@ func TestAddressCodec_TransmitterBytesToString(t *testing.T) {
}
}

func TestAddressBytesToStringWithBurning(t *testing.T) {
// Valid address
validAddr, err := address.ParseAddr("EQDtFpEwcFAEcRe5mLVh2N6C0x-_hJEM7W61_JLnSF74p4q2")
require.NoError(t, err)

// Encode valid address to user-friendly 36-byte format
validBytes, err := base64.RawURLEncoding.DecodeString(validAddr.String())
require.NoError(t, err)
require.Len(t, validBytes, 36)

tests := []struct {
name string
input []byte
wantZero bool
wantEquals *address.Address
}{
{
name: "valid user-friendly address",
input: validBytes,
wantEquals: validAddr,
},
{
name: "empty bytes",
input: []byte{},
wantZero: true,
},
{
name: "too short",
input: []byte{0x01, 0x02, 0x03},
wantZero: true,
},
{
name: "correct length but invalid CRC16",
input: func() []byte {
b := make([]byte, 36)
copy(b, validBytes)
// Corrupt the CRC16 checksum (last 2 bytes)
b[34] ^= 0xFF
b[35] ^= 0xFF
return b
}(),
wantZero: true,
},
{
name: "correct length but corrupted data",
input: func() []byte {
b := make([]byte, 36)
copy(b, validBytes)
// Corrupt address data
b[10] ^= 0xFF
return b
}(),
wantZero: true,
},
{
name: "all zeros (invalid format)",
input: make([]byte, 36),
wantZero: true,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := AddressBytesToStringWithBurning(tc.input)
require.NotNil(t, result)
if tc.wantZero {
require.Equal(t, int32(0), result.Workchain())
require.Equal(t, make([]byte, 32), result.Data())
}
if tc.wantEquals != nil {
require.True(t, tc.wantEquals.Equals(result), "expected %s, got %s", tc.wantEquals, result)
}
})
}
}

func packOracleID(oracleID uint8) []byte {
addr := make([]byte, 32)
binary.BigEndian.PutUint32(addr, uint32(oracleID))
Expand Down
26 changes: 4 additions & 22 deletions pkg/ccip/codec/executecodec.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"math/big"
"strings"

"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/tlb"
"github.com/xssnick/tonutils-go/tvm/cell"

Expand Down Expand Up @@ -101,36 +100,18 @@ func (e *executePluginCodecV1) Encode(ctx context.Context, report ccipocr3.Execu
return nil, fmt.Errorf("pack extra data: %w", err)
}

destTokenAddrStr, err := e.addressCodec.AddressBytesToString(tokenAmount.DestTokenAddress)
if err != nil {
return nil, fmt.Errorf("convert dest token address: %w", err)
}

DestPoolTonAddr, err := address.ParseAddr(destTokenAddrStr)
if err != nil {
return nil, fmt.Errorf("invalid dest token address %s: %w", destTokenAddrStr, err)
}

destPoolTonAddr := AddressBytesToStringWithBurning(tokenAmount.DestTokenAddress)
tokenAmounts = append(tokenAmounts, ocr.Any2TVMTokenTransfer{
SourcePoolAddress: poolAddrCell,
ExtraData: extraData,
DestPoolAddress: DestPoolTonAddr,
DestPoolAddress: destPoolTonAddr,
Amount: tokenAmount.Amount.Int,
DestGasAmount: destGasAmount,
})
}
}

tonReceiverAddrStr, err := e.addressCodec.AddressBytesToString(msg.Receiver)
if err != nil {
return nil, fmt.Errorf("error convert receiver address: %w", err)
}

tonReceiverAddr, err := address.ParseAddr(tonReceiverAddrStr)
if err != nil {
return nil, fmt.Errorf("invalid receiver address %s: %w", tonReceiverAddrStr, err)
}

tonReceiverAddr := AddressBytesToStringWithBurning(msg.Receiver)
header := ocr.RampMessageHeader{
Comment on lines +103 to 115
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

Encode now accepts user-friendly TON address bytes (via AddressBytesToStringWithBurning) for Receiver/DestTokenAddress, but there isn’t a test covering this new input format in executecodec_test.go. Add a test case that builds a report using 36-byte user-friendly (flags+wc+data+crc) bytes and asserts encode/decode succeeds and yields the same canonical raw address bytes.

Copilot uses AI. Check for mistakes.
MessageID: msg.Header.MessageID[:],
SourceChainSelector: uint64(msg.Header.SourceChainSelector),
Expand All @@ -139,6 +120,7 @@ func (e *executePluginCodecV1) Encode(ctx context.Context, report ccipocr3.Execu
Nonce: msg.Header.Nonce,
}

var err error
var gasLimitBigInt *big.Int
var extraArgsDecodeMap map[string]any
if len(msg.ExtraArgs) > 0 {
Expand Down
27 changes: 4 additions & 23 deletions pkg/ccip/codec/msghasher.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (

"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3"
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/tlb"
"github.com/xssnick/tonutils-go/tvm/cell"

Expand All @@ -22,7 +21,6 @@ var LeafDomainSeparator [32]byte

type messageHasherV1 struct {
lggr logger.Logger
addrCodec addressCodec
extraDataCodec ccipocr3.ExtraDataCodecBundle
}

Expand Down Expand Up @@ -67,20 +65,11 @@ func (m messageHasherV1) Hash(ctx context.Context, msg ccipocr3.Message) (ccipoc
return [32]byte{}, fmt.Errorf("pack extra data: %w", err)
}

destTokenAddrStr, err := m.addrCodec.AddressBytesToString(tokenAmount.DestTokenAddress)
if err != nil {
return [32]byte{}, fmt.Errorf("convert dest token address: %w", err)
}

DestPoolTonAddr, err := address.ParseAddr(destTokenAddrStr)
if err != nil {
return [32]byte{}, fmt.Errorf("invalid dest token address %s: %w", destTokenAddrStr, err)
}

destPoolTonAddr := AddressBytesToStringWithBurning(tokenAmount.DestTokenAddress)
tokenAmounts = append(tokenAmounts, ocr.Any2TVMTokenTransfer{
SourcePoolAddress: poolAddrCell,
ExtraData: extraData,
DestPoolAddress: DestPoolTonAddr,
DestPoolAddress: destPoolTonAddr,
Amount: tokenAmount.Amount.Int,
DestGasAmount: destGasAmount,
})
Expand All @@ -95,16 +84,8 @@ func (m messageHasherV1) Hash(ctx context.Context, msg ccipocr3.Message) (ccipoc
Nonce: msg.Header.Nonce,
}

tonReceiverAddrStr, err := m.addrCodec.AddressBytesToString(msg.Receiver)
if err != nil {
return [32]byte{}, fmt.Errorf("error convert receiver address: %w", err)
}

receiver, err := address.ParseAddr(tonReceiverAddrStr)
if err != nil {
return [32]byte{}, fmt.Errorf("invalid receiver address %s: %w", tonReceiverAddrStr, err)
}

receiver := AddressBytesToStringWithBurning(msg.Receiver)
var err error
var gasLimit *big.Int
var extraArgsDecodeMap map[string]any
if len(msg.ExtraArgs) == 0 {
Expand Down
63 changes: 59 additions & 4 deletions pkg/ccip/codec/msghasher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package codec

import (
"context"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"math/big"
Expand Down Expand Up @@ -99,13 +100,12 @@ func TestMessageHasherV1_TON(t *testing.T) {
assert.Contains(t, err.Error(), "invalid destTokenAddress address")
})

t.Run("invalid receiver address", func(t *testing.T) {
t.Run("invalid receiver address will be encoded with zero address", func(t *testing.T) {
msg := randomTONMessage(t, 5009297550715157269)
msg.Receiver = []byte("invalid_address")

_, err := hasher.Hash(ctx, msg)
require.Error(t, err)
assert.Contains(t, err.Error(), "error convert receiver address")
require.NoError(t, err)
})

t.Run("message with empty ExtraArgs", func(t *testing.T) {
Expand Down Expand Up @@ -263,7 +263,7 @@ func TestMessageHasherV1_CrossLanguageCompatibility(t *testing.T) {
lg := logger.Test(t)
hasher := NewMessageHasherV1(lg, edc)

t.Run("matches TypeScript generateMessageId", func(t *testing.T) {
t.Run("matches TypeScript generateMessageId with simple address encoding", func(t *testing.T) {
// Use exact same TON address from TypeScript test
tonAddr, err := address.ParseAddr("EQDtFpEwcFAEcRe5mLVh2N6C0x-_hJEM7W61_JLnSF74p4q2")
require.NoError(t, err)
Expand Down Expand Up @@ -317,4 +317,59 @@ func TestMessageHasherV1_CrossLanguageCompatibility(t *testing.T) {
assert.Equal(t, ccipocr3.Bytes32(expectedHashArray), hash,
"Go message hasher should produce same hash as TypeScript generateMessageId")
})

t.Run("matches TypeScript generateMessageId with user friendly address encoding", func(t *testing.T) {
// Use exact same TON address from TypeScript test
tonAddr, err := address.ParseAddr("EQDtFpEwcFAEcRe5mLVh2N6C0x-_hJEM7W61_JLnSF74p4q2")
require.NoError(t, err)

rawTonAddr, err := base64.RawURLEncoding.DecodeString(tonAddr.String())
require.NoError(t, err)
// EVM_SENDER_ADDRESS_TEST: 0x1a5fdbc891c5d4e6ad68064ae45d43146d4f9f3a
evmSenderBytes, err := hex.DecodeString("1a5fdbc891c5d4e6ad68064ae45d43146d4f9f3a")
require.NoError(t, err)

evmOnrampBytes, err := hex.DecodeString("111111c891c5d4e6ad68064ae45d43146d4f9f3a")
require.NoError(t, err)

// Create messageID as 32-byte array with value 1 (matching TypeScript messageId: 1n)
var messageID [32]byte
binary.BigEndian.PutUint64(messageID[24:], 1) // This sets the last 8 bytes to 1

ta := make([]ccipocr3.RampTokenAmount, 0)
// Create exact same message as TypeScript test
msg := ccipocr3.Message{
Header: ccipocr3.RampMessageHeader{
MessageID: messageID,
SourceChainSelector: ccipocr3.ChainSelector(909606746561742123), // CHAINSEL_EVM_TEST_90000001
DestChainSelector: ccipocr3.ChainSelector(13879075125137744094), // CHAINSEL_TON
SequenceNumber: ccipocr3.SeqNum(1),
Nonce: 0,
OnRamp: evmOnrampBytes,
},
Sender: ccipocr3.UnknownAddress(evmSenderBytes),
Data: []byte{}, // empty cell data
Receiver: rawTonAddr,
ExtraArgs: []byte{0x2}, // will be populated by mock
TokenAmounts: ta, // no token amounts
}

// Set messageID to 1
binary.BigEndian.PutUint64(msg.Header.MessageID[24:], 1)

hash, err := hasher.Hash(ctx, msg)
require.NoError(t, err)

// Run the TypeScript file to get this value:
// chainlink-ton/contracts/tests/ccip/OffRamp.spec.ts "Test generateMessageId hash compatibility with Go"
expectedHashHex := "ce60f1962af3c7c7f9d3e434dea13530564dbff46704d628ff4b2206bbc93289"
expectedHash, err := hex.DecodeString(expectedHashHex)
require.NoError(t, err)

var expectedHashArray [32]byte
copy(expectedHashArray[:], expectedHash)

assert.Equal(t, ccipocr3.Bytes32(expectedHashArray), hash,
"Go message hasher should produce same hash as TypeScript generateMessageId")
})
}
Loading