Skip to content

Commit c8ef15f

Browse files
authored
Merge pull request #234 from pushchain/gateway-fixes-main
fix: UV changes for Gateway Compatibility
2 parents 362e1dc + 3148e3c commit c8ef15f

7 files changed

Lines changed: 202 additions & 75 deletions

File tree

universalClient/chains/evm/client.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -386,23 +386,23 @@ func parseEVMChainID(caip2 string) (int64, error) {
386386
return chainID, nil
387387
}
388388

389-
// FetchVaultAddress calls the gateway's VAULT() public getter to retrieve the vault address.
389+
// FetchVaultAddress calls the gateway's vault() public getter to retrieve the vault address.
390390
func FetchVaultAddress(ctx context.Context, rpcClient *RPCClient, gatewayAddress ethcommon.Address) (ethcommon.Address, error) {
391-
// vaultCallSelector is the 4-byte selector for VAULT() public getter
392-
vaultCallSelector := crypto.Keccak256([]byte("VAULT()"))[:4]
391+
// vaultCallSelector is the 4-byte selector for vault() public getter
392+
vaultCallSelector := crypto.Keccak256([]byte("vault()"))[:4]
393393

394394
result, err := rpcClient.CallContract(ctx, gatewayAddress, vaultCallSelector, nil)
395395
if err != nil {
396-
return ethcommon.Address{}, fmt.Errorf("VAULT() call failed: %w", err)
396+
return ethcommon.Address{}, fmt.Errorf("vault() call failed: %w", err)
397397
}
398398

399399
if len(result) < 32 {
400-
return ethcommon.Address{}, fmt.Errorf("VAULT() returned invalid data (len=%d)", len(result))
400+
return ethcommon.Address{}, fmt.Errorf("vault() returned invalid data (len=%d)", len(result))
401401
}
402402

403403
addr := ethcommon.BytesToAddress(result[12:32])
404404
if addr == (ethcommon.Address{}) {
405-
return ethcommon.Address{}, fmt.Errorf("VAULT() returned zero address")
405+
return ethcommon.Address{}, fmt.Errorf("vault() returned zero address")
406406
}
407407

408408
return addr, nil

universalClient/chains/evm/client_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ func TestClientStartStop(t *testing.T) {
160160
bodyStr := string(body)
161161

162162
if strings.Contains(bodyStr, "eth_call") {
163-
// Return a mock vault address for VAULT() call
163+
// Return a mock vault address for vault() call
164164
w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":"` + mockVaultResult + `"}`))
165165
} else {
166166
// Default: eth_chainId response

universalClient/chains/evm/tx_builder_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import (
2222
const testVaultAddress = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
2323

2424
// newTestTxBuilder creates a TxBuilder for unit tests by directly setting the
25-
// vault address, bypassing the constructor's RPC call to VAULT().
25+
// vault address, bypassing the constructor's RPC call to vault().
2626
func newTestTxBuilder(t *testing.T) *TxBuilder {
2727
t.Helper()
2828
logger := zerolog.Nop()
@@ -834,15 +834,15 @@ func TestSimulateBSC_FetchVaultFromGateway(t *testing.T) {
834834
defer cancel()
835835

836836
gwAddr := ethcommon.HexToAddress(bscGatewayAddress)
837-
vaultCallSelector := crypto.Keccak256([]byte("VAULT()"))[:4]
837+
vaultCallSelector := crypto.Keccak256([]byte("vault()"))[:4]
838838
result, err := rpcClient.CallContract(ctx, gwAddr, vaultCallSelector, nil)
839-
require.NoError(t, err, "VAULT() call should succeed")
840-
require.True(t, len(result) >= 32, "VAULT() should return at least 32 bytes")
839+
require.NoError(t, err, "vault() call should succeed")
840+
require.True(t, len(result) >= 32, "vault() should return at least 32 bytes")
841841

842842
vaultAddr := ethcommon.BytesToAddress(result[12:32])
843-
assert.NotEqual(t, ethcommon.Address{}, vaultAddr, "VAULT() should not return zero address")
844-
assert.Equal(t, ethcommon.HexToAddress(bscVaultAddress), vaultAddr, "VAULT() should match expected vault address")
845-
t.Logf("VAULT() returned: %s", vaultAddr.Hex())
843+
assert.NotEqual(t, ethcommon.Address{}, vaultAddr, "vault() should not return zero address")
844+
assert.Equal(t, ethcommon.HexToAddress(bscVaultAddress), vaultAddr, "vault() should match expected vault address")
845+
t.Logf("vault() returned: %s", vaultAddr.Hex())
846846
}
847847

848848
func TestSimulateBSC_RevertUniversalTx_Native(t *testing.T) {

universalClient/chains/svm/event_parser.go

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,10 @@ func parseSendFundsEvent(log string, signature string, slot uint64, logIndex uin
104104
// - discriminator (8 bytes)
105105
// - sub_tx_id (32 bytes)
106106
// - universal_tx_id (32 bytes)
107-
// - gas_fee (8 bytes, u64 lamports)
107+
// - gas_fee (8 bytes, u64 lamports) — prepaid budget
108+
// - gas_used (8 bytes, u64 lamports) — actual lamports consumed
109+
// - gas_to_refund (8 bytes, u64 lamports) — gas_fee - gas_used returned to caller
110+
// - ata_created (1 byte, bool) — true if SPL ATA was newly created
108111
// - push_account (20 bytes)
109112
// - target (32 bytes, Pubkey)
110113
// - token (32 bytes, Pubkey)
@@ -121,11 +124,12 @@ func parseOutboundObservationEvent(log string, signature string, slot uint64, lo
121124
return nil
122125
}
123126

124-
// Minimum: 8 disc + 32 sub_tx_id + 32 universal_tx_id + 8 gas_fee = 80 bytes
125-
if len(decoded) < 80 {
127+
// Minimum: 8 disc + 32 sub_tx_id + 32 universal_tx_id + 8 gas_fee + 8 gas_used
128+
// + 8 gas_to_refund + 1 ata_created = 97 bytes.
129+
if len(decoded) < 97 {
126130
logger.Warn().
127131
Int("data_len", len(decoded)).
128-
Msg("data too short for outboundObservation event; need at least 80 bytes")
132+
Msg("data too short for outboundObservation event; need at least 97 bytes")
129133
return nil
130134
}
131135

@@ -150,14 +154,18 @@ func parseOutboundObservationEvent(log string, signature string, slot uint64, lo
150154
universalTxID := "0x" + hex.EncodeToString(decoded[offset:offset+32])
151155
offset += 32
152156

153-
// Extract gas_fee (8 bytes, u64 little-endian lamports)
154-
gasFee := binary.LittleEndian.Uint64(decoded[offset : offset+8])
157+
// Skip gas_fee (prepaid budget, 8 bytes); the audited finalize event reports
158+
// gas_used separately and that's the value we want to surface as GasFeeUsed.
159+
offset += 8
160+
161+
// Extract gas_used (8 bytes, u64 little-endian lamports) — actual gas consumed.
162+
gasUsed := binary.LittleEndian.Uint64(decoded[offset : offset+8])
155163

156164
// Create OutboundEvent payload
157165
payload := common.OutboundEvent{
158166
TxID: txID,
159167
UniversalTxID: universalTxID,
160-
GasFeeUsed: fmt.Sprintf("%d", gasFee),
168+
GasFeeUsed: fmt.Sprintf("%d", gasUsed),
161169
}
162170

163171
// Marshal payload to JSON
@@ -185,7 +193,7 @@ func parseOutboundObservationEvent(log string, signature string, slot uint64, lo
185193
Str("event_id", eventID).
186194
Str("tx_id", txID).
187195
Str("universal_tx_id", universalTxID).
188-
Str("gas_fee", fmt.Sprintf("%d", gasFee)).
196+
Str("gas_used", fmt.Sprintf("%d", gasUsed)).
189197
Msg("parsed outboundObservation event")
190198

191199
return event

universalClient/chains/svm/event_parser_test.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,19 @@ func wrapAsLog(data []byte) string {
8686
return "Program data: " + base64.StdEncoding.EncodeToString(data)
8787
}
8888

89-
// buildOutboundPayload builds the minimum 80-byte outbound event data.
90-
func buildOutboundPayload(txID [32]byte, universalTxID [32]byte, gasFee uint64) []byte {
91-
data := make([]byte, 80)
89+
// buildOutboundPayload builds the minimum 97-byte outbound event data.
90+
// The audited finalize event surfaces gas_used (offset 80..88) as the value
91+
// the parser reports as GasFeeUsed; gas_fee (offset 72..80) is the prepaid
92+
// budget and is skipped. Tests pass `gasUsed` to match what the parser will
93+
// extract; gas_fee in the payload is left zero.
94+
func buildOutboundPayload(txID [32]byte, universalTxID [32]byte, gasUsed uint64) []byte {
95+
data := make([]byte, 97)
9296
// discriminator (8 bytes, zeroed is fine)
9397
copy(data[8:40], txID[:])
9498
copy(data[40:72], universalTxID[:])
95-
binary.LittleEndian.PutUint64(data[72:80], gasFee)
99+
// gas_fee at 72..80 (prepaid budget, left zero in tests)
100+
binary.LittleEndian.PutUint64(data[80:88], gasUsed)
101+
// gas_to_refund at 88..96 (left zero); ata_created at 96 (left zero)
96102
return data
97103
}
98104

@@ -406,20 +412,21 @@ func TestParseOutboundObservationEvent(t *testing.T) {
406412
})
407413

408414
t.Run("returns nil for data too short", func(t *testing.T) {
409-
shortData := make([]byte, 72) // needs 80
415+
shortData := make([]byte, 96) // needs 97
410416
event := ParseEvent(wrapAsLog(shortData), signature, 12345, 0, EventTypeFinalizeUniversalTx, chainID, logger)
411417
assert.Nil(t, event)
412418
})
413419

414-
t.Run("parses minimum valid data (exactly 80 bytes)", func(t *testing.T) {
415-
data := make([]byte, 80)
420+
t.Run("parses minimum valid data (exactly 97 bytes)", func(t *testing.T) {
421+
data := make([]byte, 97)
416422
for i := 8; i < 40; i++ {
417423
data[i] = 0x11
418424
}
419425
for i := 40; i < 72; i++ {
420426
data[i] = 0x22
421427
}
422-
binary.LittleEndian.PutUint64(data[72:80], 12345)
428+
// gas_used at 80..88
429+
binary.LittleEndian.PutUint64(data[80:88], 12345)
423430

424431
event := ParseEvent(wrapAsLog(data), signature, 100, 0, EventTypeFinalizeUniversalTx, chainID, logger)
425432
require.NotNil(t, event)
@@ -431,7 +438,7 @@ func TestParseOutboundObservationEvent(t *testing.T) {
431438
assert.Equal(t, "12345", outbound.GasFeeUsed)
432439
})
433440

434-
t.Run("handles data longer than 80 bytes", func(t *testing.T) {
441+
t.Run("handles data longer than 97 bytes", func(t *testing.T) {
435442
var txID, utxID [32]byte
436443
for i := range txID {
437444
txID[i] = 0xAA

universalClient/chains/svm/tx_builder.go

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ func (tb *TxBuilder) GetOutboundSigningRequest(
322322
var ixData []byte
323323
var revertRecipient [32]byte
324324
var revertMint [32]byte
325+
var revertMsg []byte
325326

326327
if txType == uetypes.TxType_INBOUND_REVERT || txType == uetypes.TxType_RESCUE_FUNDS {
327328
// Revert (id=3) and rescue (id=4): instruction_id determined by TxType, no payload decode
@@ -333,6 +334,14 @@ func (tb *TxBuilder) GetOutboundSigningRequest(
333334
if !isNative {
334335
copy(revertMint[:], token[:])
335336
}
337+
// Only revert (id=3) binds keccak256(revert_msg) in the TSS message.
338+
// Rescue (id=4) doesn't carry a revert reason. Treat decode failure
339+
// as empty so the signing hash is still deterministic.
340+
if instructionID == 3 {
341+
if decoded, decErr := hex.DecodeString(removeHexPrefix(data.RevertMsg)); decErr == nil {
342+
revertMsg = decoded
343+
}
344+
}
336345
} else {
337346
// Non-revert flows: decode payload to get instruction_id.
338347
// Payload format: [accounts][ixData][instruction_id][target_program]
@@ -399,7 +408,7 @@ func (tb *TxBuilder) GetOutboundSigningRequest(
399408
instructionID, chainID, amount.Uint64(),
400409
txID, universalTxID, sender, token, gasFee,
401410
targetProgram, accounts, ixData,
402-
revertRecipient, revertMint,
411+
revertRecipient, revertMint, revertMsg,
403412
)
404413
if err != nil {
405414
return nil, fmt.Errorf("failed to construct TSS message: %w", err)
@@ -710,7 +719,7 @@ func (tb *TxBuilder) BuildOutboundTransaction(
710719
return nil, 0, fmt.Errorf("failed to derive vault PDA: %w", err)
711720
}
712721

713-
tssPDA, _, err := solana.FindProgramAddress([][]byte{[]byte("tsspda_v2")}, tb.gatewayAddress)
722+
tssPDA, _, err := solana.FindProgramAddress([][]byte{[]byte("final_tss_pda")}, tb.gatewayAddress)
714723
if err != nil {
715724
return nil, 0, fmt.Errorf("failed to derive TSS PDA: %w", err)
716725
}
@@ -893,9 +902,9 @@ func removeHexPrefix(s string) string {
893902
// - tss_eth_address: the 20-byte Ethereum address of the TSS signing group
894903
// - chain_id: identifies this Solana cluster (for cross-chain replay protection)
895904
//
896-
// Seed: ["tsspda_v2"] — must match the Rust constant TSS_SEED in state.rs
905+
// Seed: ["final_tss_pda"] — must match the Rust constant TSS_SEED in state.rs
897906
func (tb *TxBuilder) deriveTSSPDA() (solana.PublicKey, error) {
898-
seeds := [][]byte{[]byte("tsspda_v2")}
907+
seeds := [][]byte{[]byte("final_tss_pda")}
899908
address, _, err := solana.FindProgramAddress(seeds, tb.gatewayAddress)
900909
return address, err
901910
}
@@ -909,8 +918,7 @@ func (tb *TxBuilder) deriveTSSPDA() (solana.PublicKey, error) {
909918
// 8 20 tss_eth_address [u8; 20]
910919
// 28 4 chain_id length (u32, little-endian) — Borsh String prefix
911920
// 32 N chain_id bytes (UTF-8, variable length)
912-
// 32+N 32 authority (Pubkey)
913-
// 32+N+32 1 bump
921+
// 32+N 1 bump
914922
func (tb *TxBuilder) fetchTSSChainID(ctx context.Context, tssPDA solana.PublicKey) (string, error) {
915923
accountData, err := tb.rpcClient.GetAccountData(ctx, tssPDA)
916924
if err != nil {
@@ -926,7 +934,7 @@ func (tb *TxBuilder) fetchTSSChainID(ctx context.Context, tssPDA solana.PublicKe
926934
// This is NOT fixed-length — different clusters have different chain IDs.
927935
chainIDLen := binary.LittleEndian.Uint32(accountData[28:32])
928936

929-
requiredLen := 32 + int(chainIDLen) + 32 + 1
937+
requiredLen := 32 + int(chainIDLen) + 1
930938
if len(accountData) < requiredLen {
931939
return "", fmt.Errorf("invalid TSS PDA account data: too short for chain_id length %d (%d bytes)", chainIDLen, len(accountData))
932940
}
@@ -1012,6 +1020,7 @@ func (tb *TxBuilder) constructTSSMessage(
10121020
ixData []byte,
10131021
revertRecipient [32]byte,
10141022
revertMint [32]byte,
1023+
revertMsg []byte,
10151024
) ([]byte, error) {
10161025
message := []byte("PUSH_CHAIN_SVM")
10171026
message = append(message, instructionID)
@@ -1060,7 +1069,21 @@ func (tb *TxBuilder) constructTSSMessage(
10601069
message = append(message, ixDataLen...)
10611070
message = append(message, ixData...)
10621071

1063-
case 3, 4: // revert (id=3) or rescue (id=4) — same message format
1072+
case 3: // revert
1073+
message = append(message, txID[:]...)
1074+
message = append(message, universalTxID[:]...)
1075+
if revertMint != ([32]byte{}) {
1076+
// SPL: include mint before recipient
1077+
message = append(message, revertMint[:]...)
1078+
}
1079+
message = append(message, revertRecipient[:]...)
1080+
message = append(message, gasFeeBytes...)
1081+
// revert_universal_tx binds keccak256(revert_msg) as the trailing
1082+
// additional-data element so a forged revert reason cannot be
1083+
// substituted under the same TSS signature.
1084+
message = append(message, crypto.Keccak256(revertMsg)...)
1085+
1086+
case 4: // rescue — same wire format as revert minus the revert_msg binding
10641087
message = append(message, txID[:]...)
10651088
message = append(message, universalTxID[:]...)
10661089
if revertMint != ([32]byte{}) {
@@ -1076,9 +1099,7 @@ func (tb *TxBuilder) constructTSSMessage(
10761099

10771100
// Hash with keccak256. Solana's keccak::hash is the same algorithm as Ethereum's keccak256.
10781101
// NOT sha256 — Anchor uses sha256 for discriminators, but TSS messages use keccak256.
1079-
messageHash := crypto.Keccak256(message)
1080-
1081-
return messageHash, nil
1102+
return crypto.Keccak256(message), nil
10821103
}
10831104

10841105
// =============================================================================

0 commit comments

Comments
 (0)