Skip to content

Commit

Permalink
feat: sandbox ice-servers + ephemeral ports; add token-mint-signer ex…
Browse files Browse the repository at this point in the history
…ample (#118)
  • Loading branch information
8e8b2c authored Sep 16, 2024
1 parent 1deb30f commit b65de96
Show file tree
Hide file tree
Showing 8 changed files with 288 additions and 17 deletions.
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

0 comments on commit b65de96

Please sign in to comment.