Skip to content

Commit 97a4e03

Browse files
feat: add system support for EIP-191, contracts support for EIP-(712,1271,6492) (#294)
* feat: move erc7824 documentation to nitrolite (#272) * feat: move erc7824 documentation to nitrolite * feat: move GH actions to common scope * fix: comment GITHUB_TOKEN * feat(custody): add EIP-191, EIP-712 signature support (#257) * feat(custody): add EIP-191, EIP-712 support * feat(custody): correct EIP-712 sig impl * feat(utils): add POLA principle comment * feat(contract): add EIP712AdjudicatorBase * feat(contract): integrate EIP712AdjudicatorBase to supported adjs * fix(contract): tests with updated adjs * test(contract): EIP712AdjudicatorBase * test(contract): SimpleConsensus EIP191, EIP712 sigs * test(contract): Utils signatures * test(contract): Custody, SimpleConsensus integration tests for sigs * refactor(contract): migrate sig to bytes (#279) * refactor(Types): replace Signature{v,r,s} with bytes * style(contract): add empty lines at the end * refactor(clearnode): replace Signature{r,s,v} with []byte (#283) * refactor(sdk): migrate `Signature{r,s,v}` to `Hex` (#284) * refactor(sdk): replace Signature{r,s,v} with Hex * fix(NitroliteService): perform convertStateFromContract correctly * fix(rpc): expect updated signature type from rpc * style(sdk/rpc): remove ServerSignatureSchema var * feat(sdk/rpc): add empty signatures array by default * feat(sdk): add build:force command * feat(sdk): remove npm build:force * refactor(clearnode): define `Signature []byte` type (#287) * refactor(clearnode): define `Signature []byte` type * refactor(clearnode::RPCEntry): replace sig `[]string` with `[]Signature` * feat(clearnode/nitrolite): add Sigs2Strings, Strings2Sigs helpers * fix(integration): tests sig migration (#289) * feat(docker-compose): add ability to pass logger visibility to clearnode * fix(clearnode): raw ECDSA sign and verify * fix(clearnode): try to extract req id on ummarshal error * fix(integration): update sig to new type * feat(sdk): change type of sig array to contain only Hex * refactor(clearnode): remove check on nil Req, zero-init instead * fix(clearnode:nitrolite): remove unintended side-effect of sig param modif * feat(custody): add note about ephemeral final state * style(contract): run forge fmt * fix(clearnode/docs): remove legacy signature format * feat(custody): eip-1271, eip-6492 sigs support (#293) * feat(contract): add ERC-1271,6492 support * test(contract/Utils.sol): add ERC-1271, 6492 tests * refactor(contract:Utils.t.sol): separate tests into contracts * refactor(UtilsHarness): do NOT expose constants * fix(contracts/adjudicators): use verifyStateSignature instead of just EOA sig * test(contract/Custody): add ERC-1271, 6492 sig to integration test * test(contract/Custody): add challenge with EIP-712, EIP-1271 tests * refactor(contract/Utils): reorder recoverStateEIP712Signer params for consistency * test(contract): remove console.logs * feat(contract): clarify Utils comments * refactor(contract): optimize Custody and Utils sig verification functions * style(contract): run forge fmt * feat: replace `stateHash` with `packedState` in message signing (#295) * feat(contract): replace stateHash with packedState in signing * test(contract): migrate to getPackedState * feat(contract): remove getStateHash * feat(clearnode): remove state_hash, rename EncodeState to Pack * feat(sdk): remove state_hash, add getPackedState * refactor(sdk): supply channelId and state to signState * docs(website): migrate Sig to Hex, add supported signature formats --------- Co-authored-by: MaxMoskalenko <mx.msklnk@gmail.com>
1 parent 608d92e commit 97a4e03

71 files changed

Lines changed: 4428 additions & 2899 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

clearnode/channel_service.go

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"github.com/ethereum/go-ethereum/accounts/abi"
88
"github.com/ethereum/go-ethereum/common"
99
"github.com/ethereum/go-ethereum/common/hexutil"
10-
"github.com/ethereum/go-ethereum/crypto"
1110
"github.com/shopspring/decimal"
1211
"gorm.io/gorm"
1312
)
@@ -114,13 +113,12 @@ func (s *ChannelService) RequestResize(logger Logger, params *ResizeChannelParam
114113

115114
// 6) Encode & sign the new state
116115
channelIDHash := common.HexToHash(channel.ChannelID)
117-
encodedState, err := nitrolite.EncodeState(channelIDHash, nitrolite.IntentRESIZE, big.NewInt(int64(channel.Version)+1), encodedIntentions, allocations)
116+
packedState, err := nitrolite.PackState(channelIDHash, nitrolite.IntentRESIZE, big.NewInt(int64(channel.Version)+1), encodedIntentions, allocations)
118117
if err != nil {
119-
logger.Error("failed to encode state hash", "error", err)
120-
return ResizeChannelResponse{}, RPCErrorf("failed to encode state hash")
118+
logger.Error("failed to pack state", "error", err)
119+
return ResizeChannelResponse{}, RPCErrorf("failed to pack state")
121120
}
122-
stateHash := crypto.Keccak256Hash(encodedState).Hex()
123-
sig, err := s.signer.NitroSign(encodedState)
121+
sig, err := s.signer.Sign(packedState)
124122
if err != nil {
125123
logger.Error("failed to sign state", "error", err)
126124
return ResizeChannelResponse{}, RPCErrorf("failed to sign state")
@@ -131,12 +129,7 @@ func (s *ChannelService) RequestResize(logger Logger, params *ResizeChannelParam
131129
Intent: uint8(nitrolite.IntentRESIZE),
132130
Version: channel.Version + 1,
133131
StateData: hexutil.Encode(encodedIntentions),
134-
StateHash: stateHash,
135-
Signature: Signature{
136-
V: sig.V,
137-
R: hexutil.Encode(sig.R[:]),
138-
S: hexutil.Encode(sig.S[:]),
139-
},
132+
Signature: sig,
140133
}
141134

142135
for _, alloc := range allocations {
@@ -217,13 +210,12 @@ func (s *ChannelService) RequestClose(logger Logger, params *CloseChannelParams,
217210
logger.Error("failed to decode state data hex", "error", err)
218211
return CloseChannelResponse{}, RPCErrorf("failed to decode state data hex")
219212
}
220-
encodedState, err := nitrolite.EncodeState(common.HexToHash(channel.ChannelID), nitrolite.IntentFINALIZE, big.NewInt(int64(channel.Version)+1), stateDataBytes, allocations)
213+
packedState, err := nitrolite.PackState(common.HexToHash(channel.ChannelID), nitrolite.IntentFINALIZE, big.NewInt(int64(channel.Version)+1), stateDataBytes, allocations)
221214
if err != nil {
222-
logger.Error("failed to encode state hash", "error", err)
223-
return CloseChannelResponse{}, RPCErrorf("failed to encode state hash")
215+
logger.Error("failed to pack state", "error", err)
216+
return CloseChannelResponse{}, RPCErrorf("failed to pack state")
224217
}
225-
stateHash := crypto.Keccak256Hash(encodedState).Hex()
226-
sig, err := s.signer.NitroSign(encodedState)
218+
sig, err := s.signer.Sign(packedState)
227219
if err != nil {
228220
logger.Error("failed to sign state", "error", err)
229221
return CloseChannelResponse{}, RPCErrorf("failed to sign state")
@@ -234,12 +226,7 @@ func (s *ChannelService) RequestClose(logger Logger, params *CloseChannelParams,
234226
Intent: uint8(nitrolite.IntentFINALIZE),
235227
Version: channel.Version + 1,
236228
StateData: stateDataHex,
237-
StateHash: stateHash,
238-
Signature: Signature{
239-
V: sig.V,
240-
R: hexutil.Encode(sig.R[:]),
241-
S: hexutil.Encode(sig.S[:]),
242-
},
229+
Signature: sig,
243230
}
244231

245232
for _, alloc := range allocations {

clearnode/custody.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ func (c *Custody) Join(channelID string, lastStateData []byte) (common.Hash, err
117117
// The broker will always join as participant with index 1 (second participant)
118118
index := big.NewInt(1)
119119

120-
sig, err := c.signer.NitroSign(lastStateData)
120+
sig, err := c.signer.Sign(lastStateData)
121121
if err != nil {
122122
return common.Hash{}, fmt.Errorf("failed to sign data: %w", err)
123123
}
@@ -288,7 +288,7 @@ func (c *Custody) handleCreated(logger Logger, ev *nitrolite.CustodyCreated) {
288288
return
289289
}
290290

291-
encodedState, err := nitrolite.EncodeState(ev.ChannelId, nitrolite.IntentINITIALIZE, big.NewInt(0), ev.Initial.Data, ev.Initial.Allocations)
291+
encodedState, err := nitrolite.PackState(ev.ChannelId, nitrolite.IntentINITIALIZE, big.NewInt(0), ev.Initial.Data, ev.Initial.Allocations)
292292
if err != nil {
293293
logger.Error("error encoding state hash", "error", err)
294294
return

clearnode/custody_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ func createMockCreatedEvent(t *testing.T, signer *Signer, token string, amount *
135135
Version: big.NewInt(0),
136136
Data: []byte{},
137137
Allocations: allocation,
138-
Sigs: []nitrolite.Signature{},
138+
Sigs: [][]byte{},
139139
}
140140

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

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

232232
event := &nitrolite.CustodyChallenged{
@@ -314,7 +314,7 @@ func TestHandleCreatedEvent(t *testing.T) {
314314
Version: big.NewInt(0),
315315
Data: []byte{},
316316
Allocations: allocation,
317-
Sigs: []nitrolite.Signature{},
317+
Sigs: [][]byte{},
318318
}
319319

320320
mockEvent := &nitrolite.CustodyCreated{

clearnode/docs/API.md

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -830,9 +830,9 @@ In the request, the user must specify funds destination. After the channel is cl
830830
{
831831
"res": [1, "close_channel", [{
832832
"channel_id": "0x4567890123abcdef...",
833-
"intent": 3, // IntentFINALIZE - constant magic number for closing channel
833+
"intent": 3, // IntentFINALIZE - constant specifying that this is a final state
834834
"version": 123,
835-
"state_data": "0x0000000000000000000000000000000000000000000000000000000000001ec7",
835+
"state_data": "0xdeadbeef",
836836
"allocations": [
837837
{
838838
"destination": "0x1234567890abcdef...", // Provided funds address
@@ -845,12 +845,7 @@ In the request, the user must specify funds destination. After the channel is cl
845845
"amount": "50000"
846846
}
847847
],
848-
"state_hash": "0xLedgerStateHash",
849-
"server_signature": {
850-
"v": "27",
851-
"r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
852-
"s": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
853-
}
848+
"server_signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1c",
854849
}], 1619123456789],
855850
"sig": ["0xabcd1234..."]
856851
}
@@ -890,7 +885,7 @@ Example:
890885
{
891886
"res": [1, "resize_channel", [{
892887
"channel_id": "0x4567890123abcdef...",
893-
"state_data": "0x0000000000000000000000000000000000000000000000000000000000002ec7",
888+
"state_data": "0xdeadbeef",
894889
"intent": 2, // IntentRESIZE
895890
"version": 5,
896891
"allocations": [
@@ -905,12 +900,7 @@ Example:
905900
"amount": "0"
906901
}
907902
],
908-
"state_hash": "0xLedgerStateHash",
909-
"server_signature": {
910-
"v": "28",
911-
"r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
912-
"s": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
913-
}
903+
"server_signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1c",
914904
}], 1619123456789],
915905
"sig": ["0xabcd1234..."]
916906
}

clearnode/nitrolite/bindings.go

Lines changed: 281 additions & 85 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

clearnode/nitrolite/signature.go

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,49 +2,95 @@ package nitrolite
22

33
import (
44
"crypto/ecdsa"
5+
"encoding/json"
56
"fmt"
67

78
"github.com/ethereum/go-ethereum/common"
9+
"github.com/ethereum/go-ethereum/common/hexutil"
810
"github.com/ethereum/go-ethereum/crypto"
911
)
1012

13+
type Signature []byte
14+
15+
func (s Signature) MarshalJSON() ([]byte, error) {
16+
return json.Marshal(hexutil.Encode(s))
17+
}
18+
19+
func (s *Signature) UnmarshalJSON(data []byte) error {
20+
var hexStr string
21+
if err := json.Unmarshal(data, &hexStr); err != nil {
22+
return err
23+
}
24+
decoded, err := hexutil.Decode(hexStr)
25+
if err != nil {
26+
return err
27+
}
28+
*s = decoded
29+
return nil
30+
}
31+
32+
func (s Signature) String() string {
33+
return hexutil.Encode(s)
34+
}
35+
36+
func SignaturesToStrings(signatures []Signature) []string {
37+
strs := make([]string, len(signatures))
38+
for i, sig := range signatures {
39+
strs[i] = sig.String()
40+
}
41+
return strs
42+
}
43+
44+
func SignaturesFromStrings(strs []string) ([]Signature, error) {
45+
signatures := make([]Signature, len(strs))
46+
for i, str := range strs {
47+
sig, err := hexutil.Decode(str)
48+
if err != nil {
49+
return nil, fmt.Errorf("failed to decode signature %d (%s): %w", i, str, err)
50+
}
51+
signatures[i] = sig
52+
}
53+
return signatures, nil
54+
}
55+
1156
// Sign hashes the provided data using Keccak256 and signs it with the given private key.
1257
func Sign(data []byte, privateKey *ecdsa.PrivateKey) (Signature, error) {
1358
if privateKey == nil {
14-
return Signature{}, fmt.Errorf("private key is nil")
59+
return nil, fmt.Errorf("private key is nil")
1560
}
1661

1762
dataHash := crypto.Keccak256Hash(data)
1863
signature, err := crypto.Sign(dataHash.Bytes(), privateKey)
1964
if err != nil {
20-
return Signature{}, fmt.Errorf("failed to sign data: %w", err)
65+
return nil, fmt.Errorf("failed to sign data: %w", err)
2166
}
2267

2368
if len(signature) != 65 {
24-
return Signature{}, fmt.Errorf("invalid signature length: got %d, want 65", len(signature))
69+
return nil, fmt.Errorf("invalid signature length: got %d, want 65", len(signature))
2570
}
2671

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

32-
return sig, nil
77+
return signature, nil
3378
}
3479

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

39-
signature := make([]byte, 65)
40-
copy(signature[0:32], sig.R[:])
41-
copy(signature[32:64], sig.S[:])
84+
// Create a copy of the signature to avoid modifying the original
85+
sigToVerify := make(Signature, len(sig))
86+
copy(sigToVerify, sig)
4287

43-
if sig.V >= 27 {
44-
signature[64] = sig.V - 27
88+
// Ensure the signature is in the correct format
89+
if sigToVerify[64] >= 27 {
90+
sigToVerify[64] -= 27
4591
}
4692

47-
pubKeyRaw, err := crypto.Ecrecover(dataHash.Bytes(), signature)
93+
pubKeyRaw, err := crypto.Ecrecover(dataHash.Bytes(), sigToVerify)
4894
if err != nil {
4995
return false, fmt.Errorf("failed to recover public key: %w", err)
5096
}

clearnode/nitrolite/signature_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ func TestVerifyInvalidSignature(t *testing.T) {
7171
t.Fatalf("failed to sign data: %v", err)
7272
}
7373

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

7777
// Use the original public address.
7878
publicAddress := crypto.PubkeyToAddress(privateKey.PublicKey)

clearnode/nitrolite/state.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ const (
1616
IntentFINALIZE Intent = 3
1717
)
1818

19-
// EncodeState encodes channel state into a byte array using channelID, intent, version, state data, and allocations.
20-
func EncodeState(channelID common.Hash, intent Intent, version *big.Int, stateData []byte, allocations []Allocation) ([]byte, error) {
19+
// PackState encodes channel id and state into a byte array
20+
func PackState(channelID common.Hash, intent Intent, version *big.Int, stateData []byte, allocations []Allocation) ([]byte, error) {
2121
allocationType, err := abi.NewType("tuple[]", "", []abi.ArgumentMarshaling{
2222
{Name: "destination", Type: "address"},
2323
{Name: "token", Type: "address"},

clearnode/rpc.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import (
99

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

1818
// ParseRPCMessage parses a JSON string into an RPCMessage
@@ -100,7 +100,7 @@ func CreateResponse(id uint64, method string, responseParams []any) *RPCMessage
100100
Params: responseParams,
101101
Timestamp: uint64(time.Now().UnixMilli()),
102102
},
103-
Sig: []string{},
103+
Sig: []Signature{},
104104
}
105105
}
106106

clearnode/rpc_node.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"sync"
1010
"time"
1111

12-
"github.com/ethereum/go-ethereum/common/hexutil"
1312
"github.com/go-playground/validator/v10"
1413
"github.com/google/uuid"
1514
"github.com/gorilla/websocket"
@@ -168,10 +167,10 @@ read_loop:
168167
}
169168
}
170169

171-
var msg RPCMessage
170+
msg := RPCMessage{Req: &RPCData{}}
172171
if err := json.Unmarshal(messageBytes, &msg); err != nil {
173172
n.logger.Debug("invalid message format", "error", err, "message", string(messageBytes))
174-
n.sendErrorResponse(rpcConn, 0, "invalid message format")
173+
n.sendErrorResponse(rpcConn, msg.Req.RequestID, "invalid message format")
175174
continue
176175
}
177176

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

356355
responseMessage := &RPCMessage{
357356
Res: data,
358-
Sig: []string{hexutil.Encode(signature)},
357+
Sig: []Signature{signature},
359358
}
360359
resMessageBytes, err := json.Marshal(responseMessage)
361360
if err != nil {

0 commit comments

Comments
 (0)