From a0b648bd2bc580b4887d53cede8d5e46c4341ad6 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy Date: Tue, 7 Jan 2025 17:25:07 -0500 Subject: [PATCH 1/5] Add heartbeat txn support --- src/heartbeat.ts | 167 +++++++++++++++++++++++++++++++++ src/transaction.ts | 42 +++++++++ src/types/transactions/base.ts | 46 ++++++++- 3 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 src/heartbeat.ts diff --git a/src/heartbeat.ts b/src/heartbeat.ts new file mode 100644 index 000000000..56cb014ff --- /dev/null +++ b/src/heartbeat.ts @@ -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 { + return new Map([ + ['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 { + return new Map([ + ['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'), + }); + } +} diff --git a/src/transaction.ts b/src/transaction.ts index b27b63617..2cd710031 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -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; @@ -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 * */ @@ -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) }, ]) ); @@ -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)) { @@ -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( @@ -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); @@ -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}`); } @@ -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}`); diff --git a/src/types/transactions/base.ts b/src/types/transactions/base.ts index 277c9a3dd..b50b78fdb 100644 --- a/src/types/transactions/base.ts +++ b/src/types/transactions/base.ts @@ -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. @@ -38,6 +39,12 @@ export enum TransactionType { * State proof transaction */ stpf = 'stpf', + + /** + * Heartbeat transaction + */ + hb = 'hb', + } /** @@ -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 ); } @@ -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 * @@ -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; } From aaa603d6e012469302f84e69d575738ec67ab4e2 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy Date: Wed, 8 Jan 2025 16:10:07 -0500 Subject: [PATCH 2/5] fix linter --- src/heartbeat.ts | 2 +- src/transaction.ts | 16 ++++++++-------- src/types/transactions/base.ts | 2 -- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/heartbeat.ts b/src/heartbeat.ts index 56cb014ff..647fde1b5 100644 --- a/src/heartbeat.ts +++ b/src/heartbeat.ts @@ -8,7 +8,6 @@ import { allOmitEmpty, } from './encoding/schema/index.js'; - export class HeartbeatProof implements Encodable { public static readonly encodingSchema = new NamedMapSchema( allOmitEmpty([ @@ -113,6 +112,7 @@ export class Heartbeat implements Encodable { }, ]) ); + public address: Uint8Array; public proof: HeartbeatProof; diff --git a/src/transaction.ts b/src/transaction.ts index 2cd710031..e8d81c093 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -715,7 +715,7 @@ export class Transaction implements encoding.Encodable { }; } - if (params.heartbeatParams) { + if (params.heartbeatParams) { this.heartbeat = { hbAddress: ensureAddress(params.heartbeatParams.hbAddress), hbProof: params.heartbeatParams.hbProof, @@ -867,13 +867,13 @@ export class Transaction implements encoding.Encodable { } 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; - } + 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}`); } diff --git a/src/types/transactions/base.ts b/src/types/transactions/base.ts index b50b78fdb..ed6bb56c0 100644 --- a/src/types/transactions/base.ts +++ b/src/types/transactions/base.ts @@ -44,7 +44,6 @@ export enum TransactionType { * Heartbeat transaction */ hb = 'hb', - } /** @@ -504,7 +503,6 @@ export interface HeartbeatTransactionParams { hbKeyDilution: bigint; } - /** * A full list of all available transaction parameters * From 2e0c7486a0e9b4d238bcffc4ebfda742f4036d0b Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy Date: Wed, 8 Jan 2025 18:18:50 -0500 Subject: [PATCH 3/5] tests: add golden heartbeat test --- src/transaction.ts | 26 +++++--------------------- src/types/transactions/base.ts | 26 +++----------------------- tests/5.Transaction.ts | 24 ++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 44 deletions(-) diff --git a/src/transaction.ts b/src/transaction.ts index e8d81c093..ffcc1f0a4 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -33,7 +33,7 @@ import { HeartbeatTransactionParams, } from './types/transactions/base.js'; import { StateProof, StateProofMessage } from './stateproof.js'; -import { Heartbeat, HeartbeatProof } from './heartbeat.js'; +import { Heartbeat } from './heartbeat.js'; import * as utils from './utils/utils.js'; const ALGORAND_TRANSACTION_LENGTH = 52; @@ -251,11 +251,7 @@ export interface StateProofTransactionFields { } export interface HeartbeatTransactionFields { - readonly hbAddress: Address; - readonly hbProof: HeartbeatProof; - readonly hbSeed: Uint8Array; - readonly hbVoteID: Uint8Array; - readonly hbKeyDilution: bigint; + readonly heartbeat: Heartbeat; } /** @@ -717,11 +713,7 @@ 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), + heartbeat: params.heartbeatParams.heartbeat, }; } @@ -867,11 +859,7 @@ export class Transaction implements encoding.Encodable { } 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); + data.set('hb', this.heartbeat.heartbeat.toEncodingData()); return data; } @@ -1041,11 +1029,7 @@ export class Transaction implements encoding.Encodable { 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'), + heartbeat: Heartbeat.fromEncodingData(data.get('hb')), }; params.heartbeatParams = heartbeatParams; } else { diff --git a/src/types/transactions/base.ts b/src/types/transactions/base.ts index ed6bb56c0..8c59070d4 100644 --- a/src/types/transactions/base.ts +++ b/src/types/transactions/base.ts @@ -1,6 +1,6 @@ import { Address } from '../../encoding/address.js'; import { StateProof, StateProofMessage } from '../../stateproof.js'; -import { HeartbeatProof } from '../../heartbeat.js'; +import { Heartbeat } from '../../heartbeat.js'; /** * Enum for application transaction types. @@ -478,29 +478,9 @@ export interface StateProofTransactionParams { */ export interface HeartbeatTransactionParams { /* - * Account address this txn is proving onlineness for + * Heartbeat transaction fields */ - 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; + heartbeat: Heartbeat; } /** diff --git a/tests/5.Transaction.ts b/tests/5.Transaction.ts index a516a1466..6dc48cb7b 100644 --- a/tests/5.Transaction.ts +++ b/tests/5.Transaction.ts @@ -1158,6 +1158,30 @@ describe('Sign', () => { assert.deepStrictEqual(reencRep, encRep); }); + it.only('should correctly serialize and deserialize heartbeat transaction', () => { + const golden = algosdk.base64ToBytes( + 'gqRsc2lngaFsxAYLMSAyAxKjdHhuhqJmdmqiZ2jEIP9SQzAGyec/v8omzEOW3/GIM+a7bvPaU5D/ohX7qjFtomhihaFhxCBsU6oqjVx2U65owbsX9/6N7/YCmul+O3liZ0fO2L75/KJrZGSjcHJmhaFwxCAM1TyIrIbgm+yPLT9so6VDI3rKl33t4c4RSGJv6G12eaNwMXPEQBETln14zJzQ1Mb/SNjmDNl0fyQ4DPBQZML8iTEbhqBj+YDAgpNSEduWj7OuVkCSQMq4N/Er/+2HfKUHu//spgOicDLEIB9c5n7WgG+5aOdjfBmuxH3z4TYiQzDVYKjBLhv4IkNfo3Ayc8RAeKpQ+o/GJyGCH0I4f9luN0i7BPXlMlaJAuXLX5Ng8DTN0vtZtztjqYfkwp1cVOYPu+Fce3aIdJHVoUDaJaMIDqFzxEBQN41y5zAZhYHQWf2wWF6CGboqQk6MxDcQ76zXHvVtzrAPUWXZDt4IB8Ha1z+54Hc6LmEoG090pk0IYs+jLN8HonNkxCCPVPjiD5O7V0c3P/SVsHmED7slwllta7c92WiKwnvgoqN2aWTEIHBy8sOi/V0YKXJw8VtW40MbqhtUyO9HC9m/haf84xiGomx2dKNzbmTEIDAp2wPDnojyy8tTgb3sMH++26D5+l7nHZmyRvzFfLsOpHR5cGWiaGI=' + ); + + const decTxn = algosdk.decodeMsgpack(golden, algosdk.SignedTransaction); + const prepTxn = algosdk.SignedTransaction.encodingSchema.prepareMsgpack( + decTxn.toEncodingData() + ); + assert.ok(prepTxn instanceof Map && prepTxn.has('txn')); + + const reencRep = algosdk.encodeMsgpack(decTxn); + assert.deepStrictEqual(reencRep, golden); + const hbAddress = + 'NRJ2UKUNLR3FHLTIYG5RP576RXX7MAU25F7DW6LCM5D45WF67H6EFQMWNM'; + + assert.deepStrictEqual(decTxn.txn.type, algosdk.TransactionType.hb); + assert.deepStrictEqual( + decTxn.txn.heartbeat?.heartbeat.address.toString(), + hbAddress + ); + assert.deepStrictEqual(decTxn.txn.heartbeat?.heartbeat.keyDilution, 100n); + }); + it('reserializes correctly no genesis ID', () => { const expectedTxn = new algosdk.Transaction({ type: algosdk.TransactionType.pay, From a1aa79a395ef87833e6271b8d99fcf8f20de911a Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy Date: Thu, 9 Jan 2025 13:57:22 -0500 Subject: [PATCH 4/5] implement cross sdk tests --- src/heartbeat.ts | 5 ++-- tests/cucumber/steps/steps.js | 48 ++++++++++++++++++++++++++++++++++- tests/cucumber/unit.tags | 3 +++ 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/heartbeat.ts b/src/heartbeat.ts index 647fde1b5..90404473b 100644 --- a/src/heartbeat.ts +++ b/src/heartbeat.ts @@ -1,3 +1,4 @@ +import { Address } from './encoding/address.js'; import { Encodable, Schema } from './encoding/encoding.js'; import { AddressSchema, @@ -113,7 +114,7 @@ export class Heartbeat implements Encodable { ]) ); - public address: Uint8Array; + public address: Address; public proof: HeartbeatProof; @@ -124,7 +125,7 @@ export class Heartbeat implements Encodable { public keyDilution: bigint; public constructor(params: { - address: Uint8Array; + address: Address; proof: HeartbeatProof; seed: Uint8Array; voteID: Uint8Array; diff --git a/tests/cucumber/steps/steps.js b/tests/cucumber/steps/steps.js index 2efa21dd6..5dca2edf4 100644 --- a/tests/cucumber/steps/steps.js +++ b/tests/cucumber/steps/steps.js @@ -2297,7 +2297,17 @@ module.exports = function getSteps(options) { let anyBlockResponse; When('we make any Get Block call', async function () { - anyBlockResponse = await doOrDoRaw(this.v2Client.block(1)); + const req = this.v2Client.block(1); + if (responseFormat === 'json') { + // for json responses, we need to set the format query param and provide a custom decoder + // because the default block request only supports msgpack + req.query.format = responseFormat; + req.prepare = (response) => { + const body = new TextDecoder().decode(response.body); + return algosdk.decodeJSON(body, algosdk.modelsv2.BlockResponse); + }; + } + anyBlockResponse = await doOrDoRaw(req); }); Then( @@ -2314,6 +2324,23 @@ module.exports = function getSteps(options) { } ); + Then( + 'the parsed Get Block response should have heartbeat address {string}', + (hbAddress) => { + // console.log(anyBlockResponse.block.payset[0].signedTxn.signedTxn.txn.heartbeat); + // console.log(anyBlockResponse.block.payset[0].signedTxn.signedTxn.txn.heartbeat.heartbeat.address); + // console.log(anyBlockResponse.block.payset[0].signedTxn.signedTxn.txn.heartbeat.heartbeat.proof); + // console.log(anyBlockResponse.block.payset[0].signedTxn.signedTxn.txn.heartbeat.heartbeat.seed); + assert.ok( + anyBlockResponse.block.payset[0].signedTxn.signedTxn.txn.heartbeat + .heartbeat.address instanceof algosdk.Address + ); + const hbAddressString = + anyBlockResponse.block.payset[0].signedTxn.signedTxn.txn.heartbeat.heartbeat.address.toString(); + assert.strictEqual(hbAddress, hbAddressString); + } + ); + let anySuggestedTransactionsResponse; When('we make any Suggested Transaction Parameters call', async function () { @@ -3071,6 +3098,25 @@ module.exports = function getSteps(options) { } ); + Then( + 'the parsed SearchForTransactions response should be valid on round {int} and the array should be of len {int} and the element at index {int} should have hbaddress {string}', + (round, length, idx, hbAddress) => { + assert.strictEqual( + anySearchForTransactionsResponse.currentRound, + BigInt(round) + ); + assert.strictEqual( + anySearchForTransactionsResponse.transactions.length, + length + ); + assert.strictEqual( + anySearchForTransactionsResponse.transactions[idx].heartbeatTransaction + .hbAddress, + hbAddress + ); + } + ); + let anySearchForAssetsResponse; When('we make any SearchForAssets call', async function () { diff --git a/tests/cucumber/unit.tags b/tests/cucumber/unit.tags index 48232f779..c58157c40 100644 --- a/tests/cucumber/unit.tags +++ b/tests/cucumber/unit.tags @@ -2,6 +2,8 @@ @unit.abijson.byname @unit.algod @unit.algod.ledger_refactoring +@unit.algod.heartbeat +@unit.algod.heartbeat.msgp @unit.applications @unit.applications.boxes @unit.atomic_transaction_composer @@ -13,6 +15,7 @@ @unit.indexer @unit.indexer.ledger_refactoring @unit.indexer.logs +@unit.indexer.heartbeat @unit.offline @unit.program_sanity_check @unit.ready From 43c528855bc470491907c8c868a1316d21fa55c6 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy Date: Thu, 9 Jan 2025 16:01:41 -0500 Subject: [PATCH 5/5] fix double heartbeat.heartbeat field in transaction object --- src/transaction.ts | 34 +++++++++++++++++++++++++++------- src/types/transactions/base.ts | 26 +++++++++++++++++++++++--- tests/5.Transaction.ts | 6 +++--- tests/cucumber/steps/steps.js | 8 ++------ 4 files changed, 55 insertions(+), 19 deletions(-) diff --git a/src/transaction.ts b/src/transaction.ts index ffcc1f0a4..bd9864ec2 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -33,7 +33,7 @@ import { HeartbeatTransactionParams, } from './types/transactions/base.js'; import { StateProof, StateProofMessage } from './stateproof.js'; -import { Heartbeat } from './heartbeat.js'; +import { Heartbeat, HeartbeatProof } from './heartbeat.js'; import * as utils from './utils/utils.js'; const ALGORAND_TRANSACTION_LENGTH = 52; @@ -251,7 +251,11 @@ export interface StateProofTransactionFields { } export interface HeartbeatTransactionFields { - readonly heartbeat: Heartbeat; + readonly address: Address; + readonly proof: HeartbeatProof; + readonly seed: Uint8Array; + readonly voteID: Uint8Array; + readonly keyDilution: bigint; } /** @@ -712,9 +716,13 @@ export class Transaction implements encoding.Encodable { } if (params.heartbeatParams) { - this.heartbeat = { - heartbeat: params.heartbeatParams.heartbeat, - }; + this.heartbeat = new Heartbeat({ + address: params.heartbeatParams.address, + proof: params.heartbeatParams.proof, + seed: params.heartbeatParams.seed, + voteID: params.heartbeatParams.voteID, + keyDilution: params.heartbeatParams.keyDilution, + }); } // Determine fee @@ -859,7 +867,14 @@ export class Transaction implements encoding.Encodable { } if (this.heartbeat) { - data.set('hb', this.heartbeat.heartbeat.toEncodingData()); + const heartbeat = new Heartbeat({ + address: this.heartbeat.address, + proof: this.heartbeat.proof, + seed: this.heartbeat.seed, + voteID: this.heartbeat.voteID, + keyDilution: this.heartbeat.keyDilution, + }); + data.set('hb', heartbeat.toEncodingData()); return data; } @@ -1028,8 +1043,13 @@ export class Transaction implements encoding.Encodable { }; params.stateProofParams = stateProofParams; } else if (params.type === TransactionType.hb) { + const heartbeat = Heartbeat.fromEncodingData(data.get('hb')); const heartbeatParams: HeartbeatTransactionParams = { - heartbeat: Heartbeat.fromEncodingData(data.get('hb')), + address: heartbeat.address, + proof: heartbeat.proof, + seed: heartbeat.seed, + voteID: heartbeat.voteID, + keyDilution: heartbeat.keyDilution, }; params.heartbeatParams = heartbeatParams; } else { diff --git a/src/types/transactions/base.ts b/src/types/transactions/base.ts index 8c59070d4..577bab228 100644 --- a/src/types/transactions/base.ts +++ b/src/types/transactions/base.ts @@ -1,6 +1,6 @@ import { Address } from '../../encoding/address.js'; import { StateProof, StateProofMessage } from '../../stateproof.js'; -import { Heartbeat } from '../../heartbeat.js'; +import { HeartbeatProof } from '../../heartbeat.js'; /** * Enum for application transaction types. @@ -478,9 +478,29 @@ export interface StateProofTransactionParams { */ export interface HeartbeatTransactionParams { /* - * Heartbeat transaction fields + * Account address this txn is proving onlineness for */ - heartbeat: Heartbeat; + address: Address; + + /** + * Signature using HeartbeatAddress's partkey, thereby showing it is online. + */ + proof: HeartbeatProof; + + /** + * The block seed for the this transaction's firstValid block. + */ + seed: Uint8Array; + + /** + * Must match the hbAddress account's current VoteID + */ + voteID: Uint8Array; + + /** + * Must match hbAddress account's current KeyDilution. + */ + keyDilution: bigint; } /** diff --git a/tests/5.Transaction.ts b/tests/5.Transaction.ts index 6dc48cb7b..4edb45f34 100644 --- a/tests/5.Transaction.ts +++ b/tests/5.Transaction.ts @@ -1158,7 +1158,7 @@ describe('Sign', () => { assert.deepStrictEqual(reencRep, encRep); }); - it.only('should correctly serialize and deserialize heartbeat transaction', () => { + it('should correctly serialize and deserialize heartbeat transaction', () => { const golden = algosdk.base64ToBytes( 'gqRsc2lngaFsxAYLMSAyAxKjdHhuhqJmdmqiZ2jEIP9SQzAGyec/v8omzEOW3/GIM+a7bvPaU5D/ohX7qjFtomhihaFhxCBsU6oqjVx2U65owbsX9/6N7/YCmul+O3liZ0fO2L75/KJrZGSjcHJmhaFwxCAM1TyIrIbgm+yPLT9so6VDI3rKl33t4c4RSGJv6G12eaNwMXPEQBETln14zJzQ1Mb/SNjmDNl0fyQ4DPBQZML8iTEbhqBj+YDAgpNSEduWj7OuVkCSQMq4N/Er/+2HfKUHu//spgOicDLEIB9c5n7WgG+5aOdjfBmuxH3z4TYiQzDVYKjBLhv4IkNfo3Ayc8RAeKpQ+o/GJyGCH0I4f9luN0i7BPXlMlaJAuXLX5Ng8DTN0vtZtztjqYfkwp1cVOYPu+Fce3aIdJHVoUDaJaMIDqFzxEBQN41y5zAZhYHQWf2wWF6CGboqQk6MxDcQ76zXHvVtzrAPUWXZDt4IB8Ha1z+54Hc6LmEoG090pk0IYs+jLN8HonNkxCCPVPjiD5O7V0c3P/SVsHmED7slwllta7c92WiKwnvgoqN2aWTEIHBy8sOi/V0YKXJw8VtW40MbqhtUyO9HC9m/haf84xiGomx2dKNzbmTEIDAp2wPDnojyy8tTgb3sMH++26D5+l7nHZmyRvzFfLsOpHR5cGWiaGI=' ); @@ -1176,10 +1176,10 @@ describe('Sign', () => { assert.deepStrictEqual(decTxn.txn.type, algosdk.TransactionType.hb); assert.deepStrictEqual( - decTxn.txn.heartbeat?.heartbeat.address.toString(), + decTxn.txn.heartbeat?.address.toString(), hbAddress ); - assert.deepStrictEqual(decTxn.txn.heartbeat?.heartbeat.keyDilution, 100n); + assert.deepStrictEqual(decTxn.txn.heartbeat?.keyDilution, 100n); }); it('reserializes correctly no genesis ID', () => { diff --git a/tests/cucumber/steps/steps.js b/tests/cucumber/steps/steps.js index 5dca2edf4..926525436 100644 --- a/tests/cucumber/steps/steps.js +++ b/tests/cucumber/steps/steps.js @@ -2327,16 +2327,12 @@ module.exports = function getSteps(options) { Then( 'the parsed Get Block response should have heartbeat address {string}', (hbAddress) => { - // console.log(anyBlockResponse.block.payset[0].signedTxn.signedTxn.txn.heartbeat); - // console.log(anyBlockResponse.block.payset[0].signedTxn.signedTxn.txn.heartbeat.heartbeat.address); - // console.log(anyBlockResponse.block.payset[0].signedTxn.signedTxn.txn.heartbeat.heartbeat.proof); - // console.log(anyBlockResponse.block.payset[0].signedTxn.signedTxn.txn.heartbeat.heartbeat.seed); assert.ok( anyBlockResponse.block.payset[0].signedTxn.signedTxn.txn.heartbeat - .heartbeat.address instanceof algosdk.Address + .address instanceof algosdk.Address ); const hbAddressString = - anyBlockResponse.block.payset[0].signedTxn.signedTxn.txn.heartbeat.heartbeat.address.toString(); + anyBlockResponse.block.payset[0].signedTxn.signedTxn.txn.heartbeat.address.toString(); assert.strictEqual(hbAddress, hbAddressString); } );