Skip to content
Closed
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
16 changes: 4 additions & 12 deletions clearnode/channel_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func (s *ChannelService) RequestResize(logger Logger, params *ResizeChannelParam
return ResizeChannelResponse{}, RPCErrorf("failed to encode state hash")
}
stateHash := crypto.Keccak256Hash(encodedState).Hex()
sig, err := s.signer.NitroSign(encodedState)
sig, err := s.signer.Sign(encodedState)
if err != nil {
logger.Error("failed to sign state", "error", err)
return ResizeChannelResponse{}, RPCErrorf("failed to sign state")
Expand All @@ -132,11 +132,7 @@ func (s *ChannelService) RequestResize(logger Logger, params *ResizeChannelParam
Version: channel.Version + 1,
StateData: hexutil.Encode(encodedIntentions),
StateHash: stateHash,
Signature: Signature{
V: sig.V,
R: hexutil.Encode(sig.R[:]),
S: hexutil.Encode(sig.S[:]),
},
Signature: sig,
}

for _, alloc := range allocations {
Expand Down Expand Up @@ -223,7 +219,7 @@ func (s *ChannelService) RequestClose(logger Logger, params *CloseChannelParams,
return CloseChannelResponse{}, RPCErrorf("failed to encode state hash")
}
stateHash := crypto.Keccak256Hash(encodedState).Hex()
sig, err := s.signer.NitroSign(encodedState)
sig, err := s.signer.Sign(encodedState)
if err != nil {
logger.Error("failed to sign state", "error", err)
return CloseChannelResponse{}, RPCErrorf("failed to sign state")
Expand All @@ -235,11 +231,7 @@ func (s *ChannelService) RequestClose(logger Logger, params *CloseChannelParams,
Version: channel.Version + 1,
StateData: stateDataHex,
StateHash: stateHash,
Signature: Signature{
V: sig.V,
R: hexutil.Encode(sig.R[:]),
S: hexutil.Encode(sig.S[:]),
},
Signature: sig,
}

for _, alloc := range allocations {
Expand Down
2 changes: 1 addition & 1 deletion clearnode/custody.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func (c *Custody) Join(channelID string, lastStateData []byte) (common.Hash, err
// The broker will always join as participant with index 1 (second participant)
index := big.NewInt(1)

sig, err := c.signer.NitroSign(lastStateData)
sig, err := c.signer.Sign(lastStateData)
if err != nil {
return common.Hash{}, fmt.Errorf("failed to sign data: %w", err)
}
Expand Down
8 changes: 4 additions & 4 deletions clearnode/custody_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func createMockCreatedEvent(t *testing.T, signer *Signer, token string, amount *
Version: big.NewInt(0),
Data: []byte{},
Allocations: allocation,
Sigs: []nitrolite.Signature{},
Sigs: [][]byte{},
}

event := &nitrolite.CustodyCreated{
Expand Down Expand Up @@ -189,7 +189,7 @@ func createMockClosedEvent(t *testing.T, signer *Signer, token string, amount *b
Version: big.NewInt(1),
Data: []byte{},
Allocations: allocation,
Sigs: []nitrolite.Signature{},
Sigs: [][]byte{},
}

event := &nitrolite.CustodyClosed{
Expand Down Expand Up @@ -226,7 +226,7 @@ func createMockChallengedEvent(t *testing.T, signer *Signer, token string, amoun
Version: big.NewInt(2),
Data: []byte{},
Allocations: allocation,
Sigs: []nitrolite.Signature{},
Sigs: [][]byte{},
}

event := &nitrolite.CustodyChallenged{
Expand Down Expand Up @@ -314,7 +314,7 @@ func TestHandleCreatedEvent(t *testing.T) {
Version: big.NewInt(0),
Data: []byte{},
Allocations: allocation,
Sigs: []nitrolite.Signature{},
Sigs: [][]byte{},
}

mockEvent := &nitrolite.CustodyCreated{
Expand Down
18 changes: 5 additions & 13 deletions clearnode/docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -830,9 +830,9 @@ In the request, the user must specify funds destination. After the channel is cl
{
"res": [1, "close_channel", [{
"channel_id": "0x4567890123abcdef...",
"intent": 3, // IntentFINALIZE - constant magic number for closing channel
"intent": 3, // IntentFINALIZE - constant specifying that this is a final state
"version": 123,
"state_data": "0x0000000000000000000000000000000000000000000000000000000000001ec7",
"state_data": "0xdeadbeef",
"allocations": [
{
"destination": "0x1234567890abcdef...", // Provided funds address
Expand All @@ -846,11 +846,7 @@ In the request, the user must specify funds destination. After the channel is cl
}
],
"state_hash": "0xLedgerStateHash",
"server_signature": {
"v": "27",
"r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"s": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
}
"server_signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1c",
}], 1619123456789],
"sig": ["0xabcd1234..."]
}
Expand Down Expand Up @@ -890,7 +886,7 @@ Example:
{
"res": [1, "resize_channel", [{
"channel_id": "0x4567890123abcdef...",
"state_data": "0x0000000000000000000000000000000000000000000000000000000000002ec7",
"state_data": "0xdeadbeef",
"intent": 2, // IntentRESIZE
"version": 5,
"allocations": [
Expand All @@ -906,11 +902,7 @@ Example:
}
],
"state_hash": "0xLedgerStateHash",
"server_signature": {
"v": "28",
"r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"s": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
}
"server_signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1c",
}], 1619123456789],
"sig": ["0xabcd1234..."]
}
Expand Down
366 changes: 281 additions & 85 deletions clearnode/nitrolite/bindings.go

Large diffs are not rendered by default.

74 changes: 60 additions & 14 deletions clearnode/nitrolite/signature.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,95 @@ package nitrolite

import (
"crypto/ecdsa"
"encoding/json"
"fmt"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
)

type Signature []byte

func (s Signature) MarshalJSON() ([]byte, error) {
return json.Marshal(hexutil.Encode(s))
}

func (s *Signature) UnmarshalJSON(data []byte) error {
var hexStr string
if err := json.Unmarshal(data, &hexStr); err != nil {
return err
}
decoded, err := hexutil.Decode(hexStr)
if err != nil {
return err
}
*s = decoded
return nil
}

func (s Signature) String() string {
return hexutil.Encode(s)
}

func SignaturesToStrings(signatures []Signature) []string {
strs := make([]string, len(signatures))
for i, sig := range signatures {
strs[i] = sig.String()
}
return strs
}

func SignaturesFromStrings(strs []string) ([]Signature, error) {
signatures := make([]Signature, len(strs))
for i, str := range strs {
sig, err := hexutil.Decode(str)
if err != nil {
return nil, fmt.Errorf("failed to decode signature %d (%s): %w", i, str, err)
}
signatures[i] = sig
}
return signatures, nil
}

// Sign hashes the provided data using Keccak256 and signs it with the given private key.
func Sign(data []byte, privateKey *ecdsa.PrivateKey) (Signature, error) {
if privateKey == nil {
return Signature{}, fmt.Errorf("private key is nil")
return nil, fmt.Errorf("private key is nil")
}

dataHash := crypto.Keccak256Hash(data)
signature, err := crypto.Sign(dataHash.Bytes(), privateKey)
if err != nil {
return Signature{}, fmt.Errorf("failed to sign data: %w", err)
return nil, fmt.Errorf("failed to sign data: %w", err)
}

if len(signature) != 65 {
return Signature{}, fmt.Errorf("invalid signature length: got %d, want 65", len(signature))
return nil, fmt.Errorf("invalid signature length: got %d, want 65", len(signature))
}

var sig Signature
copy(sig.R[:], signature[:32])
copy(sig.S[:], signature[32:64])
sig.V = signature[64] + 27
// This step is necessary to remain compatible with the ecrecover precompile
if signature[64] < 27 {
signature[64] += 27
}

return sig, nil
return signature, nil
}

// Verify checks if the signature on the provided data was created by the given address.
func Verify(data []byte, sig Signature, address common.Address) (bool, error) {
dataHash := crypto.Keccak256Hash(data)

signature := make([]byte, 65)
copy(signature[0:32], sig.R[:])
copy(signature[32:64], sig.S[:])
// Create a copy of the signature to avoid modifying the original
sigToVerify := make(Signature, len(sig))
copy(sigToVerify, sig)

if sig.V >= 27 {
signature[64] = sig.V - 27
// Ensure the signature is in the correct format
if sigToVerify[64] >= 27 {
sigToVerify[64] -= 27
}

pubKeyRaw, err := crypto.Ecrecover(dataHash.Bytes(), signature)
pubKeyRaw, err := crypto.Ecrecover(dataHash.Bytes(), sigToVerify)
if err != nil {
return false, fmt.Errorf("failed to recover public key: %w", err)
}
Expand Down
4 changes: 2 additions & 2 deletions clearnode/nitrolite/signature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ func TestVerifyInvalidSignature(t *testing.T) {
t.Fatalf("failed to sign data: %v", err)
}

// Tamper with the signature (flip a bit in R).
sig.R[0] ^= 0xff
// Tamper with the signature (flip some bit).
sig[0] ^= 0xff

// Use the original public address.
publicAddress := crypto.PubkeyToAddress(privateKey.PublicKey)
Expand Down
10 changes: 5 additions & 5 deletions clearnode/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import (

// RPCMessage represents a complete message in the RPC protocol, including data and signatures
type RPCMessage struct {
Req *RPCData `json:"req,omitempty" validate:"required_without=Res,excluded_with=Res"`
Res *RPCData `json:"res,omitempty" validate:"required_without=Req,excluded_with=Req"`
AppSessionID string `json:"sid,omitempty"`
Sig []string `json:"sig"`
Req *RPCData `json:"req,omitempty" validate:"required_without=Res,excluded_with=Res"`
Res *RPCData `json:"res,omitempty" validate:"required_without=Req,excluded_with=Req"`
AppSessionID string `json:"sid,omitempty"`
Sig []Signature `json:"sig"`
}

// ParseRPCMessage parses a JSON string into an RPCMessage
Expand Down Expand Up @@ -100,7 +100,7 @@ func CreateResponse(id uint64, method string, responseParams []any) *RPCMessage
Params: responseParams,
Timestamp: uint64(time.Now().UnixMilli()),
},
Sig: []string{},
Sig: []Signature{},
}
}

Expand Down
7 changes: 3 additions & 4 deletions clearnode/rpc_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"sync"
"time"

"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"github.com/gorilla/websocket"
Expand Down Expand Up @@ -168,10 +167,10 @@ read_loop:
}
}

var msg RPCMessage
msg := RPCMessage{Req: &RPCData{}}
if err := json.Unmarshal(messageBytes, &msg); err != nil {
n.logger.Debug("invalid message format", "error", err, "message", string(messageBytes))
n.sendErrorResponse(rpcConn, 0, "invalid message format")
n.sendErrorResponse(rpcConn, msg.Req.RequestID, "invalid message format")
continue
}

Expand Down Expand Up @@ -355,7 +354,7 @@ func prepareRawRPCResponse(signer *Signer, data *RPCData) ([]byte, error) {

responseMessage := &RPCMessage{
Res: data,
Sig: []string{hexutil.Encode(signature)},
Sig: []Signature{signature},
}
resMessageBytes, err := json.Marshal(responseMessage)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion clearnode/rpc_node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ func TestRPCNode(t *testing.T) {

reqMsg := &RPCMessage{
Req: reqData,
Sig: []string{},
Sig: []Signature{},
}

// Send request
Expand Down
18 changes: 9 additions & 9 deletions clearnode/rpc_router.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,15 +204,15 @@ func (r *RPCRouter) MetricsMiddleware(c *RPCContext) {
}

type RPCEntry struct {
ID uint `json:"id"`
Sender string `json:"sender"`
ReqID uint64 `json:"req_id"`
Method string `json:"method"`
Params string `json:"params"`
Timestamp uint64 `json:"timestamp"`
ReqSig []string `json:"req_sig"`
Result string `json:"response"`
ResSig []string `json:"res_sig"`
ID uint `json:"id"`
Sender string `json:"sender"`
ReqID uint64 `json:"req_id"`
Method string `json:"method"`
Params string `json:"params"`
Timestamp uint64 `json:"timestamp"`
ReqSig []Signature `json:"req_sig"`
Result string `json:"response"`
ResSig []Signature `json:"res_sig"`
}

func (r *RPCRouter) HistoryMiddleware(c *RPCContext) {
Expand Down
2 changes: 1 addition & 1 deletion clearnode/rpc_router_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ func (r *RPCRouter) handleAuthJWTVerify(ctx context.Context, authParams AuthVeri
}

// handleAuthJWTVerify verifies the challenge signature and returns the policy, response data and error.
func (r *RPCRouter) handleAuthSigVerify(ctx context.Context, sig string, authParams AuthVerifyParams) (*Policy, any, error) {
func (r *RPCRouter) handleAuthSigVerify(ctx context.Context, sig Signature, authParams AuthVerifyParams) (*Policy, any, error) {
logger := LoggerFromContext(ctx)

challenge, err := r.AuthManager.GetChallenge(authParams.Challenge)
Expand Down
Loading
Loading