Skip to content

Commit

Permalink
feat: add warp message serialization/deserialization
Browse files Browse the repository at this point in the history
  • Loading branch information
erictaylor committed Nov 20, 2024
1 parent 2bab717 commit 52b0ba4
Show file tree
Hide file tree
Showing 19 changed files with 291 additions and 163 deletions.
3 changes: 3 additions & 0 deletions src/fixtures/codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getAVMManager } from '../serializable/avm/codec';
import { codec as EVMCodec } from '../serializable/evm/codec';
import { Short } from '../serializable/primitives';
import { codec } from '../serializable/pvm/codec';
import { codec as WarpCodec } from '../serializable/pvm/warp/codec';

// Check for circular imports in the fx type
// registries if tests are throwing errors
Expand All @@ -13,3 +14,5 @@ export const testCodec = () => testManager().getCodecForVersion(new Short(0));
export const testPVMCodec = () => codec;

export const testEVMCodec = () => EVMCodec;

export const testWarpCodec = () => WarpCodec;
32 changes: 32 additions & 0 deletions src/fixtures/warp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {
WarpMessage,
WarpSignature,
WarpUnsignedMessage,
} from '../serializable/pvm/warp';
import { concatBytes } from '../utils';
import { id, idBytes } from './common';
import {
blsSignature,
blsSignatureBytes,
bytes,
bytesBytes,
int,
intBytes,
} from './primitives';

export const warpUnsignedMessage = () =>
new WarpUnsignedMessage(int(), id(), bytes());

export const warpUnsignedMessageBytes = () =>
concatBytes(intBytes(), idBytes(), bytesBytes());

export const warpSignature = () => new WarpSignature(bytes(), blsSignature());

export const warpSignatureBytes = () =>
concatBytes(bytesBytes(), blsSignatureBytes());

export const warpMessage = () =>
new WarpMessage(warpUnsignedMessage(), warpSignature());

export const warpMessageBytes = () =>
concatBytes(warpUnsignedMessageBytes(), warpSignatureBytes());
5 changes: 5 additions & 0 deletions src/serializable/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,9 @@ export enum TypeSymbols {
EvmInput = 'evm.Input',
EvmOutput = 'evm.Output',
EvmImportTx = 'evm.ImportTx',

// Warp
WarpMessage = 'warp.Message',
WarpUnsignedMessage = 'warp.UnsignedMessage',
WarpSignature = 'warp.Signature',
}
2 changes: 1 addition & 1 deletion src/serializable/primitives/bytes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { TypeSymbols } from '../constants';
@serializable()
export class Bytes extends Primitives {
_type = TypeSymbols.Bytes;
constructor(private readonly bytes: Uint8Array) {
constructor(public readonly bytes: Uint8Array) {
super();
}

Expand Down
15 changes: 15 additions & 0 deletions src/serializable/pvm/warp/codec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Codec, Manager } from '../../codec';
import { WarpSignature } from './signature';

/**
* @see https://github.com/ava-labs/avalanchego/blob/master/vms/platformvm/warp/codec.go
*/
export const codec = new Codec([WarpSignature]);

let manager: Manager;
export const getWarpManager = () => {
if (manager) return manager;
manager = new Manager();
manager.RegisterCodec(0, codec);
return manager;
};
4 changes: 4 additions & 0 deletions src/serializable/pvm/warp/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { codec, getWarpManager } from './codec';
export { WarpMessage } from './message';
export { WarpSignature } from './signature';
export { WarpUnsignedMessage } from './unsignedMessage';
12 changes: 12 additions & 0 deletions src/serializable/pvm/warp/message.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { testWarpCodec } from '../../../fixtures/codec';
import { testSerialization } from '../../../fixtures/utils/serializable';
import { warpMessage, warpMessageBytes } from '../../../fixtures/warp';
import { WarpMessage } from './message';

testSerialization(
'WarpMessage',
WarpMessage,
warpMessage,
warpMessageBytes,
testWarpCodec,
);
36 changes: 36 additions & 0 deletions src/serializable/pvm/warp/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { concatBytes } from '../../../utils/buffer';
import { unpack } from '../../../utils/struct';
import type { Codec } from '../../codec';
import { serializable } from '../../common/types';
import { TypeSymbols } from '../../constants';
import type { WarpSignature } from './signature';
import { WarpUnsignedMessage } from './unsignedMessage';

@serializable()
export class WarpMessage {
_type = TypeSymbols.WarpMessage;

constructor(
public readonly unsignedMessage: WarpUnsignedMessage,
public readonly signature: WarpSignature,
) {}

static fromBytes(bytes: Uint8Array, codec: Codec): [WarpMessage, Uint8Array] {
const [unsignedMessage, signatureBytes] = unpack(
bytes,
[WarpUnsignedMessage],
codec,
);

const [signature, rest] = codec.UnpackPrefix<WarpSignature>(signatureBytes);

return [new WarpMessage(unsignedMessage, signature), rest];
}

toBytes(codec: Codec) {
return concatBytes(
this.unsignedMessage.toBytes(codec),
codec.PackPrefix(this.signature),
);
}
}
12 changes: 12 additions & 0 deletions src/serializable/pvm/warp/signature.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { testWarpCodec } from '../../../fixtures/codec';
import { testSerialization } from '../../../fixtures/utils/serializable';
import { warpSignature, warpSignatureBytes } from '../../../fixtures/warp';
import { WarpSignature } from './signature';

testSerialization(
'WarpSignature',
WarpSignature,
warpSignature,
warpSignatureBytes,
testWarpCodec,
);
46 changes: 46 additions & 0 deletions src/serializable/pvm/warp/signature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { hammingWeight } from '../../../utils/buffer';
import { pack, unpack } from '../../../utils/struct';
import type { Codec } from '../../codec';
import { serializable } from '../../common/types';
import { TypeSymbols } from '../../constants';
import { BlsSignature } from '../../fxs/common';
import { Bytes } from '../../primitives';
import { INT_LEN } from '../../primitives/int';

@serializable()
export class WarpSignature {
_type = TypeSymbols.WarpSignature;

constructor(
public readonly signers: Bytes,
public readonly signature: BlsSignature,
) {}

static fromBytes(
bytes: Uint8Array,
codec: Codec,
): [WarpSignature, Uint8Array] {
const [signers, signature, rest] = unpack(
bytes,
[Bytes, BlsSignature],
codec,
);

return [new WarpSignature(signers, signature), rest];
}

toBytes(codec: Codec) {
return pack([this.signers, this.signature], codec);
}

/**
* Number of [bls.PublicKeys] that participated in the
* {@linkcode BlsSignature}. This is exposed because users of the signatures
* typically impose a verification fee that is a function of the number of signers.
*
* This is used to calculate the Warp complexity in transactions.
*/
numOfSigners(): number {
return hammingWeight(this.signers.toBytes().slice(INT_LEN));
}
}
15 changes: 15 additions & 0 deletions src/serializable/pvm/warp/unsignedMessage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { testWarpCodec } from '../../../fixtures/codec';
import { testSerialization } from '../../../fixtures/utils/serializable';
import {
warpUnsignedMessage,
warpUnsignedMessageBytes,
} from '../../../fixtures/warp';
import { WarpUnsignedMessage } from './unsignedMessage';

testSerialization(
'WarpUnsignedMessage',
WarpUnsignedMessage,
warpUnsignedMessage,
warpUnsignedMessageBytes,
testWarpCodec,
);
34 changes: 34 additions & 0 deletions src/serializable/pvm/warp/unsignedMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { pack, unpack } from '../../../utils/struct';
import type { Codec } from '../../codec';
import { serializable } from '../../common/types';
import { TypeSymbols } from '../../constants';
import { Id } from '../../fxs/common';
import { Bytes, Int } from '../../primitives';

@serializable()
export class WarpUnsignedMessage {
_type = TypeSymbols.WarpUnsignedMessage;

constructor(
public readonly networkId: Int,
public readonly sourceChainId: Id,
public readonly payload: Bytes,
) {}

static fromBytes(
bytes: Uint8Array,
codec: Codec,
): [WarpUnsignedMessage, Uint8Array] {
const [networkId, sourceChainId, payload, rest] = unpack(
bytes,
[Int, Id, Bytes],
codec,
);

return [new WarpUnsignedMessage(networkId, sourceChainId, payload), rest];
}

toBytes(codec: Codec) {
return pack([this.networkId, this.sourceChainId, this.payload], codec);
}
}
35 changes: 34 additions & 1 deletion src/utils/buffer.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { bufferToBigInt, bufferToNumber, padLeft } from './buffer';
import {
bufferToBigInt,
bufferToNumber,
hammingWeight,
padLeft,
} from './buffer';
import { describe, it, expect } from 'vitest';

describe('bufferToBigInt', () => {
Expand Down Expand Up @@ -79,3 +84,31 @@ describe('padLeft', () => {
expect(res).toStrictEqual(new Uint8Array([0xaf, 0x72, 0x72]));
});
});

describe('hammingWeight()', () => {
it('should return expected number of `1` bits from bytes', () => {
expect(hammingWeight(new Uint8Array([0]))).toBe(0);
expect(hammingWeight(new Uint8Array([1]))).toBe(1);
expect(hammingWeight(new Uint8Array([2]))).toBe(1);
expect(hammingWeight(new Uint8Array([3]))).toBe(2);
expect(hammingWeight(new Uint8Array([4]))).toBe(1);
expect(hammingWeight(new Uint8Array([5]))).toBe(2);
expect(hammingWeight(new Uint8Array([6]))).toBe(2);
expect(hammingWeight(new Uint8Array([7]))).toBe(3);
expect(hammingWeight(new Uint8Array([8]))).toBe(1);
expect(hammingWeight(new Uint8Array([9]))).toBe(2);

expect(hammingWeight(new Uint8Array([0, 0]))).toBe(0);
expect(hammingWeight(new Uint8Array([0, 1]))).toBe(1);
expect(hammingWeight(new Uint8Array([0, 2]))).toBe(1);
expect(hammingWeight(new Uint8Array([0, 3]))).toBe(2);

expect(hammingWeight(new Uint8Array([1, 1]))).toBe(2);
expect(hammingWeight(new Uint8Array([1, 2]))).toBe(2);
expect(hammingWeight(new Uint8Array([1, 3]))).toBe(3);

expect(hammingWeight(new Uint8Array([3, 1]))).toBe(3);
expect(hammingWeight(new Uint8Array([3, 2]))).toBe(3);
expect(hammingWeight(new Uint8Array([3, 3]))).toBe(4);
});
});
26 changes: 26 additions & 0 deletions src/utils/buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,30 @@ export function padLeft(bytes: Uint8Array, length: number) {
return out;
}

/**
* Calculates the number of `1`s (set bits) in the binary
* representation a big-endian byte slice.
*
* @param input A Uint8Array
* @returns The number of bits set to 1 in the binary representation of the input
*
* @example
* ```ts
* hammingWeight(new Uint8Array([0, 1, 2, 3, 4, 5])); // 7
* ```
*/
export const hammingWeight = (input: Uint8Array): number => {
let count = 0;

for (let i = 0; i < input.length; i++) {
let num = input[i];
while (num !== 0) {
count += num & 1;
num >>= 1;
}
}

return count;
};

export { concatBytes, strip0x, add0x };
21 changes: 6 additions & 15 deletions src/utils/getBurnedAmountByTx.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ import { feeState, l1Validator } from '../fixtures/pvm';
import {
bigIntPr,
blsSignatureBytes,
bytesBytes,
stringPr,
warpMessageBytes,
} from '../fixtures/primitives';

const getUtxoMock = (
Expand Down Expand Up @@ -677,10 +677,7 @@ describe('getBurnedAmountByTx', () => {

const amounts = getBurnedAmountByTx(tx, testContext);
expect(amounts.size).toEqual(1);
expect(amounts.get(testContext.avaxAssetID)).toEqual(
// Magic number. This needs refactored. Is this even correct?
2_200n,
);
expect(amounts.get(testContext.avaxAssetID)).toEqual(2_200n);
});
});

Expand All @@ -695,20 +692,17 @@ describe('getBurnedAmountByTx', () => {
utxos: [utxo1, utxo2],
balance: bigIntPr().value(),
blsSignature: blsSignatureBytes(),
message: bytesBytes(),
message: warpMessageBytes(),
},
testContext,
).getTx() as AvaxTx;

const amounts = getBurnedAmountByTx(tx, testContext);
expect(amounts.size).toEqual(1);
expect(amounts.get(testContext.avaxAssetID)).toEqual(
// Magic number. This needs refactored. Is this even correct?
2_710n,
);
expect(amounts.get(testContext.avaxAssetID)).toEqual(3_002n);
});

it('calculates the burned amount of RegisterL1Validator tx correctly', () => {
it('calculates the burned amount of IncreaseL1ValidatorBalance tx correctly', () => {
const utxo1 = getUtxoMock(testUTXOID1, 5000000n);
const utxo2 = getUtxoMock(testUTXOID2, 6000000n);

Expand All @@ -725,9 +719,6 @@ describe('getBurnedAmountByTx', () => {

const amounts = getBurnedAmountByTx(tx, testContext);
expect(amounts.size).toEqual(1);
expect(amounts.get(testContext.avaxAssetID)).toEqual(
// Magic number. This needs refactored. Is this even correct?
548n,
);
expect(amounts.get(testContext.avaxAssetID)).toEqual(548n);
});
});
Loading

0 comments on commit 52b0ba4

Please sign in to comment.