diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 8e9356b10..412398117 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -18,7 +18,7 @@ jobs: with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version version: v1.54.2 - args: --timeout 3m0s --verbose --modules-download-mode readonly + args: --timeout 5m0s --verbose --modules-download-mode readonly - name: Run staticcheck # see: staticcheck.io uses: dominikh/staticcheck-action@v1.3.0 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..9329b4e8a --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,29 @@ +name: Publish npm package to gitea +on: + release: + types: [published] +jobs: + npm_publish: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + # https://github.com/NomicFoundation/hardhat/issues/3877 + - name: Use Node.js 18.15 + uses: actions/setup-node@v3 + with: + node-version: "18.15.0" + - name: "Install dependencies and build packages" + run: | + cd ./packages/nitro-protocol + npm ci --legacy-peer-deps + - name: Configure git.vdb.to npm registry + run: | + npm config set registry https://git.vdb.to/api/packages/cerc-io/npm/ + - name: Authenticate to git.vdb.to registry + run: | + npm config set -- '//git.vdb.to/api/packages/cerc-io/npm/:_authToken' "${{ secrets.GITEA_PUBLISH_TOKEN }}" + - name: npm publish + run: | + cd ./nitro-protocol + npm publish diff --git a/bridge/bridge.go b/bridge/bridge.go index 507e5dff8..961807526 100644 --- a/bridge/bridge.go +++ b/bridge/bridge.go @@ -119,7 +119,7 @@ func (b *Bridge) Start(configOpts BridgeConfig) (nodeL1 *node.Node, nodeL2 *node messageOptsL1 := p2pms.MessageOpts{ PkBytes: common.Hex2Bytes(configOpts.StateChannelPK), - Port: configOpts.NodeL1MsgPort, + TcpPort: configOpts.NodeL1MsgPort, BootPeers: nil, PublicIp: configOpts.BridgePublicIp, ExtMultiAddr: configOpts.NodeL1ExtMultiAddr, @@ -127,7 +127,7 @@ func (b *Bridge) Start(configOpts BridgeConfig) (nodeL1 *node.Node, nodeL2 *node messageOptsL2 := p2pms.MessageOpts{ PkBytes: common.Hex2Bytes(configOpts.StateChannelPK), - Port: configOpts.NodeL2MsgPort, + TcpPort: configOpts.NodeL2MsgPort, BootPeers: nil, PublicIp: configOpts.BridgePublicIp, ExtMultiAddr: configOpts.NodeL2ExtMultiAddr, diff --git a/cmd/create-channels/main.go b/cmd/create-channels/main.go index 5df1ea517..f2d321f0c 100644 --- a/cmd/create-channels/main.go +++ b/cmd/create-channels/main.go @@ -40,7 +40,7 @@ func createChannels() error { logging.SetupDefaultFileLogger(LOG_FILE, slog.LevelDebug) url := fmt.Sprintf(":%d/api/v1", participantOpts.RpcPort) - clientConnection, err := http.NewHttpTransportAsClient(url, 500*time.Millisecond) + clientConnection, err := http.NewHttpTransportAsClient(url, true, 500*time.Millisecond) if err != nil { return err } diff --git a/cmd/create-ledger-channel/main.go b/cmd/create-ledger-channel/main.go index c8978fbdd..4d2a4df08 100644 --- a/cmd/create-ledger-channel/main.go +++ b/cmd/create-ledger-channel/main.go @@ -49,7 +49,7 @@ func main() { Usage: "Creates a ledger channel with the specified counterparty and amount", Flags: flags, Action: func(cCtx *cli.Context) error { - clientConnection, err := http.NewHttpTransportAsClient(cCtx.String(NITRO_ENDPOINT), 10*time.Millisecond) + clientConnection, err := http.NewHttpTransportAsClient(cCtx.String(NITRO_ENDPOINT), true, 10*time.Millisecond) if err != nil { return err } diff --git a/cmd/start-bridge/main.go b/cmd/start-bridge/main.go index 04ba3afea..a77baf1eb 100644 --- a/cmd/start-bridge/main.go +++ b/cmd/start-bridge/main.go @@ -12,6 +12,7 @@ import ( "github.com/statechannels/go-nitro/cmd/utils" "github.com/statechannels/go-nitro/internal/logging" "github.com/statechannels/go-nitro/internal/rpc" + "github.com/statechannels/go-nitro/paymentsmanager" "github.com/urfave/cli/v2" "github.com/urfave/cli/v2/altsrc" ) @@ -266,12 +267,12 @@ func main() { } // RPC servers for individual nodes used only for debugging - nodeL1RpcServer, err := rpc.InitializeNodeRpcServer(nodeL1, NODEL1_RPC_PORT, false, &cert) + nodeL1RpcServer, err := rpc.InitializeNodeRpcServer(nodeL1, paymentsmanager.PaymentsManager{}, NODEL1_RPC_PORT, false, &cert) if err != nil { return err } - nodeL2RpcServer, err := rpc.InitializeNodeRpcServer(nodeL2, NODEL2_RPC_PORT, false, &cert) + nodeL2RpcServer, err := rpc.InitializeNodeRpcServer(nodeL2, paymentsmanager.PaymentsManager{}, NODEL2_RPC_PORT, false, &cert) if err != nil { return err } diff --git a/cmd/start-payment-proxy/main.go b/cmd/start-payment-proxy/main.go index 846412536..5e9ae41c5 100644 --- a/cmd/start-payment-proxy/main.go +++ b/cmd/start-payment-proxy/main.go @@ -12,10 +12,11 @@ import ( ) const ( - NITRO_ENDPOINT = "nitroendpoint" - PROXY_ADDRESS = "proxyaddress" - DESTINATION_URL = "destinationurl" - COST_PER_BYTE = "costperbyte" + NITRO_ENDPOINT = "nitroendpoint" + PROXY_ADDRESS = "proxyaddress" + DESTINATION_URL = "destinationurl" + COST_PER_BYTE = "costperbyte" + ENABLE_PAID_RPC_METHODS = "enablepaidrpcmethods" TLS_CERT_FILEPATH = "tlscertfilepath" TLS_KEY_FILEPATH = "tlskeyfilepath" @@ -61,6 +62,12 @@ func main() { Usage: "Filepath to the TLS private key. If not specified, TLS will not be used.", Value: "", }, + &cli.BoolFlag{ + Name: ENABLE_PAID_RPC_METHODS, + Usage: "Flag to enable/disable payment for RPC methods", + Value: false, + Aliases: []string{"r"}, + }, }, Action: func(c *cli.Context) error { proxyEndpoint := c.String(PROXY_ADDRESS) @@ -75,6 +82,7 @@ func main() { c.Uint64(COST_PER_BYTE), c.String(TLS_CERT_FILEPATH), c.String(TLS_KEY_FILEPATH), + c.Bool(ENABLE_PAID_RPC_METHODS), ) return proxy.Start() diff --git a/cmd/test-configs/alice.toml b/cmd/test-configs/alice.toml index 2786375d6..383a23bfd 100644 --- a/cmd/test-configs/alice.toml +++ b/cmd/test-configs/alice.toml @@ -3,6 +3,7 @@ usedurablestore = true msgport = 3005 +wsmsgport = 6005 rpcport = 4005 guiport = 5005 diff --git a/cmd/test-configs/bob.toml b/cmd/test-configs/bob.toml index 684aed636..8a8056e45 100644 --- a/cmd/test-configs/bob.toml +++ b/cmd/test-configs/bob.toml @@ -4,6 +4,7 @@ usedurablestore = true guiport = 5007 msgport = 3007 +wsmsgport = 6007 rpcport = 4007 # PeerID: 16Uiu2HAmJDxLM8rSybX78FH51iZq9PdrwCoCyyHRBCndNzcAYMes diff --git a/cmd/test-configs/irene.toml b/cmd/test-configs/irene.toml index 18b9952ad..65f19c7bd 100644 --- a/cmd/test-configs/irene.toml +++ b/cmd/test-configs/irene.toml @@ -4,6 +4,7 @@ usedurablestore = true guiport = 5006 msgport = 3006 +wsmsgport = 6006 rpcport = 4006 # PeerID: 16Uiu2HAmHntR3SGeS7iF2tdeNBefSahXBhmTrqVozVLHydxzkaZn diff --git a/doc.go b/doc.go index c0884e783..2fa465af5 100644 --- a/doc.go +++ b/doc.go @@ -24,6 +24,8 @@ // Specifies whether to deploy the adjudicator and create2deployer contracts. // -msgport int // Specifies the tcp port for the message service. (default 3005) +// -wsmsgport int +// Specifies the websocket port for the message service. (default 6005) // -naaddress string // Specifies the address of the nitro adjudicator contract. Default is the address computed by the Create2Deployer contract. (default "0xC6A55E07566416274dBF020b5548eecEdB56290c") // -pk string diff --git a/go.mod b/go.mod index ab468ebf4..eaa515a03 100644 --- a/go.mod +++ b/go.mod @@ -14,11 +14,11 @@ require ( require ( github.com/BurntSushi/toml v1.3.2 github.com/golang-jwt/jwt/v5 v5.0.0 + github.com/hashicorp/golang-lru/v2 v2.0.5 github.com/libp2p/go-libp2p-kad-dht v0.24.2 github.com/lmittmann/tint v1.0.2 github.com/tidwall/buntdb v1.2.10 github.com/urfave/cli/v2 v2.25.7 - ) require ( diff --git a/go.sum b/go.sum index 07c73919a..05afa6b42 100644 --- a/go.sum +++ b/go.sum @@ -492,6 +492,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d h1:dg1dEPuWpEqDnvIw251EVy4zlP8gWbsGj4BsUKCRpYs= github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= +github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4= github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc= diff --git a/internal/node/bridge.go b/internal/node/bridge.go index 110cb7564..480c35740 100644 --- a/internal/node/bridge.go +++ b/internal/node/bridge.go @@ -17,7 +17,7 @@ func InitializeL2Node(l2ChainOpts chainservice.L2ChainOpts, storeOpts store.Stor return nil, nil, nil, nil, err } - slog.Info("Initializing message service on port " + fmt.Sprint(messageOpts.Port) + "...") + slog.Info("Initializing message service on port " + fmt.Sprint(messageOpts.TcpPort) + "...") messageOpts.SCAddr = *ourStore.GetAddress() messageService := p2pms.NewMessageService(messageOpts) diff --git a/internal/node/node.go b/internal/node/node.go index 0710b36e8..cb28b74aa 100644 --- a/internal/node/node.go +++ b/internal/node/node.go @@ -1,7 +1,6 @@ package node import ( - "fmt" "log/slog" "github.com/statechannels/go-nitro/node" @@ -18,7 +17,7 @@ func InitializeNode(chainOpts chainservice.ChainOpts, storeOpts store.StoreOpts, return nil, nil, nil, nil, err } - slog.Info("Initializing message service on port " + fmt.Sprint(messageOpts.Port) + "...") + slog.Info("Initializing message service", "tcp port", messageOpts.TcpPort, "web socket port", messageOpts.WsMsgPort) messageOpts.SCAddr = *ourStore.GetAddress() messageService := p2pms.NewMessageService(messageOpts) diff --git a/internal/rpc/rpc.go b/internal/rpc/rpc.go index 24d9eaa56..c62c16aad 100644 --- a/internal/rpc/rpc.go +++ b/internal/rpc/rpc.go @@ -7,19 +7,20 @@ import ( "github.com/statechannels/go-nitro/bridge" "github.com/statechannels/go-nitro/node" + "github.com/statechannels/go-nitro/paymentsmanager" "github.com/statechannels/go-nitro/rpc" "github.com/statechannels/go-nitro/rpc/transport" httpTransport "github.com/statechannels/go-nitro/rpc/transport/http" "github.com/statechannels/go-nitro/rpc/transport/nats" ) -func InitializeNodeRpcServer(node *node.Node, rpcPort int, useNats bool, cert *tls.Certificate) (*rpc.NodeRpcServer, error) { +func InitializeNodeRpcServer(node *node.Node, paymentManager paymentsmanager.PaymentsManager, rpcPort int, useNats bool, cert *tls.Certificate) (*rpc.NodeRpcServer, error) { transport, err := initializeTransport(rpcPort, useNats, cert) if err != nil { return nil, err } - rpcServer, err := rpc.NewNodeRpcServer(node, transport) + rpcServer, err := rpc.NewNodeRpcServer(node, paymentManager, transport) if err != nil { return nil, err } diff --git a/internal/testactors/actors.go b/internal/testactors/actors.go index cbc8d7a92..aad64527e 100644 --- a/internal/testactors/actors.go +++ b/internal/testactors/actors.go @@ -15,6 +15,7 @@ type Actor struct { Name ActorName ChainAccountIndex uint Port uint + WSPort uint } func (a Actor) Destination() types.Destination { @@ -25,7 +26,10 @@ func (a Actor) Address() types.Address { return crypto.GetAddressFromSecretKeyBytes(a.PrivateKey) } -const START_PORT = 3200 +const ( + START_PORT = 3200 + WS_START_PORT = 6200 +) // Alice has the address 0xAAA6628Ec44A8a742987EF3A114dDFE2D4F7aDCE // peerId: 16Uiu2HAmSjXJqsyBJgcBUU2HQmykxGseafSatbpq5471XmuaUqyv @@ -35,6 +39,7 @@ var Alice Actor = Actor{ "alice", 0, START_PORT + 0, + WS_START_PORT + 0, } // Bob has the address 0xBBB676f9cFF8D242e9eaC39D063848807d3D1D94 @@ -45,6 +50,7 @@ var Bob Actor = Actor{ "bob", 1, START_PORT + 1, + WS_START_PORT + 1, } // Ivan has the address 0xA8d2D06aCE9c7FFc24Ee785C2695678aeCDfd7A0 @@ -55,6 +61,7 @@ var Ivan Actor = Actor{ "ivan", 2, START_PORT + 2, + WS_START_PORT + 2, } // Irene has the address 0x111A00868581f73AB42FEEF67D235Ca09ca1E8db @@ -65,6 +72,7 @@ var Irene Actor = Actor{ "irene", 3, START_PORT + 3, + WS_START_PORT + 3, } // Actors for L2 @@ -77,6 +85,7 @@ var AlicePrime Actor = Actor{ "alice", 0, START_PORT + 4, + WS_START_PORT + 4, } // BobPrime has the address 0xBBB676f9cFF8D242e9eaC39D063848807d3D1D94 @@ -87,4 +96,5 @@ var BobPrime Actor = Actor{ "bob", 1, START_PORT + 5, + WS_START_PORT + 5, } diff --git a/main.go b/main.go index f2a4beab3..a268a3d6b 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "os" "os/signal" "strings" + "sync" "syscall" "github.com/ethereum/go-ethereum/common" @@ -18,6 +19,7 @@ import ( "github.com/statechannels/go-nitro/node/engine/chainservice" p2pms "github.com/statechannels/go-nitro/node/engine/messageservice/p2p-message-service" "github.com/statechannels/go-nitro/node/engine/store" + "github.com/statechannels/go-nitro/paymentsmanager" "github.com/urfave/cli/v2" "github.com/urfave/cli/v2/altsrc" ) @@ -38,6 +40,7 @@ func main() { BRIDGE_ADDRESS = "bridgeaddress" PUBLIC_IP = "publicip" MSG_PORT = "msgport" + WS_MSG_PORT = "wsmsgport" RPC_PORT = "rpcport" GUI_PORT = "guiport" BOOT_PEERS = "bootpeers" @@ -60,7 +63,7 @@ func main() { TLS_KEY_FILEPATH = "tlskeyfilepath" ) var pkString, chainUrl, chainAuthToken, naAddress, vpaAddress, caAddress, bridgeAddress, chainPk, durableStoreFolder, bootPeers, publicIp, extMultiAddr string - var msgPort, rpcPort, guiPort int + var msgPort, wsMsgPort, rpcPort, guiPort int var chainStartBlock uint64 var useNats, useDurableStore, l2 bool @@ -184,6 +187,13 @@ func main() { Category: CONNECTIVITY_CATEGORY, Destination: &msgPort, }), + altsrc.NewIntFlag(&cli.IntFlag{ + Name: WS_MSG_PORT, + Usage: "Specifies the websocket port for the message service.", + Value: 6005, + Category: "Connectivity:", + Destination: &wsMsgPort, + }), altsrc.NewIntFlag(&cli.IntFlag{ Name: RPC_PORT, Usage: "Specifies the tcp port for the rpc server.", @@ -249,7 +259,8 @@ func main() { messageOpts := p2pms.MessageOpts{ PkBytes: common.Hex2Bytes(pkString), - Port: msgPort, + TcpPort: msgPort, + WsMsgPort: wsMsgPort, BootPeers: peerSlice, PublicIp: publicIp, ExtMultiAddr: extMultiAddr, @@ -288,16 +299,33 @@ func main() { if err != nil { return err } - var cert tls.Certificate + paymentsManager, err := paymentsmanager.NewPaymentsManager(node) + if err != nil { + return err + } + + wg := new(sync.WaitGroup) + defer wg.Wait() + + paymentsManager.Start(wg) + defer func() { + err := paymentsManager.Stop() + if err != nil { + panic(err) + } + }() + + var cert *tls.Certificate if tlsCertFilepath != "" && tlsKeyFilepath != "" { - cert, err = tls.LoadX509KeyPair(tlsCertFilepath, tlsKeyFilepath) + loadedCert, err := tls.LoadX509KeyPair(tlsCertFilepath, tlsKeyFilepath) if err != nil { panic(err) } + cert = &loadedCert } - rpcServer, err := rpc.InitializeNodeRpcServer(node, rpcPort, useNats, &cert) + rpcServer, err := rpc.InitializeNodeRpcServer(node, paymentsManager, rpcPort, useNats, cert) if err != nil { return err } diff --git a/node/engine/messageservice/p2p-message-service/service.go b/node/engine/messageservice/p2p-message-service/service.go index 14f226581..8c68d8c5d 100644 --- a/node/engine/messageservice/p2p-message-service/service.go +++ b/node/engine/messageservice/p2p-message-service/service.go @@ -18,6 +18,7 @@ import ( "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/protocol" "github.com/libp2p/go-libp2p/p2p/transport/tcp" + "github.com/libp2p/go-libp2p/p2p/transport/websocket" "github.com/multiformats/go-multiaddr" "github.com/statechannels/go-nitro/internal/logging" "github.com/statechannels/go-nitro/internal/safesync" @@ -32,8 +33,9 @@ type basicPeerInfo struct { } const ( - DHT_PROTOCOL_PREFIX protocol.ID = "/nitro" // use /nitro/kad/1.0.0 instead of /ipfs/kad/1.0.0 - GENERAL_MSG_PROTOCOL_ID protocol.ID = "/nitro/msg/1.0.0" + DHT_PROTOCOL_PREFIX protocol.ID = "/nitro" // use /nitro/kad/1.0.0 instead of /ipfs/kad/1.0.0 + GENERAL_MSG_PROTOCOL_ID protocol.ID = "/nitro/msg/1.0.0" + PEER_EXCHANGE_PROTOCOL_ID protocol.ID = "/nitro/peerinfo/1.0.0" DELIMITER = '\n' BUFFER_SIZE = 1_000 @@ -44,7 +46,8 @@ const ( type MessageOpts struct { PkBytes []byte - Port int + TcpPort int + WsMsgPort int BootPeers []string PublicIp string SCAddr types.Address @@ -80,7 +83,7 @@ func NewMessageService(opts MessageOpts) *P2PMessageService { } addressFactory := func(addrs []multiaddr.Multiaddr) []multiaddr.Multiaddr { - extMultiAddr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/tcp/%d", opts.PublicIp, opts.Port)) + extMultiAddr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/tcp/%d", opts.PublicIp, opts.TcpPort)) if err != nil { ms.logger.Error("failed to create publicIp multiaddress", "err", err) return addrs @@ -105,10 +108,14 @@ func NewMessageService(opts MessageOpts) *P2PMessageService { options := []libp2p.Option{ libp2p.Identity(privateKey), libp2p.AddrsFactory(addressFactory), - libp2p.ListenAddrStrings(fmt.Sprintf("/ip4/%s/tcp/%d", "0.0.0.0", opts.Port)), + libp2p.ListenAddrStrings( + fmt.Sprintf("/ip4/%s/tcp/%d", opts.PublicIp, opts.TcpPort), + fmt.Sprintf("/ip4/%s/tcp/%d/ws", opts.PublicIp, opts.WsMsgPort), + ), libp2p.Transport(tcp.NewTCPTransport), libp2p.NATPortMap(), libp2p.EnableNATService(), + libp2p.Transport(websocket.New), libp2p.DefaultMuxers, } host, err := libp2p.New(options...) @@ -116,6 +123,7 @@ func NewMessageService(opts MessageOpts) *P2PMessageService { ms.p2pHost = host ms.p2pHost.SetStreamHandler(GENERAL_MSG_PROTOCOL_ID, ms.msgStreamHandler) + ms.p2pHost.SetStreamHandler(PEER_EXCHANGE_PROTOCOL_ID, ms.receivePeerInfo) // Print out my own peerInfo peerInfo := peer.AddrInfo{ @@ -286,6 +294,39 @@ func (ms *P2PMessageService) msgStreamHandler(stream network.Stream) { ms.toEngine <- m } +// receivePeerInfo receives peer info from the given stream +func (ms *P2PMessageService) receivePeerInfo(stream network.Stream) { + ms.logger.Info("received peerInfo") + defer stream.Close() + + // Create a buffer stream for non blocking read and write. + reader := bufio.NewReader(stream) + raw, err := reader.ReadString(DELIMITER) + + // An EOF means the stream has been closed by the other side. + if errors.Is(err, io.EOF) { + return + } + if err != nil { + ms.logger.Error("error", "err", err) + return + } + + var msg *basicPeerInfo + err = json.Unmarshal([]byte(raw), &msg) + if err != nil { + ms.logger.Error("error in unmarshalling", "err", err) + return + } + + _, foundPeer := ms.peers.LoadOrStore(msg.Address.String(), msg.Id) + if !foundPeer { + peerInfo := basicPeerInfo{msg.Id, msg.Address} + ms.logger.Info("stored new peer in map", "peerInfo", peerInfo) + ms.newPeerInfo <- peerInfo + } +} + func (ms *P2PMessageService) getPeerIdFromDht(scaddr string) (peer.ID, error) { recordBytes, err := ms.dht.GetValue(context.Background(), DHT_RECORD_PREFIX+scaddr) if err != nil { diff --git a/node_test/helpers_test.go b/node_test/helpers_test.go index 4e5b2519d..f37868cbc 100644 --- a/node_test/helpers_test.go +++ b/node_test/helpers_test.go @@ -71,7 +71,8 @@ func setupMessageService(tc TestCase, tp TestParticipant, si sharedTestInfrastru case P2PMessageService: ms := p2pms.NewMessageService(p2pms.MessageOpts{ PublicIp: DEFAULT_PUBLIC_IP, - Port: int(tp.Port), + TcpPort: int(tp.Port), + WsMsgPort: int(tp.WSPort), SCAddr: tp.Address(), PkBytes: tp.PrivateKey, BootPeers: bootPeers, diff --git a/node_test/paymentproxy_test.go b/node_test/paymentproxy_test.go index 0d7154858..256c84ff6 100644 --- a/node_test/paymentproxy_test.go +++ b/node_test/paymentproxy_test.go @@ -81,7 +81,7 @@ func TestPaymentProxy(t *testing.T) { proxyAddress, bobRPCUrl, destinationServerUrl, - 1, "", "") + 1, "", "", false) defer func() { err := proxy.Stop() if err != nil { @@ -238,11 +238,11 @@ func setupNitroClients(t *testing.T, logFile string) (alice, irene, bob rpc.RpcC aliceChainService := chainservice.NewMockChainService(chain, ta.Alice.Address()) bobChainService := chainservice.NewMockChainService(chain, ta.Bob.Address()) ireneChainService := chainservice.NewMockChainService(chain, ta.Irene.Address()) - ireneClient, msgIrene, ireneCleanup := setupNitroNodeWithRPCClient(t, ta.Irene.PrivateKey, 3106, 4106, ireneChainService, transport.Http, []string{}) + ireneClient, msgIrene, ireneCleanup := setupNitroNodeWithRPCClient(t, ta.Irene.PrivateKey, 3106, 6106, 4106, ireneChainService, transport.Http, []string{}) bootPeers := []string{msgIrene.MultiAddr} - aliceClient, msgAlice, aliceCleanup := setupNitroNodeWithRPCClient(t, ta.Alice.PrivateKey, 3105, 4105, aliceChainService, transport.Http, bootPeers) + aliceClient, msgAlice, aliceCleanup := setupNitroNodeWithRPCClient(t, ta.Alice.PrivateKey, 3105, 6105, 4105, aliceChainService, transport.Http, bootPeers) - bobClient, msgBob, bobCleanup := setupNitroNodeWithRPCClient(t, ta.Bob.PrivateKey, 3107, 4107, bobChainService, transport.Http, bootPeers) + bobClient, msgBob, bobCleanup := setupNitroNodeWithRPCClient(t, ta.Bob.PrivateKey, 3107, 6107, 4107, bobChainService, transport.Http, bootPeers) slog.Info("Clients created") diff --git a/node_test/rpc_test.go b/node_test/rpc_test.go index f817fa50d..009fb83dd 100644 --- a/node_test/rpc_test.go +++ b/node_test/rpc_test.go @@ -25,6 +25,7 @@ import ( p2pms "github.com/statechannels/go-nitro/node/engine/messageservice/p2p-message-service" "github.com/statechannels/go-nitro/node/engine/store" "github.com/statechannels/go-nitro/node/query" + "github.com/statechannels/go-nitro/paymentsmanager" "github.com/statechannels/go-nitro/protocols/directfund" "github.com/statechannels/go-nitro/protocols/virtualfund" "github.com/statechannels/go-nitro/rpc" @@ -114,7 +115,7 @@ func executeNRpcTest(t *testing.T, connectionType transport.TransportType, n int // Set up the intermediaries if n > 2 { for i := 1; i < n-1; i++ { - rpcClient, msg, cleanup := setupNitroNodeWithRPCClient(t, actors[i].PrivateKey, 3105+i, 4105+i, chainServices[i], connectionType, []string{}) + rpcClient, msg, cleanup := setupNitroNodeWithRPCClient(t, actors[i].PrivateKey, 3105+i, 6105+i, 4105+i, chainServices[i], connectionType, []string{}) clients[i] = rpcClient msgServices[i] = msg bootPeers = append(bootPeers, msg.MultiAddr) @@ -124,7 +125,7 @@ func executeNRpcTest(t *testing.T, connectionType transport.TransportType, n int // Set up the first and last client for i := 0; i < n; i = i + (n - 1) { - rpcClient, msg, cleanup := setupNitroNodeWithRPCClient(t, actors[i].PrivateKey, 3105+i, 4105+i, chainServices[i], connectionType, bootPeers) + rpcClient, msg, cleanup := setupNitroNodeWithRPCClient(t, actors[i].PrivateKey, 3105+i, 6105+i, 4105+i, chainServices[i], connectionType, bootPeers) clients[i] = rpcClient msgServices[i] = msg defer cleanup() @@ -392,6 +393,7 @@ func setupNitroNodeWithRPCClient( t *testing.T, pkBytes []byte, msgPort int, + wsMsgPort int, rpcPort int, chain *chainservice.MockChainService, connectionType transport.TransportType, @@ -410,7 +412,8 @@ func setupNitroNodeWithRPCClient( slog.Info("Initializing message service on port " + fmt.Sprint(msgPort) + "...") messageService := p2pms.NewMessageService(p2pms.MessageOpts{ PkBytes: pkBytes, - Port: msgPort, + TcpPort: msgPort, + WsMsgPort: wsMsgPort, BootPeers: bootPeers, PublicIp: "127.0.0.1", SCAddr: *ourStore.GetAddress(), @@ -438,7 +441,8 @@ func setupNitroNodeWithRPCClient( panic(err) } - rpcServer, err := interRpc.InitializeNodeRpcServer(&node, rpcPort, useNats, &cert) + paymentsManager := paymentsmanager.PaymentsManager{} + rpcServer, err := interRpc.InitializeNodeRpcServer(&node, paymentsManager, rpcPort, useNats, &cert) if err != nil { t.Fatal(err) } @@ -453,7 +457,7 @@ func setupNitroNodeWithRPCClient( } case transport.Http: - clientConnection, err = http.NewHttpTransportAsClient(rpcServer.Url(), 10*time.Millisecond) + clientConnection, err = http.NewHttpTransportAsClient(rpcServer.Url(), true, 10*time.Millisecond) if err != nil { panic(err) } diff --git a/packages/nitro-protocol/hardhat-deploy/deploy-fvm.ts b/packages/nitro-protocol/hardhat-deploy/deploy-fvm.ts index 20ef71af8..3886fee49 100644 --- a/packages/nitro-protocol/hardhat-deploy/deploy-fvm.ts +++ b/packages/nitro-protocol/hardhat-deploy/deploy-fvm.ts @@ -18,6 +18,7 @@ module.exports = async (hre: HardhatRuntimeEnvironment) => { // since Ethereum's legacy transaction format is not supported on FVM, we need to specify // maxPriorityFeePerGas to instruct hardhat to use EIP-1559 tx format maxPriorityFeePerGas: ethers.BigNumber.from(1500000000), + maxFeePerGas: ethers.BigNumber.from(1500000000), skipIfAlreadyDeployed: false, log: true, }); @@ -33,6 +34,7 @@ module.exports = async (hre: HardhatRuntimeEnvironment) => { // since Ethereum's legacy transaction format is not supported on FVM, we need to specify // maxPriorityFeePerGas to instruct hardhat to use EIP-1559 tx format maxPriorityFeePerGas: ethers.BigNumber.from(1500000000), + maxFeePerGas: ethers.BigNumber.from(1500000000), skipIfAlreadyDeployed: false, log: true, }); @@ -48,6 +50,7 @@ module.exports = async (hre: HardhatRuntimeEnvironment) => { // since Ethereum's legacy transaction format is not supported on FVM, we need to specify // maxPriorityFeePerGas to instruct hardhat to use EIP-1559 tx format maxPriorityFeePerGas: ethers.BigNumber.from(1500000000), + maxFeePerGas: ethers.BigNumber.from(1500000000), skipIfAlreadyDeployed: false, log: true, }); diff --git a/packages/nitro-protocol/package.json b/packages/nitro-protocol/package.json index 607988c67..8b41763ff 100644 --- a/packages/nitro-protocol/package.json +++ b/packages/nitro-protocol/package.json @@ -1,6 +1,6 @@ { "name": "@statechannels/nitro-protocol", - "version": "2.0.1-alpha.6", + "version": "2.1.0-alpha.0", "description": "Smart contracts and typescript libraries for nitro state channel protocol.", "keywords": [], "homepage": "https://github.com/statechannels/go-nitro", diff --git a/packages/nitro-rpc-client/scripts/client-runner.ts b/packages/nitro-rpc-client/scripts/client-runner.ts index 91ecf9b0a..f856fdb9c 100755 --- a/packages/nitro-rpc-client/scripts/client-runner.ts +++ b/packages/nitro-rpc-client/scripts/client-runner.ts @@ -30,7 +30,8 @@ async function initializeClients(): Promise { const clients: Clients = new Map(); for (const clientName of clientNames) { const client = await NitroRpcClient.CreateHttpNitroClient( - getLocalRPCUrl(port) + getLocalRPCUrl(port), + true ); clients.set(clientName, client); port++; diff --git a/packages/nitro-rpc-client/src/cli.ts b/packages/nitro-rpc-client/src/cli.ts index 461cd0ee9..f5964574f 100755 --- a/packages/nitro-rpc-client/src/cli.ts +++ b/packages/nitro-rpc-client/src/cli.ts @@ -20,7 +20,18 @@ yargs(hideBin(process.argv)) type: "boolean", description: "Whether channel notifications are printed to the console", }, - h: { alias: "host", default: "127.0.0.1", type: "string" }, + h: { + alias: "host", + default: "127.0.0.1", + type: "string", + description: "Custom hostname", + }, + s: { + alias: "isSecure", + default: true, + type: "boolean", + description: "Is it a secured connection", + }, }) .command( "version", @@ -29,9 +40,11 @@ yargs(hideBin(process.argv)) async (yargs) => { const rpcPort = yargs.p; const rpcHost = yargs.h; + const isSecure = yargs.s; const rpcClient = await NitroRpcClient.CreateHttpNitroClient( - getRPCUrl(rpcHost, rpcPort) + getRPCUrl(rpcHost, rpcPort), + isSecure ); const version = await rpcClient.GetVersion(); console.log(version); @@ -46,9 +59,11 @@ yargs(hideBin(process.argv)) async (yargs) => { const rpcPort = yargs.p; const rpcHost = yargs.h; + const isSecure = yargs.s; const rpcClient = await NitroRpcClient.CreateHttpNitroClient( - getRPCUrl(rpcHost, rpcPort) + getRPCUrl(rpcHost, rpcPort), + isSecure ); const address = await rpcClient.GetAddress(); console.log(address); @@ -63,9 +78,11 @@ yargs(hideBin(process.argv)) async (yargs) => { const rpcPort = yargs.p; const rpcHost = yargs.h; + const isSecure = yargs.s; const rpcClient = await NitroRpcClient.CreateHttpNitroClient( - getRPCUrl(rpcHost, rpcPort) + getRPCUrl(rpcHost, rpcPort), + isSecure ); const ledgers = await rpcClient.GetAllLedgerChannels(); for (const ledger of ledgers) { @@ -82,9 +99,11 @@ yargs(hideBin(process.argv)) async (yargs) => { const rpcPort = yargs.p; const rpcHost = yargs.h; + const isSecure = yargs.s; const rpcClient = await NitroRpcClient.CreateHttpNitroClient( - getRPCUrl(rpcHost, rpcPort) + getRPCUrl(rpcHost, rpcPort), + isSecure ); const ledgers = await rpcClient.GetAllL2Channels(); for (const ledger of ledgers) { @@ -108,9 +127,11 @@ yargs(hideBin(process.argv)) async (yargs) => { const rpcPort = yargs.p; const rpcHost = yargs.h; + const isSecure = yargs.s; const rpcClient = await NitroRpcClient.CreateHttpNitroClient( - getRPCUrl(rpcHost, rpcPort) + getRPCUrl(rpcHost, rpcPort), + isSecure ); const paymentChans = await rpcClient.GetPaymentChannelsByLedger( yargs.ledgerId @@ -152,9 +173,11 @@ yargs(hideBin(process.argv)) async (yargs) => { const rpcPort = yargs.p; const rpcHost = yargs.h; + const isSecure = yargs.s; const rpcClient = await NitroRpcClient.CreateHttpNitroClient( - getRPCUrl(rpcHost, rpcPort) + getRPCUrl(rpcHost, rpcPort), + isSecure ); if (yargs.n) logOutChannelUpdates(rpcClient); @@ -192,9 +215,11 @@ yargs(hideBin(process.argv)) async (yargs) => { const rpcPort = yargs.p; const rpcHost = yargs.h; + const isSecure = yargs.s; const rpcClient = await NitroRpcClient.CreateHttpNitroClient( - getRPCUrl(rpcHost, rpcPort) + getRPCUrl(rpcHost, rpcPort), + isSecure ); if (yargs.n) logOutChannelUpdates(rpcClient); @@ -232,9 +257,11 @@ yargs(hideBin(process.argv)) async (yargs) => { const rpcPort = yargs.p; const rpcHost = yargs.h; + const isSecure = yargs.s; const rpcClient = await NitroRpcClient.CreateHttpNitroClient( - getRPCUrl(rpcHost, rpcPort) + getRPCUrl(rpcHost, rpcPort), + isSecure ); if (yargs.n) logOutChannelUpdates(rpcClient); @@ -274,9 +301,11 @@ yargs(hideBin(process.argv)) async (yargs) => { const rpcPort = yargs.p; const rpcHost = yargs.h; + const isSecure = yargs.s; const rpcClient = await NitroRpcClient.CreateHttpNitroClient( - getRPCUrl(rpcHost, rpcPort) + getRPCUrl(rpcHost, rpcPort), + isSecure ); if (yargs.n) logOutChannelUpdates(rpcClient); @@ -303,9 +332,11 @@ yargs(hideBin(process.argv)) async (yargs) => { const rpcPort = yargs.p; const rpcHost = yargs.h; + const isSecure = yargs.s; const rpcClient = await NitroRpcClient.CreateHttpNitroClient( - getRPCUrl(rpcHost, rpcPort) + getRPCUrl(rpcHost, rpcPort), + isSecure ); const ledgerInfo = await rpcClient.GetLedgerChannel(yargs.channelId); @@ -327,9 +358,11 @@ yargs(hideBin(process.argv)) async (yargs) => { const rpcPort = yargs.p; const rpcHost = yargs.h; + const isSecure = yargs.s; const rpcClient = await NitroRpcClient.CreateHttpNitroClient( - getRPCUrl(rpcHost, rpcPort) + getRPCUrl(rpcHost, rpcPort), + isSecure ); const paymentChannelInfo = await rpcClient.GetPaymentChannel( yargs.channelId @@ -358,9 +391,11 @@ yargs(hideBin(process.argv)) async (yargs) => { const rpcPort = yargs.p; const rpcHost = yargs.h; + const isSecure = yargs.s; const rpcClient = await NitroRpcClient.CreateHttpNitroClient( - getRPCUrl(rpcHost, rpcPort) + getRPCUrl(rpcHost, rpcPort), + isSecure ); if (yargs.n) logOutChannelUpdates(rpcClient); @@ -393,9 +428,11 @@ yargs(hideBin(process.argv)) async (yargs) => { const rpcPort = yargs.p; const rpcHost = yargs.h; + const isSecure = yargs.s; const rpcClient = await NitroRpcClient.CreateHttpNitroClient( - getRPCUrl(rpcHost, rpcPort) + getRPCUrl(rpcHost, rpcPort), + isSecure ); if (yargs.n) logOutChannelUpdates(rpcClient); @@ -435,9 +472,11 @@ yargs(hideBin(process.argv)) async (yargs) => { const rpcPort = yargs.p; const rpcHost = yargs.h; + const isSecure = yargs.s; const rpcClient = await NitroRpcClient.CreateHttpNitroClient( - getRPCUrl(rpcHost, rpcPort) + getRPCUrl(rpcHost, rpcPort), + isSecure ); if (yargs.n) logOutChannelUpdates(rpcClient); diff --git a/packages/nitro-rpc-client/src/rpc-client.ts b/packages/nitro-rpc-client/src/rpc-client.ts index 826933988..666517a3d 100644 --- a/packages/nitro-rpc-client/src/rpc-client.ts +++ b/packages/nitro-rpc-client/src/rpc-client.ts @@ -287,9 +287,10 @@ export class NitroRpcClient implements RpcClientApi { * @returns A NitroRpcClient that uses WS as the transport */ public static async CreateHttpNitroClient( - url: string + url: string, + isSecure: boolean ): Promise { - const transport = await HttpTransport.createTransport(url); + const transport = await HttpTransport.createTransport(url, isSecure); const rpcClient = new NitroRpcClient(transport); rpcClient.authToken = await rpcClient.getAuthToken(); return rpcClient; diff --git a/packages/nitro-rpc-client/src/transport/http.ts b/packages/nitro-rpc-client/src/transport/http.ts index aef81044d..bd09bfae3 100644 --- a/packages/nitro-rpc-client/src/transport/http.ts +++ b/packages/nitro-rpc-client/src/transport/http.ts @@ -16,10 +16,19 @@ import { Transport } from "."; export class HttpTransport { Notifications: EventEmitter; + isSecure: boolean; + + public static async createTransport( + server: string, + isSecure: boolean + ): Promise { + let wsPrefix = "ws://"; + if (isSecure) { + wsPrefix = "wss://"; + } - public static async createTransport(server: string): Promise { // eslint-disable-next-line new-cap - const ws = new w3cwebsocket(`wss://${server}/subscribe`); + const ws = new w3cwebsocket(`${wsPrefix}${server}/subscribe`); // throw any websocket errors so we don't fail silently ws.onerror = (e) => { @@ -30,14 +39,19 @@ export class HttpTransport { // Wait for onopen to fire so we know the connection is ready await new Promise((resolve) => (ws.onopen = () => resolve())); - const transport = new HttpTransport(ws, server); + const transport = new HttpTransport(ws, server, isSecure); return transport; } public async sendRequest( req: RPCRequestAndResponses[K][0] ): Promise { - const url = new URL(`https://${this.server}`).toString(); + let httpPrefix = "http://"; + if (this.isSecure) { + httpPrefix = "https://"; + } + + const url = new URL(`${httpPrefix}${this.server}`).toString(); const result = await axios.post(url.toString(), JSON.stringify(req)); @@ -52,9 +66,10 @@ export class HttpTransport { private server: string; - private constructor(ws: w3cwebsocket, server: string) { + private constructor(ws: w3cwebsocket, server: string, isSecure: boolean) { this.ws = ws; this.server = server; + this.isSecure = isSecure; this.Notifications = new EventEmitter(); this.ws.onmessage = (event) => { diff --git a/packages/payment-proxy-client/src/App.tsx b/packages/payment-proxy-client/src/App.tsx index 247873295..35cfcab23 100644 --- a/packages/payment-proxy-client/src/App.tsx +++ b/packages/payment-proxy-client/src/App.tsx @@ -105,7 +105,7 @@ export default function App() { const [selectedFile, setSelectedFile] = useState(files[0]); useEffect(() => { console.time("Connect to Nitro Node"); - NitroRpcClient.CreateHttpNitroClient(url) + NitroRpcClient.CreateHttpNitroClient(url, true) .then( (c) => setNitroClient(c), (e) => { diff --git a/paymentproxy/proxy.go b/paymentproxy/proxy.go index 3c62c59f6..de7c2740d 100644 --- a/paymentproxy/proxy.go +++ b/paymentproxy/proxy.go @@ -1,7 +1,9 @@ package paymentproxy import ( + "bytes" "context" + "encoding/json" "errors" "fmt" "io" @@ -26,11 +28,20 @@ const ( CHANNEL_ID_VOUCHER_PARAM = "channelId" SIGNATURE_VOUCHER_PARAM = "signature" - VOUCHER_CONTEXT_ARG contextKey = "voucher" + VOUCHER_CONTEXT_ARG contextKey = "voucher" + RPC_METHOD_CONTEXT_ARG contextKey = "rpcMethod" ErrPayment = types.ConstError("payment error") ) +// TODO: Make configurable +var paidRPCMethods = []string{ + "eth_getLogs", + "eth_getStorageAt", + "eth_getBlockByHash", + "eth_getBlockByNumber", +} + // createPaymentError wraps an error with ErrPayment. func createPaymentError(err error) error { return fmt.Errorf("%w: %w", ErrPayment, err) @@ -45,13 +56,14 @@ type PaymentProxy struct { destinationUrl *url.URL certFilePath, certKeyPath string + enablePaidRpcMethods bool } // NewPaymentProxy creates a new PaymentProxy. -func NewPaymentProxy(proxyAddress string, nitroEndpoint string, destinationURL string, costPerByte uint64, certFilePath, certKeyPath string) *PaymentProxy { +func NewPaymentProxy(proxyAddress string, nitroEndpoint string, destinationURL string, costPerByte uint64, certFilePath, certKeyPath string, enablePaidRpcMethods bool) *PaymentProxy { server := &http.Server{Addr: proxyAddress} - nitroClient, err := rpc.NewHttpRpcClient(nitroEndpoint) + nitroClient, err := rpc.NewHttpRpcClient(nitroEndpoint, true) if err != nil { panic(err) } @@ -61,18 +73,24 @@ func NewPaymentProxy(proxyAddress string, nitroEndpoint string, destinationURL s } p := &PaymentProxy{ - server: server, - nitroClient: nitroClient, - costPerByte: costPerByte, - destinationUrl: destinationUrl, - reverseProxy: &httputil.ReverseProxy{}, - certFilePath: certFilePath, - certKeyPath: certKeyPath, + server: server, + nitroClient: nitroClient, + costPerByte: costPerByte, + destinationUrl: destinationUrl, + reverseProxy: &httputil.ReverseProxy{}, + certFilePath: certFilePath, + certKeyPath: certKeyPath, + enablePaidRpcMethods: enablePaidRpcMethods, } // Wire up our handlers to the reverse proxy p.reverseProxy.Rewrite = func(pr *httputil.ProxyRequest) { pr.SetURL(p.destinationUrl) } p.reverseProxy.ModifyResponse = p.handleDestinationResponse p.reverseProxy.ErrorHandler = p.handleError + + // Setup transport with compression disabled to access content-length header in handleDestinationResponse + p.reverseProxy.Transport = http.DefaultTransport + p.reverseProxy.Transport.(*http.Transport).DisableCompression = true + // Wire up our handler to the server p.server.Handler = p @@ -100,16 +118,27 @@ func (p *PaymentProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - v, err := parseVoucher(r.URL.Query()) - if err != nil { - p.handleError(w, r, createPaymentError(fmt.Errorf("could not parse voucher: %w", err))) - return + queryParams := r.URL.Query() + requiresPayment := true + var rpcMethod string + + if p.enablePaidRpcMethods { + requiresPayment, rpcMethod = isPaymentRequired(r) } - removeVoucher(r) + if requiresPayment { + v, err := parseVoucher(queryParams) + if err != nil { + p.handleError(w, r, createPaymentError(fmt.Errorf("could not parse voucher: %w", err))) + return + } - // We add the voucher to the request context so we can access it in the response handler - r = r.WithContext(context.WithValue(r.Context(), VOUCHER_CONTEXT_ARG, v)) + removeVoucher(r) + + // We add the voucher and rpcMethod to the request context so we can access them in the response handler + r = r.WithContext(context.WithValue(r.Context(), VOUCHER_CONTEXT_ARG, v)) + r = r.WithContext(context.WithValue(r.Context(), RPC_METHOD_CONTEXT_ARG, rpcMethod)) + } p.reverseProxy.ServeHTTP(w, r) } @@ -140,11 +169,17 @@ func (p *PaymentProxy) handleDestinationResponse(r *http.Response) error { v, ok := r.Request.Context().Value(VOUCHER_CONTEXT_ARG).(payments.Voucher) if !ok { - return createPaymentError(fmt.Errorf("could not fetch voucher from context")) + // If VOUCHER_CONTEXT_ARG does not exist the request does not need payment + return nil } cost := p.costPerByte * contentLength - slog.Debug("Request cost", "cost-per-byte", p.costPerByte, "response-length", contentLength, "cost", cost) + rpcMethod, ok := r.Request.Context().Value(RPC_METHOD_CONTEXT_ARG).(string) + if ok { + slog.Debug("Request cost", "cost-per-byte", p.costPerByte, "response-length", contentLength, "cost", cost, "method", rpcMethod) + } else { + slog.Debug("Request cost", "cost-per-byte", p.costPerByte, "response-length", contentLength, "cost", cost) + } s, err := p.nitroClient.ReceiveVoucher(v) if err != nil { @@ -204,6 +239,43 @@ func (p *PaymentProxy) Stop() error { return p.nitroClient.Close() } +// Helper method to parse request and determine whether it qualifies for a payment +// Payment is required for a request if: +// - "Content-Type" header is set to "application/json" +// - Request body has non-empty "jsonrpc" and "method" fields +func isPaymentRequired(r *http.Request) (bool, string) { + if r.Header.Get("Content-Type") != "application/json" { + return false, "" + } + + var ReqBody struct { + JsonRpc string `json:"jsonrpc"` + Method string `json:"method"` + } + bodyBytes, _ := io.ReadAll(r.Body) + + err := json.Unmarshal(bodyBytes, &ReqBody) + if err != nil || ReqBody.JsonRpc == "" || ReqBody.Method == "" { + return false, "" + } + + slog.Debug("Serving RPC request", "method", ReqBody.Method) + + // Reassign request body as io.ReadAll consumes it + r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + + rpcMethod := ReqBody.Method + + // Check if payment is required for RPC method + for _, paidRPCMethod := range paidRPCMethods { + if paidRPCMethod == rpcMethod { + return true, rpcMethod + } + } + + return false, "" +} + // parseVoucher takes in an a collection of query params and parses out a voucher. func parseVoucher(params url.Values) (payments.Voucher, error) { rawChId := params.Get(CHANNEL_ID_VOUCHER_PARAM) diff --git a/paymentsmanager/http_middleware.go b/paymentsmanager/http_middleware.go new file mode 100644 index 000000000..f40d62302 --- /dev/null +++ b/paymentsmanager/http_middleware.go @@ -0,0 +1,130 @@ +package paymentsmanager + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "log/slog" + "math/big" + "net/http" + "regexp" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/statechannels/go-nitro/crypto" +) + +const ( + PAYMENT_HEADER_KEY = "x-payment" + PAYMENT_HEADER_REGEX = "vhash:(.*),vsig:(.*)" +) + +var ( + ErrHeaderMissing = errors.New("payment header x-payment not set") + ErrInvalidPaymentHeader = errors.New("invalid payment header format") + ErrUnableToRecoverSigner = errors.New("unable to recover the voucher signer") +) + +// HTTPMiddleware: extracts and validates vouchers from RPC requests +func HTTPMiddleware(next http.Handler, validator VoucherValidator, queryRates map[string]*big.Int) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Validate voucher + r, err := extractAndValidateVoucher(r, validator, queryRates) + if err != nil { + if isPaymentError(err) { + http.Error(w, err.Error(), http.StatusPaymentRequired) + } else { + http.Error(w, err.Error(), http.StatusBadRequest) + } + + return + } + + // Let the request move ahead after voucher validation + next.ServeHTTP(w, r) + }) +} + +func extractAndValidateVoucher(r *http.Request, validator VoucherValidator, queryRates map[string]*big.Int) (*http.Request, error) { + // Determine RPC method from the request + isRpcCall, rpcMethod := isRpcCall(r) + if !isRpcCall { + return r, nil + } + + // Determine the query cost + queryCost := queryRates[rpcMethod] + if queryCost == nil || queryCost.Cmp(big.NewInt(0)) == 0 { + slog.Info("Serving a free RPC request", "method", rpcMethod) + return r, nil + } + + // Extract voucher details from the header + paymentHeader := r.Header.Get(PAYMENT_HEADER_KEY) + if paymentHeader == "" { + return r, ErrHeaderMissing + } + + re := regexp.MustCompile(PAYMENT_HEADER_REGEX) + match := re.FindStringSubmatch(paymentHeader) + + var vhash, vsig string + if match != nil { + vhash = match[1] + vsig = match[2] + } else { + return r, ErrInvalidPaymentHeader + } + + // Determine signer from the voucher hash and signature + vhashBytes := common.Hex2Bytes(strings.TrimPrefix(vhash, "0x")) + signature := crypto.SplitSignature(common.Hex2Bytes(strings.TrimPrefix(vsig, "0x"))) + signer, err := crypto.RecoverEthereumMessageSigner(vhashBytes, signature) + if err != nil { + return r, ErrUnableToRecoverSigner + } + + // Remove the payment header from the request + r.Header.Del(PAYMENT_HEADER_KEY) + + err = validator.ValidateVoucher(common.HexToHash(vhash), signer, queryCost) + if err != nil { + return r, err + } + + slog.Info("Serving a paid RPC request", "method", rpcMethod, "cost", queryCost, "sender", signer.Hex()) + return r, nil +} + +// Helper method to parse request and determine whether it's a RPC call +// A request is a RPC call if: +// - "Content-Type" header is set to "application/json" +// - Request body has non-empty "jsonrpc" and "method" fields +// +// Also returns the parsed RPC method +func isRpcCall(r *http.Request) (bool, string) { + if r.Header.Get("Content-Type") != "application/json" { + return false, "" + } + + var ReqBody struct { + JsonRpc string `json:"jsonrpc"` + Method string `json:"method"` + } + bodyBytes, _ := io.ReadAll(r.Body) + + err := json.Unmarshal(bodyBytes, &ReqBody) + if err != nil || ReqBody.JsonRpc == "" || ReqBody.Method == "" { + return false, "" + } + + // Reassign request body as io.ReadAll consumes it + r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + + return true, ReqBody.Method +} + +func isPaymentError(err error) bool { + return strings.HasPrefix(err.Error(), ERR_PAYMENT) +} diff --git a/paymentsmanager/payments_manager.go b/paymentsmanager/payments_manager.go new file mode 100644 index 000000000..c78b0ac26 --- /dev/null +++ b/paymentsmanager/payments_manager.go @@ -0,0 +1,219 @@ +package paymentsmanager + +import ( + "fmt" + "log/slog" + "math/big" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/hashicorp/golang-lru/v2/expirable" + "github.com/statechannels/go-nitro/node" + "github.com/statechannels/go-nitro/node/query" + "github.com/statechannels/go-nitro/payments" + "github.com/statechannels/go-nitro/types" +) + +const ( + DEFAULT_LRU_CACHE_MAX_ACCOUNTS = 1000 + DEFAULT_LRU_CACHE_ACCOUNT_TTL = 30 * 60 // 30mins + DEFAULT_LRU_CACHE_MAX_VOUCHERS_PER_ACCOUNT = 1000 + DEFAULT_LRU_CACHE_VOUCHER_TTL = 5 * 60 // 5mins + DEFAULT_LRU_CACHE_MAX_PAYMENT_CHANNELS = 10000 + DEFAULT_LRU_CACHE_PAYMENT_CHANNEL_TTL = DEFAULT_LRU_CACHE_ACCOUNT_TTL + + // Note: Make sure the transport has timeout > DEFAULT_VOUCHER_CHECK_ATTEMPTS * DEFAULT_VOUCHER_CHECK_INTERVAL + DEFAULT_VOUCHER_CHECK_INTERVAL = 1 + DEFAULT_VOUCHER_CHECK_ATTEMPTS = 5 +) + +var ( + ERR_PAYMENT = "ERR_PAYMENT" + ERR_PAYMENT_NOT_RECEIVED = fmt.Sprintf("%s_NOT_RECEIVED", ERR_PAYMENT) + ERR_PAYMENT_AMOUNT_INSUFFICIENT = fmt.Sprintf("%s_AMOUNT_INSUFFICIENT", ERR_PAYMENT) +) + +type InFlightVoucher struct { + voucher payments.Voucher + amount *big.Int +} + +// Struct representing the payments manager service +type PaymentsManager struct { + nitro *node.Node + + // In-memory LRU cache of vouchers received on payment channels + // Map: payer -> voucher hash -> InFlightVoucher (voucher, delta amount) + receivedVouchersCache *expirable.LRU[string, *expirable.LRU[string, InFlightVoucher]] + + // LRU map to keep track of amounts paid so far on payment channels + // Map: channel id -> amount paid so far + paidSoFarOnChannel *expirable.LRU[string, *big.Int] + + // Used to signal shutdown of the service + quitChan chan bool +} + +func NewPaymentsManager(nitro *node.Node) (PaymentsManager, error) { + pm := PaymentsManager{nitro: nitro} + + pm.receivedVouchersCache = expirable.NewLRU[string, *expirable.LRU[string, InFlightVoucher]]( + DEFAULT_LRU_CACHE_MAX_ACCOUNTS, + nil, + time.Second*DEFAULT_LRU_CACHE_ACCOUNT_TTL, + ) + + pm.paidSoFarOnChannel = expirable.NewLRU[string, *big.Int]( + DEFAULT_LRU_CACHE_MAX_PAYMENT_CHANNELS, + nil, + time.Second*DEFAULT_LRU_CACHE_PAYMENT_CHANNEL_TTL, + ) + + pm.quitChan = make(chan bool) + + // Load existing open payment channels with amount paid so far from the stored state + err := pm.loadPaymentChannels() + if err != nil { + return PaymentsManager{}, err + } + + return pm, nil +} + +func (pm *PaymentsManager) Start(wg *sync.WaitGroup) { + slog.Info("starting payments manager...") + + wg.Add(1) + go func() { + defer wg.Done() + pm.run() + }() +} + +func (pm *PaymentsManager) Stop() error { + slog.Info("stopping payments manager...") + close(pm.quitChan) + return nil +} + +func (pm *PaymentsManager) ValidateVoucher(voucherHash common.Hash, signerAddress common.Address, value *big.Int) (bool, string) { + // Check the payments map for required voucher + var isPaymentReceived, isOfSufficientValue bool + for i := 0; i < DEFAULT_VOUCHER_CHECK_ATTEMPTS; i++ { + isPaymentReceived, isOfSufficientValue = pm.checkVoucherInCache(voucherHash, signerAddress, value) + + if isPaymentReceived { + if !isOfSufficientValue { + return false, ERR_PAYMENT_AMOUNT_INSUFFICIENT + } + return true, "" + } + + // Retry after an interval if voucher not found + slog.Info("Payment not found, retrying...", "payer", signerAddress, "retryInterval", DEFAULT_VOUCHER_CHECK_INTERVAL) + time.Sleep(DEFAULT_VOUCHER_CHECK_INTERVAL * time.Second) + } + + return false, ERR_PAYMENT_NOT_RECEIVED +} + +// Check for a given payment voucher in LRU cache map +// Returns whether the voucher was found, whether it was of sufficient value +func (pm *PaymentsManager) checkVoucherInCache(voucherHash common.Hash, signerAddress common.Address, minRequiredValue *big.Int) (bool, bool) { + vouchersMap, ok := pm.receivedVouchersCache.Get(signerAddress.Hex()) + if !ok { + return false, false + } + + receivedVoucher, ok := vouchersMap.Get(voucherHash.Hex()) + if !ok { + return false, false + } + + if receivedVoucher.amount.Cmp(minRequiredValue) < 0 { + return true, false + } + + // Delete the voucher from map after consuming it + vouchersMap.Remove(voucherHash.Hex()) + return true, true +} + +func (pm *PaymentsManager) run() { + slog.Info("starting voucher subscription...") + for { + select { + case voucher := <-pm.nitro.ReceivedVouchers(): + payer, err := pm.getChannelCounterparty(voucher.ChannelId) + if err != nil { + // TODO: Handle + panic(err) + } + + paidSoFar, ok := pm.paidSoFarOnChannel.Get(voucher.ChannelId.String()) + if !ok { + paidSoFar = big.NewInt(0) + } + + paymentAmount := big.NewInt(0).Sub(voucher.Amount, paidSoFar) + pm.paidSoFarOnChannel.Add(voucher.ChannelId.String(), voucher.Amount) + slog.Info("Received a voucher", "payer", payer.String(), "amount", paymentAmount.String()) + + vouchersMap, ok := pm.receivedVouchersCache.Get(payer.Hex()) + if !ok { + vouchersMap = expirable.NewLRU[string, InFlightVoucher]( + DEFAULT_LRU_CACHE_MAX_VOUCHERS_PER_ACCOUNT, + nil, + time.Second*DEFAULT_LRU_CACHE_VOUCHER_TTL, + ) + + pm.receivedVouchersCache.Add(payer.Hex(), vouchersMap) + } + + voucherHash, err := voucher.Hash() + if err != nil { + // TODO: Handle + panic(err) + } + + vouchersMap.Add(voucherHash.Hex(), InFlightVoucher{voucher: voucher, amount: paymentAmount}) + case <-pm.quitChan: + slog.Info("stopping voucher subscription loop...") + return + } + } +} + +func (pm *PaymentsManager) getChannelCounterparty(channelId types.Destination) (common.Address, error) { + paymentChannel, err := pm.nitro.GetPaymentChannel(channelId) + if err != nil { + return common.Address{}, err + } + + return paymentChannel.Balance.Payer, nil +} + +func (pm *PaymentsManager) loadPaymentChannels() error { + ledgerChannels, err := pm.nitro.GetAllLedgerChannels() + if err != nil { + return err + } + + for _, ledgerChannel := range ledgerChannels { + if ledgerChannel.Status == query.Open { + paymentChannels, err := pm.nitro.GetPaymentChannelsByLedger(ledgerChannel.ID) + if err != nil { + return err + } + + for _, paymentChannel := range paymentChannels { + if paymentChannel.Status == query.Open { + pm.paidSoFarOnChannel.Add(paymentChannel.ID.String(), (*big.Int)(paymentChannel.Balance.PaidSoFar)) + } + } + } + } + + return nil +} diff --git a/paymentsmanager/validator.go b/paymentsmanager/validator.go new file mode 100644 index 000000000..26fef3ba3 --- /dev/null +++ b/paymentsmanager/validator.go @@ -0,0 +1,31 @@ +package paymentsmanager + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +// Voucher validator interface to be satisfied by implementations +// using in / out of process Nitro nodes +type VoucherValidator interface { + ValidateVoucher(voucherHash common.Hash, signerAddress common.Address, value *big.Int) error +} + +var _ VoucherValidator = &InProcessVoucherValidator{} + +// When go-nitro is running in-process +type InProcessVoucherValidator struct { + PaymentsManager +} + +func (v InProcessVoucherValidator) ValidateVoucher(voucherHash common.Hash, signerAddress common.Address, value *big.Int) error { + success, errCode := v.PaymentsManager.ValidateVoucher(voucherHash, signerAddress, value) + + if !success { + return fmt.Errorf(errCode) + } + + return nil +} diff --git a/readme.md b/readme.md index 4a5db7d15..66ae0cb6a 100644 --- a/readme.md +++ b/readme.md @@ -43,6 +43,390 @@ go test ./... -count=2 -shuffle=on -timeout 1m -v -failfast The on-chain component of Nitro (i.e. the solidity contracts) are housed in the [`nitro-protocol`](./packages/nitro-protocol/readme.md) directory. This directory contains an yarn workspace with a hardhat / typechain / jest toolchain. +## Steps to perform payments between two go-nitro nodes + +- Follow this [doc](https://book.getfoundry.sh/getting-started/installation) to set up foundry to run anvil chain + + - Use an older foundry version to work with go-nitro + + ```bash + foundryup --version nightly-cafc2606a2187a42b236df4aa65f4e8cdfcea970 + ``` + +- Start anvil chain: + + ```bash + anvil --chain-id 1337 --block-time 1 + ``` + +- Install dependencies + + ```bash + yarn + ``` + +- Deploy the Nitro protocol contracts: + + - Change directory to `nitro-protocol` + + ```bash + cd packages/nitro-protocol + ``` + + - Deploy nitro contracts + + ```bash + yarn contracts:deploy-localhost + ``` + + Note: On restarting the chain, make sure to remove `packages/nitro-protocol/hardhat-deployments` when redeploying contracts + + ```bash + rm -rf hardhat-deployments + ``` + + - Change directory to root directory + + ```bash + cd ../../ + ``` + +- Generate TLS certificate + + - Change directory to `tls` + + ```bash + cd tls + ``` + + - Follow [README](tls/readme.md) + + - Change directory to root directory + + ```bash + cd ../ + ``` + +- Run go-nitro node for Alice: + + - Load contract addresses from environment file to current shell session + + ```bash + source packages/nitro-protocol/hardhat-deployments/localhost/.contracts.env + ``` + + - Run node for Alice + + ```bash + export NITRO_CHAIN_URL=ws://127.0.0.1:8545 + export ALICE_ADDRESS=0xAAA6628Ec44A8a742987EF3A114dDFE2D4F7aDCE + export ALICE_PK=2d999770f7b5d49b694080f987b82bbc9fc9ac2b4dcc10b0f8aba7d700f69c6d + export ALICE_CHAIN_PK=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 + + go run . -chainurl ${NITRO_CHAIN_URL} -msgport 3006 -rpcport 4006 -pk $ALICE_PK -chainpk $ALICE_CHAIN_PK -naaddress $NA_ADDRESS -vpaaddress $VPA_ADDRESS -caaddress $CA_ADDRESS -tlskeyfilepath ./tls/statechannels.org_key.pem -tlscertfilepath ./tls/statechannels.org.pem + ``` + +- Run go-nitro node for Bob in new terminal: + + - Load contract addresses from environment file to current shell session + + ```bash + source packages/nitro-protocol/hardhat-deployments/localhost/.contracts.env + ``` + + - Run node for Bob + + ```bash + export NITRO_CHAIN_URL=ws://127.0.0.1:8545 + export BOB_ADDRESS=0xBBB676f9cFF8D242e9eaC39D063848807d3D1D94 + export BOB_PK=0279651921cd800ac560c21ceea27aab0107b67daf436cdd25ce84cad30159b4 + export BOB_CHAIN_PK=59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d + + go run . -chainurl ${NITRO_CHAIN_URL} -msgport 3007 -rpcport 4007 -pk $BOB_PK -chainpk $BOB_CHAIN_PK -naaddress $NA_ADDRESS -vpaaddress $VPA_ADDRESS -caaddress $CA_ADDRESS -bootpeers "/ip4/127.0.0.1/tcp/3006/p2p/16Uiu2HAmSjXJqsyBJgcBUU2HQmykxGseafSatbpq5471XmuaUqyv" -tlskeyfilepath ./tls/statechannels.org_key.pem -tlscertfilepath ./tls/statechannels.org.pem + ``` + + Note: Alice's P2P multiaddr is provided as bootpeer to Bob + +- In a new terminal change directory to `packages/nitro-rpc-client` + + ```bash + cd packages/nitro-rpc-client + ``` + +- Set `NODE_EXTRA_CA_CERTS` environment variable to the path of a root certificate file (rootCA.pem) generated by mkcert + + ```bash + export NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem" + ``` + +- Create a ledger channel: + + ```bash + npm exec -c 'nitro-rpc-client direct-fund 0xBBB676f9cFF8D242e9eaC39D063848807d3D1D94 -p 4006' + ``` + + Example output + + ```bash + Objective started DirectFunding-0x16e30bfaa0a3ebcf1347ddcdd42df29dc960c75d0d3de530fb69ec0cbeebd8fa + Channel Open 0x16e30bfaa0a3ebcf1347ddcdd42df29dc960c75d0d3de530fb69ec0cbeebd8fa + ``` + +- Assign ledger channel id in output log above (`Channel Open `) to an environment variable + + ```bash + export LEDGER_CHANNEL_ID= + ``` + +- Check ledger channel info: + + ```bash + npm exec -c 'nitro-rpc-client get-ledger-channel $LEDGER_CHANNEL_ID -p 4006' + ``` + + Example output + + ```bash + { + ID: '0xf6523e28b39de1e9afa65e2d29c23e22949d4d4ed55137cd208d035d4a88467f', + Status: 'Open', + Balance: { + AssetAddress: '0x0000000000000000000000000000000000000000', + Me: '0xaaa6628ec44a8a742987ef3a114ddfe2d4f7adce', + Them: '0xbbb676f9cff8d242e9eac39d063848807d3d1d94', + MyBalance: 1000000n, + TheirBalance: 1000000n + } + } + ``` + +- Create a virtual payment channel: + + ```bash + npm exec -c 'nitro-rpc-client virtual-fund 0xBBB676f9cFF8D242e9eaC39D063848807d3D1D94 -p 4006' + ``` + + Example output + + ```bash + Objective started VirtualFund-0x25676acc207865bd16c12eb4507784c0d1d3997945325a37e131d985a879bdab + Channel Open 0x25676acc207865bd16c12eb4507784c0d1d3997945325a37e131d985a879bdab + ``` + +- Assign payment channel id in output log above (`Channel Open `) to an environment variable + + ```bash + export PAYMENT_CHANNEL_ID= + ``` + +- Check ledger channel info: + + ```bash + npm exec -c 'nitro-rpc-client get-ledger-channel $LEDGER_CHANNEL_ID -p 4006' + ``` + + Example output + + ```bash + { + ID: '0xdf27ffafa9fbdd5f06821a755a08982adfcdc8ea7bd12638b07c279672faf8b6', + Status: 'Open', + Balance: { + AssetAddress: '0x0000000000000000000000000000000000000000', + Me: '0xaaa6628ec44a8a742987ef3a114ddfe2d4f7adce', + Them: '0xbbb676f9cff8d242e9eac39d063848807d3d1d94', + MyBalance: 999000n, + TheirBalance: 999000n + } + } + ``` + +- Check virtual channel info: + + ```bash + npm exec -c 'nitro-rpc-client get-payment-channel $PAYMENT_CHANNEL_ID -p 4006' + ``` + + Example output + + ```bash + { + ID: '0xc024a21a6c2626b100b9f4571e788f6c4ffedbd03ab7a2031d2db6929b375d4e', + Status: 'Open', + Balance: { + AssetAddress: '0x0000000000000000000000000000000000000000', + Payee: '0xbbb676f9cff8d242e9eac39d063848807d3d1d94', + Payer: '0xaaa6628ec44a8a742987ef3a114ddfe2d4f7adce', + PaidSoFar: 0n, + RemainingFunds: 1000n + } + } + ``` + +- Make payment from Alice to Bob: + + ```bash + npm exec -c 'nitro-rpc-client pay $PAYMENT_CHANNEL_ID 50 -p 4006' + ``` + + Example output + + ```bash + { + Amount: 50, + Channel: '0xc024a21a6c2626b100b9f4571e788f6c4ffedbd03ab7a2031d2db6929b375d4e' + } + ``` + +- Check virtual channel info: + + ```bash + npm exec -c 'nitro-rpc-client get-payment-channel $PAYMENT_CHANNEL_ID -p 4006' + ``` + + Example output + + ```bash + { + ID: '0xc024a21a6c2626b100b9f4571e788f6c4ffedbd03ab7a2031d2db6929b375d4e', + Status: 'Open', + Balance: { + AssetAddress: '0x0000000000000000000000000000000000000000', + Payee: '0xbbb676f9cff8d242e9eac39d063848807d3d1d94', + Payer: '0xaaa6628ec44a8a742987ef3a114ddfe2d4f7adce', + PaidSoFar: 50n, + RemainingFunds: 950n + } + } + ``` + +- Close the virtual payment channel: + + ```bash + npm exec -c 'nitro-rpc-client virtual-defund $PAYMENT_CHANNEL_ID -p 4006' + ``` + + Example output + + ```bash + Objective started VirtualDefund-0xc024a21a6c2626b100b9f4571e788f6c4ffedbd03ab7a2031d2db6929b375d4e + Channel complete 0xc024a21a6c2626b100b9f4571e788f6c4ffedbd03ab7a2031d2db6929b375d4e + ``` + +- Check virtual channel info: + + ```bash + npm exec -c 'nitro-rpc-client get-payment-channel $PAYMENT_CHANNEL_ID -p 4006' + ``` + + Example output + + ```bash + { + ID: '0xc024a21a6c2626b100b9f4571e788f6c4ffedbd03ab7a2031d2db6929b375d4e', + Status: 'Complete', + Balance: { + AssetAddress: '0x0000000000000000000000000000000000000000', + Payee: '0xbbb676f9cff8d242e9eac39d063848807d3d1d94', + Payer: '0xaaa6628ec44a8a742987ef3a114ddfe2d4f7adce', + PaidSoFar: 50n, + RemainingFunds: 950n + } + } + ``` + +- Check ledger channel info: + + ```bash + npm exec -c 'nitro-rpc-client get-ledger-channel $LEDGER_CHANNEL_ID -p 4006' + ``` + + Example output + + ```bash + { + ID: '0xdf27ffafa9fbdd5f06821a755a08982adfcdc8ea7bd12638b07c279672faf8b6', + Status: 'Open', + Balance: { + AssetAddress: '0x0000000000000000000000000000000000000000', + Me: '0xaaa6628ec44a8a742987ef3a114ddfe2d4f7adce', + Them: '0xbbb676f9cff8d242e9eac39d063848807d3d1d94', + MyBalance: 999950n, + TheirBalance: 1000050n + } + } + ``` + +- Close the ledger channel: + + ```bash + npm exec -c 'nitro-rpc-client direct-defund $LEDGER_CHANNEL_ID -p 4006' + ``` + +- Check ledger channel info: + + ```bash + npm exec -c 'nitro-rpc-client get-ledger-channel $LEDGER_CHANNEL_ID -p 4006' + ``` + + Example output + + ```bash + { + ID: '0xf6523e28b39de1e9afa65e2d29c23e22949d4d4ed55137cd208d035d4a88467f', + Status: 'Complete', + Balance: { + AssetAddress: '0x0000000000000000000000000000000000000000', + Me: '0xaaa6628ec44a8a742987ef3a114ddfe2d4f7adce', + Them: '0xbbb676f9cff8d242e9eac39d063848807d3d1d94', + MyBalance: 999950n, + TheirBalance: 1000050n + } + } + ``` + +- Check on chain balance for Alice + + ```bash + echo $( + printf "Result: %d" $( + curl -sk -X POST -H "Content-Type: application/json" --data '{ + "jsonrpc":"2.0", + "method":"eth_getBalance", + "params": ["0xAAA6628Ec44A8a742987EF3A114dDFE2D4F7aDCE", "latest"], + "id":1 + }' http://localhost:8545 | jq -r '.result' + ) + ) + ``` + + Example output + + ```bash + Result: 999950 + ``` + +- Check on chain balance for Bob + + ```bash + echo $( + printf "Result: %d" $( + curl -sk -X POST -H "Content-Type: application/json" --data '{ + "jsonrpc":"2.0", + "method":"eth_getBalance", + "params": ["0xBBB676f9cFF8D242e9eaC39D063848807d3D1D94", "latest"], + "id":1 + }' http://localhost:8545 | jq -r '.result' + ) + ) + ``` + + Example output + + ```bash + Result: 1000050 + ``` + ## License Dual-licensed under [MIT](https://opensource.org/licenses/MIT) + [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/rpc/client.go b/rpc/client.go index bf9a36c33..68ba7ddf7 100644 --- a/rpc/client.go +++ b/rpc/client.go @@ -79,6 +79,8 @@ type RpcClientApi interface { // PaymentChannelUpdatesChan returns a channel that receives payment channel updates for the given payment channel id PaymentChannelUpdatesChan(paymentChannelId types.Destination) <-chan query.PaymentChannelInfo + + ValidateVoucher(voucherHash common.Hash, signerAddress common.Address, value uint64) (serde.ValidateVoucherResponse, error) } // rpcClient is the implementation @@ -138,8 +140,8 @@ func NewRpcClient(trans transport.Requester) (RpcClientApi, error) { } // NewHttpRpcClient creates a new rpcClient using an http transport -func NewHttpRpcClient(rpcServerUrl string) (RpcClientApi, error) { - transport, err := http.NewHttpTransportAsClient(rpcServerUrl, 10*time.Millisecond) +func NewHttpRpcClient(rpcServerUrl string, isSecure bool) (RpcClientApi, error) { + transport, err := http.NewHttpTransportAsClient(rpcServerUrl, isSecure, 10*time.Millisecond) if err != nil { return nil, err } @@ -165,6 +167,12 @@ func (rc *rpcClient) ReceiveVoucher(v payments.Voucher) (payments.ReceiveVoucher return waitForAuthorizedRequest[payments.Voucher, payments.ReceiveVoucherSummary](rc, serde.ReceiveVoucherRequestMethod, v) } +func (rc *rpcClient) ValidateVoucher(voucherHash common.Hash, signer common.Address, value uint64) (serde.ValidateVoucherResponse, error) { + req := serde.ValidateVoucherRequest{VoucherHash: voucherHash, Signer: signer, Value: value} + + return waitForAuthorizedRequest[serde.ValidateVoucherRequest, serde.ValidateVoucherResponse](rc, serde.ValidateVoucherRequestMethod, req) +} + func (rc *rpcClient) GetPaymentChannel(chId types.Destination) (query.PaymentChannelInfo, error) { req := serde.GetPaymentChannelRequest{Id: chId} diff --git a/rpc/node-server.go b/rpc/node-server.go index eb4c37212..3dc888670 100644 --- a/rpc/node-server.go +++ b/rpc/node-server.go @@ -10,6 +10,7 @@ import ( nitro "github.com/statechannels/go-nitro/node" "github.com/statechannels/go-nitro/node/query" "github.com/statechannels/go-nitro/payments" + "github.com/statechannels/go-nitro/paymentsmanager" "github.com/statechannels/go-nitro/protocols" "github.com/statechannels/go-nitro/protocols/directdefund" "github.com/statechannels/go-nitro/protocols/directfund" @@ -22,15 +23,16 @@ import ( type NodeRpcServer struct { *BaseRpcServer - node *nitro.Node + node *nitro.Node + paymentManager paymentsmanager.PaymentsManager } // newNodeRpcServerWithoutNotifications creates a new rpc server without notifications enabled func newNodeRpcServerWithoutNotifications(nitroNode *nitro.Node, trans transport.Responder) (*NodeRpcServer, error) { baseRpcServer := NewBaseRpcServer(trans) nrs := &NodeRpcServer{ - baseRpcServer, - nitroNode, + BaseRpcServer: baseRpcServer, + node: nitroNode, } if hasNitroAddress := (nitroNode.Address != nil) && (nitroNode.Address != &types.Address{}); hasNitroAddress { @@ -45,14 +47,15 @@ func newNodeRpcServerWithoutNotifications(nitroNode *nitro.Node, trans transport return nrs, nil } -func NewNodeRpcServer(node *nitro.Node, trans transport.Responder) (*NodeRpcServer, error) { +func NewNodeRpcServer(nitroNode *nitro.Node, paymentManager paymentsmanager.PaymentsManager, trans transport.Responder) (*NodeRpcServer, error) { baseRpcServer := NewBaseRpcServer(trans) nrs := &NodeRpcServer{ - baseRpcServer, - node, + BaseRpcServer: baseRpcServer, + node: nitroNode, + paymentManager: paymentManager, } - nrs.logger = logging.LoggerWithAddress(slog.Default(), *node.Address) + nrs.logger = logging.LoggerWithAddress(slog.Default(), *nitroNode.Address) ctx, cancel := context.WithCancel(context.Background()) nrs.cancel = cancel nrs.wg.Add(1) @@ -173,6 +176,12 @@ func (nrs *NodeRpcServer) registerHandlers() (err error) { nrs.node.CounterChallenge(req.ChannelId, req.Action) return req, nil }) + case serde.ValidateVoucherRequestMethod: + return processRequest(nrs.BaseRpcServer, permRead, requestData, func(req serde.ValidateVoucherRequest) (serde.ValidateVoucherResponse, error) { + success, errCode := nrs.paymentManager.ValidateVoucher(req.VoucherHash, req.Signer, big.NewInt(int64(req.Value))) + response := serde.ValidateVoucherResponse{Success: success, ErrorCode: errCode} + return response, nil + }) default: errRes := serde.NewJsonRpcErrorResponse(jsonrpcReq.Id, serde.MethodNotFoundError) return marshalResponse(errRes) diff --git a/rpc/serde/jsonrpc.go b/rpc/serde/jsonrpc.go index 0aa1de9f4..52dd17c73 100644 --- a/rpc/serde/jsonrpc.go +++ b/rpc/serde/jsonrpc.go @@ -31,6 +31,7 @@ const ( CreateVoucherRequestMethod RequestMethod = "create_voucher" ReceiveVoucherRequestMethod RequestMethod = "receive_voucher" CounterChallengeRequestMethod RequestMethod = "counter_challenge" + ValidateVoucherRequestMethod RequestMethod = "validate_voucher" // Bridge methods GetAllL2ChannelsRequestMethod RequestMethod = "get_all_l2_channels" @@ -73,6 +74,12 @@ type GetPaymentChannelsByLedgerRequest struct { LedgerId types.Destination } +type ValidateVoucherRequest struct { + VoucherHash common.Hash + Signer common.Address + Value uint64 +} + type ( NoPayloadRequest = struct{} ) @@ -89,7 +96,8 @@ type RequestPayload interface { GetPaymentChannelsByLedgerRequest | NoPayloadRequest | payments.Voucher | - CounterChallengeRequest + CounterChallengeRequest | + ValidateVoucherRequest } type NotificationPayload interface { @@ -115,6 +123,11 @@ type ( GetPaymentChannelsByLedgerResponse = []query.PaymentChannelInfo ) +type ValidateVoucherResponse struct { + Success bool + ErrorCode string +} + type ResponsePayload interface { directfund.ObjectiveResponse | protocols.ObjectiveId | @@ -128,7 +141,8 @@ type ResponsePayload interface { common.Address | string | payments.ReceiveVoucherSummary | - CounterChallengeRequest + CounterChallengeRequest | + ValidateVoucherResponse } type JsonRpcSuccessResponse[T ResponsePayload] struct { diff --git a/rpc/transport/http/client.go b/rpc/transport/http/client.go index b89c31b94..94405d8a0 100644 --- a/rpc/transport/http/client.go +++ b/rpc/transport/http/client.go @@ -19,18 +19,24 @@ type clientHttpTransport struct { notificationChan chan []byte clientWebsocket *websocket.Conn url string + isSecure bool wg *sync.WaitGroup } // NewHttpTransportAsClient creates a transport that can be used to send http requests and a websocket connection for receiving notifications // Initialization will block for 10 retries until the server endpoint is ready -func NewHttpTransportAsClient(url string, retryTimeout time.Duration) (*clientHttpTransport, error) { - err := blockUntilHttpServerIsReady(url, retryTimeout) +func NewHttpTransportAsClient(url string, isSecure bool, retryTimeout time.Duration) (*clientHttpTransport, error) { + err := blockUntilHttpServerIsReady(url, isSecure, retryTimeout) if err != nil { return nil, err } - subscribeUrl, err := urlUtil.JoinPath("wss://", url, "subscribe") + wsPrefix := "ws://" + if isSecure { + wsPrefix = "wss://" + } + + subscribeUrl, err := urlUtil.JoinPath(wsPrefix, url, "subscribe") if err != nil { return nil, err } @@ -40,7 +46,7 @@ func NewHttpTransportAsClient(url string, retryTimeout time.Duration) (*clientHt return nil, err } - t := &clientHttpTransport{notificationChan: make(chan []byte, 10), clientWebsocket: conn, url: url, wg: &sync.WaitGroup{}, logger: slog.Default()} + t := &clientHttpTransport{notificationChan: make(chan []byte, 10), clientWebsocket: conn, url: url, isSecure: isSecure, wg: &sync.WaitGroup{}, logger: slog.Default()} t.wg.Add(1) go t.readMessages() @@ -49,7 +55,7 @@ func NewHttpTransportAsClient(url string, retryTimeout time.Duration) (*clientHt } func (t *clientHttpTransport) Request(data []byte) ([]byte, error) { - requestUrl, err := httpUrl(t.url) + requestUrl, err := httpUrl(t.url, t.isSecure) if err != nil { return nil, err } @@ -98,8 +104,13 @@ func (t *clientHttpTransport) readMessages() { } // httpUrl joins the http prefix with the server url -func httpUrl(url string) (string, error) { - httpUrl, err := urlUtil.JoinPath("https://", url) +func httpUrl(url string, isSecure bool) (string, error) { + prefix := "http://" + if isSecure { + prefix = "https://" + } + + httpUrl, err := urlUtil.JoinPath(prefix, url) if err != nil { return "", err } @@ -107,12 +118,12 @@ func httpUrl(url string) (string, error) { } // blockUntilHttpServerIsReady pings the health endpoint until the server is ready -func blockUntilHttpServerIsReady(url string, retryTimeout time.Duration) error { +func blockUntilHttpServerIsReady(url string, isSecure bool, retryTimeout time.Duration) error { waitForServer := func(iteration int) { time.Sleep(retryTimeout * time.Duration(math.Pow(2, float64(iteration)))) } - httpUrl, err := httpUrl(url) + httpUrl, err := httpUrl(url, isSecure) if err != nil { return err } diff --git a/rpc/voucher_validator.go b/rpc/voucher_validator.go new file mode 100644 index 000000000..edff2f097 --- /dev/null +++ b/rpc/voucher_validator.go @@ -0,0 +1,29 @@ +package rpc + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/statechannels/go-nitro/paymentsmanager" +) + +var _ paymentsmanager.VoucherValidator = &RemoteVoucherValidator{} + +// When go-nitro is running remotely +type RemoteVoucherValidator struct { + Client RpcClientApi +} + +func (r RemoteVoucherValidator) ValidateVoucher(voucherHash common.Hash, signerAddress common.Address, value *big.Int) error { + res, err := r.Client.ValidateVoucher(voucherHash, signerAddress, value.Uint64()) + if err != nil { + return err + } + + if !res.Success { + return fmt.Errorf(res.ErrorCode) + } + + return nil +}