Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spike: Payment channel authorization grants #36 #41

Merged
merged 8 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ ENTRYPOINT ["tpodserver"]

FROM build-common as build-autoscaler

COPY cmd/autoscaler ./cmd/autoscaler
RUN --mount=type=cache,target=/root/.cache/go-build go build -v -o /usr/local/bin/autoscaler ./cmd/autoscaler
COPY autoscaler ./autoscaler
RUN --mount=type=cache,target=/root/.cache/go-build go build -v -o /usr/local/bin/autoscaler ./autoscaler

FROM run-common as autoscaler

Expand Down
16 changes: 12 additions & 4 deletions cmd/autoscaler/main.go → autoscaler/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"log"
"net/http"

"github.com/comrade-coop/apocryph/pkg/autoscaler"
pbcon "github.com/comrade-coop/apocryph/pkg/proto/protoconnect"
tpraft "github.com/comrade-coop/apocryph/pkg/raft"
"golang.org/x/net/http2"
Expand All @@ -13,14 +14,21 @@ import (

func main() {
mux := http.NewServeMux()
self, err := tpraft.NewPeer(fmt.Sprintf("/ip4/0.0.0.0/tcp/%v", RAFT_P2P_PORT))
p2pHost, err := tpraft.NewPeer(fmt.Sprintf("/ip4/0.0.0.0/tcp/%v", autoscaler.RAFT_P2P_PORT))
if err != nil {
fmt.Println("Failed creating p2p node")
return
}
fmt.Printf("PEER ID: %v\n", self.ID())
server := &AutoScalerServer{self: self}
server.mainLoop = setAppGatewayExample

log.Printf("PEER ID: %v\n", p2pHost.ID())

server, err := autoscaler.NewAutoSalerServer("http://eth-rpc.eth.svc.cluster.local:8545", p2pHost)
if err != nil {
fmt.Println("Failed creating AutoScaler Server")
return
}

server.MainLoop = autoscaler.SetAppGatewayExample
path, handler := pbcon.NewAutoscalerServiceHandler(server)
mux.Handle(path, handler)
log.Println("Autoscaler RPC Server Started")
Expand Down
2 changes: 1 addition & 1 deletion cmd/tpodserver/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ var applyManifestCmd = &cobra.Command{
response := &pb.ProvisionPodResponse{}
namespace := tpk8s.NewTrustedPodsNamespace("tpods-xx", nil)
err = tpk8s.RunInNamespaceOrRevert(cmd.Context(), cl, namespace, dryRun, func(cl client.Client) error {
return tpk8s.ApplyPodRequest(cmd.Context(), cl, namespace.ObjectMeta.Name, false, pod, images, secrets, response)
return tpk8s.ApplyPodRequest(cmd.Context(), cl, namespace.ObjectMeta.Name, false, pod, nil, images, secrets, response)
})
if err != nil {
return err
Expand Down
52 changes: 47 additions & 5 deletions cmd/trustedpods/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@
package main

import (
"crypto/ecdsa"
"crypto/rand"
"fmt"
"math/big"
"path/filepath"

"github.com/comrade-coop/apocryph/pkg/abi"
tpcrypto "github.com/comrade-coop/apocryph/pkg/crypto"
"github.com/comrade-coop/apocryph/pkg/ethereum"
"github.com/comrade-coop/apocryph/pkg/ipcr"
tpipfs "github.com/comrade-coop/apocryph/pkg/ipfs"
pb "github.com/comrade-coop/apocryph/pkg/proto"
pbcon "github.com/comrade-coop/apocryph/pkg/proto/protoconnect"
"github.com/comrade-coop/apocryph/pkg/publisher"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ipfs/kubo/client/rpc"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -93,6 +98,32 @@ var deployPodCmd = &cobra.Command{
}
configureDeployment(deployment)

if authorize {
encryptionKey, err := tpcrypto.NewKey(tpcrypto.KeyTypeAESGCM256)
if err != nil {
return fmt.Errorf("Could not create AES key: %v", err)
}
// create the keypair that will be accessible for all pods
privateKey, err := ecdsa.GenerateKey(crypto.S256(), rand.Reader)
if err != nil {
return fmt.Errorf("Could not create private key for the application: %w", err)
}
// Ensure the public key is valid before getting the address
if privateKey == nil || privateKey.PublicKey.X == nil || privateKey.PublicKey.Y == nil {
return fmt.Errorf("Generated an invalid public key")
}

pubAddress := crypto.PubkeyToAddress(privateKey.PublicKey)

encryptedPrivateKey, err := tpcrypto.EncryptWithKey(encryptionKey, crypto.FromECDSA(privateKey))
if err != nil {
return fmt.Errorf("Could not encrypt private key: %v", err)
}

pod.KeyPair = &pb.KeyPair{Key: encryptionKey, PrivateKey: encryptedPrivateKey, PubAddress: pubAddress.Hex()}
deployment.KeyPair = pod.KeyPair
}

fundsInt, _ := (&big.Int{}).SetString(funds, 10)
if fundsInt == nil {
return fmt.Errorf("Invalid number passed for funds: %s", funds)
Expand Down Expand Up @@ -130,7 +161,7 @@ var deployPodCmd = &cobra.Command{

interceptor := pbcon.NewAuthInterceptorClient(deployment, expirationOffset, sign)

var client *publisher.P2pProvisionPodServiceClient
var provisionPodclient *publisher.P2pProvisionPodServiceClient
if len(deployment.GetProvider().GetEthereumAddress()) == 0 || deployment.GetProvider().GetLibp2PAddress() == "" {
availableProviders, err := fetchAndFilterProviders(ipfs, ethClient)
if err != nil {
Expand All @@ -139,12 +170,12 @@ var deployPodCmd = &cobra.Command{
if len(availableProviders) == 0 {
return fmt.Errorf("Failed finding a provider: no available providers found matching filter")
}
client, err = publisher.SetFirstConnectingProvider(ipfsp2p, availableProviders, deployment, interceptor)
provisionPodclient, err = publisher.SetFirstConnectingProvider(ipfsp2p, availableProviders, deployment, interceptor)
if err != nil {
return fmt.Errorf("Failed setting a provider: %w", err)
}
} else {
client, err = publisher.ConnectToProvider(ipfsp2p, deployment, interceptor)
provisionPodclient, err = publisher.ConnectToProvider(ipfsp2p, deployment, interceptor)
if err != nil {
return err
}
Expand Down Expand Up @@ -179,10 +210,20 @@ var deployPodCmd = &cobra.Command{
return err
}

err = publisher.SendToProvider(cmd.Context(), ipfsp2p, pod, deployment, client)
fmt.Printf("PODID is:%v\n", common.BytesToHash(deployment.Payment.PodID))

response, err := publisher.SendToProvider(cmd.Context(), ipfsp2p, pod, deployment, provisionPodclient)
if err != nil {
return err
}
// Authorize the application to manipulate the payment channel and fund
// it to make it able to send transactions
if authorize {
err := publisher.AuthorizeAndFundApplication(cmd.Context(), response.(*pb.ProvisionPodResponse), deployment, ethClient, publisherAuth, publisherKey, 1000000000000000000)
if err != nil {
return err
}
}

return publisher.SaveDeployment(deploymentFile, deploymentFormat, deployment)
},
Expand Down Expand Up @@ -229,10 +270,11 @@ var deletePodCmd = &cobra.Command{
return err
}

err = publisher.SendToProvider(cmd.Context(), tpipfs.NewP2pApi(ipfs, ipfsMultiaddr), nil, deployment, client)
_, err = publisher.SendToProvider(cmd.Context(), tpipfs.NewP2pApi(ipfs, ipfsMultiaddr), nil, deployment, client)
if err != nil {
return err
}

return publisher.SaveDeployment(deploymentFile, deploymentFormat, deployment)
},
}
Expand Down
3 changes: 3 additions & 0 deletions cmd/trustedpods/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ var providerEthAddress string
var registryContractAddress string
var tokenContractAddress string
var expirationOffset int64
var authorize bool

var uploadFlags = &pflag.FlagSet{}
var ipfsApi string
Expand Down Expand Up @@ -59,6 +60,7 @@ var _ = func() error {
deploymentFlags.StringVar(&providerEthAddress, "provider-eth", "", "provider public address")
deploymentFlags.Int64Var(&expirationOffset, "token-expiration", 10, "authentication token expires after token-expiration seconds (expired after 10 seconds by default)")
deploymentFlags.StringVar(&ipfsApi, "ipfs", "/ip4/127.0.0.1/tcp/5001", "multiaddr where the ipfs/kubo api can be accessed")
deploymentFlags.BoolVar(&authorize, "authorize", false, "Create a key pair for the application and authorize the returned addresses to control the payment channel")

uploadFlags.StringVar(&ipfsApi, "ipfs", "/ip4/127.0.0.1/tcp/5001", "multiaddr where the ipfs/kubo api can be accessed")
uploadFlags.BoolVar(&uploadImages, "upload-images", true, "upload images")
Expand All @@ -73,6 +75,7 @@ var _ = func() error {
fundFlags.Int64Var(&unlockTime, "unlock-time", 5*60, "time for unlocking tokens (in seconds)")

syncFlags.AddFlag(uploadFlags.Lookup("ipfs"))
syncFlags.StringVar(&publisherKey, "ethereum-key", "", "account string (private key | http[s]://clef#account | /keystore#account | account (in default keystore))")

registryFlags.StringVar(&ipfsApi, "ipfs", "/ip4/127.0.0.1/tcp/5001", "multiaddr where the ipfs/kubo api can be accessed")
registryFlags.StringVar(&registryContractAddress, "registry-contract", "", "registry contract address")
Expand Down
4 changes: 2 additions & 2 deletions cmd/trustedpods/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var containerName string
var logPodCmd = &cobra.Command{
Use: fmt.Sprintf("log [%s] [deployment.yaml]", publisher.DefaultPodFile),
Short: "get pod container logs",
Args: cobra.ExactArgs(1),
Args: cobra.MaximumNArgs(2),
GroupID: "main",
RunE: func(cmd *cobra.Command, args []string) error {
_, _, pod, deployment, err := publisher.ReadPodAndDeployment(args, manifestFormat, deploymentFormat)
Expand All @@ -40,6 +40,7 @@ var logPodCmd = &cobra.Command{
}

if publisherKey == "" {
fmt.Printf("publisherKey %v\n", publisherKey)
publisherKey = common.BytesToAddress(deployment.Payment.PublisherAddress).String()
}

Expand Down Expand Up @@ -91,6 +92,5 @@ var logPodCmd = &cobra.Command{
func init() {
podCmd.AddCommand(logPodCmd)

logPodCmd.Flags().AddFlagSet(deploymentFlags)
logPodCmd.Flags().AddFlagSet(syncFlags)
}
1 change: 1 addition & 0 deletions cmd/trustedpods/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package main

import (
"crypto/sha256"

pb "github.com/comrade-coop/apocryph/pkg/proto"
"github.com/ethereum/go-ethereum/common"
"github.com/spf13/cobra"
Expand Down
2 changes: 1 addition & 1 deletion cmd/trustedpods/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ var syncPodCmd = &cobra.Command{
return err
}

err = publisher.SendToProvider(cmd.Context(), tpipfs.NewP2pApi(ipfs, ipfsMultiaddr), pod, deployment, client)
_, err = publisher.SendToProvider(cmd.Context(), tpipfs.NewP2pApi(ipfs, ipfsMultiaddr), pod, deployment, client)
if err != nil {
return err
}
Expand Down
79 changes: 70 additions & 9 deletions contracts/src/Payment.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ contract Payment {
error AmountRequired();
error ChannelLocked();
error InsufficientFunds();
error NotAuthorized();

event UnlockTimerStarted(
address indexed publisher, address indexed provider, bytes32 indexed podId, uint256 unlockedAt
Expand All @@ -30,6 +31,7 @@ contract Payment {
uint256 withdrawnByProvider;
uint256 unlockTime; // minimum time in seconds needed to unlock the funds
uint256 unlockedAt; // time @ unlock + unlockTime
mapping(address => bool) authorized;
}

// publisher => provider => token => PodID => funds
Expand Down Expand Up @@ -73,11 +75,13 @@ contract Payment {
}

// initiate the process of unlocking the funds stored in the contract
function unlock(address provider, bytes32 podId) public {
address publisher = msg.sender;
function unlock(address publisher, address provider, bytes32 podId) public {
// check if the caller is authorized to unlock channel
Channel storage channel = channels[publisher][provider][podId];
if (channel.investedByPublisher == 0) revert DoesNotExist();

if (msg.sender != publisher) {
if (channel.authorized[msg.sender] == false) revert NotAuthorized();
}
uint256 newUnlockedAt = block.timestamp + channel.unlockTime;
if (channel.unlockedAt == 0 || channel.unlockedAt < newUnlockedAt) {
channel.unlockedAt = newUnlockedAt;
Expand All @@ -86,9 +90,12 @@ contract Payment {
}

// transfer the now-unlocked funds back to the publisher
function withdrawUnlocked(address provider, bytes32 podId) public {
address publisher = msg.sender;
function withdrawUnlocked(address publisher, address provider, bytes32 podId) public {
// check if the caller is authorized to withdraw
Channel storage channel = channels[publisher][provider][podId];
if (msg.sender != publisher) {
if (channel.authorized[msg.sender] == false) revert NotAuthorized();
}
if (channel.unlockedAt == 0 || block.timestamp < channel.unlockedAt) revert ChannelLocked();

uint256 leftoverFunds = channel.investedByPublisher - channel.withdrawnByProvider;
Expand All @@ -98,13 +105,17 @@ contract Payment {

emit Unlocked(publisher, provider, podId, leftoverFunds);

token.safeTransfer(publisher, leftoverFunds);
token.safeTransfer(msg.sender, leftoverFunds);
}

// withdrawUnlockedFunds and destroy all previous traces of the channel's existence
function closeChannel(address provider, bytes32 podId) public {
address publisher = msg.sender;
function closeChannel(address publisher, address provider, bytes32 podId) public {
// check if the caller is authorized to close the channel
Channel storage channel = channels[publisher][provider][podId];
if (channel.investedByPublisher == 0) revert DoesNotExist();
if (msg.sender != publisher) {
if (channel.authorized[msg.sender] == false) revert NotAuthorized();
}
if (channel.unlockedAt == 0 || block.timestamp < channel.unlockedAt) revert ChannelLocked();

uint256 leftoverFunds = channel.investedByPublisher - channel.withdrawnByProvider;
Expand All @@ -113,7 +124,7 @@ contract Payment {
if (leftoverFunds != 0) emit Unlocked(publisher, provider, podId, leftoverFunds);
emit ChannelClosed(publisher, provider, podId);

if (leftoverFunds != 0) token.safeTransfer(publisher, leftoverFunds);
if (leftoverFunds != 0) token.safeTransfer(msg.sender, leftoverFunds);
}

// allows the provider to withdraw as many tokens as would be needed to reach totalWithdrawAmount since the opening of the channel
Expand Down Expand Up @@ -159,4 +170,54 @@ contract Payment {
Channel storage channel = channels[publisher][provider][podId];
return channel.withdrawnByProvider;
}

// authorize other addresses to create subChannels
function authorize(address _authorized, address provider, bytes32 podId) public {
Channel storage channel = channels[msg.sender][provider][podId];
if (channel.investedByPublisher == 0) revert DoesNotExist();
channel.authorized[_authorized] = true;
}

function isAuthorized(address publisher, address provider, bytes32 podId, address _address)
public
view
returns (bool)
{
return channels[publisher][provider][podId].authorized[_address];
}

// create a subChannel from a main channel
function createSubChannel(
address publisher,
address provider,
bytes32 podId,
address newProvider,
bytes32 newPodId,
uint256 amount
) public {
Channel storage channel = channels[publisher][provider][podId];
// Ensure the channel exists
if (channel.investedByPublisher == 0) revert DoesNotExist();

// Check if the caller is authorized
if (!channel.authorized[msg.sender]) revert NotAuthorized();

// Ensure there is enough invested by the publisher
if (channel.investedByPublisher < amount) revert InsufficientFunds();

// Deduct the amount from the main channel's funds
channel.investedByPublisher -= amount;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decreasing the amount invested by the publisher without requiring the channel to be unlocked first allows the publisher to drain the channel of any funds within it, by creating a subchannel with all the funds they can, then unlocking that subchannel, and withdrawing from it at a leisurely pace, without the provider able to submit a withdraw() call.

Also, since the code never checks whether channel.investedByPublisher - channel.withdrawnByProvider < amount (see, e.g. line 121, leftoverFunds = ..), it's possible to withdraw more than was initially invested into a channel, thus draining the whole contract of funds.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed as well


// Create the subChannel for the authorized caller
Channel storage subChannel = channels[msg.sender][newProvider][newPodId];

// authorize the main channel publisher to control the subchannel
subChannel.authorized[publisher] = true;

// fund the new sub channel with the deducted amount from the main channel
subChannel.investedByPublisher += amount;
subChannel.unlockTime = channel.unlockTime; // Inherit unlock time from main channel
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm reading the code right, this lets the publisher instantly unlock any channel, by doing (in all below calls, msg.sender == publisher):
0. createChannel(provider, podId, ...) (created earlier)

  1. createChannel(publisher, 0x0, 0, 1) (create a dummy channel with unlock time 0)
  2. authorize(publisher, publisher, 0x0) (authorize someone for it (in this case, ourselves))
  3. createSubChannel(publisher, publisher, 0x0, provider, podId) (oops! resets unlockTime on the original channel to 0)
  4. unlock(publisher, provider, podId) (regular unlock)
  5. withdrawUnlocked(publisher, provider, podId) (oops! can withdraw supposedly-locked funds without waiting for unlockTime) (would work even if made in the same block as all the other commands, with no chance for the provider to cash out any work done since the last withdraw(..) call)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, Should be Fixed now.


emit Deposited(msg.sender, newProvider, newPodId, amount);
}
}
2 changes: 1 addition & 1 deletion contracts/src/Registry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ contract Registry {
subscription[msg.sender][tableId] = false;
}

function isSubscribed(address provider ,uint256 tableId) external view returns (bool) {
function isSubscribed(address provider, uint256 tableId) external view returns (bool) {
return subscription[provider][tableId];
}
}
Loading
Loading