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
17 changes: 17 additions & 0 deletions .github/workflows/main-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,20 @@ jobs:
push: true
tags: |
ghcr.io/${{ github.repository }}/clearnode:${{ steps.sha.outputs.short_sha }}

build-and-preview-docs-firebase:
name: Deploy to Firebase Hosting on PR
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- run: npm install && npm run build
working-directory: erc7824-docs

- uses: FirebaseExtended/action-hosting-deploy@v0
with:
# repoToken: ${{ secrets.GITHUB_TOKEN }}
firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_ERC7824 }}
projectId: erc7824
entryPoint: ./erc7824-docs
17 changes: 17 additions & 0 deletions .github/workflows/main-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,20 @@ jobs:
⚠️ RC build or deployment was cancelled!
${{github.event.head_commit.message}}
SLACK_FOOTER: 'Nitrolite CI/CD Pipeline'

build-and-deploy-docs-firebase:
name: Deploy to Firebase Hosting on merge
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- run: npm install && npm run build
working-directory: erc7824-docs

- uses: FirebaseExtended/action-hosting-deploy@v0
with:
# repoToken: ${{ secrets.GITHUB_TOKEN }}
firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_ERC7824 }}
channelId: live
projectId: erc7824
entryPoint: ./erc7824-docs
33 changes: 10 additions & 23 deletions clearnode/channel_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
Expand Down Expand Up @@ -114,13 +113,12 @@ func (s *ChannelService) RequestResize(logger Logger, params *ResizeChannelParam

// 6) Encode & sign the new state
channelIDHash := common.HexToHash(channel.ChannelID)
encodedState, err := nitrolite.EncodeState(channelIDHash, nitrolite.IntentRESIZE, big.NewInt(int64(channel.Version)+1), encodedIntentions, allocations)
packedState, err := nitrolite.PackState(channelIDHash, nitrolite.IntentRESIZE, big.NewInt(int64(channel.Version)+1), encodedIntentions, allocations)
if err != nil {
logger.Error("failed to encode state hash", "error", err)
return ResizeChannelResponse{}, RPCErrorf("failed to encode state hash")
logger.Error("failed to pack state", "error", err)
return ResizeChannelResponse{}, RPCErrorf("failed to pack state")
}
stateHash := crypto.Keccak256Hash(encodedState).Hex()
sig, err := s.signer.NitroSign(encodedState)
sig, err := s.signer.Sign(packedState)
if err != nil {
logger.Error("failed to sign state", "error", err)
return ResizeChannelResponse{}, RPCErrorf("failed to sign state")
Expand All @@ -131,12 +129,7 @@ func (s *ChannelService) RequestResize(logger Logger, params *ResizeChannelParam
Intent: uint8(nitrolite.IntentRESIZE),
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 @@ -217,13 +210,12 @@ func (s *ChannelService) RequestClose(logger Logger, params *CloseChannelParams,
logger.Error("failed to decode state data hex", "error", err)
return CloseChannelResponse{}, RPCErrorf("failed to decode state data hex")
}
encodedState, err := nitrolite.EncodeState(common.HexToHash(channel.ChannelID), nitrolite.IntentFINALIZE, big.NewInt(int64(channel.Version)+1), stateDataBytes, allocations)
packedState, err := nitrolite.PackState(common.HexToHash(channel.ChannelID), nitrolite.IntentFINALIZE, big.NewInt(int64(channel.Version)+1), stateDataBytes, allocations)
if err != nil {
logger.Error("failed to encode state hash", "error", err)
return CloseChannelResponse{}, RPCErrorf("failed to encode state hash")
logger.Error("failed to pack state", "error", err)
return CloseChannelResponse{}, RPCErrorf("failed to pack state")
}
stateHash := crypto.Keccak256Hash(encodedState).Hex()
sig, err := s.signer.NitroSign(encodedState)
sig, err := s.signer.Sign(packedState)
if err != nil {
logger.Error("failed to sign state", "error", err)
return CloseChannelResponse{}, RPCErrorf("failed to sign state")
Expand All @@ -234,12 +226,7 @@ func (s *ChannelService) RequestClose(logger Logger, params *CloseChannelParams,
Intent: uint8(nitrolite.IntentFINALIZE),
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
4 changes: 2 additions & 2 deletions 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 Expand Up @@ -288,7 +288,7 @@ func (c *Custody) handleCreated(logger Logger, ev *nitrolite.CustodyCreated) {
return
}

encodedState, err := nitrolite.EncodeState(ev.ChannelId, nitrolite.IntentINITIALIZE, big.NewInt(0), ev.Initial.Data, ev.Initial.Allocations)
encodedState, err := nitrolite.PackState(ev.ChannelId, nitrolite.IntentINITIALIZE, big.NewInt(0), ev.Initial.Data, ev.Initial.Allocations)
if err != nil {
logger.Error("error encoding state hash", "error", err)
return
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
20 changes: 5 additions & 15 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 @@ -845,12 +845,7 @@ In the request, the user must specify funds destination. After the channel is cl
"amount": "50000"
}
],
"state_hash": "0xLedgerStateHash",
"server_signature": {
"v": "27",
"r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"s": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
}
"server_signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1c",
}], 1619123456789],
"sig": ["0xabcd1234..."]
}
Expand Down Expand Up @@ -890,7 +885,7 @@ Example:
{
"res": [1, "resize_channel", [{
"channel_id": "0x4567890123abcdef...",
"state_data": "0x0000000000000000000000000000000000000000000000000000000000002ec7",
"state_data": "0xdeadbeef",
"intent": 2, // IntentRESIZE
"version": 5,
"allocations": [
Expand All @@ -905,12 +900,7 @@ Example:
"amount": "0"
}
],
"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
4 changes: 2 additions & 2 deletions clearnode/nitrolite/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ const (
IntentFINALIZE Intent = 3
)

// EncodeState encodes channel state into a byte array using channelID, intent, version, state data, and allocations.
func EncodeState(channelID common.Hash, intent Intent, version *big.Int, stateData []byte, allocations []Allocation) ([]byte, error) {
// PackState encodes channel id and state into a byte array
func PackState(channelID common.Hash, intent Intent, version *big.Int, stateData []byte, allocations []Allocation) ([]byte, error) {
allocationType, err := abi.NewType("tuple[]", "", []abi.ArgumentMarshaling{
{Name: "destination", Type: "address"},
{Name: "token", Type: "address"},
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
Loading