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
48 changes: 16 additions & 32 deletions core/v3/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -1005,18 +1005,22 @@ func isExplicitSessionCallSignature(sig SessionCallSignature) bool {
}

// --- Hashing call with replay protection ---
//
// hashCallWithReplayProtection computes the hash of a call with chain id, space and nonce.
// It concatenates 32-byte representations of chainId, space and nonce with the call hash,
// then computes the Keccak256 hash, returning it as a hex string.
func hashCallWithReplayProtection(call PayloadCall, chainId, space, nonce *big.Int) (string, error) {
chainIdB := intToBytesBig(chainId, 32)
spaceB := intToBytesBig(space, 32)
nonceB := intToBytesBig(nonce, 32)
callHash := call.HashCall() // Assume call.HashCall() returns []byte in hex format? Adjust as needed.
data := ConcatBytes(chainIdB, spaceB, nonceB, callHash)
hash := keccak256(data)
return "0x" + hex.EncodeToString(hash), nil

// HashPayloadCallIdx computes the digest that a session key signs for
// the call at callIdx of the given payload. It matches
// SessionSig.hashPayloadCallIdx in the v3 wallet contracts:
// keccak256(Payload.hashFor(payload, wallet) ++ uint256(callIdx)), where the
// payload hash is the EIP-712 digest with the wallet as verifying contract.
// The session signature is recovered with plain ecrecover over this digest,
// without an EIP-191 prefix.
func HashPayloadCallIdx(payload CallsPayload, callIdx int) (common.Hash, error) {
if callIdx < 0 || callIdx >= len(payload.Calls) {
return common.Hash{}, fmt.Errorf("call index %v out of range [0, %v)", callIdx, len(payload.Calls))
}
payloadHash := payload.Digest().Hash
var idx [32]byte
big.NewInt(int64(callIdx)).FillBytes(idx[:])
return common.BytesToHash(keccak256(ConcatBytes(payloadHash.Bytes(), idx[:]))), nil
}

// --- Helper functions ---
Expand Down Expand Up @@ -1044,33 +1048,13 @@ func intToBytes(n int, size int) []byte {
return b
}

// intToBytesBig converts a *big.Int to a byte slice of a given size.
// It left-pads the number with zeros.
func intToBytesBig(n *big.Int, size int) []byte {
b := n.Bytes()
if len(b) > size {
return b[len(b)-size:]
}
padded := make([]byte, size)
copy(padded[size-len(b):], b)
return padded
}

// keccak256 computes the Keccak256 hash of the given data.
func keccak256(data []byte) []byte {
h := sha3.NewLegacyKeccak256()
h.Write(data)
return h.Sum(nil)
}

// --- PayloadCall interface ---
//
// For hashing calls we assume a minimal interface.
type PayloadCall interface {
// HashCall returns a []byte representing the call data to be hashed.
HashCall() []byte
}

// -----------------------------------------------------------------------------
// Helper Functions
// -----------------------------------------------------------------------------
Expand Down
86 changes: 86 additions & 0 deletions core/v3/session_digest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package v3_test

import (
"math/big"
"testing"

"github.com/0xsequence/ethkit/go-ethereum/common"
v3 "github.com/0xsequence/go-sequence/core/v3"
)

// The expected digests below are reference vectors produced by
// SessionSig.hashPayloadCallIdx in 0xsequence/wallet-contracts-v3 (via a forge
// test constructing the identical payloads), so this test asserts parity with
// the on-chain verification.
func TestHashPayloadCallIdx(t *testing.T) {
payload := v3.NewCallsPayload(
common.HexToAddress("0x1111111111111111111111111111111111111111"),
big.NewInt(42161),
[]v3.Call{
{
To: common.HexToAddress("0x2222222222222222222222222222222222222222"),
Value: new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil), // 1 ether
Data: common.Hex2Bytes("deadbeef"),
GasLimit: big.NewInt(100000),
BehaviorOnError: v3.BehaviorOnErrorRevert,
},
{
To: common.HexToAddress("0x3333333333333333333333333333333333333333"),
DelegateCall: true,
OnlyFallback: true,
BehaviorOnError: v3.BehaviorOnErrorAbort,
},
},
big.NewInt(0),
big.NewInt(7),
)

expectedPayloadHash := common.HexToHash("0x9c891ce70c80739f54b60eb7f8a0d0e80e7f90f2c9aea51a0c4f34ec1cee067e")
if digest := payload.Digest().Hash; digest != expectedPayloadHash {
t.Errorf("payload digest mismatch: got %v, expected %v", digest, expectedPayloadHash)
}

expectedCallHashes := []common.Hash{
common.HexToHash("0x1957bab0f26824823a1f40bb132e9a6ab186db88c0adb7fb806351f6766ba66c"),
common.HexToHash("0xcb5346174462b1c9a0279b5acbdfe2458ba0825cb10d35eb912fa491c1ee5cdd"),
}
for i, expected := range expectedCallHashes {
hash, err := v3.HashPayloadCallIdx(payload, i)
if err != nil {
t.Fatalf("HashPayloadCallIdx(%v): %v", i, err)
}
if hash != expected {
t.Errorf("call %v hash mismatch: got %v, expected %v", i, hash, expected)
}
}

payload2 := v3.NewCallsPayload(
common.HexToAddress("0x5555555555555555555555555555555555555555"),
big.NewInt(1),
[]v3.Call{
{
To: common.HexToAddress("0x4444444444444444444444444444444444444444"),
Data: []byte{0x00},
BehaviorOnError: v3.BehaviorOnErrorIgnore,
},
},
big.NewInt(12345),
big.NewInt(0),
)

expected2 := common.HexToHash("0xde2ebba8ab9a581d22dbb5d066086ae1ab3d884f9becc493448b2875e7f3c6d4")
hash2, err := v3.HashPayloadCallIdx(payload2, 0)
if err != nil {
t.Fatalf("HashPayloadCallIdx: %v", err)
}
if hash2 != expected2 {
t.Errorf("vector 2 hash mismatch: got %v, expected %v", hash2, expected2)
}

if _, err := v3.HashPayloadCallIdx(payload2, 1); err == nil {
t.Errorf("expected out-of-range error for call index 1")
}
if _, err := v3.HashPayloadCallIdx(payload2, -1); err == nil {
t.Errorf("expected out-of-range error for call index -1")
}
}
Loading