Skip to content

Commit 790c867

Browse files
authored
Merge pull request #167 from fleet-sdk/arobsn/i161
Add `SignedTransaction` serialization
2 parents 8deb0e4 + 9c04d3c commit 790c867

File tree

8 files changed

+1312
-39
lines changed

8 files changed

+1312
-39
lines changed

.changeset/smooth-dragons-draw.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@fleet-sdk/serializer": patch
3+
"@fleet-sdk/common": patch
4+
"@fleet-sdk/wallet": patch
5+
---
6+
7+
Add `SignedTransaction` serialization

packages/common/src/types/inputs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export type ProverResult = {
1313

1414
export type SignedInput = {
1515
readonly boxId: BoxId;
16-
readonly spendingProof: ProverResult;
16+
readonly spendingProof: ProverResult | null;
1717
};
1818

1919
export type UnsignedInput = {

packages/common/src/types/transactions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,5 @@ export type SignedTransaction = {
2828
readonly id: TransactionId;
2929
readonly inputs: SignedInput[];
3030
readonly dataInputs: DataInput[];
31-
readonly outputs: Box<string>[];
31+
readonly outputs: Box<Amount>[];
3232
};

packages/serializer/src/_test-vectors/signedTransactions.json

Lines changed: 1205 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,36 @@
11
import { blake2b256, hex } from "@fleet-sdk/crypto";
2-
import { describe, expect, it } from "vitest";
2+
import { describe, expect, it, test } from "vitest";
33
import { unsignedTransactionVectors } from "../_test-vectors/transactionVectors";
44
import { serializeTransaction } from "./transactionSerializer";
5+
import signedTransactionVectors from "../_test-vectors/signedTransactions.json";
6+
import { isEmpty } from "@fleet-sdk/common";
57

68
describe("Transaction serializer", () => {
7-
it.each(unsignedTransactionVectors)("Should serialize [$name]", (tv) => {
8-
const bytes = serializeTransaction(tv.json).toBytes();
9+
test.each(unsignedTransactionVectors)(
10+
"Should serialize unsigned transaction [$name]",
11+
(tv) => {
12+
const bytes = serializeTransaction(tv.json).toBytes();
913

10-
expect(hex.encode(bytes)).toBe(tv.hex);
11-
expect(hex.encode(blake2b256(bytes))).toBe(tv.hash);
14+
expect(hex.encode(bytes)).toBe(tv.hex);
15+
expect(hex.encode(blake2b256(bytes))).toBe(tv.hash);
16+
}
17+
);
18+
19+
test.each(signedTransactionVectors)("should serialize signed transaction", (tv) => {
20+
expect(hex.encode(serializeTransaction(tv.json).toBytes())).toBe(tv.hex);
21+
});
22+
23+
it("should serialize if input extension is undefined", () => {
24+
const tv = unsignedTransactionVectors[0];
25+
const tx = structuredClone(tv.json);
26+
27+
for (const input of tx.inputs) {
28+
if (isEmpty(input.extension)) {
29+
// @ts-expect-error intentionally setting extension to undefined
30+
input.extension = undefined;
31+
}
32+
}
33+
34+
expect(hex.encode(serializeTransaction(tx).toBytes())).toEqual(tv.hex);
1235
});
1336
});

packages/serializer/src/serializers/transactionSerializer.ts

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
1-
import {
2-
type Amount,
3-
type BoxCandidate,
4-
type DataInput,
5-
isDefined,
6-
type UnsignedInput
1+
import type {
2+
Amount,
3+
BoxCandidate,
4+
DataInput,
5+
SignedInput,
6+
SignedTransaction,
7+
UnsignedInput
78
} from "@fleet-sdk/common";
9+
import { isDefined } from "@fleet-sdk/common";
810
import { SigmaByteWriter } from "../coders";
911
import { serializeBox } from "./boxSerializer";
12+
import { hex } from "@fleet-sdk/crypto";
1013

1114
export type MinimalUnsignedTransaction = {
12-
inputs: readonly UnsignedInput[];
13-
dataInputs: readonly DataInput[];
14-
outputs: readonly BoxCandidate<Amount>[];
15+
inputs: UnsignedInput[];
16+
dataInputs: DataInput[];
17+
outputs: BoxCandidate<Amount>[];
1518
};
1619

20+
type Nullish<T> = T | null | undefined;
21+
1722
export function serializeTransaction(
18-
transaction: MinimalUnsignedTransaction
23+
transaction: MinimalUnsignedTransaction | SignedTransaction
1924
): SigmaByteWriter {
2025
const writer = new SigmaByteWriter(100_000);
2126

@@ -39,16 +44,43 @@ export function serializeTransaction(
3944
return writer;
4045
}
4146

42-
function writeInput(writer: SigmaByteWriter, input: UnsignedInput): void {
47+
function writeInput(writer: SigmaByteWriter, input: UnsignedInput | SignedInput): void {
4348
writer.writeHex(input.boxId);
44-
writer.write(0); // empty proof
49+
50+
if (isSignedInput(input)) {
51+
writeProof(writer, input.spendingProof?.proofBytes);
52+
writeExtension(writer, input.spendingProof?.extension);
53+
return;
54+
}
55+
56+
writeProof(writer, null);
4557
writeExtension(writer, input.extension);
4658
}
4759

60+
function isSignedInput(input: UnsignedInput | SignedInput): input is SignedInput {
61+
return (input as SignedInput).spendingProof !== undefined;
62+
}
63+
64+
function writeProof(writer: SigmaByteWriter, proof: Nullish<string>): void {
65+
if (!proof) {
66+
writer.write(0);
67+
return;
68+
}
69+
70+
const bytes = hex.decode(proof);
71+
writer.writeVLQ(bytes.length);
72+
writer.writeBytes(bytes);
73+
}
74+
4875
function writeExtension(
4976
writer: SigmaByteWriter,
50-
extension: Record<string, string | undefined>
77+
extension: Nullish<Record<string, string | undefined>>
5178
): void {
79+
if (!extension) {
80+
writer.write(0);
81+
return;
82+
}
83+
5284
const keys = Object.keys(extension);
5385
let length = 0;
5486

packages/wallet/src/prover/prover.spec.ts

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import { Prover } from "./prover";
1717
const height = 1234209;
1818
const externalAddress = "9gN8gmyaDBuWPZLn8zj9uZxnLUj4TE9rtedtLGNjf6cUhTmoTwc";
1919

20+
const toBytes = (input: string | undefined | null) => hex.decode(input ?? "");
21+
2022
describe("Transaction signing", () => {
2123
it("Should sign a transaction with a single secret and a single input", async () => {
2224
// generate keys
@@ -41,7 +43,7 @@ describe("Transaction signing", () => {
4143
const signedTx = prover.signTransaction(unsignedTx, [rootKey]);
4244

4345
// verify
44-
const proof = hex.decode(signedTx.inputs[0].spendingProof.proofBytes);
46+
const proof = toBytes(signedTx.inputs[0].spendingProof?.proofBytes);
4547

4648
// verify using own verifier
4749
expect(prover.verify(unsignedTx, proof, rootKey)).to.be.true;
@@ -79,7 +81,7 @@ describe("Transaction signing", () => {
7981
const signedTx = prover.signTransaction(unsignedTx, [rootKey, child1]);
8082

8183
// verify
82-
const proof = hex.decode(signedTx.inputs[0].spendingProof.proofBytes);
84+
const proof = toBytes(signedTx.inputs[0].spendingProof?.proofBytes);
8385

8486
// verify using own verifier
8587
expect(prover.verify(unsignedTx, proof, child1)).to.be.true;
@@ -119,7 +121,7 @@ describe("Transaction signing", () => {
119121
]);
120122

121123
// verify
122-
const proof = hex.decode(signedTx.inputs[0].spendingProof.proofBytes);
124+
const proof = toBytes(signedTx.inputs[0].spendingProof?.proofBytes);
123125

124126
// verify using own verifier
125127
expect(prover.verify(unsignedTx, proof, rootKey)).to.be.true;
@@ -161,8 +163,8 @@ describe("Transaction signing", () => {
161163

162164
// verify
163165
const txBytes = unsignedTx.toBytes();
164-
const proof0 = hex.decode(signedTx.inputs[0].spendingProof.proofBytes);
165-
const proof1 = hex.decode(signedTx.inputs[1].spendingProof.proofBytes);
166+
const proof0 = toBytes(signedTx.inputs[0].spendingProof?.proofBytes);
167+
const proof1 = toBytes(signedTx.inputs[1].spendingProof?.proofBytes);
166168

167169
// verify using own verifier
168170
expect(prover.verify(txBytes, proof0, rootKey)).to.be.true;
@@ -208,9 +210,9 @@ describe("Transaction signing", () => {
208210

209211
// verify
210212
const txBytes = unsignedTx.toBytes();
211-
const proof0 = hex.decode(signedTx.inputs[0].spendingProof.proofBytes);
212-
const proof1 = hex.decode(signedTx.inputs[1].spendingProof.proofBytes);
213-
const proof2 = hex.decode(signedTx.inputs[2].spendingProof.proofBytes);
213+
const proof0 = toBytes(signedTx.inputs[0].spendingProof?.proofBytes);
214+
const proof1 = toBytes(signedTx.inputs[1].spendingProof?.proofBytes);
215+
const proof2 = toBytes(signedTx.inputs[2].spendingProof?.proofBytes);
214216

215217
expect(prover.verify(txBytes, proof0, rootKey)).to.be.false; // inverted by the mapper
216218
expect(prover.verify(txBytes, proof0, child2)).to.be.true; // inverted by the mapper
@@ -250,8 +252,8 @@ describe("Transaction signing", () => {
250252

251253
// verify
252254
const txBytes = unsignedTx.toBytes();
253-
const proof0 = hex.decode(signedTx.inputs[0].spendingProof.proofBytes);
254-
const proof1 = hex.decode(signedTx.inputs[1].spendingProof.proofBytes);
255+
const proof0 = toBytes(signedTx.inputs[0].spendingProof?.proofBytes);
256+
const proof1 = toBytes(signedTx.inputs[1].spendingProof?.proofBytes);
255257

256258
expect(prover.verify(txBytes, proof0, rootKey)).to.be.false; // inverted by the mapper
257259
expect(prover.verify(txBytes, proof0, child1)).to.be.true; // inverted by the mapper
@@ -319,14 +321,14 @@ describe("Transaction signing", () => {
319321

320322
// verify all inputs using own verifier
321323
for (const input of signedTx.inputs) {
322-
const proof = hex.decode(input.spendingProof.proofBytes);
324+
const proof = toBytes(input.spendingProof?.proofBytes);
323325
expect(prover.verify(txBytes, proof, rootKey)).to.be.true;
324326
}
325327

326328
// verify all inputs using sigma-rust for comparison
327329
const addr = Address.from_public_key(rootKey.publicKey);
328330
for (const input of signedTx.inputs) {
329-
const proof = hex.decode(input.spendingProof.proofBytes);
331+
const proof = toBytes(input.spendingProof?.proofBytes);
330332
expect(verify_signature(addr, txBytes, proof)).to.be.true;
331333
}
332334
});
@@ -380,42 +382,42 @@ describe("Transaction proof verification", () => {
380382

381383
it("Should verify from bytes", () => {
382384
const prover = new Prover();
383-
const proof = signedTx.inputs[0].spendingProof.proofBytes;
385+
const proof = signedTx.inputs[0].spendingProof?.proofBytes ?? "";
384386

385387
expect(prover.verify(unsignedTx.toBytes(), proof, rootKey)).to.be.true;
386388
});
387389

388390
it("Should verify from hex", () => {
389391
const prover = new Prover();
390-
const proof = signedTx.inputs[0].spendingProof.proofBytes;
392+
const proof = signedTx.inputs[0].spendingProof?.proofBytes ?? "";
391393

392394
expect(prover.verify(hex.encode(unsignedTx.toBytes()), proof, rootKey)).to.be.true;
393395
});
394396

395397
it("Should verify from SignedTransaction", () => {
396398
const prover = new Prover();
397-
const proof = signedTx.inputs[0].spendingProof.proofBytes;
399+
const proof = signedTx.inputs[0].spendingProof?.proofBytes ?? "";
398400

399401
expect(prover.verify(signedTx, proof, rootKey)).to.be.true;
400402
});
401403

402404
it("Should verify from ErgoUnsignedTransaction", () => {
403405
const prover = new Prover();
404-
const proof = signedTx.inputs[0].spendingProof.proofBytes;
406+
const proof = signedTx.inputs[0].spendingProof?.proofBytes ?? "";
405407

406408
expect(prover.verify(unsignedTx, proof, rootKey)).to.be.true;
407409
});
408410

409411
it("Should verify from EIP12UnsignedTransaction", () => {
410412
const prover = new Prover();
411-
const proof = signedTx.inputs[0].spendingProof.proofBytes;
413+
const proof = signedTx.inputs[0].spendingProof?.proofBytes ?? "";
412414

413415
expect(prover.verify(unsignedTx.toEIP12Object(), proof, rootKey)).to.be.true;
414416
});
415417

416418
it("Should verify from PlainObject", () => {
417419
const prover = new Prover();
418-
const proof = signedTx.inputs[0].spendingProof.proofBytes;
420+
const proof = signedTx.inputs[0].spendingProof?.proofBytes ?? "";
419421

420422
expect(prover.verify(unsignedTx.toPlainObject(), proof, rootKey)).to.be.true;
421423
});

packages/wallet/src/prover/prover.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,12 @@ export class Prover implements ISigmaProver {
8383
...message,
8484
inputs: message.inputs.map((input) => ({
8585
...input,
86+
spendingProof: undefined, // remove spendingProof from inputs
8687
extension:
87-
"extension" in input ? input.extension : input.spendingProof.extension
88+
"extension" in input
89+
? input.extension
90+
: /* v8 ignore next */
91+
(input.spendingProof?.extension ?? {})
8892
}))
8993
}).toBytes();
9094
}

0 commit comments

Comments
 (0)