@@ -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
897906func (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
914922func (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