Skip to content

Commit

Permalink
Add heartbeat txn support
Browse files Browse the repository at this point in the history
  • Loading branch information
algorandskiy committed Jan 7, 2025
1 parent 0d1dd73 commit a0b648b
Show file tree
Hide file tree
Showing 3 changed files with 254 additions and 1 deletion.
167 changes: 167 additions & 0 deletions src/heartbeat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { Encodable, Schema } from './encoding/encoding.js';
import {
AddressSchema,
Uint64Schema,
ByteArraySchema,
FixedLengthByteArraySchema,
NamedMapSchema,
allOmitEmpty,
} from './encoding/schema/index.js';


export class HeartbeatProof implements Encodable {
public static readonly encodingSchema = new NamedMapSchema(
allOmitEmpty([
{
key: 's', // Sig
valueSchema: new FixedLengthByteArraySchema(64),
},
{
key: 'p', // PK
valueSchema: new FixedLengthByteArraySchema(32),
},
{
key: 'p2', // PK2
valueSchema: new FixedLengthByteArraySchema(32),
},
{
key: 'p1s', // PK1Sig
valueSchema: new FixedLengthByteArraySchema(64),
},
{
key: 'p2s', // PK2Sig
valueSchema: new FixedLengthByteArraySchema(64),
},
])
);

public sig: Uint8Array;

public pk: Uint8Array;

public pk2: Uint8Array;

public pk1Sig: Uint8Array;

public pk2Sig: Uint8Array;

public constructor(params: {
sig: Uint8Array;
pk: Uint8Array;
pk2: Uint8Array;
pk1Sig: Uint8Array;
pk2Sig: Uint8Array;
}) {
this.sig = params.sig;
this.pk = params.pk;
this.pk2 = params.pk2;
this.pk1Sig = params.pk1Sig;
this.pk2Sig = params.pk2Sig;
}

// eslint-disable-next-line class-methods-use-this
public getEncodingSchema(): Schema {
return HeartbeatProof.encodingSchema;
}

public toEncodingData(): Map<string, unknown> {
return new Map<string, unknown>([
['s', this.sig],
['p', this.pk],
['p2', this.pk2],
['p1s', this.pk1Sig],
['p2s', this.pk2Sig],
]);
}

public static fromEncodingData(data: unknown): HeartbeatProof {
if (!(data instanceof Map)) {
throw new Error(`Invalid decoded HeartbeatProof: ${data}`);
}
return new HeartbeatProof({
sig: data.get('s'),
pk: data.get('p'),
pk2: data.get('p2'),
pk1Sig: data.get('p1s'),
pk2Sig: data.get('p2s'),
});
}
}

export class Heartbeat implements Encodable {
public static readonly encodingSchema = new NamedMapSchema(
allOmitEmpty([
{
key: 'a', // HbAddress
valueSchema: new AddressSchema(),
},
{
key: 'prf', // HbProof
valueSchema: HeartbeatProof.encodingSchema,
},
{
key: 'sd', // HbSeed
valueSchema: new ByteArraySchema(),
},
{
key: 'vid', // HbVoteID
valueSchema: new FixedLengthByteArraySchema(32),
},
{
key: 'kd', // HbKeyDilution
valueSchema: new Uint64Schema(),
},
])
);
public address: Uint8Array;

public proof: HeartbeatProof;

public seed: Uint8Array;

public voteID: Uint8Array;

public keyDilution: bigint;

public constructor(params: {
address: Uint8Array;
proof: HeartbeatProof;
seed: Uint8Array;
voteID: Uint8Array;
keyDilution: bigint;
}) {
this.address = params.address;
this.proof = params.proof;
this.seed = params.seed;
this.voteID = params.voteID;
this.keyDilution = params.keyDilution;
}

// eslint-disable-next-line class-methods-use-this
public getEncodingSchema(): Schema {
return Heartbeat.encodingSchema;
}

public toEncodingData(): Map<string, unknown> {
return new Map<string, unknown>([
['a', this.address],
['prf', this.proof.toEncodingData()],
['sd', this.seed],
['vid', this.voteID],
['kd', this.keyDilution],
]);
}

public static fromEncodingData(data: unknown): Heartbeat {
if (!(data instanceof Map)) {
throw new Error(`Invalid decoded Heartbeat: ${data}`);
}
return new Heartbeat({
address: data.get('a'),
proof: HeartbeatProof.fromEncodingData(data.get('prf')),
seed: data.get('sd'),
voteID: data.get('vid'),
keyDilution: data.get('kd'),
});
}
}
42 changes: 42 additions & 0 deletions src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ import {
KeyRegistrationTransactionParams,
ApplicationCallTransactionParams,
StateProofTransactionParams,
HeartbeatTransactionParams,
} from './types/transactions/base.js';
import { StateProof, StateProofMessage } from './stateproof.js';
import { Heartbeat, HeartbeatProof } from './heartbeat.js';
import * as utils from './utils/utils.js';

const ALGORAND_TRANSACTION_LENGTH = 52;
Expand Down Expand Up @@ -248,6 +250,14 @@ export interface StateProofTransactionFields {
readonly message?: StateProofMessage;
}

export interface HeartbeatTransactionFields {
readonly hbAddress: Address;
readonly hbProof: HeartbeatProof;
readonly hbSeed: Uint8Array;
readonly hbVoteID: Uint8Array;
readonly hbKeyDilution: bigint;
}

/**
* Transaction enables construction of Algorand transactions
* */
Expand Down Expand Up @@ -438,6 +448,8 @@ export class Transaction implements encoding.Encodable {
key: 'spmsg',
valueSchema: new OptionalSchema(StateProofMessage.encodingSchema),
},
// Heartbeat
{ key: 'hb', valueSchema: new OptionalSchema(Heartbeat.encodingSchema) },
])
);

Expand Down Expand Up @@ -466,6 +478,7 @@ export class Transaction implements encoding.Encodable {
public readonly assetFreeze?: AssetFreezeTransactionFields;
public readonly applicationCall?: ApplicationTransactionFields;
public readonly stateProof?: StateProofTransactionFields;
public readonly heartbeat?: HeartbeatTransactionFields;

constructor(params: TransactionParams) {
if (!isTransactionType(params.type)) {
Expand Down Expand Up @@ -506,6 +519,7 @@ export class Transaction implements encoding.Encodable {
if (params.assetFreezeParams) fieldsPresent.push(TransactionType.afrz);
if (params.appCallParams) fieldsPresent.push(TransactionType.appl);
if (params.stateProofParams) fieldsPresent.push(TransactionType.stpf);
if (params.heartbeatParams) fieldsPresent.push(TransactionType.hb);

if (fieldsPresent.length !== 1) {
throw new Error(
Expand Down Expand Up @@ -701,6 +715,16 @@ export class Transaction implements encoding.Encodable {
};
}

if (params.heartbeatParams) {
this.heartbeat = {
hbAddress: ensureAddress(params.heartbeatParams.hbAddress),
hbProof: params.heartbeatParams.hbProof,
hbSeed: ensureUint8Array(params.heartbeatParams.hbSeed),
hbVoteID: ensureUint8Array(params.heartbeatParams.hbVoteID),
hbKeyDilution: utils.ensureUint64(params.heartbeatParams.hbKeyDilution),
};
}

// Determine fee
this.fee = utils.ensureUint64(params.suggestedParams.fee);

Expand Down Expand Up @@ -842,6 +866,15 @@ export class Transaction implements encoding.Encodable {
return data;
}

if (this.heartbeat) {
data.set('a', this.heartbeat.hbAddress);
data.set('prf',this.heartbeat.hbProof.toEncodingData());
data.set('sd', this.heartbeat.hbSeed);
data.set('vid', this.heartbeat.hbVoteID);
data.set('kd', this.heartbeat.hbKeyDilution);
return data;
}

throw new Error(`Unexpected transaction type: ${this.type}`);
}

Expand Down Expand Up @@ -1006,6 +1039,15 @@ export class Transaction implements encoding.Encodable {
: undefined,
};
params.stateProofParams = stateProofParams;
} else if (params.type === TransactionType.hb) {
const heartbeatParams: HeartbeatTransactionParams = {
hbAddress: data.get('a'),
hbProof: HeartbeatProof.fromEncodingData(data.get('prf')),
hbSeed: data.get('sd'),
hbVoteID: data.get('vid'),
hbKeyDilution: data.get('kd'),
};
params.heartbeatParams = heartbeatParams;
} else {
const exhaustiveCheck: never = params.type;
throw new Error(`Unexpected transaction type: ${exhaustiveCheck}`);
Expand Down
46 changes: 45 additions & 1 deletion src/types/transactions/base.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Address } from '../../encoding/address.js';
import { StateProof, StateProofMessage } from '../../stateproof.js';
import { HeartbeatProof } from '../../heartbeat.js';

/**
* Enum for application transaction types.
Expand Down Expand Up @@ -38,6 +39,12 @@ export enum TransactionType {
* State proof transaction
*/
stpf = 'stpf',

/**
* Heartbeat transaction
*/
hb = 'hb',

}

/**
Expand All @@ -53,7 +60,8 @@ export function isTransactionType(s: string): s is TransactionType {
s === TransactionType.axfer ||
s === TransactionType.afrz ||
s === TransactionType.appl ||
s === TransactionType.stpf
s === TransactionType.stpf ||
s === TransactionType.hb
);
}

Expand Down Expand Up @@ -466,6 +474,37 @@ export interface StateProofTransactionParams {
message?: StateProofMessage;
}

/**
* Contains heartbeat transaction parameters.
*/
export interface HeartbeatTransactionParams {
/*
* Account address this txn is proving onlineness for
*/
hbAddress: Address;

/**
* Signature using HeartbeatAddress's partkey, thereby showing it is online.
*/
hbProof: HeartbeatProof;

/**
* The block seed for the this transaction's firstValid block.
*/
hbSeed: Uint8Array;

/**
* Must match the hbAddress account's current VoteID
*/
hbVoteID: Uint8Array;

/**
* Must match hbAddress account's current KeyDilution.
*/
hbKeyDilution: bigint;
}


/**
* A full list of all available transaction parameters
*
Expand Down Expand Up @@ -540,4 +579,9 @@ export interface TransactionParams {
* State proof transaction parameters. Only set if type is TransactionType.stpf
*/
stateProofParams?: StateProofTransactionParams;

/**
* Heartbeat transaction parameters. Only set if type is TransactionType.hb
*/
heartbeatParams?: HeartbeatTransactionParams;
}

0 comments on commit a0b648b

Please sign in to comment.