Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/api/src/beacon/routes/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
ContainerDataExecutionOptimistic,
ContainerData,
} from "../../utils/index.js";
import {fromU64Str, toU64Str, U64Str} from "../../utils/serdes.js";
import {fromU64Str, fromGraffitiHex, toU64Str, U64Str, toGraffitiHex} from "../../utils/serdes.js";
import {ExecutionOptimistic} from "./beacon/block.js";

// See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes
Expand Down Expand Up @@ -380,9 +380,9 @@ export function getReqSerializers(): ReqSerializers<Api, ReqTypes> {
const produceBlock: ReqSerializers<Api, ReqTypes>["produceBlock"] = {
writeReq: (slot, randaoReveal, graffiti) => ({
params: {slot},
query: {randao_reveal: toHexString(randaoReveal), graffiti},
query: {randao_reveal: toHexString(randaoReveal), graffiti: toGraffitiHex(graffiti)},
}),
parseReq: ({params, query}) => [params.slot, fromHexString(query.randao_reveal), query.graffiti],
parseReq: ({params, query}) => [params.slot, fromHexString(query.randao_reveal), fromGraffitiHex(query.graffiti)],
schema: {
params: {slot: Schema.UintRequired},
query: {randao_reveal: Schema.StringRequired, graffiti: Schema.String},
Expand Down
29 changes: 28 additions & 1 deletion packages/api/src/utils/serdes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {JsonPath} from "@chainsafe/ssz";
import {fromHexString, JsonPath, toHexString} from "@chainsafe/ssz";

/**
* Serialize proof path to JSON.
Expand Down Expand Up @@ -66,3 +66,30 @@ export function fromU64StrOpt(u64Str: U64Str | undefined): U64 | undefined {
export function toU64StrOpt(u64: U64 | undefined): U64Str | undefined {
return u64 !== undefined ? toU64Str(u64) : undefined;
}

const GRAFFITI_HEX_LENGTH = 66;
Comment thread
nflaig marked this conversation as resolved.

export function toGraffitiHex(utf8: string): string {
const hex = toHexString(new TextEncoder().encode(utf8));

if (hex.length > GRAFFITI_HEX_LENGTH) {
// remove characters from the end if hex string is too long
return hex.slice(0, GRAFFITI_HEX_LENGTH);
}

if (hex.length < GRAFFITI_HEX_LENGTH) {
// right-pad with zeros if hex string is too short
return hex.padEnd(GRAFFITI_HEX_LENGTH, "0");
Comment thread
nflaig marked this conversation as resolved.
}

return hex;
}

export function fromGraffitiHex(hex: string): string {
try {
return new TextDecoder("utf8").decode(fromHexString(hex));
} catch {
// allow malformed graffiti hex string
return hex;
}
}
2 changes: 1 addition & 1 deletion packages/api/test/unit/beacon/testData/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {GenericServerTestCases} from "../../../utils/genericServerTest.js";
const ZERO_HASH = Buffer.alloc(32, 0);
const ZERO_HASH_HEX = "0x" + ZERO_HASH.toString("hex");
const randaoReveal = Buffer.alloc(96, 1);
const graffiti = "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2";
const graffiti = "a".repeat(32);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why is this changed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2 this value did not make too much sense in the first place, it is from the produceBlockV2 which makes sense there becasue it is is the value which should be passed to the beacon API but in our case it should be the actual validator graffiti value which is a UFT-8 string and not hex encoded.

The runGenericServerTest function currently uses the args as input to the api client and it uses that same value to check if it was received by the mock api. This means at the moment we generally assume we sent some value do some kind of encoding in writeReq and then decode in parseReq without modyfing the value.

The problem is that toGraffitiHex will trim or pad the value depending on its length, meaning the graffiti needs to be exactly 32 bytes for the test to pass.


export const testData: GenericServerTestCases<Api> = {
getAttesterDuties: {
Expand Down
70 changes: 70 additions & 0 deletions packages/api/test/unit/utils/serdes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {expect} from "chai";
import {fromGraffitiHex, toGraffitiHex} from "../../../src/utils/serdes.js";

describe("utils / serdes", () => {
describe("toGraffitiHex", () => {
it("should convert a UTF-8 graffiti to hex", () => {
expect(toGraffitiHex("a".repeat(32))).to.equal(
"0x6161616161616161616161616161616161616161616161616161616161616161"
);
});

it("should convert a graffiti with Unicode symbols to hex", () => {
expect(toGraffitiHex("🦇🔊".repeat(4))).to.equal(
"0xf09fa687f09f948af09fa687f09f948af09fa687f09f948af09fa687f09f948a"
);
});

it("should trim the hex graffiti if it is too long", () => {
expect(toGraffitiHex("a".repeat(50))).to.equal(toGraffitiHex("a".repeat(32)));
});

it("should trim the hex graffiti if the last character is a Unicode symbol", () => {
expect(toGraffitiHex("a".repeat(31) + "🐼")).to.equal(
"0x61616161616161616161616161616161616161616161616161616161616161f0"
);
});

it("should right-pad the hex graffiti with zeros if it is too short", () => {
expect(toGraffitiHex("a")).to.equal("0x6100000000000000000000000000000000000000000000000000000000000000");
expect(toGraffitiHex("ab")).to.equal("0x6162000000000000000000000000000000000000000000000000000000000000");
expect(toGraffitiHex("abc")).to.equal("0x6162630000000000000000000000000000000000000000000000000000000000");
});
});

describe("fromGraffitiHex", () => {
it("should convert a hex graffiti to UTF-8", () => {
expect(fromGraffitiHex("0x6161616161616161616161616161616161616161616161616161616161616161")).to.equal(
"a".repeat(32)
);
});

it("should convert a hex graffiti with Unicode symbols to UTF-8", () => {
expect(fromGraffitiHex("0xf09fa687f09f948af09fa687f09f948af09fa687f09f948af09fa687f09f948a")).to.equal(
"🦇🔊".repeat(4)
);
});

it("should convert a padded hex graffiti to UTF-8", () => {
expect(fromGraffitiHex("0x6100000000000000000000000000000000000000000000000000000000000000")).to.equal(
// null bytes will not be displayed/ignored later on
"a" + "\u0000".repeat(31)
);
});

it("should decode a hex graffiti with a cut off Unicode character at the end", () => {
expect(fromGraffitiHex("0x61616161616161616161616161616161616161616161616161616161616161f0")).to.equal(
// last character will be displayed as �
"a".repeat(31) + "\ufffd"
);
});

it("should not throw an error if an invalid hex graffiti is provided", () => {
expect(() => fromGraffitiHex("a")).to.not.throw();
});

it("should return the provided graffiti string if decoding fails", () => {
expect(fromGraffitiHex("a")).to.equal("a");
});
});
});