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

Add w3 CLI script for w3s driver #17

Merged
merged 7 commits into from
Feb 7, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@

# Dependency directories (remove the comment below to include it)
# vendor/
.idea/
.idea/

package-lock.json
node_modules/
41 changes: 32 additions & 9 deletions drivers/w3s.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package drivers

import (
"context"
"encoding/base64"
"fmt"
bserv "github.com/ipfs/go-blockservice"
"github.com/ipfs/go-cid"
Expand Down Expand Up @@ -145,7 +146,7 @@ func (session *W3sSession) SaveData(ctx context.Context, name string, data io.Re
}
defer deleteFile(carPath)

carCid, err := w3StoreCar(ctx, carPath)
carCid, err := w3StoreCar(ctx, session.os.ucanProof, carPath)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -237,20 +238,20 @@ func (ostore *W3sOS) Publish(ctx context.Context) (string, error) {
defer ostore.deleteRootCar()

rCar.mu.Lock()
if err := rCar.storeDir(ctx); err != nil {
if err := rCar.storeDir(ctx, ostore.ucanProof); err != nil {
return "", err
}
carCids := rCar.carCids
rCar.mu.Unlock()

if err := w3UploadCar(ctx, rootCid, carCids); err != nil {
if err := w3UploadCar(ctx, ostore.ucanProof, rootCid, carCids); err != nil {
return "", err
}

return fmt.Sprintf("https://%s.ipfs.w3s.link", rootCid), nil
}

func (rc *rootCar) storeDir(ctx context.Context) error {
func (rc *rootCar) storeDir(ctx context.Context, proof string) error {
carFile, err := os.CreateTemp("", "car")
if err != nil {
return err
Expand All @@ -259,7 +260,7 @@ func (rc *rootCar) storeDir(ctx context.Context) error {
car.WriteCar(ctx, rc.dag, []cid.Cid{rc.root.Cid()}, carFile, merkledag.IgnoreMissing())
carFile.Close()

storedCid, err := w3StoreCar(ctx, carFile.Name())
storedCid, err := w3StoreCar(ctx, proof, carFile.Name())
if err != nil {
return err
}
Expand Down Expand Up @@ -336,19 +337,41 @@ func ipfsCarPack(ctx context.Context, filePath string) (string, string, error) {
}

// w3StoreCar uses external binary `w3` to store a CAR file in web3.storage.
func w3StoreCar(ctx context.Context, carPath string) (string, error) {
out, err := exec.CommandContext(ctx, "w3", "can", "store", "add", carPath).Output()
func w3StoreCar(ctx context.Context, proof, carPath string) (string, error) {
out, err := runWithCredentials(exec.CommandContext(ctx, "livepeer-w3", "can", "store", "add", carPath), proof)
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}

// w3StoreCar uses external binary `w3` to bind and publish multiple CARs.
func w3UploadCar(ctx context.Context, rootCid string, carCids []string) error {
func w3UploadCar(ctx context.Context, proof, rootCid string, carCids []string) error {
args := []string{"can", "upload", "add"}
args = append(args, rootCid)
args = append(args, carCids...)
_, err := exec.CommandContext(ctx, "w3", args...).Output()
_, err := runWithCredentials(exec.CommandContext(ctx, "livepeer-w3", args...), proof)
return err
}

func runWithCredentials(cmd *exec.Cmd, proof string) ([]byte, error) {
if proof == "" {
return nil, fmt.Errorf("UCAN proof not found")
}
base64Proof, err := base64UrlToBase64(proof)
if err != nil {
return nil, fmt.Errorf("invalid UCAN proof format: %s", err)
}
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("W3_PRINCIPAL_KEY='%s'", W3sUcanKey))
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if there is an opp to simplify how this key is configured. I see that right now the flow is:

  • Pass value in a flag for a catalyst-api
  • Set the package var W3sUcanKey with the value
  • Read W3sUcanKey and set it as an env var when calling the livepeer-w3 script

An alternative might be:

  • Set W3_PRINCIPAL_KEY in the environment that catalyst-api is running in i.e. in the Docker container
  • livepeer-w3 just reads the W3_PRINCIPAL_KEY env var value directly without any additional value passing which reduces the # of code paths that have to be aware of this value

I see we've already merged the relevant catalyst-api PRs so maybe consider whether this might be a worthwhile simplification separately.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My initial thinking was that not storing the private key in the environment variable is more secure because even if you access the container, you still cannot access it.

However, now I think you're right because anyway we pass the key in the env variable to catalyst. So, maybe let's get rid of the Catalyst flag and just use the private key passed in the W3_PRINCIPAL_KEY. Thanks for this comment @yondonfu

cmd.Env = append(cmd.Env, fmt.Sprintf("W3_DELEGATION_PROOF='%s'", base64Proof))
return cmd.Output()
}

func base64UrlToBase64(proof string) (string, error) {
ucanProofByte, err := base64.URLEncoding.DecodeString(proof)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(ucanProofByte), nil
}
95 changes: 95 additions & 0 deletions w3/livepeer-w3.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env node

import fs from "fs";
import { CID } from "multiformats";
import { CarReader } from "@ipld/car";
import { derive } from "@ucanto/principal/ed25519";
import { importDAG } from "@ucanto/core/delegation";
import { AgentData } from "@web3-storage/access";
import { Client } from "@web3-storage/w3up-client";

async function getClient() {
// create a client with UCAN private key passed in the env variable
const principal = await derive(
Buffer.from(process.env.W3_PRINCIPAL_KEY, "base64")
);
const data = await AgentData.create({ principal });
const client = new Client(data);

// create a space with the delegation proof passsed in the env variable
const blocks = [];
const reader = await CarReader.fromBytes(
Buffer.from(process.env.W3_DELEGATION_PROOF, "base64")
);
for await (const block of reader.blocks()) {
blocks.push(block);
}
const proof = importDAG(blocks);

const space = await client.addSpace(proof);
await client.setCurrentSpace(space.did());

return client;
}

async function storeAdd(carPath) {
const client = await getClient();

let blob;
try {
const data = await fs.promises.readFile(carPath);
blob = new Blob([data]);
} catch (err) {
console.log(err);
process.exit(1);
}

const cid = await client.capability.store.add(blob);
console.log(cid.toString());
}

async function uploadAdd(root, carCids) {
const client = await getClient();

let rootCID;
try {
rootCID = CID.parse(root);
} catch (err) {
console.error(`Error: failed to parse root CID: ${root}: ${err.message}`);
process.exit(1);
}

const shards = [];
for (const cid of carCids) {
try {
shards.push(CID.parse(cid));
} catch (err) {
console.error(`Error: failed to parse shard CID: ${cid}: ${err.message}`);
process.exit(1);
}
}

await client.capability.upload.add(rootCID, shards);
console.log(root);
}

(async () => {
try {
const command = process.argv.slice(2, 5);
const args = process.argv.slice(5);
if (command.join(" ") === "can store add") {
const carPath = args[0];
await storeAdd(carPath);
} else if (command.join(" ") === "can upload add") {
const root = args[0];
const carCids = args.slice(1);
await uploadAdd(root, carCids);
} else {
console.log(`Invalid command: ${command.join(" ")}`);
process.exit(1);
}
} catch (err) {
console.log(err);
process.exit(1);
}
})();
20 changes: 20 additions & 0 deletions w3/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "livepeer-w3",
"type": "module",
"version": "1.0.0",
"description": "W3 CLI which enables executing a command with the given UCAN key and delegation proof",
"main": "livepeer-w3.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"@ipld/car": "^5.1.0",
"@web3-storage/w3up-client": "^4.1.0",
"multiformats": "^11.0.1"
},
"bin": {
"livepeer-w3": "livepeer-w3.js"
}
}