diff --git a/core/v3/session.go b/core/v3/session.go index f9088e67..e7c53303 100644 --- a/core/v3/session.go +++ b/core/v3/session.go @@ -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 --- @@ -1044,18 +1048,6 @@ 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() @@ -1063,14 +1055,6 @@ func keccak256(data []byte) []byte { 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 // ----------------------------------------------------------------------------- diff --git a/core/v3/session_digest_test.go b/core/v3/session_digest_test.go new file mode 100644 index 00000000..4b8f1774 --- /dev/null +++ b/core/v3/session_digest_test.go @@ -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") + } +}