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

feat: sandbox ice-servers + ephemeral ports; add token-mint-signer ex #118

Merged
merged 1 commit into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# An image with holochain, lair-keystore, hc and node tsx

# We use ubuntu as it's glibc version is compatible with the prebuilt binaries
FROM ubuntu

Expand All @@ -24,13 +26,9 @@ RUN chmod 755 /usr/local/bin/hc /usr/local/bin/holochain /usr/local/bin/lair-key
WORKDIR /home/node
RUN /bin/bash -c "source $NVM_DIR/nvm.sh && npm i tsx"

# Copy the actual server script
COPY ./co2-sensor.ts ./co2-sensor.ts
COPY ./start.sh ./start.sh
RUN chmod +x ./start.sh

# So container runs with nvm loaded
SHELL ["/bin/bash", "--login", "-c"]

# The ./packages directory is mounted from the locally built npm workspace. We
# npm install at launch to avoid the need for the package.json to know the
# latest version or features of the workspace.
CMD npm install ./packages/types ./packages/sandbox; npx tsx ./co2-sensor.ts
CMD ["./start.sh"]
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ async function main() {
{
bootstrapServerUrl: new URL("https://bootstrap-0.infra.holochain.org"),
signalingServerUrl: new URL("wss://sbd-0.main.infra.holo.host"),
iceServers: [
"stun:stun-0.main.infra.holo.host:443",
"stun:stun-1.main.infra.holo.host:443",
],
ephemeralPorts: {
min: "40000",
max: "40255",
},
password: "password",
}
);
Expand Down Expand Up @@ -46,7 +54,7 @@ async function ensureListedAsPublisher(
function arrEqual(arr1: Uint8Array, arr2: Uint8Array): boolean {
if (arr1.length !== arr2.length) return false;
for (let i = 0; i < arr1.length; i++) {
if (arr1![i] !== arr2[i]) return false;
if (arr1[i] !== arr2[i]) return false;
}
return true;
}
Expand Down
219 changes: 219 additions & 0 deletions examples/emissions/agents/token-mint-signer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/**
* A mock CO₂ sensor, that emits a random measure in grams roughly every 2
* seconds. The "measurements" are published by a sandboxed holochain agent
* that serves no other role in the network.
*/

import { ensureAndConnectToHapp } from "@holoom/sandbox";
import {
UsernameRegistryCoordinator,
RecordsCoordinator,
Recipe,
SignedEvmSigningOffer,
EvmU256Item,
} from "@holoom/types";
import { encodeHashToBase64, AppClient, AgentPubKey } from "@holochain/client";
import {
BytesSigner,
EvmBytesSignerClient,
OfferCreator,
} from "@holoom/authority";
import { decodeAppEntry } from "@holoom/client";

async function main() {
// Create a conductor sandbox (with holoom installed) at the specified
// directory if it doesn't already exist, and connect to it.
const { appWs } = await ensureAndConnectToHapp(
"/sandbox",
"/workdir/holoom.happ",
"emissions-local-test-2024-09-04T12:59",
{
bootstrapServerUrl: new URL("https://bootstrap-0.infra.holochain.org"),
signalingServerUrl: new URL("wss://sbd-0.main.infra.holo.host"),
iceServers: [
"stun:stun-0.main.infra.holo.host:443",
"stun:stun-1.main.infra.holo.host:443",
],
ephemeralPorts: {
min: "40300",
max: "40555",
},
password: "password",
}
);
const app = new TokenMintSigner(appWs);
await app.run();
}

// Auto creates a recipe + offer as defined below and listens for signing requests
class TokenMintSigner {
bytesSigner: BytesSigner;
offerCreator: OfferCreator;
usernameRegistryCoordinator: UsernameRegistryCoordinator;
recordsCoordinator: RecordsCoordinator;
evmBytesSignerClient: EvmBytesSignerClient;
constructor(appClient: AppClient) {
// First private key of seed phrase:
// test test test test test test test test test test test junk
const EVM_PRIVATE_KEY =
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
this.bytesSigner = new BytesSigner(EVM_PRIVATE_KEY);
this.offerCreator = new OfferCreator(appClient, this.bytesSigner);
this.usernameRegistryCoordinator = new UsernameRegistryCoordinator(
appClient
);
this.recordsCoordinator = new RecordsCoordinator(appClient);
this.evmBytesSignerClient = new EvmBytesSignerClient(
appClient,
this.bytesSigner
);
}

async run() {
await this.autoPublishSigningOfferAndRecipe();
// Start listening for requests
await this.evmBytesSignerClient.setup();
}

// In a more realistic setup this step would be action manually by a human who
// has convinced themselves of which agent(s) they want to use as a data
// source. This example automates this step to reduce test tedium.
async autoPublishSigningOfferAndRecipe() {
console.log("Waiting for co2-sensor author to appear");
const co2SensorAuthor = await this.untilTrustedAuthorSelected();
console.log(
`Trusting ${encodeHashToBase64(co2SensorAuthor)} as co2-sensor author`
);

await this.ensureRecipe(...recipeForMint(co2SensorAuthor));
}

async untilTrustedAuthorSelected() {
while (true) {
const publishers =
await this.usernameRegistryCoordinator.getAllPublishers();
const pair = publishers.find(([_, tag]) => tag === "co2-sensor");
if (pair) return pair[0];
await new Promise((r) => setTimeout(r, 1000));
}
}

async ensureRecipe(expectedRecipe: Recipe, expectU256Items: EvmU256Item[]) {
const offerAhs =
await this.usernameRegistryCoordinator.getSigningOfferAhsForEvmAddress(
this.bytesSigner.address
);

for (const offerAh of offerAhs) {
const offerRecord = await this.recordsCoordinator.getRecord(offerAh);
if (!offerRecord) {
console.warn(
`Signing offer record ${encodeHashToBase64(offerAh)} not found`
);
continue;
}
const signedSigningOffer =
decodeAppEntry<SignedEvmSigningOffer>(offerRecord);
const recipeRecord = await this.recordsCoordinator.getRecord(
signedSigningOffer.offer.recipe_ah
);
if (!recipeRecord) {
console.warn(`Recipe record ${encodeHashToBase64(offerAh)} not found`);
continue;
}
const recipe = decodeAppEntry<Recipe>(recipeRecord);
if (
deepEqual(recipe, expectedRecipe) &&
deepEqual(signedSigningOffer.offer.u256_items, expectU256Items)
) {
console.log(
`Found existing matching signing offer ${encodeHashToBase64(
offerAh
)} and recipe ${encodeHashToBase64(
recipeRecord.signed_action.hashed.hash
)}`
);
}
}
const createdRecipeRecord =
await this.usernameRegistryCoordinator.createRecipe(expectedRecipe);
const createdSigningOfferRecord = await this.offerCreator.createOffer(
"mint-credit",
createdRecipeRecord.signed_action.hashed.hash,
expectU256Items
);
console.log(
`Create recipe ${encodeHashToBase64(
createdRecipeRecord.signed_action.hashed.hash
)} with offer ${createdSigningOfferRecord.signed_action.hashed.hash}`
);
}
}

const JQ_RANGE_TO_NAMES = `
[range(.from | tonumber; .until | tonumber)] |
map("co2-sensor/time/\\(.)")
`;

const JQ_ADD_READINGS = `
map(.gramsCo2) | add | [.]
`;

function recipeForMint(
trustedCo2SensorAuthor: AgentPubKey
): [Recipe, EvmU256Item[]] {
const recipe: Recipe = {
trusted_authors: [trustedCo2SensorAuthor],
arguments: [
["from", { type: "String" }],
["until", { type: "String" }],
],
instructions: [
[
"reading_names",
{
type: "Jq",
input_var_names: { type: "List", var_names: ["from", "until"] },
program: JQ_RANGE_TO_NAMES,
},
],
["readings", { type: "GetDocsListedByVar", var_name: "reading_names" }],
[
"$return",
{
type: "Jq",
input_var_names: { type: "Single", var_name: "readings" },
program: JQ_ADD_READINGS,
},
],
],
};
const items: EvmU256Item[] = [{ type: "Uint" }];
return [recipe, items];
}

function deepEqual(x: unknown, y: unknown) {
if (x === y) {
return true;
}
// Not shallowly equal, therefore only possible to be deeply equal if both
// are instances.
if (typeof x !== "object" || !x || typeof y !== "object" || !y) {
return false;
}
if (Object.keys(x).length != Object.keys(y).length) {
return false;
}
for (const prop in x) {
if (!y.hasOwnProperty(prop)) {
return false;
}
if (!deepEqual(x[prop as keyof typeof x], y[prop as keyof typeof x])) {
return false;
}
}

return true;
}

main();
16 changes: 14 additions & 2 deletions examples/emissions/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
services:

co2-sensor:
build: ./co2-sensor
build: .
volumes:
- ./agents/co2-sensor.ts:/home/node/agent.ts
- ../../packages:/home/node/packages
- ../../workdir:/workdir
- ../../workdir:/workdir
ports:
- 40000-40255:40000-40255

token-mint-signer:
build: .
volumes:
- ./agents/token-mint-signer.ts:/home/node/agent.ts
- ../../packages:/home/node/packages
- ../../workdir:/workdir
ports:
- 40300-40555:40300-40555
20 changes: 20 additions & 0 deletions examples/emissions/start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#! /bin/bash
set -e
npm install ./packages/types ./packages/authority ./packages/client ./packages/sandbox

# Link installed modules to volume for latest changes
cd ./packages/types
npm link
cd ../authority
npm link
cd ../client
npm link
cd ../sandbox
npm link
cd ../..
npm link @holoom/types
npm link @holoom/authority
npm link @holoom/client
npm link @holoom/sandbox

npx tsx watch ./agent.ts
6 changes: 2 additions & 4 deletions packages/authority/src/evm-bytes-signer/offer-creator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ActionHash, AppClient, Record } from "@holochain/client";
import {
CreateEvmSigningOfferPayload,
EvmSigningOffer,
EvmU256Item,
RecordsCoordinator,
Expand All @@ -23,7 +22,7 @@ export class OfferCreator {

async createOffer(
identifier: string,
recipeAh: number[],
recipeAh: ActionHash,
items: EvmU256Item[]
) {
const offer: EvmSigningOffer = {
Expand All @@ -45,8 +44,7 @@ export class OfferCreator {
},
});
console.log("Created record", record);
const actionHash = Array.from(record.signed_action.hashed.hash);
return actionHash;
return record;
}

private async untilRecipeGossiped(recipeAh: ActionHash) {
Expand Down
17 changes: 16 additions & 1 deletion packages/sandbox/src/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import yaml from "yaml";
export interface SandboxOptions {
bootstrapServerUrl: URL;
signalingServerUrl: URL;
iceServers: string[];
ephemeralPorts?: { min: string; max: string };
password: string;
}

Expand Down Expand Up @@ -86,12 +88,25 @@ export async function createSandbox(path: string, options: SandboxOptions) {
});
await createConductorPromise;

// Disable dpki
// Tweak conductor config
const conductorConfigPath = `${path}/conductor-config.yaml`;
const conductorConfig = yaml.parse(
await fs.readFile(conductorConfigPath, "utf8")
);
// Disable dpki
conductorConfig.dpki.no_dpki = true;
// Set WebRTC config
conductorConfig.network.transport_pool[0].webrtc_config = options.iceServers
.length
? { iceServers: options.iceServers.map((url) => ({ url })) }
: null;
// Set ephemeral port range
if (options.ephemeralPorts) {
conductorConfig.network.tuning_params.tx5_min_ephemeral_udp_port =
options.ephemeralPorts.min;
conductorConfig.network.tuning_params.tx5_max_ephemeral_udp_port =
options.ephemeralPorts.max;
}
await fs.writeFile(conductorConfigPath, yaml.stringify(conductorConfig));
}

Expand Down
5 changes: 3 additions & 2 deletions packages/tryorama/src/e2e/signing-offer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ test("e2e signing offer", async () => {
// This would normally be behind an admin authorised POST endpoint
await evmBytesSignerService.offerCreator.createOffer(
"123",
Array.from(recipeRecord.signed_action.hashed.hash),
recipeRecord.signed_action.hashed.hash,
[
{ type: "Uint" },
{ type: "Hex" },
Expand All @@ -95,7 +95,8 @@ test("e2e signing offer", async () => {
});

const evmSignedResult = await new HoloomClient(
alice.appWs as AppClient
alice.appWs as AppClient,
authority.agentPubKey
).requestEvmSignatureOverRecipeExecutionResult(
recipeExecutionRecord.signed_action.hashed.hash,
signingOfferAh
Expand Down