From 83fd45d03c3aaa012bcddd99f954d6123027f256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Mej=C3=ADas=20Gil?= Date: Sat, 20 Jun 2026 20:14:53 +0200 Subject: [PATCH 01/12] added example of general multi-request external-signing flow --- sdk/tests/data/test-programs.ts | 123 +++++++ sdk/tests/external-signing.test.ts | 480 +++++++++++++++++++++---- wasm/Cargo.lock | 55 --- wasm/Cargo.toml | 13 + wasm/src/programs/manager/authorize.rs | 124 +++++++ wasm/src/programs/request.rs | 149 +++++++- 6 files changed, 826 insertions(+), 118 deletions(-) diff --git a/sdk/tests/data/test-programs.ts b/sdk/tests/data/test-programs.ts index 86441370a..c0aa1d6c3 100644 --- a/sdk/tests/data/test-programs.ts +++ b/sdk/tests/data/test-programs.ts @@ -168,3 +168,126 @@ function run_alpha: call multi_fn_test.aleo/alpha r0 into r1; output r1 as u32.private; `; + +// Batches multiple credits.aleo records into a single private transfer. +// Mirrors snarkVM's construct_authorization_examples program source. +// https://testnet.explorer.provable.com/program/ldgbatcher_p28.aleo +export const LDGBATCHER_P28_PROGRAM = `import credits.aleo; + +program ldgbatcher_p28.aleo; + +function transfer_private_2: + input r0 as credits.aleo/credits.record; + input r1 as credits.aleo/credits.record; + input r2 as address.private; + input r3 as u64.private; + call credits.aleo/join r0 r1 into r4; + call credits.aleo/transfer_private r4 r2 r3 into r5 r6; + output r5 as credits.aleo/credits.record; + output r6 as credits.aleo/credits.record; + +function transfer_private_3: + input r0 as credits.aleo/credits.record; + input r1 as credits.aleo/credits.record; + input r2 as credits.aleo/credits.record; + input r3 as address.private; + input r4 as u64.private; + call credits.aleo/join r0 r1 into r5; + call credits.aleo/join r2 r5 into r6; + call credits.aleo/transfer_private r6 r3 r4 into r7 r8; + output r7 as credits.aleo/credits.record; + output r8 as credits.aleo/credits.record; + +function transfer_private_4: + input r0 as credits.aleo/credits.record; + input r1 as credits.aleo/credits.record; + input r2 as credits.aleo/credits.record; + input r3 as credits.aleo/credits.record; + input r4 as address.private; + input r5 as u64.private; + call credits.aleo/join r0 r1 into r6; + call credits.aleo/join r2 r6 into r7; + call credits.aleo/join r3 r7 into r8; + call credits.aleo/transfer_private r8 r4 r5 into r9 r10; + output r9 as credits.aleo/credits.record; + output r10 as credits.aleo/credits.record; + +function transfer_private_5: + input r0 as credits.aleo/credits.record; + input r1 as credits.aleo/credits.record; + input r2 as credits.aleo/credits.record; + input r3 as credits.aleo/credits.record; + input r4 as credits.aleo/credits.record; + input r5 as address.private; + input r6 as u64.private; + call credits.aleo/join r0 r1 into r7; + call credits.aleo/join r2 r7 into r8; + call credits.aleo/join r3 r8 into r9; + call credits.aleo/join r4 r9 into r10; + call credits.aleo/transfer_private r10 r5 r6 into r11 r12; + output r11 as credits.aleo/credits.record; + output r12 as credits.aleo/credits.record; + +function transfer_private_6: + input r0 as credits.aleo/credits.record; + input r1 as credits.aleo/credits.record; + input r2 as credits.aleo/credits.record; + input r3 as credits.aleo/credits.record; + input r4 as credits.aleo/credits.record; + input r5 as credits.aleo/credits.record; + input r6 as address.private; + input r7 as u64.private; + call credits.aleo/join r0 r1 into r8; + call credits.aleo/join r2 r8 into r9; + call credits.aleo/join r3 r9 into r10; + call credits.aleo/join r4 r10 into r11; + call credits.aleo/join r5 r11 into r12; + call credits.aleo/transfer_private r12 r6 r7 into r13 r14; + output r13 as credits.aleo/credits.record; + output r14 as credits.aleo/credits.record; + +function transfer_private_7: + input r0 as credits.aleo/credits.record; + input r1 as credits.aleo/credits.record; + input r2 as credits.aleo/credits.record; + input r3 as credits.aleo/credits.record; + input r4 as credits.aleo/credits.record; + input r5 as credits.aleo/credits.record; + input r6 as credits.aleo/credits.record; + input r7 as address.private; + input r8 as u64.private; + call credits.aleo/join r0 r1 into r9; + call credits.aleo/join r2 r9 into r10; + call credits.aleo/join r3 r10 into r11; + call credits.aleo/join r4 r11 into r12; + call credits.aleo/join r5 r12 into r13; + call credits.aleo/join r6 r13 into r14; + call credits.aleo/transfer_private r14 r7 r8 into r15 r16; + output r15 as credits.aleo/credits.record; + output r16 as credits.aleo/credits.record; + +function transfer_private_8: + input r0 as credits.aleo/credits.record; + input r1 as credits.aleo/credits.record; + input r2 as credits.aleo/credits.record; + input r3 as credits.aleo/credits.record; + input r4 as credits.aleo/credits.record; + input r5 as credits.aleo/credits.record; + input r6 as credits.aleo/credits.record; + input r7 as credits.aleo/credits.record; + input r8 as address.private; + input r9 as u64.private; + call credits.aleo/join r0 r1 into r10; + call credits.aleo/join r2 r10 into r11; + call credits.aleo/join r3 r11 into r12; + call credits.aleo/join r4 r12 into r13; + call credits.aleo/join r5 r13 into r14; + call credits.aleo/join r6 r14 into r15; + call credits.aleo/join r7 r15 into r16; + call credits.aleo/transfer_private r16 r8 r9 into r17 r18; + output r17 as credits.aleo/credits.record; + output r18 as credits.aleo/credits.record; + +constructor: + assert.eq edition 0u16; +`; diff --git a/sdk/tests/external-signing.test.ts b/sdk/tests/external-signing.test.ts index f11fb038a..95243684f 100644 --- a/sdk/tests/external-signing.test.ts +++ b/sdk/tests/external-signing.test.ts @@ -6,6 +6,9 @@ import { Field, Group, Plaintext, + Poseidon2, + Poseidon4, + Poseidon8, PrivateKey, Program, ProgramImportsBuilder, @@ -27,6 +30,7 @@ import type { SignatureLike, AddressLike, InputStrategy, + ExternalSigningInput, } from "../src/node.js"; import { isViewKeyStrategy, @@ -39,10 +43,159 @@ import { addressString, beaconPrivateKeyString, } from "./data/account-data.js"; -import { MULTIPLY_PROGRAM, DOUBLE_PROGRAM } from "./data/test-programs.js"; +import { MULTIPLY_PROGRAM, DOUBLE_PROGRAM, LDGBATCHER_P28_PROGRAM } from "./data/test-programs.js"; const message = Uint8Array.from([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]); +/** + * Output of the signer-side {@link fromPreprocessedInputs}: the freshly-sampled transition view key, + * the derived transition commitment, the request signature, and the gammas of the static-record + * inputs (one per record input, in input order). + */ +interface PreprocessedSigningResult { + tvk: Field; + tcm: Field; + signature: Signature; + gammas: Group[]; +} + +/** + * Produces a request signature from preprocessed inputs, mirroring the standard request + * signing algorithm. The signer holds the private key; the preprocessed inputs (produced by the + * authorizer via {@link computeExternalSigningInputs}) contain the to-fields representation of each + * input except for static records, where they contain the (`H`, `tag`) tuple. + * + * The function: + * 1. Samples a fresh transition view key `tvk` and derives `tcm = Hash(tvk)`. + * 2. Builds the message payload corresponding to each non-static-record input. + * 3. Calls `ExecutionRequest.signRequestFromPreprocessedInputs`, which samples the + * transition secret `r`, computes `r * H` and `gamma = sk_sig * H` for every record, assembles + * the message, and signs it. + * + * @returns The sampled `tvk`, the `tcm`, the `signature`, and the per-record `gammas`. + */ +function fromPreprocessedInputs( + privateKey: PrivateKey, + programName: string, + functionName: string, + authExternalInputs: ExternalSigningInput<"string">, +): PreprocessedSigningResult { + + // Initialize the hashers. + const poseidon4 = new Poseidon4(); + const poseidon8 = new Poseidon8(); + + // The WASM bindings take `Field` arguments by value and consume (move) them: a `Field` instance + // cannot be reused across two WASM calls. We therefore keep the reused values as strings and mint + // a fresh `Field` for each use via `f(...)`. + const str_to_f = (s: string): Field => Field.fromString(s); + + // Sample the transition view key and derive the transition commitment `tcm = Hash(tvk)`. + const tvkStr = Field.random().toString(); + const tcmStr = new Poseidon2().hash([str_to_f(tvkStr)]).toString(); + const functionIdStr = authExternalInputs.functionId; + + // Build the per-input signing material. Records are passed as `[H, tag]` (the signer derives + // `r * H` and `gamma`); every other input is passed as its precomputed input ID field, which is + // derived from the freshly-sampled `tvk` exactly as the standard signing algorithm does. + const inputIds = authExternalInputs.requestInputs.map((input): Field | [Field, Field] => { + const index = input.index; + const data = (): Field[] => input.data.map(str_to_f); + switch (input.signingInputType) { + case "record": + return [str_to_f(input.h!), str_to_f(input.tag!)]; + case "constant": + case "public": + // `Hash(function_id || data || tcm || index)`. + return poseidon8.hash([str_to_f(functionIdStr), ...data(), str_to_f(tcmStr), str_to_f(index)]); + case "private": { + // Encrypt with input view key `Hash(function_id || tvk || index)`, hash the ciphertext. + const inputViewKey = poseidon4.hash([str_to_f(functionIdStr), str_to_f(tvkStr), str_to_f(index)]); + const ciphertext = Plaintext.fromFields(data()).encryptSymmetric(inputViewKey); + return poseidon8.hash(Array.from(ciphertext.toFields() as ArrayLike)); + } + case "external_record": + case "dynamic_record": + // `Hash(function_id || data || tvk || index)`. + return poseidon8.hash([str_to_f(functionIdStr), ...data(), str_to_f(tvkStr), str_to_f(index)]); + default: + throw new Error(`Unsupported signing input type: ${input.signingInputType}`); + } + }); + + const checksum = authExternalInputs.checksum != null ? str_to_f(authExternalInputs.checksum) : undefined; + + const signed: any = (ExecutionRequest as any).signRequestFromPreprocessedInputs( + privateKey, + programName, + functionName, + str_to_f(tvkStr), + authExternalInputs.isRoot, + checksum, + inputIds, + ); + + return { + tvk: signed.tvk as Field, + tcm: signed.tcm as Field, + signature: signed.signature as Signature, + gammas: Array.from(signed.gammas as ArrayLike), + }; +} + +/** + * Computes the nonce of a static output record minted at `outputIndex` by a request whose transition + * view key is `tvk`, exactly as the `cast` instruction does on-chain: + * `nonce = g * HashToScalar([tvk, output_index])`. + */ +function computeMintedNonce(tvk: Field, outputIndex: number): Group { + const index = Field.fromString(`${outputIndex}field`); + const randomizer = new Poseidon2().hashToScalar([tvk, index]); + return Group.generator().scalarMultiply(randomizer); +} + +/** + * Writes a freshly-minted record nonce into the consumer input that receives it: replaces the + * `_nonce` of the record at `inputsByRequest[consumerRequestIndex][inputIndex]`. + */ +function applyMintedNonce( + inputsByRequest: string[][], + consumerRequestIndex: number, + inputIndex: number, + nonce: Group, +): void { + const inputs = inputsByRequest[consumerRequestIndex]; + inputs[inputIndex] = inputs[inputIndex].replace(/_nonce:\s*\d+group/, `_nonce: ${nonce.toString()}`); +} + +/** + * Maps a function-input definition returned by {@link Program.getFunctionInputs} to its snarkVM + * value-type string (the format consumed by `computeExternalSigningInputs`, the + * `fromExternallySignedData*` methods and `verify`). The shape returned by `getFunctionInputs` + * differs per input kind, so a plain `${type}.${visibility}` is only correct for literals. + */ +function valueTypeFromInputDef(def: any): string { + switch (def.type) { + case "record": + // Internal record: `.record`. + return `${def.record}.record`; + case "external_record": + // External record: `/.record`. + return `${def.locator}.record`; + case "future": + return `${def.locator}.future`; + case "struct": + return `${def.struct_id}.${def.visibility}`; + case "dynamic.record": + case "dynamic.future": + return def.type; + default: + // Literal plaintext (address, u64, field, ...): `.`. + return `${def.type}.${def.visibility}`; + } +} + +// TODO (Antonio) duplicated from external-signing.test.ts describe('External Signing Utilities', () => { const privateKey = PrivateKey.from_string(privateKeyString); const viewKey = privateKey.to_view_key(); @@ -706,67 +859,6 @@ describe('External Signing ExecutionRequest integration', () => { expect(roundTrippedPR.requests().length).to.equal(2); }); - it('Should match ExecutionRequest.sign for transfer_private', async () => { - // Use beacon private key - record is owned by beacon address - const privateKey = PrivateKey.from_string(beaconPrivateKeyString); - const beaconRecord = RecordPlaintext.fromString(recordBeaconOwned); - const gamma = beaconRecord.gamma("credits.aleo", "credits", privateKey); - const beaconViewKey = ViewKey.from_private_key(PrivateKey.from_string(beaconPrivateKeyString)); - const externalSigningInputs = await computeExternalSigningInputs({ - programName: "credits.aleo", - functionName: "transfer_private", - inputs: transferPrivateInputs, - inputTypes: transferPrivateInputTypes, - isRoot: true, - checksum: null, - viewKey: beaconViewKey, - }); - expect(externalSigningInputs.signer).to.be.a("string"); - expect(externalSigningInputs.skTag).to.be.a("string"); - - // Verify per-input recordViewKey is set on the record input - expect(externalSigningInputs.requestInputs[0].recordViewKey).to.be.a("string"); - expect(externalSigningInputs.requestInputs[0].recordViewKey).to.match(/field$/); - - // Verify non-record inputs don't have recordViewKey - expect(externalSigningInputs.requestInputs[1].recordViewKey).to.be.undefined; - expect(externalSigningInputs.requestInputs[2].recordViewKey).to.be.undefined; - const signedRequest = ExecutionRequest.sign( - privateKey, - "credits.aleo", - "transfer_private", - transferPrivateInputs, - transferPrivateInputTypes, - undefined, - undefined, - true, - ); - expect(signedRequest.program_id()).to.equal("credits.aleo"); - expect(signedRequest.function_name()).to.equal("transfer_private"); - expect(signedRequest.inputs().length).to.equal(3); - expect(externalSigningInputs.requestInputs.length).to.equal(signedRequest.inputs().length); - - const sig = signedRequest.signature(); - const tvk = signedRequest.tvk(); - // Use pre-computed input_ids via buildExecutionRequestFromExternallySignedDataWithInputIds - const externallySignedRequest = ExecutionRequest.fromExternallySignedDataWithInputIds( - "credits.aleo", - "transfer_private", - transferPrivateInputs, - transferPrivateInputTypes, - sig, - tvk, - signedRequest.signer(), - signedRequest.sk_tag(), - signedRequest.input_ids(), - ); - expect(externallySignedRequest.program_id()).to.equal(signedRequest.program_id()); - expect(externallySignedRequest.function_name()).to.equal(signedRequest.function_name()); - expect(externallySignedRequest.inputs().length).to.equal(signedRequest.inputs().length); - expect(externallySignedRequest.input_ids().length).to.equal(signedRequest.input_ids().length); - expect(externallySignedRequest.toString()).to.equal(signedRequest.toString()); - }); - it('Should not include recordViewKey on inputs when no viewKey is provided', async () => { const externalSigningInputs = await computeExternalSigningInputs({ programName: "credits.aleo", @@ -906,4 +998,268 @@ describe('External Signing ExecutionRequest integration', () => { assertFieldsRoundTripThroughPlaintext(secondInput.data, transferPrivateInputs[1]); assertFieldsRoundTripThroughPlaintext(thirdInput.data, transferPrivateInputs[2]); }); + + + it('Should match ExecutionRequest.sign for arbitrary flow with multiple requests', async () => { + // Deploy scenario: ldgbatcher_p28.aleo batches three minted credits.aleo records into a + // single private transfer via transfer_private_3 + // Nonetheless, the authorization/signing procedure below is general and should work for + // any flow of calls and records in a valid transaction. + + // Below, we prefix the names of some authorizer-side variables with "auth" and signer-side + // ones with "sig" + const sigPrivateKeyStr = privateKeyStr; + const sigPrivateKey = PrivateKey.from_string(sigPrivateKeyStr); + + const authViewKeyStr = ViewKey.from_private_key(sigPrivateKey).to_string(); + + const addressStr = Address.from_private_key(sigPrivateKey).to_string(); + + + // ----------------------------------------------------------------------------------------- + // Setup: deploy ldgbatcher_p28.aleo and mint three credits.aleo records + + // 1. "Deploy" ldgbatcher_p28.aleo (which imports credits.aleo) by registering its + // source in the imports builder. credits.aleo is always available in the process, + // so addProgram does not need to be called for it. + // 2. Produce three private credits.aleo records. Since the SDK test has no local + // ledger, instead of executing transfer_public_to_private three times we use + // pre-generated credits.aleo/credits record plaintexts owned by the caller. + const ProgramName = "ldgbatcher_p28.aleo"; + const imports = new ProgramImportsBuilder(); + imports.addProgram(ProgramName, LDGBATCHER_P28_PROGRAM); + + // Each record contains 1_000_000 microcredits owned by the caller. The nonces are + // distinct, valid group elements so each record is a distinct, parseable credits record. + const shieldAmount = "1000000u64"; + const recordNonces = [ + "3634848344765318974603121890869676775499130077229666060613233255327643175219group", + "3077450429259593211617823051143573281856129402760267155982965992208217472983group", + "8327477210335641151082470829879168522735279120730137538049818239556464339772group", + ]; + const records = recordNonces.map((nonce) => + RecordPlaintext.fromString( + `{ owner: ${addressStr}.private, microcredits: ${shieldAmount}.private, _nonce: ${nonce}.public, _version: 1u8.public }`, + ), + ); + expect(records.length).to.equal(3); + + // Auxiliary function to resolve the program given its name: imported programs (e.g. the + // root ldgbatcher_p28.aleo) live in the imports builder, whereas credits.aleo is built into + // the process and not registered as an import. This is only used for resolving input types + // and can be done in different ways (for instance, sample_authorization_extended could + // return this information directly instead). + const programForRequest = (programName: string): Program => { + const source = imports.getProgram(programName); + if (source != null) { + return Program.fromString(source); + } + if (programName === "credits.aleo") { + return Program.getCreditsProgram(); + } + throw new Error(`No program source available for ${programName}`); + }; + + // ----------------------------------------------------------------------------------------- + // Part 1 [Authorizer]: Call sampleAuthorizationExtended to produce the mock authorization + // and auxiliary data + + // Prepare the root-call target and inputs + const recipientPrivateKey = new PrivateKey(); + const recipientAddress = Address.from_private_key(recipientPrivateKey); + const transferAmount = "2000000u64"; + const functionName = "transfer_private_3"; + const inputs = [ + records[0].toString(), + records[1].toString(), + records[2].toString(), + recipientAddress.to_string(), + transferAmount, + ]; + + // ldgbatcher_p28.aleo declares `constructor: assert.eq edition 0u16;`. + const EDITION = 0; + + const extended: any = await (ProgramManagerBase as any).sampleAuthorizationExtended( + Address.from_string(addressStr), + LDGBATCHER_P28_PROGRAM, + functionName, + inputs, + undefined, // legacy imports object + EDITION, + imports.clone(), // clone: the builder is consumed (moved) by value + ); + + // Convert the returned object into the expected shapes + const authMockAuthorization = extended.authorization; + + const authRecordTracking = extended.recordTracking as Array<{ + minterRequestIndex: number; + outputIndex: number; + consumers: Array<{ consumerRequestIndex: number; inputIndex: number }>; + }>; + + const authProgramChecksums = extended.programChecksums as Array<{ requestIndex: number; checksum: string }>; + + // Note that extended.recordNames is not needed in this flow. + + // The authorization should contain exactly four requests: + // 0. ldgbatcher_p28.aleo/transfer_private_3 + // 1. credits.aleo/join + // 2. credits.aleo/join + // 3. credits.aleo/transfer_private + // This is verified at the end, alongside various other assertions. + + // ----------------------------------------------------------------------------------------- + // Part 2: Populate the requests, updating future tvk-dependent inputs eagerly + // Each request requires a round of authorizer-signer communication + + const authSignedRequests = Array.from(authMockAuthorization.requests() as ArrayLike); + + // Mutable per-request inputs. As the signer progressively samples request tvks, if one of + // them mints a static record which is passed to a subsequent request (possibly after + // conversion to a external or dynamic record), the authorizer patches the corresponding + // inputs of the receiving requests with record nonces computed from the actual tvk of the + // minter. + const authInputsByRequest: string[][] = authSignedRequests.map((request: any) => + Array.from(request.inputs() as ArrayLike).map((v: any) => v.toString()), + ); + + // The root request's tvk (index 0), kept as a string. Every child request needs it to + // compute its scm. It is set on the first loop iteration and read by every subsequent one. + // (WASM `Field`s are consumed when passed by value, so we store the string and mint fresh + // `Field` instances at each use site.) + let rootTvkStr!: string; + + // This list will accumulate the finalised, correct requests produced by the authorizer + // after receiving the signatures and other related data. + const authPopulatedRequests: ExecutionRequest[] = []; + + // Traversal order is fundamental: the order in the requests of the mock authorization + // guarantees subsequent-request inputs will have the nonces of the relevant inputs + // populated in time. + for (const [requestIndex, signedRequest] of authSignedRequests.entries()) { + + // [Authorizer] + // The authorizer pre-processes inputs, preparing "H" and "tag" in the case of records. + // Note that, crucially, the tvk-dependent nonces of static, external and dynamic records + // has been patched correctly by previous iterations of the loop. + const isRoot = requestIndex === 0; + const programName: string = signedRequest.program_id(); + const requestFunctionName: string = signedRequest.function_name(); + + // Read the (nonce-patched, if relevant) inputs for this request. + const requestInputs = authInputsByRequest[requestIndex]; + + // Resolve the input types from the program's function definition. + const program = programForRequest(programName); + const inputDefs = Array.from(program.getFunctionInputs(requestFunctionName)) as any[]; + const inputTypes = inputDefs.map(valueTypeFromInputDef); + + const checksum = authProgramChecksums.find((c) => c.requestIndex === requestIndex)?.checksum ?? null; + + // [Authorizer] + // Preprocess the inputs. Passing the view key computes "H", "tag", and the record view + // key for static-record inputs (whose tvk-dependent nonces have already been patched in by + // previous iterations), along with the signer and sk_tag. + const authExternalInputs = await computeExternalSigningInputs({ + programName, + functionName: requestFunctionName, + inputs: requestInputs, + inputTypes, + isRoot, + checksum, + viewKey: authViewKeyStr, + }); + + // [Authorizer] sends [Signer] the target program and function + + // [Signer] + // Produce the signature from the preprocessed inputs: this samples the tvk and r, + // derives `r * H` and gamma for the record inputs, constructs the message payload and + // signs it. A fresh private key is used because the WASM consumes (moves) the private + // key passed by value. + const signed = fromPreprocessedInputs( + PrivateKey.from_string(sigPrivateKeyStr), + programName, + requestFunctionName, + authExternalInputs, + ); + + const tvkStr = (signed.tvk as Field).toString(); + + if (isRoot) { + rootTvkStr = tvkStr; + } + + // [Signer] sends [Authorizer] the tvk, signature and record-input gammas. + + // [Authorizer] + // Reconstruct the full request from the signed data and the learnt tvk. + const rebuilt = ExecutionRequest.fromExternallySignedDataWithViewKey( + programName, + requestFunctionName, + requestInputs, + inputTypes, + signed.signature, + Field.fromString(tvkStr), + Address.from_string(addressStr), + Field.fromString(authExternalInputs.skTag!), + ViewKey.from_string(authViewKeyStr), + signed.gammas, + Field.fromString(rootTvkStr), + ); + + authPopulatedRequests.push(rebuilt); + + // [Authorizer] + // Patch the nonces of static/external/dynamic records input to subsequent requests and + // coming from static records minted by this request. The nonce needs to be recomputed + // using the actual tvk returned by the signer. The record-tracking information returned + // by sampleAuthorizationExtended indicates which request inputs must be updated. + for (const entry of authRecordTracking) { + if (entry.minterRequestIndex === requestIndex) { + const nonce = computeMintedNonce(Field.fromString(tvkStr), entry.outputIndex); + for (const consumer of entry.consumers) { + applyMintedNonce(authInputsByRequest, consumer.consumerRequestIndex, consumer.inputIndex, nonce); + } + } + } + } + + // ----------------------------------------------------------------------------------------- + // Checks + // The flow is now complete. We verify each reconstructed request. + + // Every request in the authorization was reconstructed. + expect(authPopulatedRequests.length).to.equal(authSignedRequests.length); + + // The authorization must contain exactly four requests with the expected program/function + // targets, in call order. + const expectedTargets = [ + { programId: "ldgbatcher_p28.aleo", functionName: "transfer_private_3" }, + { programId: "credits.aleo", functionName: "join" }, + { programId: "credits.aleo", functionName: "join" }, + { programId: "credits.aleo", functionName: "transfer_private" }, + ]; + expect(authPopulatedRequests.length).to.equal(expectedTargets.length); + for (const [requestIndex, rebuilt] of authPopulatedRequests.entries()) { + expect(rebuilt.program_id()).to.equal(expectedTargets[requestIndex].programId); + expect(rebuilt.function_name()).to.equal(expectedTargets[requestIndex].functionName); + } + + // Each reconstructed request must pass verification: this recomputes the input IDs from the + // inputs and tvk and checks the signature against the signer. + for (const [requestIndex, rebuilt] of authPopulatedRequests.entries()) { + const isRoot = requestIndex === 0; + const program = programForRequest(rebuilt.program_id()); + const inputDefs = Array.from(program.getFunctionInputs(rebuilt.function_name())) as any[]; + const inputTypes = inputDefs.map(valueTypeFromInputDef); + const checksum = authProgramChecksums.find((c) => c.requestIndex === requestIndex)?.checksum ?? null; + + expect( + rebuilt.verify(inputTypes, isRoot, checksum != null ? Field.fromString(checksum) : undefined), + ).to.equal(true); + } + }); }); diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index fa284e604..a1940cac2 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -2599,7 +2599,6 @@ dependencies = [ [[package]] name = "snarkvm-algorithms" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "aleo-std", "anyhow", @@ -2626,7 +2625,6 @@ dependencies = [ [[package]] name = "snarkvm-circuit" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "snarkvm-circuit-account", "snarkvm-circuit-algorithms", @@ -2640,7 +2638,6 @@ dependencies = [ [[package]] name = "snarkvm-circuit-account" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "snarkvm-circuit-network", "snarkvm-circuit-types", @@ -2650,7 +2647,6 @@ dependencies = [ [[package]] name = "snarkvm-circuit-algorithms" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "snarkvm-circuit-types", "snarkvm-console-algorithms", @@ -2660,7 +2656,6 @@ dependencies = [ [[package]] name = "snarkvm-circuit-collections" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "snarkvm-circuit-algorithms", "snarkvm-circuit-types", @@ -2670,7 +2665,6 @@ dependencies = [ [[package]] name = "snarkvm-circuit-environment" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "anyhow", "indexmap", @@ -2690,12 +2684,10 @@ dependencies = [ [[package]] name = "snarkvm-circuit-environment-witness" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" [[package]] name = "snarkvm-circuit-network" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "snarkvm-circuit-algorithms", "snarkvm-circuit-collections", @@ -2706,7 +2698,6 @@ dependencies = [ [[package]] name = "snarkvm-circuit-program" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "snarkvm-circuit-account", "snarkvm-circuit-algorithms", @@ -2720,7 +2711,6 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-address", @@ -2735,7 +2725,6 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-address" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2748,7 +2737,6 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-boolean" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "snarkvm-circuit-environment", "snarkvm-console-types-boolean", @@ -2757,7 +2745,6 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-field" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2767,7 +2754,6 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-group" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2779,7 +2765,6 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-integers" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2791,7 +2776,6 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-scalar" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2802,7 +2786,6 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-string" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2814,7 +2797,6 @@ dependencies = [ [[package]] name = "snarkvm-console" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "snarkvm-console-account", "snarkvm-console-algorithms", @@ -2827,7 +2809,6 @@ dependencies = [ [[package]] name = "snarkvm-console-account" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "bs58", "snarkvm-console-network", @@ -2838,7 +2819,6 @@ dependencies = [ [[package]] name = "snarkvm-console-algorithms" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "blake2s_simd", "hex", @@ -2854,7 +2834,6 @@ dependencies = [ [[package]] name = "snarkvm-console-collections" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "aleo-std", "parking_lot", @@ -2867,7 +2846,6 @@ dependencies = [ [[package]] name = "snarkvm-console-network" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "anyhow", "enum-iterator", @@ -2887,7 +2865,6 @@ dependencies = [ [[package]] name = "snarkvm-console-network-environment" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "anyhow", "bech32", @@ -2905,7 +2882,6 @@ dependencies = [ [[package]] name = "snarkvm-console-program" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "enum-iterator", "enum_index", @@ -2926,7 +2902,6 @@ dependencies = [ [[package]] name = "snarkvm-console-types" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-address", @@ -2941,7 +2916,6 @@ dependencies = [ [[package]] name = "snarkvm-console-types-address" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2952,7 +2926,6 @@ dependencies = [ [[package]] name = "snarkvm-console-types-boolean" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "snarkvm-console-network-environment", ] @@ -2960,7 +2933,6 @@ dependencies = [ [[package]] name = "snarkvm-console-types-field" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2970,7 +2942,6 @@ dependencies = [ [[package]] name = "snarkvm-console-types-group" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2981,7 +2952,6 @@ dependencies = [ [[package]] name = "snarkvm-console-types-integers" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2992,7 +2962,6 @@ dependencies = [ [[package]] name = "snarkvm-console-types-scalar" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -3003,7 +2972,6 @@ dependencies = [ [[package]] name = "snarkvm-console-types-string" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -3014,7 +2982,6 @@ dependencies = [ [[package]] name = "snarkvm-curves" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "rand 0.10.1", "rustc_version", @@ -3027,7 +2994,6 @@ dependencies = [ [[package]] name = "snarkvm-fields" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "aleo-std", "anyhow", @@ -3044,7 +3010,6 @@ dependencies = [ [[package]] name = "snarkvm-ledger-authority" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "anyhow", "rand 0.10.1", @@ -3056,7 +3021,6 @@ dependencies = [ [[package]] name = "snarkvm-ledger-block" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "anyhow", "indexmap", @@ -3080,7 +3044,6 @@ dependencies = [ [[package]] name = "snarkvm-ledger-committee" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "indexmap", "rayon", @@ -3092,7 +3055,6 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-batch-certificate" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "indexmap", "rayon", @@ -3105,7 +3067,6 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-batch-header" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "indexmap", "rayon", @@ -3117,7 +3078,6 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-data" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "bytes", "serde_json", @@ -3127,7 +3087,6 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-subdag" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "indexmap", "rayon", @@ -3142,7 +3101,6 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-transmission-id" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "snarkvm-console", "snarkvm-ledger-puzzle", @@ -3151,7 +3109,6 @@ dependencies = [ [[package]] name = "snarkvm-ledger-puzzle" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "aleo-std", "anyhow", @@ -3171,7 +3128,6 @@ dependencies = [ [[package]] name = "snarkvm-ledger-puzzle-epoch" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "aleo-std", "anyhow", @@ -3193,7 +3149,6 @@ dependencies = [ [[package]] name = "snarkvm-ledger-query" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "anyhow", "async-trait", @@ -3210,7 +3165,6 @@ dependencies = [ [[package]] name = "snarkvm-ledger-store" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "aleo-std-storage", "anyhow", @@ -3235,7 +3189,6 @@ dependencies = [ [[package]] name = "snarkvm-parameters" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "aleo-std", "anyhow", @@ -3260,7 +3213,6 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "aleo-std", "anyhow", @@ -3295,7 +3247,6 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer-error" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "anyhow", "snarkvm-circuit-environment", @@ -3307,7 +3258,6 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer-process" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "aleo-std", "colored", @@ -3333,7 +3283,6 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer-program" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "enum-iterator", "indexmap", @@ -3354,7 +3303,6 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer-snark" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "bincode", "serde_json", @@ -3367,7 +3315,6 @@ dependencies = [ [[package]] name = "snarkvm-utilities" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "aleo-std", "anyhow", @@ -3390,7 +3337,6 @@ dependencies = [ [[package]] name = "snarkvm-utilities-derives" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "proc-macro2", "quote 1.0.42", @@ -3400,7 +3346,6 @@ dependencies = [ [[package]] name = "snarkvm-wasm" version = "4.7.3" -source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=357899f8e85d6340bda5db8373b1cdffdf88a6d7#357899f8e85d6340bda5db8373b1cdffdf88a6d7" dependencies = [ "getrandom 0.4.2", "snarkvm-console", diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index 3d69e14bd..3e013c179 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -161,6 +161,19 @@ browser = [ ] testnet = [ ] mainnet = [ ] +# TODO (Antonio) remove +[patch."https://github.com/ProvableHQ/snarkVM.git"] +snarkvm-algorithms = { path = "../../snarkVM/algorithms" } +snarkvm-circuit-network = { path = "../../snarkVM/circuit/network" } +snarkvm-console = { path = "../../snarkVM/console" } +snarkvm-ledger-block = { path = "../../snarkVM/ledger/block" } +snarkvm-ledger-query = { path = "../../snarkVM/ledger/query" } +snarkvm-ledger-store = { path = "../../snarkVM/ledger/store" } +snarkvm-parameters = { path = "../../snarkVM/parameters" } +snarkvm-synthesizer-program = { path = "../../snarkVM/synthesizer/program" } +snarkvm-synthesizer = { path = "../../snarkVM/synthesizer" } +snarkvm-wasm = { path = "../../snarkVM/wasm" } + ## Profiles [profile.release] opt-level = 3 diff --git a/wasm/src/programs/manager/authorize.rs b/wasm/src/programs/manager/authorize.rs index 25f00cf03..fdbb289ff 100644 --- a/wasm/src/programs/manager/authorize.rs +++ b/wasm/src/programs/manager/authorize.rs @@ -43,6 +43,7 @@ use crate::{ use js_sys::{Array, Object}; use std::str::FromStr; +use wasm_bindgen::JsValue; #[wasm_bindgen] impl ProgramManager { @@ -275,6 +276,129 @@ impl ProgramManager { Ok(Authorization::from(authorization)) } + /// Produce a mocked `Authorization` for a program function call without needing a private key, + /// alongside the auxiliary information gathered while traversing the call graph. + /// + /// This behaves like {@link sampleAuthorization} but calls the underlying + /// `Stack::sample_authorization_extended`, which additionally surfaces record-tracking, + /// record-name, and program-checksum information needed to populate the mocked `Request`s. + /// The returned object has the following shape: + /// + /// ```text + /// { + /// authorization: Authorization, + /// // Each entry indicates that the `minterRequestIndex`-th request output a static record at + /// // register `outputIndex` which was (possibly after conversion to an external or dynamic + /// // record) passed as the `inputIndex`-th input to each listed `consumerRequestIndex`. + /// recordTracking: { minterRequestIndex, outputIndex, consumers: { consumerRequestIndex, inputIndex }[] }[], + /// // The m-th input of the n-th request is a static record named `recordName`. + /// recordNames: { requestIndex, inputIndex, recordName }[], + /// // The n-th request corresponds to a program with the given checksum. + /// programChecksums: { requestIndex, checksum }[], + /// } + /// ``` + /// + /// @param {Address} address The address used as the signer in the mocked requests. + /// @param {string} program The program source code containing the entry function. + /// @param {string} function_name The entry function to authorize. + /// @param {Array} inputs A javascript array of inputs to the function. + /// @param {Object | undefined} imports The imports to the program in `{"name.aleo":"source"}` format. + /// @param {number | undefined} edition The program edition (defaults to 1). + /// @param {ProgramImports | undefined} program_imports Pre-loaded imports builder. + #[wasm_bindgen(js_name = sampleAuthorizationExtended)] + pub async fn sample_authorization_extended( + address: &Address, + program: &str, + function_name: &str, + inputs: Array, + imports: Option, + edition: Option, + program_imports: Option, + ) -> Result { + let edition = edition.unwrap_or(1); + + let mut resolved = ResolvedProcess::resolve(&program_imports, program, edition, imports)?; + let program_native = resolved.program().clone(); + let process = resolved.process_mut(); + + let rng = &mut rand::rng(); + + // Parse inputs as strings (same pattern as the authorize! macro). + let inputs_native = process_inputs!(inputs); + + // Parse the function name. + let function_name_native = IdentifierNative::from_str(function_name) + .map_err(|_| "The function name provided was invalid".to_string())?; + + // Ensure the top-level program is in the process. + if program_native.id().to_string() != "credits.aleo" && !process.contains_program(program_native.id()) { + log("Adding program to the process"); + process.lock().add_program_with_edition(&program_native, edition).map_err(|e| e.to_string())?; + } + + // Get the stack for the top-level program. + let stack = process.get_stack(program_native.id()).map_err(|e| e.to_string())?; + + let address_native = AddressNative::from(address); + let program_id = *program_native.id(); + + // Produce the mock authorization with additional request-population information + let (authorization, record_tracking, record_names, program_checksums) = stack + .sample_authorization_extended::( + address_native, + program_id, + function_name_native, + inputs_native.into_iter(), + rng, + ) + .map_err(|e| e.to_string())?; + + // Record-tracking: (minter_request, output_register) -> [(consumer_request, input_index), ...]. + // We flatten the tuple key into individual values (since JS maps cannot key on tuples) and + // keep the consumers as a nested array. + let record_tracking_js = Array::new(); + for ((minter_request_index, output_index), consumers) in record_tracking { + let consumers_js = Array::new(); + for (consumer_request_index, input_index) in consumers { + consumers_js.push(&JsValue::from(crate::object! { + "consumerRequestIndex": consumer_request_index as u32, + "inputIndex": input_index as u32, + })); + } + record_tracking_js.push(&JsValue::from(crate::object! { + "minterRequestIndex": minter_request_index as u32, + "outputIndex": output_index as f64, + "consumers": consumers_js, + })); + } + + // Record-names: (request_index, input_index) -> record_name (for all input static records). + let record_names_js = Array::new(); + for ((request_index, input_index), record_name) in record_names { + record_names_js.push(&JsValue::from(crate::object! { + "requestIndex": request_index as u32, + "inputIndex": input_index as u32, + "recordName": record_name.to_string(), + })); + } + + // Program-checksums: request_index -> checksum (programs with no checksum: no entry). + let program_checksums_js = Array::new(); + for (request_index, checksum) in program_checksums { + program_checksums_js.push(&JsValue::from(crate::object! { + "requestIndex": request_index as u32, + "checksum": checksum.to_string(), + })); + } + + Ok(crate::object! { + "authorization": Authorization::from(authorization), + "recordTracking": record_tracking_js, + "recordNames": record_names_js, + "programChecksums": program_checksums_js, + }) + } + /// Create an `Authorization` for `credits.aleo/fee_public` or `credits.aleo/fee_private`. /// This object requires an associated execution or deployment ID. This can be gained from /// any previously created authorization by calling (authorization.toExecutionId()). diff --git a/wasm/src/programs/request.rs b/wasm/src/programs/request.rs index c61789cfe..f850eb9d8 100644 --- a/wasm/src/programs/request.rs +++ b/wasm/src/programs/request.rs @@ -25,6 +25,8 @@ use crate::{ native_type_from_wasm_object_array, object, types::native::{ + AddressNative, + ComputeKeyNative, CurrentNetwork, FieldNative, IdentifierNative, @@ -32,6 +34,7 @@ use crate::{ ProgramIDNative, RecordPlaintextNative, RequestNative, + SignatureNative, U16Native, ValueNative, ValueTypeNative, @@ -39,7 +42,7 @@ use crate::{ }; use snarkvm_console::{ network::Network, - prelude::{One, ToFields, Zero}, + prelude::{One, ToField, ToFields, Uniform, Zero}, program::compute_function_id, }; use snarkvm_wasm::utilities::{FromBytes, ToBytes}; @@ -292,6 +295,150 @@ impl ExecutionRequest { Ok(ExecutionRequest(request)) } + /// Produces a request signature from preprocessed (externally-computed) inputs. + /// + /// This mirrors `Request::sign`, with two differences: + /// - The input IDs are supplied by the caller rather than recomputed. Each element of + /// `input_ids` is either: + /// * a `Field` — the precomputed input ID of a constant/public/private/external-record/ + /// dynamic-record input, contributed to the signature message verbatim; or + /// * a 2-element `Array` `[H, tag]` for a (static) record input, where `H` is the + /// x-coordinate of `HashToGroup(serial_number_domain || commitment)` and `tag` is the + /// record tag. For these, the secret-dependent pieces `gamma = sk_sig * H` and + /// `h_r = r * H` are computed here (as in `Request::sign`) and the record contributes + /// `(H, h_r, gamma)` x-coordinates followed by `tag` to the message. + /// - The transition view key `tvk` is supplied by the caller (and echoed back) rather than + /// derived. The transition secret `r` is sampled internally (from a random nonce, as in + /// `Request::sign`) and used for `g_r` and the per-record `h_r`; it is independent of the + /// supplied `tvk`, mirroring how the mock-authorization flow establishes `tvk` separately. + /// + /// @param {PrivateKey} private_key The signer's private key. + /// @param {string} program_id The id of the program. + /// @param {string} function_name The function name. + /// @param {Field} tvk The transition view key. + /// @param {boolean} is_root Flag to indicate if this is the top level function in the call graph. + /// @param {Field | undefined} program_checksum The program checksum (required if the program has a constructor). + /// @param {(Field | [Field, Field])[]} input_ids Per-input data: a Field input ID, or `[H, tag]` for a record. + /// @returns {{ tvk: Field, tcm: Field, signature: Signature, gammas: Group[] }} + #[wasm_bindgen(js_name = "signRequestFromPreprocessedInputs")] + #[allow(clippy::too_many_arguments)] + pub fn sign_request_from_preprocessed_inputs( + private_key: PrivateKey, + program_id: String, + function_name: String, + tvk: Field, + is_root: bool, + program_checksum: Option, + input_ids: Array, + ) -> Result { + // Parse the program ID and function name. + let program_id = ProgramIDNative::from_str(&program_id).map_err(|e| e.to_string())?; + let function_name = IdentifierNative::from_str(&function_name).map_err(|e| e.to_string())?; + + // Derive the signing key material from the private key. + let private_key_native = *private_key; + let sk_sig = private_key_native.sk_sig(); + let compute_key = ComputeKeyNative::try_from(&private_key_native).map_err(|e| e.to_string())?; + let pk_sig = compute_key.pk_sig(); + let pr_sig = compute_key.pr_sig(); + let signer = AddressNative::try_from(compute_key).map_err(|e| e.to_string())?; + + // Derive the transition secret `r` and `g_r = r * G`, exactly as `Request::sign`. + let tvk_native = *tvk; + // Sample a random nonce from system entropy. + let mut rng = rand::rng(); + + // Important note: this should be sampled in a cryptographically secure random fashion in + // production environments. + // Additional note: instead of sampling the nonce and computing r from it, r can be + // sampled directly: the nonce is not used anywhere else and the derivation mechanism for r + // below is not enforced at any point. It is merely an extra layer of protection against + // faulty rngs. + let nonce = FieldNative::rand(&mut rng); + let r = CurrentNetwork::hash_to_scalar_psd4(&[ + CurrentNetwork::serial_number_domain(), + sk_sig.to_field().map_err(|e| e.to_string())?, + nonce, + ]) + .map_err(|e| e.to_string())?; + let g_r = CurrentNetwork::g_scalar_multiply(&r); + + // Compute the transition commitment `tcm = Hash(tvk)`. + let tcm = CurrentNetwork::hash_psd2(&[tvk_native]).map_err(|e| e.to_string())?; + + // Compute `is_root` as a field element. + let is_root_field = if is_root { FieldNative::one() } else { FieldNative::zero() }; + + // Compute the function ID. + let network_id = U16Native::new(CurrentNetwork::ID); + let function_id = + compute_function_id(&network_id, &program_id, &function_name).map_err(|e| e.to_string())?; + let program_checksum = program_checksum.map(|checksum| *checksum); + + // Construct the message as + // `(g_r, pk_sig, pr_sig, signer, [tvk, tcm, function ID, is_root, program checksum?, input IDs])`. + let mut message = Vec::with_capacity(9 + 2 * input_ids.length() as usize); + message.extend([g_r, pk_sig, pr_sig, *signer].map(|point| point.to_x_coordinate())); + message.extend([tvk_native, tcm, function_id, is_root_field]); + // Add the program checksum to the message if it was provided. + if let Some(program_checksum) = program_checksum { + message.push(program_checksum); + } + + // Append each input's contribution and collect the gammas of the record inputs. + let gammas = Array::new(); + for (i, id_js) in input_ids.iter().enumerate() { + if Array::is_array(&id_js) { + // Record input: `[H, tag]` (the x-coordinate of `H`, then the tag). + let id_array = Array::from(&id_js); + if id_array.length() != 2 { + return Err(format!( + "input_ids[{i}] is an array but has length {} (expected 2 for a record: [H, tag])", + id_array.length() + )); + } + let h_x = FieldNative::from( + Field::try_from_js_value(id_array.get(0)) + .map_err(|_| format!("input_ids[{i}][0] (H) must be a Field"))?, + ); + let tag = FieldNative::from( + Field::try_from_js_value(id_array.get(1)) + .map_err(|_| format!("input_ids[{i}][1] (tag) must be a Field"))?, + ); + // Recover the full point `H` from its x-coordinate (lossless for subgroup elements). + let h = GroupNative::from_x_coordinate(h_x).map_err(|e| e.to_string())?; + // Compute `gamma = sk_sig * H` and `h_r = r * H`. + let gamma = h * sk_sig; + let h_r = h * r; + // The record contributes `(H, h_r, gamma)` x-coordinates followed by the tag. + message.extend([h, h_r, gamma].iter().map(|point| point.to_x_coordinate())); + message.push(tag); + gammas.push(&JsValue::from(Group::from(gamma))); + } else { + // Non-record input: a single precomputed input ID field. + let input_id = FieldNative::from( + Field::try_from_js_value(id_js) + .map_err(|_| format!("input_ids[{i}] must be a Field or an Array"))?, + ); + message.push(input_id); + } + } + + // Compute `challenge` as `HashToScalar(message)` and `response` as `r - challenge * sk_sig`. + let challenge = CurrentNetwork::hash_to_scalar_psd8(&message).map_err(|e| e.to_string())?; + let response = r - challenge * sk_sig; + let signature = SignatureNative::from((challenge, response, compute_key)); + + // Assemble the result object. + let result = object! { + "tvk": JsValue::from(Field::from(tvk_native)), + "tcm": JsValue::from(Field::from(tcm)), + "signature": JsValue::from(Signature::from(signature)), + "gammas": JsValue::from(gammas), + }; + Ok(result) + } + /// Builds a request from externally-signed data using externally-supplied record view keys. /// /// For record inputs, the caller provides `record_view_keys` and `gammas` directly. From 8263ed4d973a11f0797cec72fd9b4e2d8525199f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Mej=C3=ADas=20Gil?= Date: Mon, 22 Jun 2026 14:37:19 +0200 Subject: [PATCH 02/12] added full prove-verify flow to test, passing --- sdk/rollup.test.js | 1 - sdk/tests/external-signing.test.ts | 60 +++++++++- wasm/build.js | 1 - wasm/src/programs/manager/authorize.rs | 56 ++++++++- wasm/src/programs/manager/execute.rs | 83 +++++++++++++ wasm/src/programs/offline_query.rs | 156 ++++++++++++++++++++++++- wasm/src/types/group.rs | 11 ++ 7 files changed, 362 insertions(+), 6 deletions(-) diff --git a/sdk/rollup.test.js b/sdk/rollup.test.js index 487707588..b6383c3b6 100644 --- a/sdk/rollup.test.js +++ b/sdk/rollup.test.js @@ -5,7 +5,6 @@ import $package from "./package.json" with { type: "json" }; const networks = [ "testnet", - "mainnet", ]; function inputs() { diff --git a/sdk/tests/external-signing.test.ts b/sdk/tests/external-signing.test.ts index 95243684f..cd7d6b36b 100644 --- a/sdk/tests/external-signing.test.ts +++ b/sdk/tests/external-signing.test.ts @@ -5,6 +5,7 @@ import { ExecutionRequest, Field, Group, + OfflineQuery, Plaintext, Poseidon2, Poseidon4, @@ -14,6 +15,7 @@ import { ProgramImportsBuilder, ProgramManagerBase, ProvingRequest, + QueryOption, RecordPlaintext, Signature, ViewKey, @@ -151,7 +153,9 @@ function fromPreprocessedInputs( function computeMintedNonce(tvk: Field, outputIndex: number): Group { const index = Field.fromString(`${outputIndex}field`); const randomizer = new Poseidon2().hashToScalar([tvk, index]); - return Group.generator().scalarMultiply(randomizer); + // The nonce uses the Aleo account/signature generator `G` (Network::g_scalar_multiply), not the + // curve's prime-subgroup generator returned by `Group.generator()`. + return Group.gScalarMultiply(randomizer); } /** @@ -1261,5 +1265,59 @@ describe('External Signing ExecutionRequest integration', () => { rebuilt.verify(inputTypes, isRoot, checksum != null ? Field.fromString(checksum) : undefined), ).to.equal(true); } + + // ----------------------------------------------------------------------------------------- + // Build the on-chain Authorization from the reconstructed requests via snarkVM's + // `authorize_requests`. This re-traverses the call graph of the root function, consuming the + // supplied requests to populate the authorization, so a successful build already proves the + // reconstructed requests are mutually consistent (correct input IDs, signatures, tvks, scms). + // + // NOTE: `buildAuthorizationFromExecutionRequests` consumes (moves) each ExecutionRequest, so + // `authPopulatedRequests` must not be used after this call. + const authorization = await (ProgramManagerBase as any).buildAuthorizationFromExecutionRequests( + authPopulatedRequests, + LDGBATCHER_P28_PROGRAM, + EDITION, + undefined, // legacy imports object + imports.clone(), + ); + + // The authorization has one transition per request, targets the root function, and yields a + // derivable execution id. + expect(authorization.len()).to.equal(expectedTargets.length); + expect(Array.from(authorization.transitions() as ArrayLike).length).to.equal(expectedTargets.length); + expect(authorization.functionName()).to.equal(functionName); + expect(authorization.toExecutionId().toString().length).to.be.greaterThan(0); + + // ----------------------------------------------------------------------------------------- + // Prove the authorization and verify the resulting execution proof, fully offline. + // + // The three original credits.aleo records are consumed (as serial numbers) by the nested + // credits.aleo/join and transfer_private calls, so the execution needs an inclusion proof + // for each of their commitments. Since the SDK test has no ledger, we fabricate a single + // self-consistent global state root that contains all three commitments and serve the + // corresponding state paths via an OfflineQuery. The intermediate (joined) records are + // created and consumed within the same execution, so they use local inclusion and need no + // state path. + const recordCommitments = records.map((record) => { + const recordViewKey = record.recordViewKey(ViewKey.from_string(authViewKeyStr)); + return record.commitment("credits.aleo", "credits", recordViewKey.toString()).toString(); + }); + + // V16 is active at u32::MAX on the (non-test) testnet consensus schedule; this matches the + // height the mock authorization and records were generated for. + const V16_BLOCK_HEIGHT = 4294967295; + const offlineQuery = OfflineQuery.sampleStatePaths(V16_BLOCK_HEIGHT, recordCommitments); + + // proveAndVerifyAuthorization consumes (moves) the authorization, so it must come last. + const proofVerified = await (ProgramManagerBase as any).proveAndVerifyAuthorization( + authorization, + LDGBATCHER_P28_PROGRAM, + QueryOption.offlineQuery(offlineQuery), + EDITION, + undefined, // legacy imports object + imports.clone(), + ); + expect(proofVerified).to.equal(true); }); }); diff --git a/wasm/build.js b/wasm/build.js index 8b7ea750d..7cc233472 100644 --- a/wasm/build.js +++ b/wasm/build.js @@ -241,7 +241,6 @@ console.time("Building wasm"); const networks = [ "testnet", - "mainnet", ]; await Promise.all(networks.map(build)); diff --git a/wasm/src/programs/manager/authorize.rs b/wasm/src/programs/manager/authorize.rs index fdbb289ff..cd44bce09 100644 --- a/wasm/src/programs/manager/authorize.rs +++ b/wasm/src/programs/manager/authorize.rs @@ -43,7 +43,7 @@ use crate::{ use js_sys::{Array, Object}; use std::str::FromStr; -use wasm_bindgen::JsValue; +use wasm_bindgen::{JsValue, convert::TryFromJsValue}; #[wasm_bindgen] impl ProgramManager { @@ -211,6 +211,60 @@ impl ProgramManager { Ok(Authorization::from(authorization)) } + /// Build an `Authorization` from a full set of already-signed `Request`s. + /// + /// This wraps snarkVM's `Stack::authorize_requests`: it re-traverses the call graph of the root + /// function, consuming the supplied requests (one per transition, in call order) to populate the + /// authorization. The first request must target `program`. Unlike + /// {@link buildAuthorizationFromExecutionRequest}, no private key is needed because the requests + /// are already signed. + /// + /// @param {ExecutionRequest[]} requests The signed requests, in call order (root first). + /// @param {string} program The program source code containing the root function. + /// @param {number | undefined} edition The edition of the program. + /// @param {object | undefined} imports The imports to the program in the format {"programname.aleo":"aleo instructions source code"}. + /// @param {ProgramImports | undefined} program_imports Pre-loaded imports builder. + /// @returns {Authorization} + #[wasm_bindgen(js_name = buildAuthorizationFromExecutionRequests)] + pub async fn authorize_requests( + requests: Array, + program: &str, + edition: Option, + imports: Option, + program_imports: Option, + ) -> Result { + let edition = edition.unwrap_or(1); + + let mut resolved = ResolvedProcess::resolve(&program_imports, program, edition, imports)?; + let program_native = resolved.program().clone(); + let process = resolved.process_mut(); + + // Convert the JS array of `ExecutionRequest`s into native requests, in order. + let mut requests_native = Vec::with_capacity(requests.length() as usize); + for value in requests.iter() { + let request = ExecutionRequest::try_from_js_value(value) + .map_err(|_| "`requests` must be an array of ExecutionRequest".to_string())?; + requests_native.push(RequestNative::from(&request)); + } + if requests_native.is_empty() { + return Err("`requests` must contain at least one request".to_string()); + } + + // Add the top-level program to the process (no-op if it is already present). + if program_native.id().to_string() != "credits.aleo" && !process.contains_program(program_native.id()) { + log("Adding program to the process"); + process.lock().add_program_with_edition(&program_native, edition).map_err(|e| e.to_string())?; + } + + // Get the stack for the top-level program and build the authorization. + let stack = process.get_stack(program_native.id()).map_err(|e| e.to_string())?; + let rng = &mut rand::rng(); + let authorization = + stack.authorize_requests::(requests_native, rng).map_err(|e| e.to_string())?; + + Ok(Authorization::from(authorization)) + } + /// Produce a mocked `Authorization` for a program function call without needing a private key. /// /// The resulting Authorization has the same structure as a real one — the call graph is fully diff --git a/wasm/src/programs/manager/execute.rs b/wasm/src/programs/manager/execute.rs index 7d856cc5a..7f4892ab5 100644 --- a/wasm/src/programs/manager/execute.rs +++ b/wasm/src/programs/manager/execute.rs @@ -364,6 +364,89 @@ impl ProgramManager { Ok(transaction) } + /// Prove an `Authorization` and verify the resulting execution proof, fully offline. + /// + /// This is a self-contained "prove -> verify" check intended for testing: it executes the + /// authorization to produce a trace, prepares the inclusion proofs using the supplied `query` + /// (use an {@link OfflineQuery} to stay offline), proves the execution, and then verifies the + /// resulting proof. Proving keys are synthesized on demand if not already cached. Returns `true` + /// when the proof verifies (otherwise it returns an error). + /// + /// @param {Authorization} authorization The authorization to prove. + /// @param {string} program The program source code containing the root function. + /// @param {QueryOption} query The query used to source inclusion state paths (e.g. an OfflineQuery). + /// @param {number | undefined} edition The edition of the program (defaults to 1). + /// @param {object | undefined} imports The legacy imports object {"name.aleo":"source"}. + /// @param {ProgramImports | undefined} program_imports Pre-loaded imports builder. + /// @returns {boolean} True if the execution proof verifies. + #[wasm_bindgen(js_name = proveAndVerifyAuthorization)] + pub async fn prove_and_verify_authorization( + authorization: Authorization, + program: &str, + query: QueryOption, + edition: Option, + imports: Option, + program_imports: Option, + ) -> Result { + let edition = edition.unwrap_or(1); + + let mut resolved = ResolvedProcess::resolve(&program_imports, program, edition, imports)?; + let program_native = resolved.program().clone(); + let process = resolved.process_mut(); + + // Add the top-level program to the process (no-op if it is already present). + if program_native.id().to_string() != "credits.aleo" && !process.contains_program(program_native.id()) { + process.lock().add_program_with_edition(&program_native, edition).map_err(|e| e.to_string())?; + } + + let rng = &mut rand::rng(); + let authorization = AuthorizationNative::from(authorization); + + // Determine the locator of the root function. + let locator = { + let request = authorization.peek_next().map_err(|e| e.to_string())?; + LocatorNative::new(*request.program_id(), *request.function_name()).to_string() + }; + + // Determine the consensus and proof-system versions from the query's block height. + let latest_height = query.current_block_height_async().await.map_err(|e| e.to_string())?; + let consensus_version = + ::CONSENSUS_VERSION(latest_height).map_err(|e| e.to_string())?; + authorization.check_valid_edition(process, consensus_version).map_err(|e| e.to_string())?; + authorization.check_valid_records(consensus_version).map_err(|e| e.to_string())?; + let varuna_version = match (ConsensusVersion::V1..=ConsensusVersion::V3).contains(&consensus_version) { + true => VarunaVersion::V1, + false => VarunaVersion::V2, + }; + + // Execute the authorization and prove the resulting trace. + let (_, mut trace) = process.execute::(authorization, rng).map_err(|e| e.to_string())?; + log("Preparing inclusion proofs for execution"); + trace.prepare_async(&query).await.map_err(|e| e.to_string())?; + log("Proving execution"); + let execution = trace + .prove_execution::(&locator, varuna_version, &mut rand::rng()) + .map_err(|e| e.to_string())?; + + // Verify the execution proof. + log("Verifying execution"); + let inclusion_upgrade_height = + ::INCLUSION_UPGRADE_HEIGHT().map_err(|e| e.to_string())?; + let inclusion_version = + if latest_height >= inclusion_upgrade_height { InclusionVersion::V1 } else { InclusionVersion::V0 }; + let execution_stacks = execution_stacks_for_execution(process, &execution)?; + ProcessNative::verify_execution( + consensus_version, + varuna_version, + inclusion_version, + &execution, + &execution_stacks, + ) + .map_err(|e| e.to_string())?; + + Ok(true) + } + /// Generate an execution transaction without a proof. /// Intended for use with the Leo devnode tool. /// diff --git a/wasm/src/programs/offline_query.rs b/wasm/src/programs/offline_query.rs index 458cdfeb4..14a423a88 100644 --- a/wasm/src/programs/offline_query.rs +++ b/wasm/src/programs/offline_query.rs @@ -14,13 +14,30 @@ // You should have received a copy of the GNU General Public License // along with the Provable SDK library. If not, see . -use crate::types::native::{CurrentNetwork, FieldNative, StatePathNative}; -use snarkvm_console::network::Network; +use crate::types::native::{CurrentNetwork, FieldNative, StatePathNative, U64Native}; +use snarkvm_console::{ + network::{ + Network, + prelude::{ToBits, Uniform, Zero}, + }, + program::{ + BLOCKS_DEPTH, + BlockPath, + HeaderLeaf, + HeaderTree, + TransactionLeaf, + TransactionTree, + TransactionsTree, + TransitionLeaf, + TransitionTree, + }, +}; use snarkvm_ledger_query::QueryTrait; use anyhow::{Result, anyhow, ensure}; use async_trait::async_trait; use indexmap::IndexMap; +use js_sys::Array; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::wasm_bindgen; @@ -70,6 +87,37 @@ impl OfflineQuery { Ok(()) } + /// Build an `OfflineQuery` whose state paths place all the given record `commitments` under a + /// single, self-consistent global state root. + /// + /// This fabricates a minimal block / transaction / transition Merkle structure that contains the + /// commitments and returns a query pre-populated with the resulting (shared) global state root + /// and one state path per commitment. It is intended only for OFFLINE testing of inclusion + /// proofs: the returned root does not correspond to any real ledger state, but it is internally + /// consistent, so a proof produced against it will also verify against it. + /// + /// @param {number} block_height The block height to report. + /// @param {string[]} commitments The record commitments (as strings) to include. + /// @returns {OfflineQuery} A query containing a state path for every commitment. + #[wasm_bindgen(js_name = "sampleStatePaths")] + pub fn sample_state_paths(block_height: u32, commitments: Array) -> Result { + let commitments = commitments + .iter() + .map(|commitment| { + let commitment = commitment.as_string().ok_or_else(|| "Commitments must be strings".to_string())?; + FieldNative::from_str(&commitment).map_err(|e| e.to_string()) + }) + .collect::, String>>()?; + + let (state_root, paths) = sample_global_state_paths(&commitments).map_err(|e| e.to_string())?; + + let mut state_paths = IndexMap::new(); + for (commitment, path) in commitments.into_iter().zip(paths) { + state_paths.insert(commitment, path); + } + Ok(Self { block_height, state_paths, state_root }) + } + /// Get a json string representation of the offline query object. /// /// @returns {string} JSON string representation of the offline query object. @@ -131,6 +179,110 @@ impl QueryTrait for OfflineQuery { } } +/// Fabricates a self-consistent global state path for every supplied commitment, all sharing a +/// single global state root. The commitments are placed as leaves (variant 3 = Record) in one +/// transition tree; the transaction, transactions, header and block layers above are shared, so +/// every returned state path resolves to the same global state root. +/// +/// This mirrors `snarkvm_console::program::state_path::test_helpers::sample_global_state_path`, but +/// supports multiple commitments under one root (required because an execution's inclusion proof +/// must reference a single global state root across all of its input records). +fn sample_global_state_paths( + commitments: &[FieldNative], +) -> Result<(::StateRoot, Vec)> { + type N = CurrentNetwork; + let rng = &mut rand::rng(); + + // A single transition commitment shared by all leaves. + let tcm = FieldNative::rand(rng); + + // Build a single transition tree whose leaves are the record commitments. + let transition_leaves = commitments + .iter() + .enumerate() + .map(|(index, commitment)| { + // The number of record commitments in a single execution is far below `u8::MAX`. + let index = u8::try_from(index).map_err(|_| anyhow!("Too many commitments for a single transition"))?; + Ok(TransitionLeaf::new(index, 3, *commitment)) + }) + .collect::>>()?; + let transition_tree: TransitionTree = + N::merkle_tree_bhp(&transition_leaves.iter().map(|leaf| leaf.to_bits_le()).collect::>())?; + let transition_root = *transition_tree.root(); + let transition_id = N::hash_bhp512(&(transition_root, tcm).to_bits_le())?; + + // The transaction, transactions, header and block layers are shared across all commitments. + let transaction_leaf = TransactionLeaf::new_execution(0, transition_id); + let transaction_tree: TransactionTree = N::merkle_tree_bhp(&[transaction_leaf.to_bits_le()])?; + let transaction_id = *transaction_tree.root(); + let transaction_path = transaction_tree.prove(0, &transaction_leaf.to_bits_le())?; + + let transactions_tree: TransactionsTree = N::merkle_tree_bhp(&[transaction_id.to_bits_le()])?; + let transactions_root = transactions_tree.root(); + let transactions_path = transactions_tree.prove(0, &transaction_id.to_bits_le())?; + + let header_leaf = HeaderLeaf::::new(1, *transactions_root); + let header_tree: HeaderTree = + N::merkle_tree_bhp(&[FieldNative::zero().to_bits_le(), header_leaf.to_bits_le()])?; + let header_root = header_tree.root(); + let header_path = header_tree.prove(1, &header_leaf.to_bits_le())?; + + let previous_block_hash: ::BlockHash = FieldNative::rand(rng).into(); + let preimage = (*previous_block_hash).to_bits_le().into_iter().chain(header_root.to_bits_le()); + let block_hash = N::hash_bhp1024(&preimage.collect::>())?; + + // The inclusion (V1) circuit requires that records consumed by `credits.aleo` were created at a + // block height at or after `INCLUSION_UPGRADE_HEIGHT + 1`. Rather than build a full block tree + // (2^BLOCKS_DEPTH leaves), we construct the block Merkle path directly at that leaf index with + // zero siblings, then derive the resulting global state root by folding the block-hash leaf up + // the path exactly as `MerklePath::verify` does (BHP leaf/path hashing). + let block_index = u64::from(N::INCLUSION_UPGRADE_HEIGHT()?.saturating_add(1)); + let block_siblings = vec![FieldNative::zero(); BLOCKS_DEPTH as usize]; + + // Leaf hash: BHP1024 over `false || block_hash_bits`. + let mut current = { + let mut leaf_preimage = vec![false]; + block_hash.write_bits_le(&mut leaf_preimage); + N::hash_bhp1024(&leaf_preimage)? + }; + // Path hash: BHP512 over `true || left_bits || right_bits`, ordered by the leaf-index bits. + for (level, sibling) in block_siblings.iter().enumerate() { + let current_is_left = ((block_index >> level) & 1) == 0; + let (left, right) = if current_is_left { (current, *sibling) } else { (*sibling, current) }; + let mut node_preimage = vec![true]; + left.write_bits_le(&mut node_preimage); + right.write_bits_le(&mut node_preimage); + current = N::hash_bhp512(&node_preimage)?; + } + let global_state_root = current; + let block_path = BlockPath::::try_from((U64Native::new(block_index), block_siblings))?; + + // Build one state path per commitment, sharing every layer except the transition leaf/path. + let mut state_paths = Vec::with_capacity(commitments.len()); + for (index, transition_leaf) in transition_leaves.iter().enumerate() { + let transition_path = transition_tree.prove(index, &transition_leaf.to_bits_le())?; + state_paths.push(StatePathNative::from( + global_state_root.into(), + block_path.clone(), + block_hash.into(), + previous_block_hash, + *header_root, + header_path.clone(), + header_leaf, + transactions_path.clone(), + transaction_id.into(), + transaction_path.clone(), + transaction_leaf, + transition_root, + tcm, + transition_path, + *transition_leaf, + )); + } + + Ok((global_state_root.into(), state_paths)) +} + #[cfg(test)] mod tests { use super::*; diff --git a/wasm/src/types/group.rs b/wasm/src/types/group.rs index e5f524954..8d2bc4d5e 100644 --- a/wasm/src/types/group.rs +++ b/wasm/src/types/group.rs @@ -27,6 +27,7 @@ use crate::{ Boolean, integer::{I8, I16, I32, I64, I128, U8, U16, U32, U64, U128}, native::{ + CurrentNetwork, GroupNative, I8Native, I16Native, @@ -44,6 +45,7 @@ use crate::{ }, }, }; +use snarkvm_console::network::Network; use snarkvm_console::prelude::{ Double, FromBits, @@ -198,6 +200,15 @@ impl Group { Group::from(GroupNative::generator()) } + /// Returns `scalar * G`, where `G` is the Aleo account/signature generator used by the network + /// (`Network::g_scalar_multiply`). This is the generator used to derive record nonces and the + /// signature nonce `g_r`, and differs from {@link generator}, which returns the curve's + /// prime-subgroup generator. + #[wasm_bindgen(js_name = gScalarMultiply)] + pub fn g_scalar_multiply(scalar: &Scalar) -> Group { + Group::from(CurrentNetwork::g_scalar_multiply(&**scalar)) + } + // ── cast conversions ─────────────────────────────────────────────── /// Cast the group element to a Field (returns x-coordinate). From 26f30bd22383ec8f921e46f789d7b1efa1acfa3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Mej=C3=ADas=20Gil?= Date: Mon, 22 Jun 2026 14:37:26 +0200 Subject: [PATCH 03/12] Revert "added full prove-verify flow to test, passing" This reverts commit 8263ed4d973a11f0797cec72fd9b4e2d8525199f. --- sdk/rollup.test.js | 1 + sdk/tests/external-signing.test.ts | 60 +--------- wasm/build.js | 1 + wasm/src/programs/manager/authorize.rs | 56 +-------- wasm/src/programs/manager/execute.rs | 83 ------------- wasm/src/programs/offline_query.rs | 156 +------------------------ wasm/src/types/group.rs | 11 -- 7 files changed, 6 insertions(+), 362 deletions(-) diff --git a/sdk/rollup.test.js b/sdk/rollup.test.js index b6383c3b6..487707588 100644 --- a/sdk/rollup.test.js +++ b/sdk/rollup.test.js @@ -5,6 +5,7 @@ import $package from "./package.json" with { type: "json" }; const networks = [ "testnet", + "mainnet", ]; function inputs() { diff --git a/sdk/tests/external-signing.test.ts b/sdk/tests/external-signing.test.ts index cd7d6b36b..95243684f 100644 --- a/sdk/tests/external-signing.test.ts +++ b/sdk/tests/external-signing.test.ts @@ -5,7 +5,6 @@ import { ExecutionRequest, Field, Group, - OfflineQuery, Plaintext, Poseidon2, Poseidon4, @@ -15,7 +14,6 @@ import { ProgramImportsBuilder, ProgramManagerBase, ProvingRequest, - QueryOption, RecordPlaintext, Signature, ViewKey, @@ -153,9 +151,7 @@ function fromPreprocessedInputs( function computeMintedNonce(tvk: Field, outputIndex: number): Group { const index = Field.fromString(`${outputIndex}field`); const randomizer = new Poseidon2().hashToScalar([tvk, index]); - // The nonce uses the Aleo account/signature generator `G` (Network::g_scalar_multiply), not the - // curve's prime-subgroup generator returned by `Group.generator()`. - return Group.gScalarMultiply(randomizer); + return Group.generator().scalarMultiply(randomizer); } /** @@ -1265,59 +1261,5 @@ describe('External Signing ExecutionRequest integration', () => { rebuilt.verify(inputTypes, isRoot, checksum != null ? Field.fromString(checksum) : undefined), ).to.equal(true); } - - // ----------------------------------------------------------------------------------------- - // Build the on-chain Authorization from the reconstructed requests via snarkVM's - // `authorize_requests`. This re-traverses the call graph of the root function, consuming the - // supplied requests to populate the authorization, so a successful build already proves the - // reconstructed requests are mutually consistent (correct input IDs, signatures, tvks, scms). - // - // NOTE: `buildAuthorizationFromExecutionRequests` consumes (moves) each ExecutionRequest, so - // `authPopulatedRequests` must not be used after this call. - const authorization = await (ProgramManagerBase as any).buildAuthorizationFromExecutionRequests( - authPopulatedRequests, - LDGBATCHER_P28_PROGRAM, - EDITION, - undefined, // legacy imports object - imports.clone(), - ); - - // The authorization has one transition per request, targets the root function, and yields a - // derivable execution id. - expect(authorization.len()).to.equal(expectedTargets.length); - expect(Array.from(authorization.transitions() as ArrayLike).length).to.equal(expectedTargets.length); - expect(authorization.functionName()).to.equal(functionName); - expect(authorization.toExecutionId().toString().length).to.be.greaterThan(0); - - // ----------------------------------------------------------------------------------------- - // Prove the authorization and verify the resulting execution proof, fully offline. - // - // The three original credits.aleo records are consumed (as serial numbers) by the nested - // credits.aleo/join and transfer_private calls, so the execution needs an inclusion proof - // for each of their commitments. Since the SDK test has no ledger, we fabricate a single - // self-consistent global state root that contains all three commitments and serve the - // corresponding state paths via an OfflineQuery. The intermediate (joined) records are - // created and consumed within the same execution, so they use local inclusion and need no - // state path. - const recordCommitments = records.map((record) => { - const recordViewKey = record.recordViewKey(ViewKey.from_string(authViewKeyStr)); - return record.commitment("credits.aleo", "credits", recordViewKey.toString()).toString(); - }); - - // V16 is active at u32::MAX on the (non-test) testnet consensus schedule; this matches the - // height the mock authorization and records were generated for. - const V16_BLOCK_HEIGHT = 4294967295; - const offlineQuery = OfflineQuery.sampleStatePaths(V16_BLOCK_HEIGHT, recordCommitments); - - // proveAndVerifyAuthorization consumes (moves) the authorization, so it must come last. - const proofVerified = await (ProgramManagerBase as any).proveAndVerifyAuthorization( - authorization, - LDGBATCHER_P28_PROGRAM, - QueryOption.offlineQuery(offlineQuery), - EDITION, - undefined, // legacy imports object - imports.clone(), - ); - expect(proofVerified).to.equal(true); }); }); diff --git a/wasm/build.js b/wasm/build.js index 7cc233472..8b7ea750d 100644 --- a/wasm/build.js +++ b/wasm/build.js @@ -241,6 +241,7 @@ console.time("Building wasm"); const networks = [ "testnet", + "mainnet", ]; await Promise.all(networks.map(build)); diff --git a/wasm/src/programs/manager/authorize.rs b/wasm/src/programs/manager/authorize.rs index cd44bce09..fdbb289ff 100644 --- a/wasm/src/programs/manager/authorize.rs +++ b/wasm/src/programs/manager/authorize.rs @@ -43,7 +43,7 @@ use crate::{ use js_sys::{Array, Object}; use std::str::FromStr; -use wasm_bindgen::{JsValue, convert::TryFromJsValue}; +use wasm_bindgen::JsValue; #[wasm_bindgen] impl ProgramManager { @@ -211,60 +211,6 @@ impl ProgramManager { Ok(Authorization::from(authorization)) } - /// Build an `Authorization` from a full set of already-signed `Request`s. - /// - /// This wraps snarkVM's `Stack::authorize_requests`: it re-traverses the call graph of the root - /// function, consuming the supplied requests (one per transition, in call order) to populate the - /// authorization. The first request must target `program`. Unlike - /// {@link buildAuthorizationFromExecutionRequest}, no private key is needed because the requests - /// are already signed. - /// - /// @param {ExecutionRequest[]} requests The signed requests, in call order (root first). - /// @param {string} program The program source code containing the root function. - /// @param {number | undefined} edition The edition of the program. - /// @param {object | undefined} imports The imports to the program in the format {"programname.aleo":"aleo instructions source code"}. - /// @param {ProgramImports | undefined} program_imports Pre-loaded imports builder. - /// @returns {Authorization} - #[wasm_bindgen(js_name = buildAuthorizationFromExecutionRequests)] - pub async fn authorize_requests( - requests: Array, - program: &str, - edition: Option, - imports: Option, - program_imports: Option, - ) -> Result { - let edition = edition.unwrap_or(1); - - let mut resolved = ResolvedProcess::resolve(&program_imports, program, edition, imports)?; - let program_native = resolved.program().clone(); - let process = resolved.process_mut(); - - // Convert the JS array of `ExecutionRequest`s into native requests, in order. - let mut requests_native = Vec::with_capacity(requests.length() as usize); - for value in requests.iter() { - let request = ExecutionRequest::try_from_js_value(value) - .map_err(|_| "`requests` must be an array of ExecutionRequest".to_string())?; - requests_native.push(RequestNative::from(&request)); - } - if requests_native.is_empty() { - return Err("`requests` must contain at least one request".to_string()); - } - - // Add the top-level program to the process (no-op if it is already present). - if program_native.id().to_string() != "credits.aleo" && !process.contains_program(program_native.id()) { - log("Adding program to the process"); - process.lock().add_program_with_edition(&program_native, edition).map_err(|e| e.to_string())?; - } - - // Get the stack for the top-level program and build the authorization. - let stack = process.get_stack(program_native.id()).map_err(|e| e.to_string())?; - let rng = &mut rand::rng(); - let authorization = - stack.authorize_requests::(requests_native, rng).map_err(|e| e.to_string())?; - - Ok(Authorization::from(authorization)) - } - /// Produce a mocked `Authorization` for a program function call without needing a private key. /// /// The resulting Authorization has the same structure as a real one — the call graph is fully diff --git a/wasm/src/programs/manager/execute.rs b/wasm/src/programs/manager/execute.rs index 7f4892ab5..7d856cc5a 100644 --- a/wasm/src/programs/manager/execute.rs +++ b/wasm/src/programs/manager/execute.rs @@ -364,89 +364,6 @@ impl ProgramManager { Ok(transaction) } - /// Prove an `Authorization` and verify the resulting execution proof, fully offline. - /// - /// This is a self-contained "prove -> verify" check intended for testing: it executes the - /// authorization to produce a trace, prepares the inclusion proofs using the supplied `query` - /// (use an {@link OfflineQuery} to stay offline), proves the execution, and then verifies the - /// resulting proof. Proving keys are synthesized on demand if not already cached. Returns `true` - /// when the proof verifies (otherwise it returns an error). - /// - /// @param {Authorization} authorization The authorization to prove. - /// @param {string} program The program source code containing the root function. - /// @param {QueryOption} query The query used to source inclusion state paths (e.g. an OfflineQuery). - /// @param {number | undefined} edition The edition of the program (defaults to 1). - /// @param {object | undefined} imports The legacy imports object {"name.aleo":"source"}. - /// @param {ProgramImports | undefined} program_imports Pre-loaded imports builder. - /// @returns {boolean} True if the execution proof verifies. - #[wasm_bindgen(js_name = proveAndVerifyAuthorization)] - pub async fn prove_and_verify_authorization( - authorization: Authorization, - program: &str, - query: QueryOption, - edition: Option, - imports: Option, - program_imports: Option, - ) -> Result { - let edition = edition.unwrap_or(1); - - let mut resolved = ResolvedProcess::resolve(&program_imports, program, edition, imports)?; - let program_native = resolved.program().clone(); - let process = resolved.process_mut(); - - // Add the top-level program to the process (no-op if it is already present). - if program_native.id().to_string() != "credits.aleo" && !process.contains_program(program_native.id()) { - process.lock().add_program_with_edition(&program_native, edition).map_err(|e| e.to_string())?; - } - - let rng = &mut rand::rng(); - let authorization = AuthorizationNative::from(authorization); - - // Determine the locator of the root function. - let locator = { - let request = authorization.peek_next().map_err(|e| e.to_string())?; - LocatorNative::new(*request.program_id(), *request.function_name()).to_string() - }; - - // Determine the consensus and proof-system versions from the query's block height. - let latest_height = query.current_block_height_async().await.map_err(|e| e.to_string())?; - let consensus_version = - ::CONSENSUS_VERSION(latest_height).map_err(|e| e.to_string())?; - authorization.check_valid_edition(process, consensus_version).map_err(|e| e.to_string())?; - authorization.check_valid_records(consensus_version).map_err(|e| e.to_string())?; - let varuna_version = match (ConsensusVersion::V1..=ConsensusVersion::V3).contains(&consensus_version) { - true => VarunaVersion::V1, - false => VarunaVersion::V2, - }; - - // Execute the authorization and prove the resulting trace. - let (_, mut trace) = process.execute::(authorization, rng).map_err(|e| e.to_string())?; - log("Preparing inclusion proofs for execution"); - trace.prepare_async(&query).await.map_err(|e| e.to_string())?; - log("Proving execution"); - let execution = trace - .prove_execution::(&locator, varuna_version, &mut rand::rng()) - .map_err(|e| e.to_string())?; - - // Verify the execution proof. - log("Verifying execution"); - let inclusion_upgrade_height = - ::INCLUSION_UPGRADE_HEIGHT().map_err(|e| e.to_string())?; - let inclusion_version = - if latest_height >= inclusion_upgrade_height { InclusionVersion::V1 } else { InclusionVersion::V0 }; - let execution_stacks = execution_stacks_for_execution(process, &execution)?; - ProcessNative::verify_execution( - consensus_version, - varuna_version, - inclusion_version, - &execution, - &execution_stacks, - ) - .map_err(|e| e.to_string())?; - - Ok(true) - } - /// Generate an execution transaction without a proof. /// Intended for use with the Leo devnode tool. /// diff --git a/wasm/src/programs/offline_query.rs b/wasm/src/programs/offline_query.rs index 14a423a88..458cdfeb4 100644 --- a/wasm/src/programs/offline_query.rs +++ b/wasm/src/programs/offline_query.rs @@ -14,30 +14,13 @@ // You should have received a copy of the GNU General Public License // along with the Provable SDK library. If not, see . -use crate::types::native::{CurrentNetwork, FieldNative, StatePathNative, U64Native}; -use snarkvm_console::{ - network::{ - Network, - prelude::{ToBits, Uniform, Zero}, - }, - program::{ - BLOCKS_DEPTH, - BlockPath, - HeaderLeaf, - HeaderTree, - TransactionLeaf, - TransactionTree, - TransactionsTree, - TransitionLeaf, - TransitionTree, - }, -}; +use crate::types::native::{CurrentNetwork, FieldNative, StatePathNative}; +use snarkvm_console::network::Network; use snarkvm_ledger_query::QueryTrait; use anyhow::{Result, anyhow, ensure}; use async_trait::async_trait; use indexmap::IndexMap; -use js_sys::Array; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::wasm_bindgen; @@ -87,37 +70,6 @@ impl OfflineQuery { Ok(()) } - /// Build an `OfflineQuery` whose state paths place all the given record `commitments` under a - /// single, self-consistent global state root. - /// - /// This fabricates a minimal block / transaction / transition Merkle structure that contains the - /// commitments and returns a query pre-populated with the resulting (shared) global state root - /// and one state path per commitment. It is intended only for OFFLINE testing of inclusion - /// proofs: the returned root does not correspond to any real ledger state, but it is internally - /// consistent, so a proof produced against it will also verify against it. - /// - /// @param {number} block_height The block height to report. - /// @param {string[]} commitments The record commitments (as strings) to include. - /// @returns {OfflineQuery} A query containing a state path for every commitment. - #[wasm_bindgen(js_name = "sampleStatePaths")] - pub fn sample_state_paths(block_height: u32, commitments: Array) -> Result { - let commitments = commitments - .iter() - .map(|commitment| { - let commitment = commitment.as_string().ok_or_else(|| "Commitments must be strings".to_string())?; - FieldNative::from_str(&commitment).map_err(|e| e.to_string()) - }) - .collect::, String>>()?; - - let (state_root, paths) = sample_global_state_paths(&commitments).map_err(|e| e.to_string())?; - - let mut state_paths = IndexMap::new(); - for (commitment, path) in commitments.into_iter().zip(paths) { - state_paths.insert(commitment, path); - } - Ok(Self { block_height, state_paths, state_root }) - } - /// Get a json string representation of the offline query object. /// /// @returns {string} JSON string representation of the offline query object. @@ -179,110 +131,6 @@ impl QueryTrait for OfflineQuery { } } -/// Fabricates a self-consistent global state path for every supplied commitment, all sharing a -/// single global state root. The commitments are placed as leaves (variant 3 = Record) in one -/// transition tree; the transaction, transactions, header and block layers above are shared, so -/// every returned state path resolves to the same global state root. -/// -/// This mirrors `snarkvm_console::program::state_path::test_helpers::sample_global_state_path`, but -/// supports multiple commitments under one root (required because an execution's inclusion proof -/// must reference a single global state root across all of its input records). -fn sample_global_state_paths( - commitments: &[FieldNative], -) -> Result<(::StateRoot, Vec)> { - type N = CurrentNetwork; - let rng = &mut rand::rng(); - - // A single transition commitment shared by all leaves. - let tcm = FieldNative::rand(rng); - - // Build a single transition tree whose leaves are the record commitments. - let transition_leaves = commitments - .iter() - .enumerate() - .map(|(index, commitment)| { - // The number of record commitments in a single execution is far below `u8::MAX`. - let index = u8::try_from(index).map_err(|_| anyhow!("Too many commitments for a single transition"))?; - Ok(TransitionLeaf::new(index, 3, *commitment)) - }) - .collect::>>()?; - let transition_tree: TransitionTree = - N::merkle_tree_bhp(&transition_leaves.iter().map(|leaf| leaf.to_bits_le()).collect::>())?; - let transition_root = *transition_tree.root(); - let transition_id = N::hash_bhp512(&(transition_root, tcm).to_bits_le())?; - - // The transaction, transactions, header and block layers are shared across all commitments. - let transaction_leaf = TransactionLeaf::new_execution(0, transition_id); - let transaction_tree: TransactionTree = N::merkle_tree_bhp(&[transaction_leaf.to_bits_le()])?; - let transaction_id = *transaction_tree.root(); - let transaction_path = transaction_tree.prove(0, &transaction_leaf.to_bits_le())?; - - let transactions_tree: TransactionsTree = N::merkle_tree_bhp(&[transaction_id.to_bits_le()])?; - let transactions_root = transactions_tree.root(); - let transactions_path = transactions_tree.prove(0, &transaction_id.to_bits_le())?; - - let header_leaf = HeaderLeaf::::new(1, *transactions_root); - let header_tree: HeaderTree = - N::merkle_tree_bhp(&[FieldNative::zero().to_bits_le(), header_leaf.to_bits_le()])?; - let header_root = header_tree.root(); - let header_path = header_tree.prove(1, &header_leaf.to_bits_le())?; - - let previous_block_hash: ::BlockHash = FieldNative::rand(rng).into(); - let preimage = (*previous_block_hash).to_bits_le().into_iter().chain(header_root.to_bits_le()); - let block_hash = N::hash_bhp1024(&preimage.collect::>())?; - - // The inclusion (V1) circuit requires that records consumed by `credits.aleo` were created at a - // block height at or after `INCLUSION_UPGRADE_HEIGHT + 1`. Rather than build a full block tree - // (2^BLOCKS_DEPTH leaves), we construct the block Merkle path directly at that leaf index with - // zero siblings, then derive the resulting global state root by folding the block-hash leaf up - // the path exactly as `MerklePath::verify` does (BHP leaf/path hashing). - let block_index = u64::from(N::INCLUSION_UPGRADE_HEIGHT()?.saturating_add(1)); - let block_siblings = vec![FieldNative::zero(); BLOCKS_DEPTH as usize]; - - // Leaf hash: BHP1024 over `false || block_hash_bits`. - let mut current = { - let mut leaf_preimage = vec![false]; - block_hash.write_bits_le(&mut leaf_preimage); - N::hash_bhp1024(&leaf_preimage)? - }; - // Path hash: BHP512 over `true || left_bits || right_bits`, ordered by the leaf-index bits. - for (level, sibling) in block_siblings.iter().enumerate() { - let current_is_left = ((block_index >> level) & 1) == 0; - let (left, right) = if current_is_left { (current, *sibling) } else { (*sibling, current) }; - let mut node_preimage = vec![true]; - left.write_bits_le(&mut node_preimage); - right.write_bits_le(&mut node_preimage); - current = N::hash_bhp512(&node_preimage)?; - } - let global_state_root = current; - let block_path = BlockPath::::try_from((U64Native::new(block_index), block_siblings))?; - - // Build one state path per commitment, sharing every layer except the transition leaf/path. - let mut state_paths = Vec::with_capacity(commitments.len()); - for (index, transition_leaf) in transition_leaves.iter().enumerate() { - let transition_path = transition_tree.prove(index, &transition_leaf.to_bits_le())?; - state_paths.push(StatePathNative::from( - global_state_root.into(), - block_path.clone(), - block_hash.into(), - previous_block_hash, - *header_root, - header_path.clone(), - header_leaf, - transactions_path.clone(), - transaction_id.into(), - transaction_path.clone(), - transaction_leaf, - transition_root, - tcm, - transition_path, - *transition_leaf, - )); - } - - Ok((global_state_root.into(), state_paths)) -} - #[cfg(test)] mod tests { use super::*; diff --git a/wasm/src/types/group.rs b/wasm/src/types/group.rs index 8d2bc4d5e..e5f524954 100644 --- a/wasm/src/types/group.rs +++ b/wasm/src/types/group.rs @@ -27,7 +27,6 @@ use crate::{ Boolean, integer::{I8, I16, I32, I64, I128, U8, U16, U32, U64, U128}, native::{ - CurrentNetwork, GroupNative, I8Native, I16Native, @@ -45,7 +44,6 @@ use crate::{ }, }, }; -use snarkvm_console::network::Network; use snarkvm_console::prelude::{ Double, FromBits, @@ -200,15 +198,6 @@ impl Group { Group::from(GroupNative::generator()) } - /// Returns `scalar * G`, where `G` is the Aleo account/signature generator used by the network - /// (`Network::g_scalar_multiply`). This is the generator used to derive record nonces and the - /// signature nonce `g_r`, and differs from {@link generator}, which returns the curve's - /// prime-subgroup generator. - #[wasm_bindgen(js_name = gScalarMultiply)] - pub fn g_scalar_multiply(scalar: &Scalar) -> Group { - Group::from(CurrentNetwork::g_scalar_multiply(&**scalar)) - } - // ── cast conversions ─────────────────────────────────────────────── /// Cast the group element to a Field (returns x-coordinate). From 8c96e4a82a7c43239da4d96d95d5785eeffde863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Mej=C3=ADas=20Gil?= Date: Mon, 22 Jun 2026 15:13:26 +0200 Subject: [PATCH 04/12] brought back relevant fixes --- sdk/tests/algorithm.test.ts | 6 +-- sdk/tests/external-signing.test.ts | 26 +++++++++--- wasm/src/programs/manager/authorize.rs | 56 +++++++++++++++++++++++++- wasm/src/types/group.rs | 10 +++++ 4 files changed, 89 insertions(+), 9 deletions(-) diff --git a/sdk/tests/algorithm.test.ts b/sdk/tests/algorithm.test.ts index bac64448a..c0b7f4444 100644 --- a/sdk/tests/algorithm.test.ts +++ b/sdk/tests/algorithm.test.ts @@ -82,19 +82,19 @@ describe('Hash Function Export Tests', () => { expect(Poseidon2Hasher.hash(deepCopyFieldArray(fieldArray)).toString()).equals(algebraicData.expectedPoseidon2Hash); expect(Poseidon2Hasher.hashToScalar(deepCopyFieldArray(fieldArray)).toString()).equals(algebraicData.expectedPoseidon2HashToScalar); expect(Poseidon2Hasher.hashToGroup(deepCopyFieldArray(fieldArray)).toString()).equals(algebraicData.expectedPoseidon2HashToGroup); - expect(Poseidon2Hasher.hashMany(deepCopyFieldArray(fieldArray), 2).map(field => field.toString())).deep.equals(algebraicData.expectedPoseidon2HashMany); + expect(Poseidon2Hasher.hashMany(deepCopyFieldArray(fieldArray), 2).map((field: Field) => field.toString())).deep.equals(algebraicData.expectedPoseidon2HashMany); // Run all possible operations for Poseidon4. expect(Poseidon4Hasher.hash(deepCopyFieldArray(fieldArray)).toString()).equals(algebraicData.expectedPoseidon4Hash); expect(Poseidon4Hasher.hashToScalar(deepCopyFieldArray(fieldArray)).toString()).equals(algebraicData.expectedPoseidon4HashToScalar); expect(Poseidon4Hasher.hashToGroup(deepCopyFieldArray(fieldArray)).toString()).equals(algebraicData.expectedPoseidon4HashToGroup); - expect(Poseidon4Hasher.hashMany(deepCopyFieldArray(fieldArray), 2).map(field => field.toString())).deep.equals(algebraicData.expectedPoseidon4HashMany); + expect(Poseidon4Hasher.hashMany(deepCopyFieldArray(fieldArray), 2).map((field: Field) => field.toString())).deep.equals(algebraicData.expectedPoseidon4HashMany); // Run all possible operations for Poseidon8. expect(Poseidon8Hasher.hash(deepCopyFieldArray(fieldArray)).toString()).equals(algebraicData.expectedPoseidon8Hash); expect(Poseidon8Hasher.hashToScalar(deepCopyFieldArray(fieldArray)).toString()).equals(algebraicData.expectedPoseidon8HashToScalar); expect(Poseidon8Hasher.hashToGroup(deepCopyFieldArray(fieldArray)).toString()).equals(algebraicData.expectedPoseidon8HashToGroup); - expect(Poseidon8Hasher.hashMany(deepCopyFieldArray(fieldArray), 2).map(field => field.toString())).deep.equals(algebraicData.expectedPoseidon8HashMany); + expect(Poseidon8Hasher.hashMany(deepCopyFieldArray(fieldArray), 2).map((field: Field) => field.toString())).deep.equals(algebraicData.expectedPoseidon8HashMany); }); }); }); diff --git a/sdk/tests/external-signing.test.ts b/sdk/tests/external-signing.test.ts index 95243684f..5fbd057f2 100644 --- a/sdk/tests/external-signing.test.ts +++ b/sdk/tests/external-signing.test.ts @@ -146,12 +146,12 @@ function fromPreprocessedInputs( /** * Computes the nonce of a static output record minted at `outputIndex` by a request whose transition * view key is `tvk`, exactly as the `cast` instruction does on-chain: - * `nonce = g * HashToScalar([tvk, output_index])`. + * `nonce = HashToScalar([tvk, output_index]) * G`. */ function computeMintedNonce(tvk: Field, outputIndex: number): Group { const index = Field.fromString(`${outputIndex}field`); const randomizer = new Poseidon2().hashToScalar([tvk, index]); - return Group.generator().scalarMultiply(randomizer); + return Group.gScalarMultiply(randomizer); } /** @@ -1018,7 +1018,7 @@ describe('External Signing ExecutionRequest integration', () => { // ----------------------------------------------------------------------------------------- // Setup: deploy ldgbatcher_p28.aleo and mint three credits.aleo records - + // // 1. "Deploy" ldgbatcher_p28.aleo (which imports credits.aleo) by registering its // source in the imports builder. credits.aleo is always available in the process, // so addProgram does not need to be called for it. @@ -1228,8 +1228,7 @@ describe('External Signing ExecutionRequest integration', () => { } // ----------------------------------------------------------------------------------------- - // Checks - // The flow is now complete. We verify each reconstructed request. + // Checks: The flow is now complete. We perform some simple sanity checks. // Every request in the authorization was reconstructed. expect(authPopulatedRequests.length).to.equal(authSignedRequests.length); @@ -1261,5 +1260,22 @@ describe('External Signing ExecutionRequest integration', () => { rebuilt.verify(inputTypes, isRoot, checksum != null ? Field.fromString(checksum) : undefined), ).to.equal(true); } + + // Build the on-chain Authorization from the externally-signed requests. In some settings, + // this will be done by the recipient before proving and is therefore not necessary here. + const authorization = await (ProgramManagerBase as any).buildAuthorizationFromExecutionRequests( + authPopulatedRequests, + LDGBATCHER_P28_PROGRAM, + EDITION, + undefined, // legacy imports object + imports.clone(), // clone: the builder is consumed (moved) by value + ); + + // The authorization has one transition per request, targets the root function, and yields a + // derivable execution id. + expect(authorization.len()).to.equal(expectedTargets.length); + expect(Array.from(authorization.transitions() as ArrayLike).length).to.equal(expectedTargets.length); + expect(authorization.functionName()).to.equal(functionName); + expect(authorization.toExecutionId().toString().length).to.be.greaterThan(0); }); }); diff --git a/wasm/src/programs/manager/authorize.rs b/wasm/src/programs/manager/authorize.rs index fdbb289ff..cd44bce09 100644 --- a/wasm/src/programs/manager/authorize.rs +++ b/wasm/src/programs/manager/authorize.rs @@ -43,7 +43,7 @@ use crate::{ use js_sys::{Array, Object}; use std::str::FromStr; -use wasm_bindgen::JsValue; +use wasm_bindgen::{JsValue, convert::TryFromJsValue}; #[wasm_bindgen] impl ProgramManager { @@ -211,6 +211,60 @@ impl ProgramManager { Ok(Authorization::from(authorization)) } + /// Build an `Authorization` from a full set of already-signed `Request`s. + /// + /// This wraps snarkVM's `Stack::authorize_requests`: it re-traverses the call graph of the root + /// function, consuming the supplied requests (one per transition, in call order) to populate the + /// authorization. The first request must target `program`. Unlike + /// {@link buildAuthorizationFromExecutionRequest}, no private key is needed because the requests + /// are already signed. + /// + /// @param {ExecutionRequest[]} requests The signed requests, in call order (root first). + /// @param {string} program The program source code containing the root function. + /// @param {number | undefined} edition The edition of the program. + /// @param {object | undefined} imports The imports to the program in the format {"programname.aleo":"aleo instructions source code"}. + /// @param {ProgramImports | undefined} program_imports Pre-loaded imports builder. + /// @returns {Authorization} + #[wasm_bindgen(js_name = buildAuthorizationFromExecutionRequests)] + pub async fn authorize_requests( + requests: Array, + program: &str, + edition: Option, + imports: Option, + program_imports: Option, + ) -> Result { + let edition = edition.unwrap_or(1); + + let mut resolved = ResolvedProcess::resolve(&program_imports, program, edition, imports)?; + let program_native = resolved.program().clone(); + let process = resolved.process_mut(); + + // Convert the JS array of `ExecutionRequest`s into native requests, in order. + let mut requests_native = Vec::with_capacity(requests.length() as usize); + for value in requests.iter() { + let request = ExecutionRequest::try_from_js_value(value) + .map_err(|_| "`requests` must be an array of ExecutionRequest".to_string())?; + requests_native.push(RequestNative::from(&request)); + } + if requests_native.is_empty() { + return Err("`requests` must contain at least one request".to_string()); + } + + // Add the top-level program to the process (no-op if it is already present). + if program_native.id().to_string() != "credits.aleo" && !process.contains_program(program_native.id()) { + log("Adding program to the process"); + process.lock().add_program_with_edition(&program_native, edition).map_err(|e| e.to_string())?; + } + + // Get the stack for the top-level program and build the authorization. + let stack = process.get_stack(program_native.id()).map_err(|e| e.to_string())?; + let rng = &mut rand::rng(); + let authorization = + stack.authorize_requests::(requests_native, rng).map_err(|e| e.to_string())?; + + Ok(Authorization::from(authorization)) + } + /// Produce a mocked `Authorization` for a program function call without needing a private key. /// /// The resulting Authorization has the same structure as a real one — the call graph is fully diff --git a/wasm/src/types/group.rs b/wasm/src/types/group.rs index e5f524954..17ef356ab 100644 --- a/wasm/src/types/group.rs +++ b/wasm/src/types/group.rs @@ -27,6 +27,7 @@ use crate::{ Boolean, integer::{I8, I16, I32, I64, I128, U8, U16, U32, U64, U128}, native::{ + CurrentNetwork, GroupNative, I8Native, I16Native, @@ -44,6 +45,7 @@ use crate::{ }, }, }; +use snarkvm_console::network::Network; use snarkvm_console::prelude::{ Double, FromBits, @@ -198,6 +200,14 @@ impl Group { Group::from(GroupNative::generator()) } + /// Returns `scalar * G`, where `G` is the distinguished point on the Aleo protocol curve used + /// for account derivation, i. This corresponds to `Network::g_scalar_multiply`. Note `G` is + /// different from {@link generator}, which returns the a different generator. + #[wasm_bindgen(js_name = gScalarMultiply)] + pub fn g_scalar_multiply(scalar: &Scalar) -> Group { + Group::from(CurrentNetwork::g_scalar_multiply(&**scalar)) + } + // ── cast conversions ─────────────────────────────────────────────── /// Cast the group element to a Field (returns x-coordinate). From 89f72156d655756edfdb84aba7caa48220d01827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Mej=C3=ADas=20Gil?= Date: Tue, 23 Jun 2026 11:21:13 +0200 Subject: [PATCH 05/12] polishing --- sdk/src/browser.ts | 1 + sdk/src/external-signing.ts | 25 ++++ sdk/tests/external-signing.test.ts | 186 +++++++++---------------- wasm/Cargo.lock | 11 +- wasm/src/programs/manager/authorize.rs | 2 +- wasm/src/programs/request.rs | 140 ++++++++++++++++++- 6 files changed, 235 insertions(+), 130 deletions(-) diff --git a/sdk/src/browser.ts b/sdk/src/browser.ts index 857036664..2190b2fbe 100644 --- a/sdk/src/browser.ts +++ b/sdk/src/browser.ts @@ -335,6 +335,7 @@ export { // Builder buildExecutionRequestFromExternallySignedData, computeExternalSigningInputs, + computeMintedNonce, } from "./external-signing.js"; export type { FieldLike, diff --git a/sdk/src/external-signing.ts b/sdk/src/external-signing.ts index 966a39df8..7b07b380e 100644 --- a/sdk/src/external-signing.ts +++ b/sdk/src/external-signing.ts @@ -3,6 +3,7 @@ import { ExecutionRequest, Field, Group, + Poseidon2, } from "./wasm.js"; import { logAndThrow } from "./utils/utils.js"; import { @@ -19,6 +20,7 @@ import type { ExternalSigningInput, ExternalSigningOptions, ExecutionRequestParams, + FieldLike, InputStrategy, OutputFormat, RequestSignInput, @@ -198,6 +200,29 @@ export async function computeExternalSigningInputs( } } +// --------------------------------------------------------------------------- +// computeMintedNonce +// --------------------------------------------------------------------------- + +/** + * Computes the nonce of a static output record minted at `outputIndex` by a request whose transition + * view key is `tvk`, exactly as the `cast` instruction does on-chain: + * `nonce = HashToScalar([tvk, output_index]) * G`. + * + * This is useful when building multi-request flows where a record minted by one request is consumed + * by a later request: the consumer must reference the minted record's nonce, which is derived from + * the producing request's transition view key and the record's output index. + * + * @param {FieldLike} tvk - The transition view key of the request that mints the record. + * @param {number} outputIndex - The output index at which the record is minted. + * @returns {Group} The minted record nonce. + */ +export function computeMintedNonce(tvk: FieldLike, outputIndex: number): Group { + const index = Field.fromString(`${outputIndex}field`); + const randomizer = new Poseidon2().hashToScalar([toField(tvk), index]); + return Group.gScalarMultiply(randomizer); +} + // --------------------------------------------------------------------------- // Bytes conversion helpers // --------------------------------------------------------------------------- diff --git a/sdk/tests/external-signing.test.ts b/sdk/tests/external-signing.test.ts index 5fbd057f2..4cea37534 100644 --- a/sdk/tests/external-signing.test.ts +++ b/sdk/tests/external-signing.test.ts @@ -6,9 +6,6 @@ import { Field, Group, Plaintext, - Poseidon2, - Poseidon4, - Poseidon8, PrivateKey, Program, ProgramImportsBuilder, @@ -24,13 +21,13 @@ import { toSignature, toAddress, buildExecutionRequestFromExternallySignedData, + computeMintedNonce, } from "../src/node.js"; import type { FieldLike, SignatureLike, AddressLike, InputStrategy, - ExternalSigningInput, } from "../src/node.js"; import { isViewKeyStrategy, @@ -47,113 +44,6 @@ import { MULTIPLY_PROGRAM, DOUBLE_PROGRAM, LDGBATCHER_P28_PROGRAM } from "./data const message = Uint8Array.from([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]); -/** - * Output of the signer-side {@link fromPreprocessedInputs}: the freshly-sampled transition view key, - * the derived transition commitment, the request signature, and the gammas of the static-record - * inputs (one per record input, in input order). - */ -interface PreprocessedSigningResult { - tvk: Field; - tcm: Field; - signature: Signature; - gammas: Group[]; -} - -/** - * Produces a request signature from preprocessed inputs, mirroring the standard request - * signing algorithm. The signer holds the private key; the preprocessed inputs (produced by the - * authorizer via {@link computeExternalSigningInputs}) contain the to-fields representation of each - * input except for static records, where they contain the (`H`, `tag`) tuple. - * - * The function: - * 1. Samples a fresh transition view key `tvk` and derives `tcm = Hash(tvk)`. - * 2. Builds the message payload corresponding to each non-static-record input. - * 3. Calls `ExecutionRequest.signRequestFromPreprocessedInputs`, which samples the - * transition secret `r`, computes `r * H` and `gamma = sk_sig * H` for every record, assembles - * the message, and signs it. - * - * @returns The sampled `tvk`, the `tcm`, the `signature`, and the per-record `gammas`. - */ -function fromPreprocessedInputs( - privateKey: PrivateKey, - programName: string, - functionName: string, - authExternalInputs: ExternalSigningInput<"string">, -): PreprocessedSigningResult { - - // Initialize the hashers. - const poseidon4 = new Poseidon4(); - const poseidon8 = new Poseidon8(); - - // The WASM bindings take `Field` arguments by value and consume (move) them: a `Field` instance - // cannot be reused across two WASM calls. We therefore keep the reused values as strings and mint - // a fresh `Field` for each use via `f(...)`. - const str_to_f = (s: string): Field => Field.fromString(s); - - // Sample the transition view key and derive the transition commitment `tcm = Hash(tvk)`. - const tvkStr = Field.random().toString(); - const tcmStr = new Poseidon2().hash([str_to_f(tvkStr)]).toString(); - const functionIdStr = authExternalInputs.functionId; - - // Build the per-input signing material. Records are passed as `[H, tag]` (the signer derives - // `r * H` and `gamma`); every other input is passed as its precomputed input ID field, which is - // derived from the freshly-sampled `tvk` exactly as the standard signing algorithm does. - const inputIds = authExternalInputs.requestInputs.map((input): Field | [Field, Field] => { - const index = input.index; - const data = (): Field[] => input.data.map(str_to_f); - switch (input.signingInputType) { - case "record": - return [str_to_f(input.h!), str_to_f(input.tag!)]; - case "constant": - case "public": - // `Hash(function_id || data || tcm || index)`. - return poseidon8.hash([str_to_f(functionIdStr), ...data(), str_to_f(tcmStr), str_to_f(index)]); - case "private": { - // Encrypt with input view key `Hash(function_id || tvk || index)`, hash the ciphertext. - const inputViewKey = poseidon4.hash([str_to_f(functionIdStr), str_to_f(tvkStr), str_to_f(index)]); - const ciphertext = Plaintext.fromFields(data()).encryptSymmetric(inputViewKey); - return poseidon8.hash(Array.from(ciphertext.toFields() as ArrayLike)); - } - case "external_record": - case "dynamic_record": - // `Hash(function_id || data || tvk || index)`. - return poseidon8.hash([str_to_f(functionIdStr), ...data(), str_to_f(tvkStr), str_to_f(index)]); - default: - throw new Error(`Unsupported signing input type: ${input.signingInputType}`); - } - }); - - const checksum = authExternalInputs.checksum != null ? str_to_f(authExternalInputs.checksum) : undefined; - - const signed: any = (ExecutionRequest as any).signRequestFromPreprocessedInputs( - privateKey, - programName, - functionName, - str_to_f(tvkStr), - authExternalInputs.isRoot, - checksum, - inputIds, - ); - - return { - tvk: signed.tvk as Field, - tcm: signed.tcm as Field, - signature: signed.signature as Signature, - gammas: Array.from(signed.gammas as ArrayLike), - }; -} - -/** - * Computes the nonce of a static output record minted at `outputIndex` by a request whose transition - * view key is `tvk`, exactly as the `cast` instruction does on-chain: - * `nonce = HashToScalar([tvk, output_index]) * G`. - */ -function computeMintedNonce(tvk: Field, outputIndex: number): Group { - const index = Field.fromString(`${outputIndex}field`); - const randomizer = new Poseidon2().hashToScalar([tvk, index]); - return Group.gScalarMultiply(randomizer); -} - /** * Writes a freshly-minted record nonce into the consumer input that receives it: replaces the * `_nonce` of the record at `inputsByRequest[consumerRequestIndex][inputIndex]`. @@ -195,7 +85,6 @@ function valueTypeFromInputDef(def: any): string { } } -// TODO (Antonio) duplicated from external-signing.test.ts describe('External Signing Utilities', () => { const privateKey = PrivateKey.from_string(privateKeyString); const viewKey = privateKey.to_view_key(); @@ -859,6 +748,67 @@ describe('External Signing ExecutionRequest integration', () => { expect(roundTrippedPR.requests().length).to.equal(2); }); + it('Should match ExecutionRequest.sign for transfer_private', async () => { + // Use beacon private key - record is owned by beacon address + const privateKey = PrivateKey.from_string(beaconPrivateKeyString); + const beaconRecord = RecordPlaintext.fromString(recordBeaconOwned); + const gamma = beaconRecord.gamma("credits.aleo", "credits", privateKey); + const beaconViewKey = ViewKey.from_private_key(PrivateKey.from_string(beaconPrivateKeyString)); + const externalSigningInputs = await computeExternalSigningInputs({ + programName: "credits.aleo", + functionName: "transfer_private", + inputs: transferPrivateInputs, + inputTypes: transferPrivateInputTypes, + isRoot: true, + checksum: null, + viewKey: beaconViewKey, + }); + expect(externalSigningInputs.signer).to.be.a("string"); + expect(externalSigningInputs.skTag).to.be.a("string"); + + // Verify per-input recordViewKey is set on the record input + expect(externalSigningInputs.requestInputs[0].recordViewKey).to.be.a("string"); + expect(externalSigningInputs.requestInputs[0].recordViewKey).to.match(/field$/); + + // Verify non-record inputs don't have recordViewKey + expect(externalSigningInputs.requestInputs[1].recordViewKey).to.be.undefined; + expect(externalSigningInputs.requestInputs[2].recordViewKey).to.be.undefined; + const signedRequest = ExecutionRequest.sign( + privateKey, + "credits.aleo", + "transfer_private", + transferPrivateInputs, + transferPrivateInputTypes, + undefined, + undefined, + true, + ); + expect(signedRequest.program_id()).to.equal("credits.aleo"); + expect(signedRequest.function_name()).to.equal("transfer_private"); + expect(signedRequest.inputs().length).to.equal(3); + expect(externalSigningInputs.requestInputs.length).to.equal(signedRequest.inputs().length); + + const sig = signedRequest.signature(); + const tvk = signedRequest.tvk(); + // Use pre-computed input_ids via buildExecutionRequestFromExternallySignedDataWithInputIds + const externallySignedRequest = ExecutionRequest.fromExternallySignedDataWithInputIds( + "credits.aleo", + "transfer_private", + transferPrivateInputs, + transferPrivateInputTypes, + sig, + tvk, + signedRequest.signer(), + signedRequest.sk_tag(), + signedRequest.input_ids(), + ); + expect(externallySignedRequest.program_id()).to.equal(signedRequest.program_id()); + expect(externallySignedRequest.function_name()).to.equal(signedRequest.function_name()); + expect(externallySignedRequest.inputs().length).to.equal(signedRequest.inputs().length); + expect(externallySignedRequest.input_ids().length).to.equal(signedRequest.input_ids().length); + expect(externallySignedRequest.toString()).to.equal(signedRequest.toString()); + }); + it('Should not include recordViewKey on inputs when no viewKey is provided', async () => { const externalSigningInputs = await computeExternalSigningInputs({ programName: "credits.aleo", @@ -999,7 +949,6 @@ describe('External Signing ExecutionRequest integration', () => { assertFieldsRoundTripThroughPlaintext(thirdInput.data, transferPrivateInputs[2]); }); - it('Should match ExecutionRequest.sign for arbitrary flow with multiple requests', async () => { // Deploy scenario: ldgbatcher_p28.aleo batches three minted credits.aleo records into a // single private transfer via transfer_private_3 @@ -1179,7 +1128,7 @@ describe('External Signing ExecutionRequest integration', () => { // derives `r * H` and gamma for the record inputs, constructs the message payload and // signs it. A fresh private key is used because the WASM consumes (moves) the private // key passed by value. - const signed = fromPreprocessedInputs( + const signed: any = (ExecutionRequest as any).fromPreprocessedInputs( PrivateKey.from_string(sigPrivateKeyStr), programName, requestFunctionName, @@ -1187,6 +1136,7 @@ describe('External Signing ExecutionRequest integration', () => { ); const tvkStr = (signed.tvk as Field).toString(); + const gammas = Array.from(signed.gammas as ArrayLike); if (isRoot) { rootTvkStr = tvkStr; @@ -1206,7 +1156,7 @@ describe('External Signing ExecutionRequest integration', () => { Address.from_string(addressStr), Field.fromString(authExternalInputs.skTag!), ViewKey.from_string(authViewKeyStr), - signed.gammas, + gammas, Field.fromString(rootTvkStr), ); @@ -1263,12 +1213,12 @@ describe('External Signing ExecutionRequest integration', () => { // Build the on-chain Authorization from the externally-signed requests. In some settings, // this will be done by the recipient before proving and is therefore not necessary here. - const authorization = await (ProgramManagerBase as any).buildAuthorizationFromExecutionRequests( + const authorization = await (ProgramManagerBase as any).authorizeRequests( authPopulatedRequests, LDGBATCHER_P28_PROGRAM, EDITION, - undefined, // legacy imports object - imports.clone(), // clone: the builder is consumed (moved) by value + undefined, + imports.clone(), // the builder is consumed ); // The authorization has one transition per request, targets the root function, and yields a diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index a1940cac2..50ad5189d 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -383,7 +383,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -4137,15 +4137,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" diff --git a/wasm/src/programs/manager/authorize.rs b/wasm/src/programs/manager/authorize.rs index cd44bce09..b31ae7086 100644 --- a/wasm/src/programs/manager/authorize.rs +++ b/wasm/src/programs/manager/authorize.rs @@ -225,7 +225,7 @@ impl ProgramManager { /// @param {object | undefined} imports The imports to the program in the format {"programname.aleo":"aleo instructions source code"}. /// @param {ProgramImports | undefined} program_imports Pre-loaded imports builder. /// @returns {Authorization} - #[wasm_bindgen(js_name = buildAuthorizationFromExecutionRequests)] + #[wasm_bindgen(js_name = authorizeRequests)] pub async fn authorize_requests( requests: Array, program: &str, diff --git a/wasm/src/programs/request.rs b/wasm/src/programs/request.rs index f850eb9d8..50c770038 100644 --- a/wasm/src/programs/request.rs +++ b/wasm/src/programs/request.rs @@ -31,6 +31,7 @@ use crate::{ FieldNative, IdentifierNative, InputIDNative, + PlaintextNative, ProgramIDNative, RecordPlaintextNative, RequestNative, @@ -42,7 +43,7 @@ use crate::{ }; use snarkvm_console::{ network::Network, - prelude::{One, ToField, ToFields, Uniform, Zero}, + prelude::{FromFields, One, ToField, ToFields, Uniform, Zero}, program::compute_function_id, }; use snarkvm_wasm::utilities::{FromBytes, ToBytes}; @@ -439,6 +440,143 @@ impl ExecutionRequest { Ok(result) } + /// Produces a request signature directly from the preprocessed inputs produced by + /// {@link computeExternalSigningInputs}. + /// + /// This is the one-call equivalent of: sampling a fresh transition view key `tvk`, deriving + /// `tcm = Hash(tvk)`, building each input's signing material exactly as the standard signing + /// algorithm does, and invoking {@link signRequestFromPreprocessedInputs}. Records are kept as + /// their `[H, tag]` pair so the secret-dependent pieces (`gamma`, `h_r`) are computed during + /// signing; every other input is reduced to its precomputed input ID field: + /// - constant / public: `Hash(function_id || data || tcm || index)` + /// - private: encrypt `data` with input view key `Hash(function_id || tvk || index)` and hash + /// the ciphertext fields + /// - external_record / dynamic_record: `Hash(function_id || data || tvk || index)` + /// + /// @param {PrivateKey} private_key The signer's private key. + /// @param {string} program_id The id of the program. + /// @param {string} function_name The function name. + /// @param {ExternalSigningInput} external_inputs The preprocessed inputs from `computeExternalSigningInputs`. + /// @returns {{ tvk: Field, tcm: Field, signature: Signature, gammas: Group[] }} + #[wasm_bindgen(js_name = "fromPreprocessedInputs")] + pub fn from_preprocessed_inputs( + private_key: PrivateKey, + program_id: String, + function_name: String, + external_inputs: Object, + ) -> Result { + // Read the top-level fields of the preprocessed-inputs object. + let get_field = |target: &JsValue, key: &str| -> Result { + Reflect::get(target, &JsValue::from_str(key)).map_err(|_| format!("external_inputs.{key} is missing")) + }; + let function_id_str = get_field(&external_inputs, "functionId")? + .as_string() + .ok_or_else(|| "external_inputs.functionId must be a string".to_string())?; + let function_id = FieldNative::from_str(&function_id_str).map_err(|e| e.to_string())?; + let is_root = get_field(&external_inputs, "isRoot")? + .as_bool() + .ok_or_else(|| "external_inputs.isRoot must be a boolean".to_string())?; + let checksum = match get_field(&external_inputs, "checksum")? { + value if value.is_undefined() || value.is_null() => None, + value => { + let s = value.as_string().ok_or_else(|| "external_inputs.checksum must be a string".to_string())?; + Some(Field::from(FieldNative::from_str(&s).map_err(|e| e.to_string())?)) + } + }; + let request_inputs = Array::from(&get_field(&external_inputs, "requestInputs")?); + + // Sample the transition view key and derive the transition commitment `tcm = Hash(tvk)`. + let mut rng = rand::rng(); + let tvk = FieldNative::rand(&mut rng); + let tcm = CurrentNetwork::hash_psd2(&[tvk]).map_err(|e| e.to_string())?; + + // Build the per-input signing material in input order. + let input_ids = Array::new(); + for (i, input_js) in request_inputs.iter().enumerate() { + let signing_type = Reflect::get(&input_js, &JsValue::from_str("signingInputType")) + .ok() + .and_then(|v| v.as_string()) + .ok_or_else(|| format!("requestInputs[{i}].signingInputType must be a string"))?; + let index_str = Reflect::get(&input_js, &JsValue::from_str("index")) + .ok() + .and_then(|v| v.as_string()) + .ok_or_else(|| format!("requestInputs[{i}].index must be a string"))?; + let index = FieldNative::from_str(&index_str).map_err(|e| e.to_string())?; + + // Parse the `data` field array (strings -> Field), used by every non-record input. + let parse_data = || -> Result, String> { + let data = Array::from( + &Reflect::get(&input_js, &JsValue::from_str("data")) + .map_err(|_| format!("requestInputs[{i}].data is missing"))?, + ); + data.iter() + .enumerate() + .map(|(j, d)| { + let s = d.as_string().ok_or_else(|| format!("requestInputs[{i}].data[{j}] must be a string"))?; + FieldNative::from_str(&s).map_err(|e| e.to_string()) + }) + .collect() + }; + + // Reads a required string property of the current input. + let get_str = |key: &str| -> Result { + let s = Reflect::get(&input_js, &JsValue::from_str(key)) + .ok() + .and_then(|v| v.as_string()) + .ok_or_else(|| format!("requestInputs[{i}].{key} must be a string for a record input"))?; + FieldNative::from_str(&s).map_err(|e| e.to_string()) + }; + + match signing_type.as_str() { + "record" => { + // Pass `[H, tag]`; the signer computes `gamma` and `h_r` from its secret key. + let pair = Array::new(); + pair.push(&JsValue::from(Field::from(get_str("h")?))); + pair.push(&JsValue::from(Field::from(get_str("tag")?))); + input_ids.push(&pair); + } + "constant" | "public" => { + // `Hash(function_id || data || tcm || index)`. + let mut preimage = vec![function_id]; + preimage.extend(parse_data()?); + preimage.extend([tcm, index]); + let id = CurrentNetwork::hash_psd8(&preimage).map_err(|e| e.to_string())?; + input_ids.push(&JsValue::from(Field::from(id))); + } + "private" => { + // Encrypt with input view key `Hash(function_id || tvk || index)`, hash the ciphertext. + let input_view_key = + CurrentNetwork::hash_psd4(&[function_id, tvk, index]).map_err(|e| e.to_string())?; + let plaintext = PlaintextNative::from_fields(&parse_data()?).map_err(|e| e.to_string())?; + let ciphertext = plaintext.encrypt_symmetric(input_view_key).map_err(|e| e.to_string())?; + let id = CurrentNetwork::hash_psd8(&ciphertext.to_fields().map_err(|e| e.to_string())?) + .map_err(|e| e.to_string())?; + input_ids.push(&JsValue::from(Field::from(id))); + } + "external_record" | "dynamic_record" => { + // `Hash(function_id || data || tvk || index)`. + let mut preimage = vec![function_id]; + preimage.extend(parse_data()?); + preimage.extend([tvk, index]); + let id = CurrentNetwork::hash_psd8(&preimage).map_err(|e| e.to_string())?; + input_ids.push(&JsValue::from(Field::from(id))); + } + other => return Err(format!("Unsupported signing input type: {other}")), + } + } + + // Delegate to the signing routine, which samples `r`, finalizes records, and signs. + Self::sign_request_from_preprocessed_inputs( + private_key, + program_id, + function_name, + Field::from(tvk), + is_root, + checksum, + input_ids, + ) + } + /// Builds a request from externally-signed data using externally-supplied record view keys. /// /// For record inputs, the caller provides `record_view_keys` and `gammas` directly. From f2892ad430dcbc3fb0afe0964d08784f20fdb9a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Mej=C3=ADas=20Gil?= Date: Tue, 23 Jun 2026 13:59:32 +0200 Subject: [PATCH 06/12] Potential fix for pull request finding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Antonio Mejías Gil --- wasm/src/types/group.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wasm/src/types/group.rs b/wasm/src/types/group.rs index 17ef356ab..8fd5a291f 100644 --- a/wasm/src/types/group.rs +++ b/wasm/src/types/group.rs @@ -201,8 +201,8 @@ impl Group { } /// Returns `scalar * G`, where `G` is the distinguished point on the Aleo protocol curve used - /// for account derivation, i. This corresponds to `Network::g_scalar_multiply`. Note `G` is - /// different from {@link generator}, which returns the a different generator. + /// for account derivation. This corresponds to `Network::g_scalar_multiply`. + /// Note: `G` is different from {@link generator}, which returns a different generator. #[wasm_bindgen(js_name = gScalarMultiply)] pub fn g_scalar_multiply(scalar: &Scalar) -> Group { Group::from(CurrentNetwork::g_scalar_multiply(&**scalar)) From 36906602c77be1987be3c9dd92594d4c42857b15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Mej=C3=ADas=20Gil?= Date: Tue, 23 Jun 2026 14:00:13 +0200 Subject: [PATCH 07/12] number validation for error clarity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Antonio Mejías Gil --- sdk/src/external-signing.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sdk/src/external-signing.ts b/sdk/src/external-signing.ts index 7b07b380e..dab06883a 100644 --- a/sdk/src/external-signing.ts +++ b/sdk/src/external-signing.ts @@ -218,6 +218,9 @@ export async function computeExternalSigningInputs( * @returns {Group} The minted record nonce. */ export function computeMintedNonce(tvk: FieldLike, outputIndex: number): Group { + if (!Number.isSafeInteger(outputIndex) || outputIndex < 0) { + throw new Error(`computeMintedNonce: outputIndex must be a non-negative safe integer. Received: ${outputIndex}`); + } const index = Field.fromString(`${outputIndex}field`); const randomizer = new Poseidon2().hashToScalar([toField(tvk), index]); return Group.gScalarMultiply(randomizer); From 34f670415f03d6815381fcd0bb0ad3151845e03d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Mej=C3=ADas=20Gil?= Date: Tue, 23 Jun 2026 14:01:25 +0200 Subject: [PATCH 08/12] made isRoot accepted in more representations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Antonio Mejías Gil --- wasm/src/programs/request.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/wasm/src/programs/request.rs b/wasm/src/programs/request.rs index 50c770038..86e31b489 100644 --- a/wasm/src/programs/request.rs +++ b/wasm/src/programs/request.rs @@ -473,9 +473,14 @@ impl ExecutionRequest { .as_string() .ok_or_else(|| "external_inputs.functionId must be a string".to_string())?; let function_id = FieldNative::from_str(&function_id_str).map_err(|e| e.to_string())?; - let is_root = get_field(&external_inputs, "isRoot")? - .as_bool() - .ok_or_else(|| "external_inputs.isRoot must be a boolean".to_string())?; + let is_root_value = get_field(&external_inputs, "isRoot")?; + let is_root = if let Some(b) = is_root_value.as_bool() { + b + } else if let Some(s) = is_root_value.as_string() { + FieldNative::from_str(&s).map_err(|e| e.to_string())? != FieldNative::zero() + } else { + return Err("external_inputs.isRoot must be a boolean or field string ('0field'/'1field')".to_string()); + }; let checksum = match get_field(&external_inputs, "checksum")? { value if value.is_undefined() || value.is_null() => None, value => { From 1bcc7d2dda01b3c9ec08a237836ef24d847d9560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Mej=C3=ADas=20Gil?= Date: Tue, 23 Jun 2026 15:01:18 +0200 Subject: [PATCH 09/12] made functionId and function_id both valid --- wasm/src/programs/request.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/wasm/src/programs/request.rs b/wasm/src/programs/request.rs index 86e31b489..3f6c70fb6 100644 --- a/wasm/src/programs/request.rs +++ b/wasm/src/programs/request.rs @@ -469,9 +469,8 @@ impl ExecutionRequest { let get_field = |target: &JsValue, key: &str| -> Result { Reflect::get(target, &JsValue::from_str(key)).map_err(|_| format!("external_inputs.{key} is missing")) }; - let function_id_str = get_field(&external_inputs, "functionId")? - .as_string() - .ok_or_else(|| "external_inputs.functionId must be a string".to_string())?; + let function_id_str = get_field(&external_inputs, "functionId").or_else(|_| get_field(&external_inputs, "function_id"))? + .as_string().ok_or_else(|| "external_inputs.functionId (or function_id) must be a string".to_string())?; let function_id = FieldNative::from_str(&function_id_str).map_err(|e| e.to_string())?; let is_root_value = get_field(&external_inputs, "isRoot")?; let is_root = if let Some(b) = is_root_value.as_bool() { From 81266c71979f29a1fed875655585aefa95c45b30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Mej=C3=ADas=20Gil?= Date: Wed, 24 Jun 2026 17:13:05 +0200 Subject: [PATCH 10/12] changed to new snarkVM function name, removed local patch and pointed to snarkVM rev --- sdk/tests/external-signing.test.ts | 13 +++--- wasm/Cargo.lock | 55 ++++++++++++++++++++++++++ wasm/Cargo.toml | 33 +++++----------- wasm/src/programs/manager/authorize.rs | 12 +++--- 4 files changed, 78 insertions(+), 35 deletions(-) diff --git a/sdk/tests/external-signing.test.ts b/sdk/tests/external-signing.test.ts index 4cea37534..ddd9f8ced 100644 --- a/sdk/tests/external-signing.test.ts +++ b/sdk/tests/external-signing.test.ts @@ -996,8 +996,8 @@ describe('External Signing ExecutionRequest integration', () => { // Auxiliary function to resolve the program given its name: imported programs (e.g. the // root ldgbatcher_p28.aleo) live in the imports builder, whereas credits.aleo is built into // the process and not registered as an import. This is only used for resolving input types - // and can be done in different ways (for instance, sample_authorization_extended could - // return this information directly instead). + // and can be done in different ways (for instance, + // sample_authorization_with_record_tracking could return this information directly instead). const programForRequest = (programName: string): Program => { const source = imports.getProgram(programName); if (source != null) { @@ -1010,8 +1010,8 @@ describe('External Signing ExecutionRequest integration', () => { }; // ----------------------------------------------------------------------------------------- - // Part 1 [Authorizer]: Call sampleAuthorizationExtended to produce the mock authorization - // and auxiliary data + // Part 1 [Authorizer]: Call sampleAuthorizationWithRecordTracking to produce the mock + // authorization and auxiliary data // Prepare the root-call target and inputs const recipientPrivateKey = new PrivateKey(); @@ -1029,7 +1029,7 @@ describe('External Signing ExecutionRequest integration', () => { // ldgbatcher_p28.aleo declares `constructor: assert.eq edition 0u16;`. const EDITION = 0; - const extended: any = await (ProgramManagerBase as any).sampleAuthorizationExtended( + const extended: any = await (ProgramManagerBase as any).sampleAuthorizationWithRecordTracking( Address.from_string(addressStr), LDGBATCHER_P28_PROGRAM, functionName, @@ -1166,7 +1166,8 @@ describe('External Signing ExecutionRequest integration', () => { // Patch the nonces of static/external/dynamic records input to subsequent requests and // coming from static records minted by this request. The nonce needs to be recomputed // using the actual tvk returned by the signer. The record-tracking information returned - // by sampleAuthorizationExtended indicates which request inputs must be updated. + // by sampleAuthorizationWithRecordTracking indicates which request inputs must be + // updated. for (const entry of authRecordTracking) { if (entry.minterRequestIndex === requestIndex) { const nonce = computeMintedNonce(Field.fromString(tvkStr), entry.outputIndex); diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 50ad5189d..25e22b12d 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -2599,6 +2599,7 @@ dependencies = [ [[package]] name = "snarkvm-algorithms" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "aleo-std", "anyhow", @@ -2625,6 +2626,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "snarkvm-circuit-account", "snarkvm-circuit-algorithms", @@ -2638,6 +2640,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-account" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "snarkvm-circuit-network", "snarkvm-circuit-types", @@ -2647,6 +2650,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-algorithms" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "snarkvm-circuit-types", "snarkvm-console-algorithms", @@ -2656,6 +2660,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-collections" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "snarkvm-circuit-algorithms", "snarkvm-circuit-types", @@ -2665,6 +2670,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-environment" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "anyhow", "indexmap", @@ -2684,10 +2690,12 @@ dependencies = [ [[package]] name = "snarkvm-circuit-environment-witness" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" [[package]] name = "snarkvm-circuit-network" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "snarkvm-circuit-algorithms", "snarkvm-circuit-collections", @@ -2698,6 +2706,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-program" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "snarkvm-circuit-account", "snarkvm-circuit-algorithms", @@ -2711,6 +2720,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-address", @@ -2725,6 +2735,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-address" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2737,6 +2748,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-boolean" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "snarkvm-circuit-environment", "snarkvm-console-types-boolean", @@ -2745,6 +2757,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-field" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2754,6 +2767,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-group" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2765,6 +2779,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-integers" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2776,6 +2791,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-scalar" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2786,6 +2802,7 @@ dependencies = [ [[package]] name = "snarkvm-circuit-types-string" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "snarkvm-circuit-environment", "snarkvm-circuit-types-boolean", @@ -2797,6 +2814,7 @@ dependencies = [ [[package]] name = "snarkvm-console" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "snarkvm-console-account", "snarkvm-console-algorithms", @@ -2809,6 +2827,7 @@ dependencies = [ [[package]] name = "snarkvm-console-account" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "bs58", "snarkvm-console-network", @@ -2819,6 +2838,7 @@ dependencies = [ [[package]] name = "snarkvm-console-algorithms" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "blake2s_simd", "hex", @@ -2834,6 +2854,7 @@ dependencies = [ [[package]] name = "snarkvm-console-collections" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "aleo-std", "parking_lot", @@ -2846,6 +2867,7 @@ dependencies = [ [[package]] name = "snarkvm-console-network" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "anyhow", "enum-iterator", @@ -2865,6 +2887,7 @@ dependencies = [ [[package]] name = "snarkvm-console-network-environment" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "anyhow", "bech32", @@ -2882,6 +2905,7 @@ dependencies = [ [[package]] name = "snarkvm-console-program" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "enum-iterator", "enum_index", @@ -2902,6 +2926,7 @@ dependencies = [ [[package]] name = "snarkvm-console-types" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-address", @@ -2916,6 +2941,7 @@ dependencies = [ [[package]] name = "snarkvm-console-types-address" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2926,6 +2952,7 @@ dependencies = [ [[package]] name = "snarkvm-console-types-boolean" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "snarkvm-console-network-environment", ] @@ -2933,6 +2960,7 @@ dependencies = [ [[package]] name = "snarkvm-console-types-field" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2942,6 +2970,7 @@ dependencies = [ [[package]] name = "snarkvm-console-types-group" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2952,6 +2981,7 @@ dependencies = [ [[package]] name = "snarkvm-console-types-integers" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2962,6 +2992,7 @@ dependencies = [ [[package]] name = "snarkvm-console-types-scalar" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2972,6 +3003,7 @@ dependencies = [ [[package]] name = "snarkvm-console-types-string" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "snarkvm-console-network-environment", "snarkvm-console-types-boolean", @@ -2982,6 +3014,7 @@ dependencies = [ [[package]] name = "snarkvm-curves" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "rand 0.10.1", "rustc_version", @@ -2994,6 +3027,7 @@ dependencies = [ [[package]] name = "snarkvm-fields" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "aleo-std", "anyhow", @@ -3010,6 +3044,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-authority" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "anyhow", "rand 0.10.1", @@ -3021,6 +3056,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-block" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "anyhow", "indexmap", @@ -3044,6 +3080,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-committee" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "indexmap", "rayon", @@ -3055,6 +3092,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-batch-certificate" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "indexmap", "rayon", @@ -3067,6 +3105,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-batch-header" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "indexmap", "rayon", @@ -3078,6 +3117,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-data" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "bytes", "serde_json", @@ -3087,6 +3127,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-subdag" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "indexmap", "rayon", @@ -3101,6 +3142,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-narwhal-transmission-id" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "snarkvm-console", "snarkvm-ledger-puzzle", @@ -3109,6 +3151,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-puzzle" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "aleo-std", "anyhow", @@ -3128,6 +3171,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-puzzle-epoch" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "aleo-std", "anyhow", @@ -3149,6 +3193,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-query" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "anyhow", "async-trait", @@ -3165,6 +3210,7 @@ dependencies = [ [[package]] name = "snarkvm-ledger-store" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "aleo-std-storage", "anyhow", @@ -3189,6 +3235,7 @@ dependencies = [ [[package]] name = "snarkvm-parameters" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "aleo-std", "anyhow", @@ -3213,6 +3260,7 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "aleo-std", "anyhow", @@ -3247,6 +3295,7 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer-error" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "anyhow", "snarkvm-circuit-environment", @@ -3258,6 +3307,7 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer-process" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "aleo-std", "colored", @@ -3283,6 +3333,7 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer-program" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "enum-iterator", "indexmap", @@ -3303,6 +3354,7 @@ dependencies = [ [[package]] name = "snarkvm-synthesizer-snark" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "bincode", "serde_json", @@ -3315,6 +3367,7 @@ dependencies = [ [[package]] name = "snarkvm-utilities" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "aleo-std", "anyhow", @@ -3337,6 +3390,7 @@ dependencies = [ [[package]] name = "snarkvm-utilities-derives" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "proc-macro2", "quote 1.0.42", @@ -3346,6 +3400,7 @@ dependencies = [ [[package]] name = "snarkvm-wasm" version = "4.7.3" +source = "git+https://github.com/ProvableHQ/snarkVM.git?rev=8e2397ee719636d660ec39cf7789af38b33f6645#8e2397ee719636d660ec39cf7789af38b33f6645" dependencies = [ "getrandom 0.4.2", "snarkvm-console", diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index 3e013c179..c26b474ad 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -23,16 +23,16 @@ doctest = false [dependencies.snarkvm-algorithms] git = "https://github.com/ProvableHQ/snarkVM.git" -rev = "357899f8e85d6340bda5db8373b1cdffdf88a6d7" +rev = "8e2397ee719636d660ec39cf7789af38b33f6645" [dependencies.snarkvm-circuit-network] git = "https://github.com/ProvableHQ/snarkVM.git" -rev = "357899f8e85d6340bda5db8373b1cdffdf88a6d7" +rev = "8e2397ee719636d660ec39cf7789af38b33f6645" features = ["wasm"] [dependencies.snarkvm-console] git = "https://github.com/ProvableHQ/snarkVM.git" -rev = "357899f8e85d6340bda5db8373b1cdffdf88a6d7" +rev = "8e2397ee719636d660ec39cf7789af38b33f6645" default-features = false features = [ "account", @@ -46,37 +46,37 @@ features = [ [dependencies.snarkvm-ledger-block] git = "https://github.com/ProvableHQ/snarkVM.git" -rev = "357899f8e85d6340bda5db8373b1cdffdf88a6d7" +rev = "8e2397ee719636d660ec39cf7789af38b33f6645" features = ["wasm"] [dependencies.snarkvm-ledger-query] git = "https://github.com/ProvableHQ/snarkVM.git" -rev = "357899f8e85d6340bda5db8373b1cdffdf88a6d7" +rev = "8e2397ee719636d660ec39cf7789af38b33f6645" features = ["async", "wasm"] [dependencies.snarkvm-ledger-store] git = "https://github.com/ProvableHQ/snarkVM.git" -rev = "357899f8e85d6340bda5db8373b1cdffdf88a6d7" +rev = "8e2397ee719636d660ec39cf7789af38b33f6645" [dependencies.snarkvm-parameters] git = "https://github.com/ProvableHQ/snarkVM.git" -rev = "357899f8e85d6340bda5db8373b1cdffdf88a6d7" +rev = "8e2397ee719636d660ec39cf7789af38b33f6645" default-features = false features = ["wasm"] [dependencies.snarkvm-synthesizer-program] git = "https://github.com/ProvableHQ/snarkVM.git" -rev = "357899f8e85d6340bda5db8373b1cdffdf88a6d7" +rev = "8e2397ee719636d660ec39cf7789af38b33f6645" features = ["wasm"] [dependencies.snarkvm-synthesizer] git = "https://github.com/ProvableHQ/snarkVM.git" -rev = "357899f8e85d6340bda5db8373b1cdffdf88a6d7" +rev = "8e2397ee719636d660ec39cf7789af38b33f6645" features = ["async", "wasm"] [dependencies.snarkvm-wasm] git = "https://github.com/ProvableHQ/snarkVM.git" -rev = "357899f8e85d6340bda5db8373b1cdffdf88a6d7" +rev = "8e2397ee719636d660ec39cf7789af38b33f6645" features = ["fields", "utilities"] [dependencies.anyhow] @@ -161,19 +161,6 @@ browser = [ ] testnet = [ ] mainnet = [ ] -# TODO (Antonio) remove -[patch."https://github.com/ProvableHQ/snarkVM.git"] -snarkvm-algorithms = { path = "../../snarkVM/algorithms" } -snarkvm-circuit-network = { path = "../../snarkVM/circuit/network" } -snarkvm-console = { path = "../../snarkVM/console" } -snarkvm-ledger-block = { path = "../../snarkVM/ledger/block" } -snarkvm-ledger-query = { path = "../../snarkVM/ledger/query" } -snarkvm-ledger-store = { path = "../../snarkVM/ledger/store" } -snarkvm-parameters = { path = "../../snarkVM/parameters" } -snarkvm-synthesizer-program = { path = "../../snarkVM/synthesizer/program" } -snarkvm-synthesizer = { path = "../../snarkVM/synthesizer" } -snarkvm-wasm = { path = "../../snarkVM/wasm" } - ## Profiles [profile.release] opt-level = 3 diff --git a/wasm/src/programs/manager/authorize.rs b/wasm/src/programs/manager/authorize.rs index b31ae7086..0ee41ca98 100644 --- a/wasm/src/programs/manager/authorize.rs +++ b/wasm/src/programs/manager/authorize.rs @@ -334,9 +334,9 @@ impl ProgramManager { /// alongside the auxiliary information gathered while traversing the call graph. /// /// This behaves like {@link sampleAuthorization} but calls the underlying - /// `Stack::sample_authorization_extended`, which additionally surfaces record-tracking, - /// record-name, and program-checksum information needed to populate the mocked `Request`s. - /// The returned object has the following shape: + /// `Stack::sample_authorization_with_record_tracking`, which additionally surfaces + /// record-tracking, record-name, and program-checksum information needed to populate the mocked + /// `Request`s. The returned object has the following shape: /// /// ```text /// { @@ -359,8 +359,8 @@ impl ProgramManager { /// @param {Object | undefined} imports The imports to the program in `{"name.aleo":"source"}` format. /// @param {number | undefined} edition The program edition (defaults to 1). /// @param {ProgramImports | undefined} program_imports Pre-loaded imports builder. - #[wasm_bindgen(js_name = sampleAuthorizationExtended)] - pub async fn sample_authorization_extended( + #[wasm_bindgen(js_name = sampleAuthorizationWithRecordTracking)] + pub async fn sample_authorization_with_record_tracking( address: &Address, program: &str, function_name: &str, @@ -398,7 +398,7 @@ impl ProgramManager { // Produce the mock authorization with additional request-population information let (authorization, record_tracking, record_names, program_checksums) = stack - .sample_authorization_extended::( + .sample_authorization_with_record_tracking::( address_native, program_id, function_name_native, From 904ff54ef5ac1ea36b4f2b4a9b9c2af00c2a515a Mon Sep 17 00:00:00 2001 From: Michael Turner Date: Wed, 24 Jun 2026 19:31:02 -0500 Subject: [PATCH 11/12] Fix SDK format lints + failing wasm and JS tests --- sdk/tests/wasm.test.ts | 4 +- wasm/package.json | 2 +- wasm/src/programs/manager/proving_request.rs | 6 +-- wasm/src/programs/request.rs | 41 ++++++++++++++++++-- wasm/src/record/record_ciphertext.rs | 20 ++++------ wasm/src/synthesizer/proving_request.rs | 9 +++-- 6 files changed, 57 insertions(+), 25 deletions(-) diff --git a/sdk/tests/wasm.test.ts b/sdk/tests/wasm.test.ts index 21fc1cebe..1112573c2 100644 --- a/sdk/tests/wasm.test.ts +++ b/sdk/tests/wasm.test.ts @@ -629,9 +629,9 @@ describe('WASM Objects', () => { describe('Set development consensus version heights', () => { it('Consensus version heights can be set externally', async () => { if (process.env["RUN_SKIPPED"]) { - const heights = getOrInitConsensusVersionTestHeights("0,1,2,3,4,5,6,7,8,9,10,11,12,13,14"); + const heights = getOrInitConsensusVersionTestHeights("0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15"); console.log(heights); - expect(heights).to.deep.equal([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14]); + expect(heights).to.deep.equal([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]); } }); }); diff --git a/wasm/package.json b/wasm/package.json index d0d9e6b55..309872e99 100644 --- a/wasm/package.json +++ b/wasm/package.json @@ -1,7 +1,7 @@ { "name": "@provablehq/wasm", "version": "0.11.1", - "type": "module", + "description": "SnarkVM WASM binaries with javascript bindings", "collaborators": [ "The Provable Team" diff --git a/wasm/src/programs/manager/proving_request.rs b/wasm/src/programs/manager/proving_request.rs index f3a840433..bb11d922f 100644 --- a/wasm/src/programs/manager/proving_request.rs +++ b/wasm/src/programs/manager/proving_request.rs @@ -25,7 +25,7 @@ use crate::{ authorize_fee, log, process_inputs, - types::native::{AuthorizationNative, CurrentAleo, IdentifierNative, ProgramNative, RecordPlaintextNative}, + types::native::{CurrentAleo, IdentifierNative, ProgramNative, RecordPlaintextNative}, }; use js_sys::{Array, Object}; @@ -149,7 +149,6 @@ mod tests { use crate::{ ExecutionRequest, PrivateKey, - Program, array, test::{PUZZLE_SPINNER_V002, generate_puzzle_imports, generate_puzzle_inputs, get_env}, }; @@ -181,8 +180,7 @@ mod tests { let request = transfer_public_execution_request(); let requests = array![request]; - let proving_request = - ProgramManager::proving_request_from_execution_request(requests, None, false).unwrap(); + let proving_request = ProgramManager::proving_request_from_execution_request(requests, None, false).unwrap(); assert_eq!(proving_request.kind(), "request"); let reqs = proving_request.requests().unwrap(); diff --git a/wasm/src/programs/request.rs b/wasm/src/programs/request.rs index 3f6c70fb6..506498300 100644 --- a/wasm/src/programs/request.rs +++ b/wasm/src/programs/request.rs @@ -613,7 +613,15 @@ impl ExecutionRequest { root_tvk: Option, ) -> Result { let (program_id, function_name, inputs_native, input_types_native, scm, tcm, network_id, function_id) = - Self::parse_common_args(&program_id, &function_name, &inputs, &input_types, &signer, &tvk, root_tvk.as_ref())?; + Self::parse_common_args( + &program_id, + &function_name, + &inputs, + &input_types, + &signer, + &tvk, + root_tvk.as_ref(), + )?; let mut record_view_keys_native = native_type_from_wasm_object_array!(record_view_keys.unwrap_or(Array::new()), Field, FieldNative)?; @@ -678,7 +686,15 @@ impl ExecutionRequest { root_tvk: Option, ) -> Result { let (program_id, function_name, inputs_native, input_types_native, scm, tcm, network_id, function_id) = - Self::parse_common_args(&program_id, &function_name, &inputs, &input_types, &signer, &tvk, root_tvk.as_ref())?; + Self::parse_common_args( + &program_id, + &function_name, + &inputs, + &input_types, + &signer, + &tvk, + root_tvk.as_ref(), + )?; // Derive record_view_keys from the view key for each record input. let mut record_view_keys_native = Vec::new(); @@ -754,7 +770,15 @@ impl ExecutionRequest { root_tvk: Option, ) -> Result { let (program_id, function_name, inputs_native, input_types_native, scm, tcm, network_id, _function_id) = - Self::parse_common_args(&program_id, &function_name, &inputs, &input_types, &signer, &tvk, root_tvk.as_ref())?; + Self::parse_common_args( + &program_id, + &function_name, + &inputs, + &input_types, + &signer, + &tvk, + root_tvk.as_ref(), + )?; if input_ids.length() as usize != inputs_native.len() { return Err(format!( @@ -1387,6 +1411,7 @@ mod tests { signed_request.sk_tag(), view_key, None, // no record inputs + None, ) .expect("from_externally_signed_data_with_view_key should succeed"); @@ -1438,6 +1463,7 @@ mod tests { signed_request.sk_tag(), view_key, Some(gammas), + None, ) .expect("from_externally_signed_data_with_view_key should succeed"); @@ -1704,6 +1730,7 @@ mod tests { signed.sk_tag(), None, // no record_view_keys None, // no gammas + None, ) .expect("fromExternallySignedData with no records should succeed"); @@ -1742,6 +1769,7 @@ mod tests { signed.sk_tag(), Some(record_view_keys), Some(gammas), + None, ) .expect("fromExternallySignedData with records should succeed"); @@ -1771,6 +1799,7 @@ mod tests { signed.sk_tag(), view_key, None, // no record inputs + None, ) .expect("fromExternallySignedDataWithViewKey with no records should succeed"); @@ -1803,6 +1832,7 @@ mod tests { signed.sk_tag(), view_key, Some(gammas), + None, ) .expect("fromExternallySignedDataWithViewKey with records should succeed"); @@ -1854,6 +1884,7 @@ mod tests { signed.signer(), signed.sk_tag(), input_ids, + None, ) .expect("fromExternallySignedDataWithInputIds with public inputs should succeed"); @@ -1933,6 +1964,7 @@ mod tests { signed.signer(), signed.sk_tag(), input_ids, + None, ) .expect("fromExternallySignedDataWithInputIds with records should succeed"); @@ -1966,6 +1998,7 @@ mod tests { signed.sk_tag(), None, // missing record_view_keys Some(gammas), + None, ); assert!(result.is_err(), "Should fail when record_view_keys missing for record inputs"); } @@ -1993,6 +2026,7 @@ mod tests { signed.signer(), signed.sk_tag(), input_ids, + None, ); assert!(result.is_err(), "Should fail when input_ids length doesn't match inputs length"); } @@ -2027,6 +2061,7 @@ mod tests { signed.signer(), signed.sk_tag(), input_ids, + None, ); assert!(result.is_err(), "Should fail when scalar Field passed for record input type"); let err = match result { diff --git a/wasm/src/record/record_ciphertext.rs b/wasm/src/record/record_ciphertext.rs index bfdbd0583..28d87c3fd 100644 --- a/wasm/src/record/record_ciphertext.rs +++ b/wasm/src/record/record_ciphertext.rs @@ -48,14 +48,13 @@ impl RecordCiphertext { // Bech32 is case-insensitive in the sense that mixed-case is rejected, but the canonical // form is all-lowercase. Some upstream sources hand us all-uppercase strings, which the // strict parser rejects, so we normalize to lowercase here before parsing. - let normalized = if record.chars().any(|c| c.is_ascii_uppercase()) && !record.chars().any(|c| c.is_ascii_lowercase()) { - record.to_ascii_lowercase() - } else { - record.to_string() - }; - Self::from_str(&normalized).map_err(|_| { - format!("The record ciphertext string provided was invalid: {record}") - }) + let normalized = + if record.chars().any(|c| c.is_ascii_uppercase()) && !record.chars().any(|c| c.is_ascii_lowercase()) { + record.to_ascii_lowercase() + } else { + record.to_string() + }; + Self::from_str(&normalized).map_err(|_| format!("The record ciphertext string provided was invalid: {record}")) } /// Return the string representation of the record ciphertext @@ -247,10 +246,7 @@ mod tests { #[wasm_bindgen_test] fn test_invalid_strings() { let invalid_bech32 = "record2qqj3a67efazf0awe09grqqg44htnh9vaw7l729vl309c972x7ldquqq2k2cax8s7qsqqyqtpgvqqyqsq4seyrzvfa98fkggzccqr68af8e9m0q8rzeqh8a8aqql3a854v58sgrygdv4jn9s8ckwfd48vujrmv0rtfasqh8ygn88ch34ftck8szspvfpsqqszqzvxx9t8s9g66teeepgxmvnw5ymgapcwt2lpy9d5eus580k08wpq544jcl437wjv206u5pxst6few9ll4yhufwldgpx80rlwq8nhssqywmfsd85skg564vqhm3gxsp8q6r30udmqxrxmxx2v8xycdg8pn5ps3dhfvv"; - assert_eq!( - RecordCiphertext::from_string("garbage").err(), - Some("The record ciphertext string provided was invalid".to_string()) - ); + assert!(RecordCiphertext::from_string("garbage").is_err()); assert!(RecordCiphertext::from_string(invalid_bech32).is_err()); } diff --git a/wasm/src/synthesizer/proving_request.rs b/wasm/src/synthesizer/proving_request.rs index 1870ac31f..1196c4aa0 100644 --- a/wasm/src/synthesizer/proving_request.rs +++ b/wasm/src/synthesizer/proving_request.rs @@ -22,13 +22,12 @@ use crate::{ use snarkvm_wasm::utilities::ToBytes; use js_sys::{Array, Uint8Array}; -use wasm_bindgen::{JsValue, convert::TryFromJsValue}; use std::{ fmt::{Debug, Display}, ops::Deref, str::FromStr, }; -use wasm_bindgen::prelude::*; +use wasm_bindgen::{JsValue, convert::TryFromJsValue, prelude::*}; /// Represents a proving request to a prover. /// @@ -70,7 +69,11 @@ impl ProvingRequest { /// @param {ExecutionRequest} fee_request Optional signed request for the fee function. When omitted, the prover generates and pays the fee. /// @param {boolean} broadcast Flag that indicates whether the remote proving service should attempt to submit the transaction on the caller's behalf. #[wasm_bindgen(js_name = "fromRequests")] - pub fn from_requests(requests: Array, fee_request: Option, broadcast: bool) -> Result { + pub fn from_requests( + requests: Array, + fee_request: Option, + broadcast: bool, + ) -> Result { let requests_native: Vec = requests .iter() .enumerate() From dd3c831b0dfdf93c8809a5ee3cf13df9d223c39d Mon Sep 17 00:00:00 2001 From: Michael Turner Date: Wed, 24 Jun 2026 19:58:39 -0500 Subject: [PATCH 12/12] Fix rustfmt lints --- wasm/src/programs/manager/authorize.rs | 2 +- wasm/src/programs/request.rs | 12 +++++++----- wasm/src/types/group.rs | 15 +++------------ 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/wasm/src/programs/manager/authorize.rs b/wasm/src/programs/manager/authorize.rs index 0ee41ca98..10e912c14 100644 --- a/wasm/src/programs/manager/authorize.rs +++ b/wasm/src/programs/manager/authorize.rs @@ -406,7 +406,7 @@ impl ProgramManager { rng, ) .map_err(|e| e.to_string())?; - + // Record-tracking: (minter_request, output_register) -> [(consumer_request, input_index), ...]. // We flatten the tuple key into individual values (since JS maps cannot key on tuples) and // keep the consumers as a nested array. diff --git a/wasm/src/programs/request.rs b/wasm/src/programs/request.rs index 506498300..24a1fe6e6 100644 --- a/wasm/src/programs/request.rs +++ b/wasm/src/programs/request.rs @@ -372,8 +372,7 @@ impl ExecutionRequest { // Compute the function ID. let network_id = U16Native::new(CurrentNetwork::ID); - let function_id = - compute_function_id(&network_id, &program_id, &function_name).map_err(|e| e.to_string())?; + let function_id = compute_function_id(&network_id, &program_id, &function_name).map_err(|e| e.to_string())?; let program_checksum = program_checksum.map(|checksum| *checksum); // Construct the message as @@ -469,8 +468,10 @@ impl ExecutionRequest { let get_field = |target: &JsValue, key: &str| -> Result { Reflect::get(target, &JsValue::from_str(key)).map_err(|_| format!("external_inputs.{key} is missing")) }; - let function_id_str = get_field(&external_inputs, "functionId").or_else(|_| get_field(&external_inputs, "function_id"))? - .as_string().ok_or_else(|| "external_inputs.functionId (or function_id) must be a string".to_string())?; + let function_id_str = get_field(&external_inputs, "functionId") + .or_else(|_| get_field(&external_inputs, "function_id"))? + .as_string() + .ok_or_else(|| "external_inputs.functionId (or function_id) must be a string".to_string())?; let function_id = FieldNative::from_str(&function_id_str).map_err(|e| e.to_string())?; let is_root_value = get_field(&external_inputs, "isRoot")?; let is_root = if let Some(b) = is_root_value.as_bool() { @@ -516,7 +517,8 @@ impl ExecutionRequest { data.iter() .enumerate() .map(|(j, d)| { - let s = d.as_string().ok_or_else(|| format!("requestInputs[{i}].data[{j}] must be a string"))?; + let s = + d.as_string().ok_or_else(|| format!("requestInputs[{i}].data[{j}] must be a string"))?; FieldNative::from_str(&s).map_err(|e| e.to_string()) }) .collect() diff --git a/wasm/src/types/group.rs b/wasm/src/types/group.rs index 8fd5a291f..341853561 100644 --- a/wasm/src/types/group.rs +++ b/wasm/src/types/group.rs @@ -45,18 +45,9 @@ use crate::{ }, }, }; -use snarkvm_console::network::Network; -use snarkvm_console::prelude::{ - Double, - FromBits, - FromBytes, - FromField, - FromFields, - ToBits, - ToBytes, - ToFields, - Uniform, - Zero, +use snarkvm_console::{ + network::Network, + prelude::{Double, FromBits, FromBytes, FromField, FromFields, ToBits, ToBytes, ToFields, Uniform, Zero}, }; use js_sys::{Array, Uint8Array};