Skip to content

Commit

Permalink
Session key fixes and test (#2138)
Browse files Browse the repository at this point in the history
* Session key fixes and test

* Session key fixes and test
  • Loading branch information
tudor-malene authored Nov 12, 2024
1 parent 8d69de4 commit 7f88ff5
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 28 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.21.11
replace github.com/docker/docker => github.com/docker/docker v20.10.3-0.20220224222438-c78f6963a1c0+incompatible

require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0
github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v1.1.0
github.com/FantasyJony/openzeppelin-merkle-tree-go v1.1.3
github.com/Microsoft/go-winio v0.6.2
Expand Down Expand Up @@ -60,7 +61,6 @@ require (
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/DataDog/zstd v1.5.6 // indirect
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkcyr+I7TeNrr/foo4Kpk8=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 h1:JZg6HRh6W6U4OLl6lk7BZ7BLisIzM9dG1R50zUk9C/M=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0/go.mod h1:YL1xnZ6QejvQHWJrX/AvhFl4WW4rqHVoKspWNVwFk0M=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
Expand Down
140 changes: 139 additions & 1 deletion integration/tengateway/tengateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import (
"testing"
"time"

"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ten-protocol/go-ten/go/common/gethapi"

"github.com/ten-protocol/go-ten/go/responses"

"github.com/ten-protocol/go-ten/lib/gethfork/rpc"

"github.com/ten-protocol/go-ten/tools/walletextension"
Expand Down Expand Up @@ -51,7 +56,7 @@ func init() { //nolint:gochecknoinits
LogDir: testLogs,
TestType: "tengateway",
TestSubtype: "test",
LogLevel: log.LvlInfo,
LogLevel: log.LvlTrace,
})
}

Expand Down Expand Up @@ -115,6 +120,7 @@ func TestTenGateway(t *testing.T) {
"testInvokeNonSensitiveMethod": testInvokeNonSensitiveMethod,
"testGetStorageAtForReturningUserID": testGetStorageAtForReturningUserID,
"testRateLimiter": testRateLimiter,
"testSessionKeys": testSessionKeys,
} {
t.Run(name, func(t *testing.T) {
test(t, startPort, httpURL, wsURL, w)
Expand Down Expand Up @@ -167,6 +173,138 @@ func testRateLimiter(t *testing.T, _ int, httpURL, wsURL string, w wallet.Wallet
require.Equal(t, "rate limit exceeded", err.Error())
}

func testSessionKeys(t *testing.T, _ int, httpURL, wsURL string, w wallet.Wallet) {
user0, err := NewGatewayUser([]wallet.Wallet{w, datagenerator.RandomWallet(integration.TenChainID)}, httpURL, wsURL)
require.NoError(t, err)
testlog.Logger().Info("Created user with encryption token", "t", user0.tgClient.UserID())
err = user0.RegisterAccounts()
require.NoError(t, err)

var amountToTransfer int64 = 1_000_000_000_000_000_000
_, err = transferETHToAddress(user0.HTTPClient, user0.Wallets[0], user0.Wallets[0].Address(), amountToTransfer)
require.NoError(t, err)

_, err = user0.HTTPClient.BalanceAt(context.Background(), user0.Wallets[0].Address(), nil)
require.NoError(t, err)

contractAddr := deployContract(t, w, user0)

// create session key
skAddr, err := user0.HTTPClient.StorageAt(context.Background(), gethcommon.HexToAddress(common.CreateSessionKeyCQMethod), gethcommon.Hash{}, nil)
require.NoError(t, err)
skAddress := gethcommon.BytesToAddress(skAddr)

// move some funds to the SK
var skAmount int64 = 100_000_000_000_000_000
_, err = transferETHToAddress(user0.HTTPClient, user0.Wallets[0], skAddress, skAmount)
require.NoError(t, err)

// activate SK
_, err = user0.HTTPClient.StorageAt(context.Background(), gethcommon.HexToAddress(common.ActivateSessionKeyCQMethod), gethcommon.Hash{}, nil)
require.NoError(t, err)

skNonce := uint64(0)

// interact with the contract - unsigned tx calling "sendRawTransaction"
contractInteractionData, err := eventsContractABI.Pack("setMessage", "user0PrivateEvent")
require.NoError(t, err)
rec, err := interactWithSmartContractUnsigned(user0.HTTPClient, true, skNonce, contractAddr, contractInteractionData, nil)
require.NoError(t, err)
require.Equal(t, uint64(0x1), rec.Status)

// move money back - unsigned tx calling "sendTransaction"
skNonce++
rec1, err := interactWithSmartContractUnsigned(user0.HTTPClient, false, skNonce, user0.Wallets[0].Address(), nil, big.NewInt(1_000))
require.NoError(t, err)
require.Equal(t, uint64(0x1), rec1.Status)

// deactivate
_, err = user0.HTTPClient.StorageAt(context.Background(), gethcommon.HexToAddress(common.DeactivateSessionKeyCQMethod), gethcommon.Hash{}, nil)
require.NoError(t, err)

// interact with the contract - unsigned - should fail
skNonce++
rec2, err := interactWithSmartContractUnsigned(user0.HTTPClient, false, skNonce, contractAddr, contractInteractionData, nil)
require.Error(t, err)
require.Nil(t, rec2)
}

func deployContract(t *testing.T, w wallet.Wallet, user0 *GatewayUser) gethcommon.Address {
// deploy events contract
deployTx := &types.LegacyTx{
Nonce: w.GetNonceAndIncrement(),
Gas: uint64(1_000_000),
GasPrice: gethcommon.Big1,
Data: gethcommon.FromHex(eventsContractBytecode),
}

err := getFeeAndGas(user0.HTTPClient, w, deployTx)
require.NoError(t, err)

signedTx, err := w.SignTransaction(deployTx)
require.NoError(t, err)

err = user0.HTTPClient.SendTransaction(context.Background(), signedTx)
require.NoError(t, err)

contractReceipt, err := integrationCommon.AwaitReceiptEth(context.Background(), user0.HTTPClient, signedTx.Hash(), time.Minute)
require.NoError(t, err)
return contractReceipt.ContractAddress
}

func interactWithSmartContractUnsigned(client *ethclient.Client, sendRaw bool, nonce uint64, contractAddress gethcommon.Address, contractInteractionData []byte, value *big.Int) (*types.Receipt, error) {
var result responses.GasPriceType
err := client.Client().CallContext(context.Background(), &result, tenrpc.GasPrice)
if err != nil {
return nil, err
}

var txHash gethcommon.Hash

if sendRaw {
interactionTx := types.LegacyTx{
Nonce: nonce,
To: &contractAddress,
Gas: uint64(10_000_000),
GasPrice: result.ToInt(),
Data: contractInteractionData,
Value: value,
}
unSignedTx := types.NewTx(&interactionTx)
blob, err := unSignedTx.MarshalBinary()
if err != nil {
return nil, err
}
err = client.Client().CallContext(context.Background(), &txHash, "eth_sendRawTransaction", hexutil.Encode(blob))
if err != nil {
return nil, err
}
} else {
n := hexutil.Uint64(nonce)
g := hexutil.Uint64(10_000_000)
d := hexutil.Bytes(contractInteractionData)
interactionTx := gethapi.TransactionArgs{
Nonce: &n,
To: &contractAddress,
Gas: &g,
GasPrice: &result,
Data: &d,
Value: (*hexutil.Big)(value),
}
err = client.Client().CallContext(context.Background(), &txHash, "eth_sendTransaction", interactionTx)
if err != nil {
return nil, err
}
}

txReceipt, err := integrationCommon.AwaitReceiptEth(context.Background(), client, txHash, 10*time.Second)
if err != nil {
return nil, err
}

return txReceipt, nil
}

func testNewHeadsSubscription(t *testing.T, _ int, httpURL, wsURL string, w wallet.Wallet) {
user0, err := NewGatewayUser([]wallet.Wallet{w, datagenerator.RandomWallet(integration.TenChainID)}, httpURL, wsURL)
require.NoError(t, err)
Expand Down
46 changes: 36 additions & 10 deletions tools/walletextension/rpcapi/transaction_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package rpcapi

import (
"context"
"fmt"

"github.com/ten-protocol/go-ten/tools/walletextension/cache"

Expand Down Expand Up @@ -103,13 +104,26 @@ func (s *TransactionAPI) GetTransactionReceipt(ctx context.Context, hash common.
}

func (s *TransactionAPI) SendTransaction(ctx context.Context, args gethapi.TransactionArgs) (common.Hash, error) {
//txRec, err := ExecAuthRPC[common.Hash](ctx, s.we, &AuthExecCfg{account: args.From, timeout: sendTransactionDuration}, "eth_sendTransaction", args)
//if err != nil {
// return common.Hash{}, err
//}
//return *txRec, err
// not implemented for now. We might use this for session keys.
return common.Hash{}, rpcNotImplemented
user, err := extractUserForRequest(ctx, s.we)
if err != nil {
return common.Hash{}, err
}
if !user.ActiveSK {
return common.Hash{}, fmt.Errorf("please activate session key")
}

// when there is an active Session Key, sign all incoming transactions with that SK
signedTx, err := s.we.SKManager.SignTx(ctx, user, args.ToTransaction())
if err != nil {
return common.Hash{}, err
}

blob, err := signedTx.MarshalBinary()
if err != nil {
return common.Hash{}, err
}

return s.sendRawTx(ctx, blob)
}

type SignTransactionResult struct {
Expand All @@ -127,16 +141,28 @@ func (s *TransactionAPI) SendRawTransaction(ctx context.Context, input hexutil.B
return common.Hash{}, err
}

signedTx := input
signedTxBlob := input
// when there is an active Session Key, sign all incoming transactions with that SK
if user.ActiveSK && user.SessionKey != nil {
signedTx, err = s.we.SKManager.SignTx(ctx, user, input)
tx := new(types.Transaction)
if err = tx.UnmarshalBinary(input); err != nil {
return common.Hash{}, err
}
signedTx, err := s.we.SKManager.SignTx(ctx, user, tx)
if err != nil {
return common.Hash{}, err
}
signedTxBlob, err = signedTx.MarshalBinary()
if err != nil {
return common.Hash{}, err
}
}

txRec, err := ExecAuthRPC[common.Hash](ctx, s.we, &AuthExecCfg{tryAll: true, timeout: sendTransactionDuration}, "eth_sendRawTransaction", signedTx)
return s.sendRawTx(ctx, signedTxBlob)
}

func (s *TransactionAPI) sendRawTx(ctx context.Context, input hexutil.Bytes) (common.Hash, error) {
txRec, err := ExecAuthRPC[common.Hash](ctx, s.we, &AuthExecCfg{tryAll: true, timeout: sendTransactionDuration}, "eth_sendRawTransaction", input)
if err != nil {
return common.Hash{}, err
}
Expand Down
10 changes: 10 additions & 0 deletions tools/walletextension/services/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Implement session keys - guide for app developers

If the user selects "no-click UX" (or something) when the game starts, do the following:

1) call eth_getStorageAt with the address 0x0000000000000000000000000000000000000003 (the other parameters don't matter). This will return the address of the session key.
2) Create a transaction that transfers some eth to this address. (Maybe you ask the user to decide how many moves they want to prepay or something). The user has to sign this in their wallet. Then submit the tx.
3) Once the receipt is received you call eth_getStorageAt with 0x0000000000000000000000000000000000000004 . This means that you tell the gateway to activate the session key.
4) All the moves made by the user now can be sent with eth_sendRawTransaction or eth_sendTransaction unsigned. They will be signed by the gateway with the session key.
5) When the game is finished create a tx that moves the funds back from the SK to the main address. This will get singed with the SK by the gateeway
6) Call: eth_getStorageAt with 0x0000000000000000000000000000000000000005 - this deactivates the key.
23 changes: 11 additions & 12 deletions tools/walletextension/services/sk_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package services
import (
"context"
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"

"github.com/ethereum/go-ethereum/crypto"
Expand All @@ -23,7 +23,7 @@ import (
// From the POV of the Ten network - a session key is a normal account key
type SKManager interface {
CreateSessionKey(user *common.GWUser) (*common.GWSessionKey, error)
SignTx(ctx context.Context, user *common.GWUser, input hexutil.Bytes) (hexutil.Bytes, error)
SignTx(ctx context.Context, user *common.GWUser, input *types.Transaction) (*types.Transaction, error)
}

type skManager struct {
Expand Down Expand Up @@ -92,17 +92,16 @@ func (m *skManager) createSK(user *common.GWUser) (*common.GWSessionKey, error)
}, nil
}

func (m *skManager) SignTx(ctx context.Context, user *common.GWUser, input hexutil.Bytes) (hexutil.Bytes, error) {
tx := new(types.Transaction)
if err := tx.UnmarshalBinary(input); err != nil {
return hexutil.Bytes{}, err
}

signer := types.NewLondonSigner(tx.ChainId())
func (m *skManager) SignTx(ctx context.Context, user *common.GWUser, tx *types.Transaction) (*types.Transaction, error) {
prvKey := user.SessionKey.PrivateKey.ExportECDSA()
signer := types.NewCancunSigner(big.NewInt(int64(m.config.TenChainID)))

tx, err := types.SignTx(tx, signer, user.SessionKey.PrivateKey.ExportECDSA())
stx, err := types.SignTx(tx, signer, prvKey)
if err != nil {
return hexutil.Bytes{}, err
return nil, err
}
return tx.MarshalBinary()

m.logger.Debug("Signed transaction with session key", "stxHash", stx.Hash().Hex())

return stx, nil
}
5 changes: 3 additions & 2 deletions tools/walletextension/storage/database/common/db_types.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package common

import (
"crypto/x509"
"fmt"

"github.com/ethereum/go-ethereum/crypto"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto/ecies"
"github.com/ten-protocol/go-ten/go/common/viewingkey"
Expand Down Expand Up @@ -50,7 +51,7 @@ func (userDB *GWUserDB) ToGWUser() (*wecommon.GWUser, error) {
}

if userDB.SessionKey != nil {
ecdsaPrivateKey, err := x509.ParseECPrivateKey(userDB.SessionKey.PrivateKey)
ecdsaPrivateKey, err := crypto.ToECDSA(userDB.SessionKey.PrivateKey)
if err != nil {
return nil, fmt.Errorf("failed to parse ECDSA private key: %w", err)
}
Expand Down

0 comments on commit 7f88ff5

Please sign in to comment.