trac-msb 0.2.12 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/package.json +9 -4
  2. package/proto/network/v1/enums/message_type.proto +16 -0
  3. package/proto/network/v1/enums/result_code.proto +84 -0
  4. package/proto/network/v1/messages/broadcast_transaction_request.proto +9 -0
  5. package/proto/network/v1/messages/broadcast_transaction_response.proto +13 -0
  6. package/proto/network/v1/messages/liveness_request.proto +8 -0
  7. package/proto/network/v1/messages/liveness_response.proto +11 -0
  8. package/proto/network/v1/network_message.proto +22 -0
  9. package/rpc/rpc_services.js +22 -4
  10. package/scripts/generate-protobufs.js +37 -12
  11. package/src/config/config.js +26 -5
  12. package/src/config/env.js +25 -11
  13. package/src/core/network/Network.js +73 -36
  14. package/src/core/network/protocols/LegacyProtocol.js +21 -11
  15. package/src/core/network/protocols/NetworkMessages.js +38 -17
  16. package/src/core/network/protocols/ProtocolInterface.js +14 -2
  17. package/src/core/network/protocols/ProtocolSession.js +144 -17
  18. package/src/core/network/protocols/V1Protocol.js +37 -18
  19. package/src/core/network/protocols/connectionPolicies.js +88 -0
  20. package/src/core/network/protocols/legacy/NetworkMessageRouter.js +25 -19
  21. package/src/core/network/protocols/{shared/handlers/base/BaseOperationHandler.js → legacy/handlers/BaseStateOperationHandler.js} +23 -12
  22. package/src/core/network/protocols/legacy/handlers/{GetRequestHandler.js → LegacyGetRequestHandler.js} +6 -6
  23. package/src/core/network/protocols/legacy/handlers/LegacyResponseHandler.js +23 -0
  24. package/src/core/network/protocols/{shared/handlers/RoleOperationHandler.js → legacy/handlers/LegacyRoleOperationHandler.js} +18 -11
  25. package/src/core/network/protocols/{shared/handlers/SubnetworkOperationHandler.js → legacy/handlers/LegacySubnetworkOperationHandler.js} +28 -17
  26. package/src/core/network/protocols/{shared/handlers/TransferOperationHandler.js → legacy/handlers/LegacyTransferOperationHandler.js} +17 -11
  27. package/src/core/network/protocols/shared/errors/SharedValidatorRejectionError.js +27 -0
  28. package/src/core/network/protocols/shared/validators/{PartialBootstrapDeployment.js → PartialBootstrapDeploymentValidator.js} +9 -4
  29. package/src/core/network/protocols/shared/validators/{base/PartialOperation.js → PartialOperationValidator.js} +47 -25
  30. package/src/core/network/protocols/shared/validators/{PartialRoleAccess.js → PartialRoleAccessValidator.js} +51 -17
  31. package/src/core/network/protocols/shared/validators/{PartialTransaction.js → PartialTransactionValidator.js} +21 -7
  32. package/src/core/network/protocols/shared/validators/{PartialTransfer.js → PartialTransferValidator.js} +26 -9
  33. package/src/core/network/protocols/v1/NetworkMessageRouter.js +91 -7
  34. package/src/core/network/protocols/v1/V1ProtocolError.js +91 -0
  35. package/src/core/network/protocols/v1/handlers/V1BaseOperationHandler.js +65 -0
  36. package/src/core/network/protocols/v1/handlers/V1BroadcastTransactionOperationHandler.js +389 -0
  37. package/src/core/network/protocols/v1/handlers/V1LivenessOperationHandler.js +87 -0
  38. package/src/core/network/protocols/v1/validators/V1BaseOperation.js +211 -0
  39. package/src/core/network/protocols/v1/validators/V1BroadcastTransactionRequest.js +26 -0
  40. package/src/core/network/protocols/v1/validators/V1BroadcastTransactionResponse.js +276 -0
  41. package/src/core/network/protocols/v1/validators/V1LivenessRequest.js +15 -0
  42. package/src/core/network/protocols/v1/validators/V1LivenessResponse.js +17 -0
  43. package/src/core/network/protocols/v1/validators/V1ValidationSchema.js +210 -0
  44. package/src/core/network/services/ConnectionManager.js +146 -94
  45. package/src/core/network/services/MessageOrchestrator.js +151 -27
  46. package/src/core/network/services/PendingRequestService.js +172 -0
  47. package/src/core/network/services/TransactionCommitService.js +149 -0
  48. package/src/core/network/services/TransactionPoolService.js +129 -18
  49. package/src/core/network/services/TransactionRateLimiterService.js +52 -34
  50. package/src/core/network/services/ValidatorHealthCheckService.js +127 -0
  51. package/src/core/network/services/ValidatorObserverService.js +18 -26
  52. package/src/core/state/State.js +70 -19
  53. package/src/index.js +5 -4
  54. package/src/messages/network/v1/NetworkMessageBuilder.js +59 -79
  55. package/src/messages/network/v1/NetworkMessageDirector.js +16 -50
  56. package/src/utils/Scheduler.js +0 -8
  57. package/src/utils/constants.js +71 -5
  58. package/src/utils/deepEqualApplyPayload.js +40 -0
  59. package/src/utils/helpers.js +10 -1
  60. package/src/utils/logger.js +25 -0
  61. package/src/utils/normalizers.js +38 -0
  62. package/src/utils/protobuf/networkV1.generated.cjs +2460 -0
  63. package/src/utils/protobuf/operationHelpers.js +24 -3
  64. package/tests/acceptance/v1/account/account.test.mjs +8 -2
  65. package/tests/acceptance/v1/tx/tx.test.mjs +23 -1
  66. package/tests/acceptance/v1/tx-details/tx-details.test.mjs +34 -6
  67. package/tests/fixtures/networkV1.fixtures.js +2 -28
  68. package/tests/helpers/transactionPayloads.mjs +2 -2
  69. package/tests/unit/messages/network/NetworkMessageBuilder.test.js +239 -79
  70. package/tests/unit/messages/network/NetworkMessageDirector.test.js +223 -77
  71. package/tests/unit/network/LegacyNetworkMessageRouter.test.js +54 -0
  72. package/tests/unit/network/ProtocolSession.test.js +127 -0
  73. package/tests/unit/network/networkModule.test.js +4 -1
  74. package/tests/unit/network/services/ConnectionManager.test.js +450 -0
  75. package/tests/unit/network/services/MessageOrchestrator.test.js +445 -0
  76. package/tests/unit/network/services/PendingRequestService.test.js +431 -0
  77. package/tests/unit/network/services/TransactionCommitService.test.js +246 -0
  78. package/tests/unit/network/services/TransactionPoolService.test.js +489 -0
  79. package/tests/unit/network/services/TransactionRateLimiterService.test.js +139 -0
  80. package/tests/unit/network/services/ValidatorHealthCheckService.test.js +115 -0
  81. package/tests/unit/network/services/services.test.js +17 -0
  82. package/tests/unit/network/utils/v1TestUtils.js +153 -0
  83. package/tests/unit/network/v1/NetworkMessageRouterV1.test.js +151 -0
  84. package/tests/unit/network/v1/V1BaseOperation.test.js +356 -0
  85. package/tests/unit/network/v1/V1BroadcastTransactionOperationHandler.test.js +129 -0
  86. package/tests/unit/network/v1/V1BroadcastTransactionRequest.test.js +53 -0
  87. package/tests/unit/network/v1/V1BroadcastTransactionResponse.test.js +512 -0
  88. package/tests/unit/network/v1/V1LivenessRequest.test.js +32 -0
  89. package/tests/unit/network/v1/V1LivenessResponse.test.js +45 -0
  90. package/tests/unit/network/v1/V1ResultCode.test.js +84 -0
  91. package/tests/unit/network/v1/V1ValidationSchema.test.js +13 -0
  92. package/tests/unit/network/v1/connectionPolicies.test.js +49 -0
  93. package/tests/unit/network/v1/handlers/V1BaseOperationHandler.test.js +284 -0
  94. package/tests/unit/network/v1/handlers/V1BroadcastTransactionOperationHandler.test.js +794 -0
  95. package/tests/unit/network/v1/handlers/V1LivenessOperationHandler.test.js +193 -0
  96. package/tests/unit/network/v1/v1.handlers.test.js +15 -0
  97. package/tests/unit/network/v1/v1.test.js +19 -0
  98. package/tests/unit/network/v1/v1ValidationSchema/broadcastTransactionRequest.test.js +119 -0
  99. package/tests/unit/network/v1/v1ValidationSchema/broadcastTransactionResponse.test.js +136 -0
  100. package/tests/unit/network/v1/v1ValidationSchema/common.test.js +308 -0
  101. package/tests/unit/network/v1/v1ValidationSchema/livenessRequest.test.js +90 -0
  102. package/tests/unit/network/v1/v1ValidationSchema/livenessResponse.test.js +133 -0
  103. package/tests/unit/unit.test.js +2 -2
  104. package/tests/unit/utils/deepEqualApplyPayload/deepEqualApplyPayload.test.js +102 -0
  105. package/tests/unit/utils/protobuf/operationHelpers.test.js +2 -4
  106. package/tests/unit/utils/utils.test.js +1 -0
  107. package/.github/workflows/acceptance-tests.yml +0 -38
  108. package/.github/workflows/lint-pr-title.yml +0 -26
  109. package/.github/workflows/publish.yml +0 -33
  110. package/.github/workflows/unit-tests.yml +0 -34
  111. package/proto/network.proto +0 -74
  112. package/src/core/network/protocols/legacy/handlers/ResponseHandler.js +0 -37
  113. package/src/utils/protobuf/network.cjs +0 -840
  114. package/tests/unit/network/ConnectionManager.test.js +0 -191
@@ -1,7 +1,17 @@
1
1
  import applyOperations from './applyOperations.cjs';
2
- import networkV1Operations from './network.cjs';
2
+ import networkV1Generated from './networkV1.generated.cjs';
3
3
  import b4a from 'b4a';
4
4
 
5
+ const networkV1Operations = networkV1Generated.network.v1;
6
+ const NETWORK_TO_OBJECT_OPTIONS = Object.freeze({
7
+ enums: Number,
8
+ longs: Number,
9
+ bytes: Buffer,
10
+ defaults: true,
11
+ arrays: true,
12
+ oneofs: false
13
+ });
14
+
5
15
  /**
6
16
  * Safely encodes an operation using `applyOperations.Operation.encode`.
7
17
  * If the encoding fails (e.g., due to an invalid payload), returns an empty Buffer.
@@ -36,6 +46,14 @@ export const safeDecodeApplyOperation = (payload) => {
36
46
  return null;
37
47
  }
38
48
 
49
+ export const unsafeDecodeApplyOperation= (payload) => {
50
+ return applyOperations.Operation.decode(payload);
51
+ }
52
+
53
+ export const unsafeEncodeApplyOperation = (payload) => {
54
+ return applyOperations.Operation.encode(payload);
55
+ }
56
+
39
57
  export const normalizeIncomingMessage = (message) => {
40
58
  if (!message) return null;
41
59
  if (b4a.isBuffer(message)) {
@@ -51,10 +69,13 @@ export const normalizeIncomingMessage = (message) => {
51
69
  };
52
70
 
53
71
  export const encodeV1networkOperation = (payload) => {
54
- return networkV1Operations.MessageHeader.encode(payload);
72
+ return b4a.from(networkV1Operations.MessageHeader.encode(payload).finish());
55
73
  }
56
74
 
57
75
 
58
76
  export const decodeV1networkOperation = (payload) => {
59
- return networkV1Operations.MessageHeader.decode(payload);
77
+ return networkV1Operations.MessageHeader.toObject(
78
+ networkV1Operations.MessageHeader.decode(payload),
79
+ NETWORK_TO_OBJECT_OPTIONS
80
+ );
60
81
  }
@@ -108,11 +108,17 @@ export const registerAccountTests = (context) => {
108
108
 
109
109
  it("returns 500 on internal error", async () => {
110
110
  const originalGetNodeEntry = context.rpcMsb.state.getNodeEntry
111
+ const failingAddress = randomAddress(context.rpcMsb.config.addressPrefix)
111
112
 
112
- context.rpcMsb.state.getNodeEntry = async () => { throw new Error("test") }
113
+ context.rpcMsb.state.getNodeEntry = async (address) => {
114
+ if (address === failingAddress) {
115
+ throw new Error("test")
116
+ }
117
+ return originalGetNodeEntry.call(context.rpcMsb.state, address)
118
+ }
113
119
 
114
120
  try {
115
- const res = await request(context.server).get(`/v1/account/${context.wallet.address}`)
121
+ const res = await request(context.server).get(`/v1/account/${failingAddress}`)
116
122
  expect(res.statusCode).toBe(500)
117
123
  expect(res.body).toEqual({ error: 'An error occurred processing the request.' })
118
124
  } finally {
@@ -1,5 +1,24 @@
1
1
  import request from "supertest"
2
2
  import { buildRpcSelfTransferPayload, waitForConnection } from "../../../helpers/transactionPayloads.mjs"
3
+ import { sleep } from "../../../../src/utils/helpers.js"
4
+
5
+ const TX_ENDPOINT_TIMEOUT_MS = 4000
6
+ const TX_ENDPOINT_RETRY_INTERVAL_MS = 100
7
+
8
+ const waitForStatusCode = async (requestFactory, expectedStatusCode, timeoutMs = TX_ENDPOINT_TIMEOUT_MS) => {
9
+ const startedAt = Date.now()
10
+ let response = null
11
+
12
+ while ((Date.now() - startedAt) < timeoutMs) {
13
+ response = await requestFactory()
14
+ if (response.statusCode === expectedStatusCode) {
15
+ return response
16
+ }
17
+ await sleep(TX_ENDPOINT_RETRY_INTERVAL_MS)
18
+ }
19
+
20
+ return response
21
+ }
3
22
 
4
23
  export const registerTxTests = (context) => {
5
24
  describe("GET /v1/tx/:hash", () => {
@@ -17,7 +36,10 @@ export const registerTxTests = (context) => {
17
36
  .send(JSON.stringify({ payload }))
18
37
  expect(broadcastRes.statusCode).toBe(200)
19
38
 
20
- const res = await request(context.server).get(`/v1/tx/${txHashHex}`)
39
+ const res = await waitForStatusCode(
40
+ () => request(context.server).get(`/v1/tx/${txHashHex}`),
41
+ 200
42
+ )
21
43
  expect(res.statusCode).toBe(200)
22
44
  expect(res.body).toMatchObject({ txDetails: expect.any(Object) })
23
45
  })
@@ -1,5 +1,24 @@
1
1
  import request from "supertest"
2
2
  import { buildRpcSelfTransferPayload, waitForConnection } from "../../../helpers/transactionPayloads.mjs"
3
+ import { sleep } from "../../../../src/utils/helpers.js"
4
+
5
+ const TX_DETAILS_TIMEOUT_MS = 4000
6
+ const TX_DETAILS_RETRY_INTERVAL_MS = 100
7
+
8
+ const waitForStatusCode = async (requestFactory, expectedStatusCode, timeoutMs = TX_DETAILS_TIMEOUT_MS) => {
9
+ const startedAt = Date.now()
10
+ let response = null
11
+
12
+ while ((Date.now() - startedAt) < timeoutMs) {
13
+ response = await requestFactory()
14
+ if (response.statusCode === expectedStatusCode) {
15
+ return response
16
+ }
17
+ await sleep(TX_DETAILS_RETRY_INTERVAL_MS)
18
+ }
19
+
20
+ return response
21
+ }
3
22
 
4
23
  export const registerTxDetailsTests = (context) => {
5
24
  describe("GET /v1/tx/details", () => {
@@ -17,8 +36,11 @@ export const registerTxDetailsTests = (context) => {
17
36
  .send(JSON.stringify({ payload }))
18
37
  expect(broadcastRes.statusCode).toBe(200)
19
38
 
20
- const resConfirmed = await request(context.server)
21
- .get(`/v1/tx/details/${txHashHex}?confirmed=true`)
39
+ const resConfirmed = await waitForStatusCode(
40
+ () => request(context.server)
41
+ .get(`/v1/tx/details/${txHashHex}?confirmed=true`),
42
+ 200
43
+ )
22
44
  expect(resConfirmed.statusCode).toBe(200)
23
45
 
24
46
  expect(resConfirmed.body).toMatchObject({
@@ -27,8 +49,11 @@ export const registerTxDetailsTests = (context) => {
27
49
  fee: expect.any(String)
28
50
  })
29
51
 
30
- const resUnconfirmed = await request(context.server)
31
- .get(`/v1/tx/details/${txHashHex}?confirmed=false`)
52
+ const resUnconfirmed = await waitForStatusCode(
53
+ () => request(context.server)
54
+ .get(`/v1/tx/details/${txHashHex}?confirmed=false`),
55
+ 200
56
+ )
32
57
  expect(resUnconfirmed.statusCode).toBe(200)
33
58
 
34
59
  expect(resUnconfirmed.body).toMatchObject({
@@ -57,8 +82,11 @@ export const registerTxDetailsTests = (context) => {
57
82
  .send(JSON.stringify({ payload }))
58
83
  expect(broadcastRes.statusCode).toBe(200)
59
84
 
60
- const res = await request(context.server)
61
- .get(`/v1/tx/details/${txHashHex}?confirmed=false`)
85
+ const res = await waitForStatusCode(
86
+ () => request(context.server)
87
+ .get(`/v1/tx/details/${txHashHex}?confirmed=false`),
88
+ 200
89
+ )
62
90
  expect(res.statusCode).toBe(200)
63
91
 
64
92
  expect(res.body).toMatchObject({
@@ -1,32 +1,6 @@
1
1
  import b4a from 'b4a';
2
2
  import { v7 as uuidv7 } from 'uuid';
3
3
  import { NetworkOperationType, ResultCode as NetworkResultCode } from '../../src/utils/constants.js';
4
- import { asAddress } from '../helpers/address.js';
5
-
6
- const payloadValidatorConnectionRequest = {
7
- type: NetworkOperationType.VALIDATOR_CONNECTION_REQUEST,
8
- id: uuidv7(),
9
- timestamp: 123,
10
- validator_connection_request: {
11
- issuer_address: asAddress('36fdaf941de4afe602cbb1e2f56dc582466ef23fad1da55c09fd6dd841cbd117'),
12
- nonce: b4a.from('00', 'hex'),
13
- signature: b4a.from('01', 'hex')
14
- },
15
- capabilities: ['cap:a', 'cap:b']
16
- };
17
-
18
- const payloadValidatorConnectionResponse = {
19
- type: NetworkOperationType.VALIDATOR_CONNECTION_RESPONSE,
20
- id: uuidv7(),
21
- timestamp: 456,
22
- validator_connection_response: {
23
- issuer_address: asAddress('36fdaf941de4afe602cbb1e2f56dc582466ef23fad1da55c09fd6dd841cbd117'),
24
- nonce: b4a.from('02', 'hex'),
25
- signature: b4a.from('03', 'hex'),
26
- result: NetworkResultCode.OK
27
- },
28
- capabilities: ['cap:a']
29
- };
30
4
 
31
5
  const payloadLivenessRequest = {
32
6
  type: NetworkOperationType.LIVENESS_REQUEST,
@@ -70,14 +44,14 @@ const payloadBroadcastTransactionResponse = {
70
44
  broadcast_transaction_response: {
71
45
  nonce: b4a.from('0a', 'hex'),
72
46
  signature: b4a.from('0b', 'hex'),
47
+ proof: b4a.from('0c', 'hex'),
48
+ timestamp: 161719,
73
49
  result: NetworkResultCode.OK
74
50
  },
75
51
  capabilities: ['cap:b']
76
52
  };
77
53
 
78
54
  export default {
79
- payloadValidatorConnectionRequest,
80
- payloadValidatorConnectionResponse,
81
55
  payloadLivenessRequest,
82
56
  payloadLivenessResponse,
83
57
  payloadBroadcastTransactionRequest,
@@ -23,10 +23,10 @@ export const waitForConnection = async node => {
23
23
 
24
24
  /**
25
25
  * Build a base64-encoded transfer payload and matching tx hash
26
- * that are compatible with MSB's PartialTransfer validator.
26
+ * that are compatible with MSB's PartialTransferValidator validator.
27
27
  *
28
28
  * This helper mirrors the hashing/signing logic used by
29
- * PartialOperation.validateSignature, so that tests broadcast
29
+ * PartialOperationValidator.validateSignature, so that tests broadcast
30
30
  * transactions the node will accept without touching consensus code.
31
31
  *
32
32
  * @param {object} context - General context
@@ -17,7 +17,6 @@ import {
17
17
  idToBuffer,
18
18
  timestampToBuffer
19
19
  } from '../../../../src/utils/buffer.js';
20
- import { addressToBuffer } from '../../../../src/core/state/utils/address.js';
21
20
  import { config } from '../../../helpers/config.js';
22
21
  import { asAddress } from '../../../helpers/address.js';
23
22
  import { testKeyPair1 } from '../../../fixtures/apply.fixtures.js';
@@ -38,81 +37,6 @@ function uniqueResultCodes() {
38
37
  return [...new Set(Object.values(NetworkResultCode))].sort((a, b) => a - b);
39
38
  }
40
39
 
41
- test('NetworkMessageBuilder builds validator connection request and verifies signature', async t => {
42
- const wallet = createWallet();
43
- const builder = new NetworkMessageBuilder(wallet, config);
44
-
45
- const id = uuidv7();
46
- const caps = ['cap:b', 'cap:a'];
47
-
48
- await builder
49
- .setType(NetworkOperationType.VALIDATOR_CONNECTION_REQUEST)
50
- .setId(id)
51
- .setTimestamp()
52
- .setIssuerAddress(wallet.address)
53
- .setCapabilities(caps)
54
- .buildPayload();
55
-
56
- const payload = builder.getResult();
57
- t.is(payload.type, NetworkOperationType.VALIDATOR_CONNECTION_REQUEST);
58
- t.is(payload.id, id);
59
- t.alike(payload.capabilities, caps);
60
- t.ok(b4a.isBuffer(payload.validator_connection_request.nonce));
61
- t.ok(b4a.isBuffer(payload.validator_connection_request.signature));
62
-
63
- const message = createMessage(
64
- payload.type,
65
- idToBuffer(id),
66
- timestampToBuffer(payload.timestamp),
67
- addressToBuffer(wallet.address, config.addressPrefix),
68
- payload.validator_connection_request.nonce,
69
- encodeCapabilities(caps)
70
- );
71
- const hash = await PeerWallet.blake3(message);
72
- t.ok(wallet.verify(payload.validator_connection_request.signature, hash, wallet.publicKey));
73
-
74
- const roundTrip = decodeV1networkOperation(encodeV1networkOperation(payload));
75
- t.is(roundTrip.type, NetworkOperationType.VALIDATOR_CONNECTION_REQUEST);
76
- });
77
-
78
- test('NetworkMessageBuilder iterates validator connection response ResultCode values', async t => {
79
- const wallet = createWallet();
80
- const builder = new NetworkMessageBuilder(wallet, config);
81
- const id = uuidv7();
82
- const otherAddress = asAddress('36fdaf941de4afe602cbb1e2f56dc582466ef23fad1da55c09fd6dd841cbd117');
83
- const caps = ['cap:b', 'cap:a'];
84
-
85
- for (const code of uniqueResultCodes()) {
86
- await builder
87
- .setType(NetworkOperationType.VALIDATOR_CONNECTION_RESPONSE)
88
- .setId(id)
89
- .setTimestamp()
90
- .setIssuerAddress(otherAddress)
91
- .setCapabilities(caps)
92
- .setResultCode(code)
93
- .buildPayload();
94
-
95
- const payload = builder.getResult();
96
- t.is(payload.type, NetworkOperationType.VALIDATOR_CONNECTION_RESPONSE);
97
- t.is(payload.validator_connection_response.result, code);
98
-
99
- const msg = createMessage(
100
- payload.type,
101
- idToBuffer(payload.id),
102
- timestampToBuffer(payload.timestamp),
103
- addressToBuffer(otherAddress, config.addressPrefix),
104
- payload.validator_connection_response.nonce,
105
- safeWriteUInt32BE(code, 0),
106
- encodeCapabilities(caps)
107
- );
108
- const hash = await PeerWallet.blake3(msg);
109
- t.ok(wallet.verify(payload.validator_connection_response.signature, hash, wallet.publicKey));
110
-
111
- const decoded = decodeV1networkOperation(encodeV1networkOperation(payload));
112
- t.is(decoded.validator_connection_response.result, code);
113
- }
114
- });
115
-
116
40
  test('NetworkMessageBuilder iterates liveness response ResultCode values', async t => {
117
41
  const wallet = createWallet();
118
42
  const builder = new NetworkMessageBuilder(wallet, config);
@@ -125,7 +49,6 @@ test('NetworkMessageBuilder iterates liveness response ResultCode values', async
125
49
  .setType(NetworkOperationType.LIVENESS_RESPONSE)
126
50
  .setId(id)
127
51
  .setTimestamp()
128
- .setData(data)
129
52
  .setCapabilities(caps)
130
53
  .setResultCode(code)
131
54
  .buildPayload();
@@ -156,13 +79,11 @@ test('NetworkMessageBuilder builds liveness request and verifies signature (data
156
79
 
157
80
  const id = uuidv7();
158
81
  const caps = ['cap:b', 'cap:a'];
159
- const data = b4a.from('ping', 'utf8');
160
82
 
161
83
  await builder
162
84
  .setType(NetworkOperationType.LIVENESS_REQUEST)
163
85
  .setId(id)
164
86
  .setTimestamp()
165
- .setData(data)
166
87
  .setCapabilities(caps)
167
88
  .buildPayload();
168
89
 
@@ -187,36 +108,275 @@ test('NetworkMessageBuilder iterates broadcast transaction response ResultCode v
187
108
  const builder = new NetworkMessageBuilder(wallet, config);
188
109
  const id = uuidv7();
189
110
  const caps = ['cap:b', 'cap:a'];
111
+ const proof = b4a.from('deadbeef', 'hex');
112
+ const timestamp = Date.now();
113
+ const emptyProof = b4a.alloc(0);
190
114
 
191
115
  for (const code of uniqueResultCodes()) {
116
+ const includeProof = code === NetworkResultCode.OK;
117
+ const proofUnavailable = code === NetworkResultCode.TX_ACCEPTED_PROOF_UNAVAILABLE;
118
+ const responseProof = includeProof ? proof : emptyProof;
119
+ const responseTimestampLedger = includeProof || proofUnavailable ? timestamp : 0;
192
120
  await builder
193
121
  .setType(NetworkOperationType.BROADCAST_TRANSACTION_RESPONSE)
194
122
  .setId(id)
195
123
  .setTimestamp()
196
124
  .setCapabilities(caps)
125
+ .setProof(responseProof)
126
+ .setTimestampLedger(responseTimestampLedger)
197
127
  .setResultCode(code)
198
128
  .buildPayload();
199
129
 
200
130
  const payload = builder.getResult();
201
131
  t.is(payload.type, NetworkOperationType.BROADCAST_TRANSACTION_RESPONSE);
202
132
  t.is(payload.broadcast_transaction_response.result, code);
133
+ t.alike(payload.broadcast_transaction_response.proof, responseProof);
134
+ t.is(payload.broadcast_transaction_response.timestamp, responseTimestampLedger);
203
135
 
204
136
  const msg = createMessage(
205
137
  payload.type,
206
138
  idToBuffer(payload.id),
207
139
  timestampToBuffer(payload.timestamp),
208
140
  payload.broadcast_transaction_response.nonce,
141
+ responseProof,
142
+ timestampToBuffer(responseTimestampLedger),
209
143
  safeWriteUInt32BE(code, 0),
210
144
  encodeCapabilities(caps)
211
145
  );
146
+
212
147
  const hash = await PeerWallet.blake3(msg);
213
148
  t.ok(wallet.verify(payload.broadcast_transaction_response.signature, hash, wallet.publicKey));
214
149
 
215
150
  const decoded = decodeV1networkOperation(encodeV1networkOperation(payload));
216
151
  t.is(decoded.broadcast_transaction_response.result, code);
152
+ t.alike(decoded.broadcast_transaction_response.proof, responseProof);
153
+ t.is(decoded.broadcast_transaction_response.timestamp, responseTimestampLedger);
217
154
  }
218
155
  });
219
156
 
157
+ test('NetworkMessageBuilder builds broadcast transaction response with proof and timestamp', async t => {
158
+ const wallet = createWallet();
159
+ const builder = new NetworkMessageBuilder(wallet, config);
160
+ const id = uuidv7();
161
+ const caps = ['cap:b', 'cap:a'];
162
+ const proof = b4a.from('deadbeef', 'hex');
163
+ const timestamp = Date.now();
164
+
165
+ await builder
166
+ .setType(NetworkOperationType.BROADCAST_TRANSACTION_RESPONSE)
167
+ .setId(id)
168
+ .setTimestamp()
169
+ .setCapabilities(caps)
170
+ .setProof(proof)
171
+ .setTimestampLedger(timestamp)
172
+ .setResultCode(NetworkResultCode.OK)
173
+ .buildPayload();
174
+
175
+ const payload = builder.getResult();
176
+ t.alike(payload.broadcast_transaction_response.proof, proof);
177
+ t.is(payload.broadcast_transaction_response.timestamp, timestamp);
178
+
179
+ const msg = createMessage(
180
+ payload.type,
181
+ idToBuffer(payload.id),
182
+ timestampToBuffer(payload.timestamp),
183
+ payload.broadcast_transaction_response.nonce,
184
+ proof,
185
+ timestampToBuffer(timestamp),
186
+ safeWriteUInt32BE(NetworkResultCode.OK, 0),
187
+ encodeCapabilities(caps)
188
+ );
189
+ const hash = await PeerWallet.blake3(msg);
190
+ t.ok(wallet.verify(payload.broadcast_transaction_response.signature, hash, wallet.publicKey));
191
+
192
+ const decoded = decodeV1networkOperation(encodeV1networkOperation(payload));
193
+ t.alike(decoded.broadcast_transaction_response.proof, proof);
194
+ t.is(decoded.broadcast_transaction_response.timestamp, timestamp);
195
+ });
196
+
197
+ test('NetworkMessageBuilder rejects OK response when proof is provided without timestamp', async t => {
198
+ const wallet = createWallet();
199
+ const builder = new NetworkMessageBuilder(wallet, config);
200
+ const id = uuidv7();
201
+ const caps = ['cap:b', 'cap:a'];
202
+ const proof = b4a.from('deadbeef', 'hex');
203
+
204
+ await t.exception(
205
+ () =>
206
+ builder
207
+ .setType(NetworkOperationType.BROADCAST_TRANSACTION_RESPONSE)
208
+ .setId(id)
209
+ .setTimestamp()
210
+ .setCapabilities(caps)
211
+ .setProof(proof)
212
+ .setResultCode(NetworkResultCode.OK)
213
+ .buildPayload(),
214
+ errorMessageIncludes('Result code OK requires non-empty proof and timestamp > 0.')
215
+ );
216
+ });
217
+
218
+ test('NetworkMessageBuilder rejects OK response when timestamp is provided without proof', async t => {
219
+ const wallet = createWallet();
220
+ const builder = new NetworkMessageBuilder(wallet, config);
221
+ const id = uuidv7();
222
+ const caps = ['cap:b', 'cap:a'];
223
+ const timestamp = Date.now();
224
+
225
+ await t.exception(
226
+ () =>
227
+ builder
228
+ .setType(NetworkOperationType.BROADCAST_TRANSACTION_RESPONSE)
229
+ .setId(id)
230
+ .setTimestamp()
231
+ .setCapabilities(caps)
232
+ .setTimestampLedger(timestamp)
233
+ .setResultCode(NetworkResultCode.OK)
234
+ .buildPayload(),
235
+ errorMessageIncludes('Result code OK requires non-empty proof and timestamp > 0.')
236
+ );
237
+ });
238
+
239
+ test('NetworkMessageBuilder allows TX_ACCEPTED_PROOF_UNAVAILABLE response with timestamp and empty proof', async t => {
240
+ const wallet = createWallet();
241
+ const builder = new NetworkMessageBuilder(wallet, config);
242
+ const id = uuidv7();
243
+ const caps = ['cap:b', 'cap:a'];
244
+ const timestamp = Date.now();
245
+ const emptyProof = b4a.alloc(0);
246
+
247
+ await builder
248
+ .setType(NetworkOperationType.BROADCAST_TRANSACTION_RESPONSE)
249
+ .setId(id)
250
+ .setTimestamp()
251
+ .setCapabilities(caps)
252
+ .setProof(emptyProof)
253
+ .setTimestampLedger(timestamp)
254
+ .setResultCode(NetworkResultCode.TX_ACCEPTED_PROOF_UNAVAILABLE)
255
+ .buildPayload();
256
+
257
+ const payload = builder.getResult();
258
+ t.alike(payload.broadcast_transaction_response.proof, emptyProof);
259
+ t.is(payload.broadcast_transaction_response.timestamp, timestamp);
260
+ t.is(payload.broadcast_transaction_response.result, NetworkResultCode.TX_ACCEPTED_PROOF_UNAVAILABLE);
261
+
262
+ const msg = createMessage(
263
+ payload.type,
264
+ idToBuffer(payload.id),
265
+ timestampToBuffer(payload.timestamp),
266
+ payload.broadcast_transaction_response.nonce,
267
+ emptyProof,
268
+ timestampToBuffer(timestamp),
269
+ safeWriteUInt32BE(NetworkResultCode.TX_ACCEPTED_PROOF_UNAVAILABLE, 0),
270
+ encodeCapabilities(caps)
271
+ );
272
+ const hash = await PeerWallet.blake3(msg);
273
+ t.ok(wallet.verify(payload.broadcast_transaction_response.signature, hash, wallet.publicKey));
274
+
275
+ const decoded = decodeV1networkOperation(encodeV1networkOperation(payload));
276
+ t.is(decoded.broadcast_transaction_response.timestamp, timestamp);
277
+ t.is(decoded.broadcast_transaction_response.result, NetworkResultCode.TX_ACCEPTED_PROOF_UNAVAILABLE);
278
+ });
279
+
280
+ test('NetworkMessageBuilder rejects OK response when proof and timestamp are both missing', async t => {
281
+ const wallet = createWallet();
282
+ const builder = new NetworkMessageBuilder(wallet, config);
283
+ const id = uuidv7();
284
+ const caps = ['cap:b', 'cap:a'];
285
+
286
+ await t.exception(
287
+ () =>
288
+ builder
289
+ .setType(NetworkOperationType.BROADCAST_TRANSACTION_RESPONSE)
290
+ .setId(id)
291
+ .setTimestamp()
292
+ .setCapabilities(caps)
293
+ .setResultCode(NetworkResultCode.OK)
294
+ .buildPayload(),
295
+ errorMessageIncludes('Result code OK requires non-empty proof and timestamp > 0.')
296
+ );
297
+ });
298
+
299
+ test('NetworkMessageBuilder rejects TX_ACCEPTED_PROOF_UNAVAILABLE response when timestamp is missing', async t => {
300
+ const wallet = createWallet();
301
+ const builder = new NetworkMessageBuilder(wallet, config);
302
+ const id = uuidv7();
303
+ const caps = ['cap:b', 'cap:a'];
304
+
305
+ await t.exception(
306
+ () =>
307
+ builder
308
+ .setType(NetworkOperationType.BROADCAST_TRANSACTION_RESPONSE)
309
+ .setId(id)
310
+ .setTimestamp()
311
+ .setCapabilities(caps)
312
+ .setResultCode(NetworkResultCode.TX_ACCEPTED_PROOF_UNAVAILABLE)
313
+ .buildPayload(),
314
+ errorMessageIncludes('Result code TX_ACCEPTED_PROOF_UNAVAILABLE requires timestamp > 0.')
315
+ );
316
+ });
317
+
318
+ test('NetworkMessageBuilder rejects TX_ACCEPTED_PROOF_UNAVAILABLE response when proof is non-empty', async t => {
319
+ const wallet = createWallet();
320
+ const builder = new NetworkMessageBuilder(wallet, config);
321
+ const id = uuidv7();
322
+ const caps = ['cap:b', 'cap:a'];
323
+
324
+ await t.exception(
325
+ () =>
326
+ builder
327
+ .setType(NetworkOperationType.BROADCAST_TRANSACTION_RESPONSE)
328
+ .setId(id)
329
+ .setTimestamp()
330
+ .setCapabilities(caps)
331
+ .setProof(b4a.from('deadbeef', 'hex'))
332
+ .setTimestampLedger(Date.now())
333
+ .setResultCode(NetworkResultCode.TX_ACCEPTED_PROOF_UNAVAILABLE)
334
+ .buildPayload(),
335
+ errorMessageIncludes('Result code TX_ACCEPTED_PROOF_UNAVAILABLE requires empty proof.')
336
+ );
337
+ });
338
+
339
+ test('NetworkMessageBuilder rejects non-OK response when proof is non-empty', async t => {
340
+ const wallet = createWallet();
341
+ const builder = new NetworkMessageBuilder(wallet, config);
342
+ const id = uuidv7();
343
+ const caps = ['cap:b', 'cap:a'];
344
+
345
+ await t.exception(
346
+ () =>
347
+ builder
348
+ .setType(NetworkOperationType.BROADCAST_TRANSACTION_RESPONSE)
349
+ .setId(id)
350
+ .setTimestamp()
351
+ .setCapabilities(caps)
352
+ .setProof(b4a.from('deadbeef', 'hex'))
353
+ .setTimestampLedger(0)
354
+ .setResultCode(NetworkResultCode.INVALID_PAYLOAD)
355
+ .buildPayload(),
356
+ errorMessageIncludes('Non-OK result code requires empty proof.')
357
+ );
358
+ });
359
+
360
+ test('NetworkMessageBuilder rejects non-OK response with timestamp > 0 unless proof is unavailable', async t => {
361
+ const wallet = createWallet();
362
+ const builder = new NetworkMessageBuilder(wallet, config);
363
+ const id = uuidv7();
364
+ const caps = ['cap:b', 'cap:a'];
365
+
366
+ await t.exception(
367
+ () =>
368
+ builder
369
+ .setType(NetworkOperationType.BROADCAST_TRANSACTION_RESPONSE)
370
+ .setId(id)
371
+ .setTimestamp()
372
+ .setCapabilities(caps)
373
+ .setTimestampLedger(Date.now())
374
+ .setResultCode(NetworkResultCode.INVALID_PAYLOAD)
375
+ .buildPayload(),
376
+ errorMessageIncludes('Non-OK result code requires timestamp to be 0, except TX_ACCEPTED_PROOF_UNAVAILABLE.')
377
+ );
378
+ });
379
+
220
380
  test('NetworkMessageBuilder builds broadcast transaction request and verifies signature', async t => {
221
381
  const wallet = createWallet();
222
382
  const builder = new NetworkMessageBuilder(wallet, config);