From 31d8394664ed452a58489c431f15d0422c17e0eb Mon Sep 17 00:00:00 2001 From: Abhijit Madhusudan Date: Wed, 21 Jan 2026 14:13:16 +0530 Subject: [PATCH 1/2] fix(sdk-coin-trx): fix BANDWIDTH transaction signature mismatch (SIGERROR) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix protobuf3 serialization mismatch for TRON BANDWIDTH transactions. Problem: - BitGoJS explicitly encoded resource=BANDWIDTH (value 0) in protobuf - TRON node re-serializes transactions and omits default values per protobuf3 - Different bytes → different txID → signature mismatch (SIGERROR) Solution: - Omit resource field when it's BANDWIDTH (the default value = 0) - Update decode functions to treat missing resource as BANDWIDTH - Add comprehensive round-trip tests for BANDWIDTH serialization Files changed: - freezeBalanceTxBuilder.ts: omit resource field for BANDWIDTH - unfreezeBalanceTxBuilder.ts: omit resource field for BANDWIDTH - delegateResourceTxBuilder.ts: omit resource field for BANDWIDTH - undelegateResourceTxBuilder.ts: omit resource field for BANDWIDTH - utils.ts: handle missing resource as BANDWIDTH in decode functions Ticket: SC-5030 --- .../src/lib/delegateResourceTxBuilder.ts | 17 ++- .../src/lib/freezeBalanceTxBuilder.ts | 16 ++- .../src/lib/undelegateResourceTxBuilder.ts | 17 ++- .../src/lib/unfreezeBalanceTxBuilder.ts | 16 ++- modules/sdk-coin-trx/src/lib/utils.ts | 35 +++-- .../delegateResourceTxBuilder.ts | 130 ++++++++++++++++++ .../freezeBalanceTxBuilder.ts | 125 +++++++++++++++++ .../undelegateResourceTxBuilder.ts | 130 ++++++++++++++++++ .../unfreezeBalanceTxBuilder.ts | 126 +++++++++++++++++ 9 files changed, 585 insertions(+), 27 deletions(-) diff --git a/modules/sdk-coin-trx/src/lib/delegateResourceTxBuilder.ts b/modules/sdk-coin-trx/src/lib/delegateResourceTxBuilder.ts index f9cb36940f..6015194080 100644 --- a/modules/sdk-coin-trx/src/lib/delegateResourceTxBuilder.ts +++ b/modules/sdk-coin-trx/src/lib/delegateResourceTxBuilder.ts @@ -74,12 +74,25 @@ export class DelegateResourceTxBuilder extends ResourceManagementTxBuilder { * @returns {string} the delegate resource transaction raw data hex */ protected getResourceManagementTxRawDataHex(): string { - const rawContract = { + const rawContract: { + ownerAddress: number[]; + receiverAddress: number[]; + balance: string; + resource?: string; + } = { ownerAddress: getByteArrayFromHexAddress(this._ownerAddress), receiverAddress: getByteArrayFromHexAddress(this._receiverAddress), balance: this._balance, - resource: this._resource, }; + + // Only include resource if it's not BANDWIDTH (the default value = 0) + // In protobuf3, default values are typically not encoded in the wire format. + // TRON's node re-serializes transactions and omits default values, + // so we must match that behavior to ensure consistent transaction hashes. + if (this._resource !== 'BANDWIDTH') { + rawContract.resource = this._resource; + } + const delegateResourceContract = protocol.DelegateResourceContract.fromObject(rawContract); const delegateResourceContractBytes = protocol.DelegateResourceContract.encode(delegateResourceContract).finish(); const txContract = { diff --git a/modules/sdk-coin-trx/src/lib/freezeBalanceTxBuilder.ts b/modules/sdk-coin-trx/src/lib/freezeBalanceTxBuilder.ts index ff99ad4667..fa1257cb39 100644 --- a/modules/sdk-coin-trx/src/lib/freezeBalanceTxBuilder.ts +++ b/modules/sdk-coin-trx/src/lib/freezeBalanceTxBuilder.ts @@ -163,11 +163,23 @@ export class FreezeBalanceTxBuilder extends TransactionBuilder { * @returns {string} the freeze balance transaction raw data hex */ private getFreezeRawDataHex(): string { - const rawContract = { + const rawContract: { + ownerAddress: number[]; + frozenBalance: string; + resource?: string; + } = { ownerAddress: getByteArrayFromHexAddress(this._ownerAddress), frozenBalance: this._frozenBalance, - resource: this._resource, }; + + // Only include resource if it's not BANDWIDTH (the default value = 0) + // In protobuf3, default values are typically not encoded in the wire format. + // TRON's node re-serializes transactions and omits default values, + // so we must match that behavior to ensure consistent transaction hashes. + if (this._resource !== 'BANDWIDTH') { + rawContract.resource = this._resource; + } + const freezeContract = protocol.FreezeBalanceV2Contract.fromObject(rawContract); const freezeContractBytes = protocol.FreezeBalanceV2Contract.encode(freezeContract).finish(); const txContract = { diff --git a/modules/sdk-coin-trx/src/lib/undelegateResourceTxBuilder.ts b/modules/sdk-coin-trx/src/lib/undelegateResourceTxBuilder.ts index ebd0d533a1..052fa09a74 100644 --- a/modules/sdk-coin-trx/src/lib/undelegateResourceTxBuilder.ts +++ b/modules/sdk-coin-trx/src/lib/undelegateResourceTxBuilder.ts @@ -74,12 +74,25 @@ export class UndelegateResourceTxBuilder extends ResourceManagementTxBuilder { * @returns {string} the undelegate resource transaction raw data hex */ protected getResourceManagementTxRawDataHex(): string { - const rawContract = { + const rawContract: { + ownerAddress: number[]; + receiverAddress: number[]; + balance: string; + resource?: string; + } = { ownerAddress: getByteArrayFromHexAddress(this._ownerAddress), receiverAddress: getByteArrayFromHexAddress(this._receiverAddress), balance: this._balance, - resource: this._resource, }; + + // Only include resource if it's not BANDWIDTH (the default value = 0) + // In protobuf3, default values are typically not encoded in the wire format. + // TRON's node re-serializes transactions and omits default values, + // so we must match that behavior to ensure consistent transaction hashes. + if (this._resource !== 'BANDWIDTH') { + rawContract.resource = this._resource; + } + const undelegateResourceContract = protocol.UnDelegateResourceContract.fromObject(rawContract); const undelegateResourceContractBytes = protocol.UnDelegateResourceContract.encode(undelegateResourceContract).finish(); diff --git a/modules/sdk-coin-trx/src/lib/unfreezeBalanceTxBuilder.ts b/modules/sdk-coin-trx/src/lib/unfreezeBalanceTxBuilder.ts index cf6b1f4fdc..046eeb062e 100644 --- a/modules/sdk-coin-trx/src/lib/unfreezeBalanceTxBuilder.ts +++ b/modules/sdk-coin-trx/src/lib/unfreezeBalanceTxBuilder.ts @@ -156,11 +156,23 @@ export class UnfreezeBalanceTxBuilder extends TransactionBuilder { * @returns {string} the freeze balance transaction raw data hex */ private getUnfreezeRawDataHex(): string { - const rawContract = { + const rawContract: { + ownerAddress: number[]; + unfreezeBalance: string; + resource?: string; + } = { ownerAddress: getByteArrayFromHexAddress(this._ownerAddress), unfreezeBalance: this._unfreezeBalance, - resource: this._resource, }; + + // Only include resource if it's not BANDWIDTH (the default value = 0) + // In protobuf3, default values are typically not encoded in the wire format. + // TRON's node re-serializes transactions and omits default values, + // so we must match that behavior to ensure consistent transaction hashes. + if (this._resource !== 'BANDWIDTH') { + rawContract.resource = this._resource; + } + const unfreezeContract = protocol.UnfreezeBalanceV2Contract.fromObject(rawContract); const unfreezeContractBytes = protocol.UnfreezeBalanceV2Contract.encode(unfreezeContract).finish(); const txContract = { diff --git a/modules/sdk-coin-trx/src/lib/utils.ts b/modules/sdk-coin-trx/src/lib/utils.ts index 1306550f78..49c7bf12d4 100644 --- a/modules/sdk-coin-trx/src/lib/utils.ts +++ b/modules/sdk-coin-trx/src/lib/utils.ts @@ -447,10 +447,6 @@ export function decodeFreezeBalanceV2Contract(base64: string): FreezeBalanceCont throw new UtilsError('Owner address does not exist in this freeze contract.'); } - if (freezeContract.resource === undefined) { - throw new UtilsError('Resource type does not exist in this freeze contract.'); - } - if (freezeContract.frozenBalance === undefined) { throw new UtilsError('Frozen balance does not exist in this freeze contract.'); } @@ -459,7 +455,12 @@ export function decodeFreezeBalanceV2Contract(base64: string): FreezeBalanceCont getByteArrayFromHexAddress(Buffer.from(freezeContract.ownerAddress, 'base64').toString('hex')) ); - const resourceValue = freezeContract.resource === 'BANDWIDTH' ? TronResource.BANDWIDTH : TronResource.ENERGY; + // In protobuf3, default values (BANDWIDTH = 0) may be omitted from serialization. + // If resource is undefined, default to BANDWIDTH. + const resourceValue = + freezeContract.resource === undefined || freezeContract.resource === 'BANDWIDTH' + ? TronResource.BANDWIDTH + : TronResource.ENERGY; return [ { @@ -549,10 +550,6 @@ export function decodeUnfreezeBalanceV2Contract(base64: string): UnfreezeBalance throw new UtilsError('Owner address does not exist in this unfreeze contract.'); } - if (unfreezeContract.resource === undefined) { - throw new UtilsError('Resource type does not exist in this unfreeze contract.'); - } - if (unfreezeContract.unfreezeBalance === undefined) { throw new UtilsError('Unfreeze balance does not exist in this unfreeze contract.'); } @@ -562,9 +559,13 @@ export function decodeUnfreezeBalanceV2Contract(base64: string): UnfreezeBalance getByteArrayFromHexAddress(Buffer.from(unfreezeContract.ownerAddress, 'base64').toString('hex')) ); - // Convert ResourceCode enum value to string resource name + // In protobuf3, default values (BANDWIDTH = 0) may be omitted from serialization. + // If resource is undefined, default to BANDWIDTH. const resourceValue = unfreezeContract.resource; - const resourceEnum = resourceValue === protocol.ResourceCode.BANDWIDTH ? TronResource.BANDWIDTH : TronResource.ENERGY; + const resourceEnum = + resourceValue === undefined || resourceValue === protocol.ResourceCode.BANDWIDTH + ? TronResource.BANDWIDTH + : TronResource.ENERGY; return [ { @@ -670,10 +671,6 @@ export function decodeDelegateResourceContract(base64: string): ResourceManageme throw new UtilsError('Receiver address does not exist in this delegate resource contract.'); } - if (delegateResourceContract.resource === undefined) { - throw new UtilsError('Resource type does not exist in this delegate resource contract.'); - } - if (delegateResourceContract.balance === undefined) { throw new UtilsError('Balance does not exist in this delegate resource contract.'); } @@ -686,6 +683,8 @@ export function decodeDelegateResourceContract(base64: string): ResourceManageme getByteArrayFromHexAddress(Buffer.from(delegateResourceContract.receiverAddress, 'base64').toString('hex')) ); + // In protobuf3, default values (BANDWIDTH = 0) may be omitted from serialization. + // If resource is undefined or falsy, default to BANDWIDTH. const resourceValue = !delegateResourceContract.resource ? TronResource.BANDWIDTH : TronResource.ENERGY; return [ @@ -724,10 +723,6 @@ export function decodeUnDelegateResourceContract(base64: string): ResourceManage throw new UtilsError('Receiver address does not exist in this delegate resource contract.'); } - if (undelegateResourceContract.resource === undefined) { - throw new UtilsError('Resource type does not exist in this delegate resource contract.'); - } - if (undelegateResourceContract.balance === undefined) { throw new UtilsError('Balance does not exist in this delegate resource contract.'); } @@ -740,6 +735,8 @@ export function decodeUnDelegateResourceContract(base64: string): ResourceManage getByteArrayFromHexAddress(Buffer.from(undelegateResourceContract.receiverAddress, 'base64').toString('hex')) ); + // In protobuf3, default values (BANDWIDTH = 0) may be omitted from serialization. + // If resource is undefined or falsy, default to BANDWIDTH. const resourceValue = !undelegateResourceContract.resource ? TronResource.BANDWIDTH : TronResource.ENERGY; return [ diff --git a/modules/sdk-coin-trx/test/unit/transactionBuilder/delegateResourceTxBuilder.ts b/modules/sdk-coin-trx/test/unit/transactionBuilder/delegateResourceTxBuilder.ts index eea7a2b7d4..495a804c02 100644 --- a/modules/sdk-coin-trx/test/unit/transactionBuilder/delegateResourceTxBuilder.ts +++ b/modules/sdk-coin-trx/test/unit/transactionBuilder/delegateResourceTxBuilder.ts @@ -8,6 +8,7 @@ import { BLOCK_NUMBER, EXPIRATION, RESOURCE_ENERGY, + RESOURCE_BANDWIDTH, DELEGATION_BALANCE, DELEGATE_RESOURCE_CONTRACT, } from '../../resources'; @@ -323,4 +324,133 @@ describe('Tron DelegateResource builder', function () { }); }); }); + + describe('BANDWIDTH serialization (protobuf3 compatibility)', () => { + it('should round-trip BANDWIDTH transactions correctly', async () => { + // Build a BANDWIDTH transaction + const builder = (getBuilder('ttrx') as WrappedBuilder).getDelegateResourceTxBuilder(); + builder + .source({ address: PARTICIPANTS.from.address }) + .setReceiverAddress({ address: PARTICIPANTS.to.address }) + .block({ number: BLOCK_NUMBER, hash: BLOCK_HASH }) + .setBalance(DELEGATION_BALANCE) + .setResource(RESOURCE_BANDWIDTH); + + const tx = await builder.build(); + const txJson = tx.toJson(); + + // Verify the built transaction has BANDWIDTH resource + assert.equal( + txJson.raw_data.contract[0].parameter.value.resource, + RESOURCE_BANDWIDTH, + 'Built transaction should have BANDWIDTH resource' + ); + + // Round-trip: deserialize from broadcast format and rebuild + const builder2 = getBuilder('ttrx').from(tx.toBroadcastFormat()); + const tx2 = await builder2.build(); + const tx2Json = tx2.toJson(); + + // Verify resource is preserved after round-trip + assert.equal( + tx2Json.raw_data.contract[0].parameter.value.resource, + RESOURCE_BANDWIDTH, + 'Resource should be BANDWIDTH after round-trip from broadcast format' + ); + + // Round-trip: deserialize from JSON and rebuild + const builder3 = getBuilder('ttrx').from(tx.toJson()); + const tx3 = await builder3.build(); + const tx3Json = tx3.toJson(); + + // Verify resource is preserved after round-trip from JSON + assert.equal( + tx3Json.raw_data.contract[0].parameter.value.resource, + RESOURCE_BANDWIDTH, + 'Resource should be BANDWIDTH after round-trip from JSON' + ); + }); + + it('should round-trip ENERGY transactions correctly', async () => { + // Build an ENERGY transaction + const builder = (getBuilder('ttrx') as WrappedBuilder).getDelegateResourceTxBuilder(); + builder + .source({ address: PARTICIPANTS.from.address }) + .setReceiverAddress({ address: PARTICIPANTS.to.address }) + .block({ number: BLOCK_NUMBER, hash: BLOCK_HASH }) + .setBalance(DELEGATION_BALANCE) + .setResource(RESOURCE_ENERGY); + + const tx = await builder.build(); + const txJson = tx.toJson(); + + // Verify the built transaction has ENERGY resource + assert.equal( + txJson.raw_data.contract[0].parameter.value.resource, + RESOURCE_ENERGY, + 'Built transaction should have ENERGY resource' + ); + + // Round-trip: deserialize from broadcast format and rebuild + const builder2 = getBuilder('ttrx').from(tx.toBroadcastFormat()); + const tx2 = await builder2.build(); + const tx2Json = tx2.toJson(); + + // Verify resource is preserved after round-trip + assert.equal( + tx2Json.raw_data.contract[0].parameter.value.resource, + RESOURCE_ENERGY, + 'Resource should be ENERGY after round-trip from broadcast format' + ); + }); + + it('should produce consistent transaction IDs for BANDWIDTH transactions', async () => { + // Build a BANDWIDTH transaction + const builder = (getBuilder('ttrx') as WrappedBuilder).getDelegateResourceTxBuilder(); + builder + .source({ address: PARTICIPANTS.from.address }) + .setReceiverAddress({ address: PARTICIPANTS.to.address }) + .block({ number: BLOCK_NUMBER, hash: BLOCK_HASH }) + .setBalance(DELEGATION_BALANCE) + .setResource(RESOURCE_BANDWIDTH); + + const tx = await builder.build(); + const originalTxId = tx.toJson().txID; + + // Round-trip and verify txID is consistent + const builder2 = getBuilder('ttrx').from(tx.toBroadcastFormat()); + const tx2 = await builder2.build(); + const roundTripTxId = tx2.toJson().txID; + + assert.equal(originalTxId, roundTripTxId, 'Transaction ID should be consistent after round-trip'); + }); + + it('should allow signing BANDWIDTH transactions after round-trip', async () => { + // Build a BANDWIDTH transaction + const builder = (getBuilder('ttrx') as WrappedBuilder).getDelegateResourceTxBuilder(); + builder + .source({ address: PARTICIPANTS.from.address }) + .setReceiverAddress({ address: PARTICIPANTS.to.address }) + .block({ number: BLOCK_NUMBER, hash: BLOCK_HASH }) + .setBalance(DELEGATION_BALANCE) + .setResource(RESOURCE_BANDWIDTH); + + const tx = await builder.build(); + + // Round-trip and sign + const builder2 = getBuilder('ttrx').from(tx.toBroadcastFormat()); + builder2.sign({ key: PARTICIPANTS.from.pk }); + const signedTx = await builder2.build(); + + // Verify signature was added + assert.equal(signedTx.toJson().signature.length, 1, 'Transaction should have one signature'); + + // Verify resource is still BANDWIDTH + assert.equal( + signedTx.toJson().raw_data.contract[0].parameter.value.resource, + RESOURCE_BANDWIDTH, + 'Resource should still be BANDWIDTH after signing' + ); + }); + }); }); diff --git a/modules/sdk-coin-trx/test/unit/transactionBuilder/freezeBalanceTxBuilder.ts b/modules/sdk-coin-trx/test/unit/transactionBuilder/freezeBalanceTxBuilder.ts index 6beb3d9091..9b54f6f722 100644 --- a/modules/sdk-coin-trx/test/unit/transactionBuilder/freezeBalanceTxBuilder.ts +++ b/modules/sdk-coin-trx/test/unit/transactionBuilder/freezeBalanceTxBuilder.ts @@ -362,4 +362,129 @@ describe('Tron FreezeBalanceV2 builder', function () { assert.equal(contract.parameter.value.resource, RESOURCE_BANDWIDTH, 'Resource type should be BANDWIDTH'); }); }); + + describe('BANDWIDTH serialization (protobuf3 compatibility)', () => { + it('should round-trip BANDWIDTH transactions correctly', async () => { + // Build a BANDWIDTH transaction + const builder = (getBuilder('ttrx') as WrappedBuilder).getFreezeBalanceV2TxBuilder(); + builder + .source({ address: PARTICIPANTS.custodian.address }) + .block({ number: BLOCK_NUMBER, hash: BLOCK_HASH }) + .setFrozenBalance(FROZEN_BALANCE) + .setResource(RESOURCE_BANDWIDTH); + + const tx = await builder.build(); + const txJson = tx.toJson(); + + // Verify the built transaction has BANDWIDTH resource + assert.equal( + txJson.raw_data.contract[0].parameter.value.resource, + RESOURCE_BANDWIDTH, + 'Built transaction should have BANDWIDTH resource' + ); + + // Round-trip: deserialize from broadcast format and rebuild + const builder2 = getBuilder('ttrx').from(tx.toBroadcastFormat()); + const tx2 = await builder2.build(); + const tx2Json = tx2.toJson(); + + // Verify resource is preserved after round-trip + assert.equal( + tx2Json.raw_data.contract[0].parameter.value.resource, + RESOURCE_BANDWIDTH, + 'Resource should be BANDWIDTH after round-trip from broadcast format' + ); + + // Round-trip: deserialize from JSON and rebuild + const builder3 = getBuilder('ttrx').from(tx.toJson()); + const tx3 = await builder3.build(); + const tx3Json = tx3.toJson(); + + // Verify resource is preserved after round-trip from JSON + assert.equal( + tx3Json.raw_data.contract[0].parameter.value.resource, + RESOURCE_BANDWIDTH, + 'Resource should be BANDWIDTH after round-trip from JSON' + ); + }); + + it('should round-trip ENERGY transactions correctly', async () => { + // Build an ENERGY transaction + const builder = (getBuilder('ttrx') as WrappedBuilder).getFreezeBalanceV2TxBuilder(); + builder + .source({ address: PARTICIPANTS.custodian.address }) + .block({ number: BLOCK_NUMBER, hash: BLOCK_HASH }) + .setFrozenBalance(FROZEN_BALANCE) + .setResource(RESOURCE_ENERGY); + + const tx = await builder.build(); + const txJson = tx.toJson(); + + // Verify the built transaction has ENERGY resource + assert.equal( + txJson.raw_data.contract[0].parameter.value.resource, + RESOURCE_ENERGY, + 'Built transaction should have ENERGY resource' + ); + + // Round-trip: deserialize from broadcast format and rebuild + const builder2 = getBuilder('ttrx').from(tx.toBroadcastFormat()); + const tx2 = await builder2.build(); + const tx2Json = tx2.toJson(); + + // Verify resource is preserved after round-trip + assert.equal( + tx2Json.raw_data.contract[0].parameter.value.resource, + RESOURCE_ENERGY, + 'Resource should be ENERGY after round-trip from broadcast format' + ); + }); + + it('should produce consistent transaction IDs for BANDWIDTH transactions', async () => { + // Build a BANDWIDTH transaction + const builder = (getBuilder('ttrx') as WrappedBuilder).getFreezeBalanceV2TxBuilder(); + builder + .source({ address: PARTICIPANTS.custodian.address }) + .block({ number: BLOCK_NUMBER, hash: BLOCK_HASH }) + .setFrozenBalance(FROZEN_BALANCE) + .setResource(RESOURCE_BANDWIDTH); + + const tx = await builder.build(); + const originalTxId = tx.toJson().txID; + + // Round-trip and verify txID is consistent + const builder2 = getBuilder('ttrx').from(tx.toBroadcastFormat()); + const tx2 = await builder2.build(); + const roundTripTxId = tx2.toJson().txID; + + assert.equal(originalTxId, roundTripTxId, 'Transaction ID should be consistent after round-trip'); + }); + + it('should allow signing BANDWIDTH transactions after round-trip', async () => { + // Build a BANDWIDTH transaction + const builder = (getBuilder('ttrx') as WrappedBuilder).getFreezeBalanceV2TxBuilder(); + builder + .source({ address: PARTICIPANTS.custodian.address }) + .block({ number: BLOCK_NUMBER, hash: BLOCK_HASH }) + .setFrozenBalance(FROZEN_BALANCE) + .setResource(RESOURCE_BANDWIDTH); + + const tx = await builder.build(); + + // Round-trip and sign + const builder2 = getBuilder('ttrx').from(tx.toBroadcastFormat()); + builder2.sign({ key: PARTICIPANTS.custodian.pk }); + const signedTx = await builder2.build(); + + // Verify signature was added + assert.equal(signedTx.toJson().signature.length, 1, 'Transaction should have one signature'); + + // Verify resource is still BANDWIDTH + assert.equal( + signedTx.toJson().raw_data.contract[0].parameter.value.resource, + RESOURCE_BANDWIDTH, + 'Resource should still be BANDWIDTH after signing' + ); + }); + }); }); diff --git a/modules/sdk-coin-trx/test/unit/transactionBuilder/undelegateResourceTxBuilder.ts b/modules/sdk-coin-trx/test/unit/transactionBuilder/undelegateResourceTxBuilder.ts index 958ee38a96..03f631f4ed 100644 --- a/modules/sdk-coin-trx/test/unit/transactionBuilder/undelegateResourceTxBuilder.ts +++ b/modules/sdk-coin-trx/test/unit/transactionBuilder/undelegateResourceTxBuilder.ts @@ -8,6 +8,7 @@ import { BLOCK_NUMBER, EXPIRATION, RESOURCE_ENERGY, + RESOURCE_BANDWIDTH, UNDELEGATION_BALANCE, UNDELEGATE_RESOURCE_CONTRACT, } from '../../resources'; @@ -323,4 +324,133 @@ describe('Tron UnDelegateResource builder', function () { }); }); }); + + describe('BANDWIDTH serialization (protobuf3 compatibility)', () => { + it('should round-trip BANDWIDTH transactions correctly', async () => { + // Build a BANDWIDTH transaction + const builder = (getBuilder('ttrx') as WrappedBuilder).getUnDelegateResourceTxBuilder(); + builder + .source({ address: PARTICIPANTS.from.address }) + .setReceiverAddress({ address: PARTICIPANTS.to.address }) + .block({ number: BLOCK_NUMBER, hash: BLOCK_HASH }) + .setBalance(UNDELEGATION_BALANCE) + .setResource(RESOURCE_BANDWIDTH); + + const tx = await builder.build(); + const txJson = tx.toJson(); + + // Verify the built transaction has BANDWIDTH resource + assert.equal( + txJson.raw_data.contract[0].parameter.value.resource, + RESOURCE_BANDWIDTH, + 'Built transaction should have BANDWIDTH resource' + ); + + // Round-trip: deserialize from broadcast format and rebuild + const builder2 = getBuilder('ttrx').from(tx.toBroadcastFormat()); + const tx2 = await builder2.build(); + const tx2Json = tx2.toJson(); + + // Verify resource is preserved after round-trip + assert.equal( + tx2Json.raw_data.contract[0].parameter.value.resource, + RESOURCE_BANDWIDTH, + 'Resource should be BANDWIDTH after round-trip from broadcast format' + ); + + // Round-trip: deserialize from JSON and rebuild + const builder3 = getBuilder('ttrx').from(tx.toJson()); + const tx3 = await builder3.build(); + const tx3Json = tx3.toJson(); + + // Verify resource is preserved after round-trip from JSON + assert.equal( + tx3Json.raw_data.contract[0].parameter.value.resource, + RESOURCE_BANDWIDTH, + 'Resource should be BANDWIDTH after round-trip from JSON' + ); + }); + + it('should round-trip ENERGY transactions correctly', async () => { + // Build an ENERGY transaction + const builder = (getBuilder('ttrx') as WrappedBuilder).getUnDelegateResourceTxBuilder(); + builder + .source({ address: PARTICIPANTS.from.address }) + .setReceiverAddress({ address: PARTICIPANTS.to.address }) + .block({ number: BLOCK_NUMBER, hash: BLOCK_HASH }) + .setBalance(UNDELEGATION_BALANCE) + .setResource(RESOURCE_ENERGY); + + const tx = await builder.build(); + const txJson = tx.toJson(); + + // Verify the built transaction has ENERGY resource + assert.equal( + txJson.raw_data.contract[0].parameter.value.resource, + RESOURCE_ENERGY, + 'Built transaction should have ENERGY resource' + ); + + // Round-trip: deserialize from broadcast format and rebuild + const builder2 = getBuilder('ttrx').from(tx.toBroadcastFormat()); + const tx2 = await builder2.build(); + const tx2Json = tx2.toJson(); + + // Verify resource is preserved after round-trip + assert.equal( + tx2Json.raw_data.contract[0].parameter.value.resource, + RESOURCE_ENERGY, + 'Resource should be ENERGY after round-trip from broadcast format' + ); + }); + + it('should produce consistent transaction IDs for BANDWIDTH transactions', async () => { + // Build a BANDWIDTH transaction + const builder = (getBuilder('ttrx') as WrappedBuilder).getUnDelegateResourceTxBuilder(); + builder + .source({ address: PARTICIPANTS.from.address }) + .setReceiverAddress({ address: PARTICIPANTS.to.address }) + .block({ number: BLOCK_NUMBER, hash: BLOCK_HASH }) + .setBalance(UNDELEGATION_BALANCE) + .setResource(RESOURCE_BANDWIDTH); + + const tx = await builder.build(); + const originalTxId = tx.toJson().txID; + + // Round-trip and verify txID is consistent + const builder2 = getBuilder('ttrx').from(tx.toBroadcastFormat()); + const tx2 = await builder2.build(); + const roundTripTxId = tx2.toJson().txID; + + assert.equal(originalTxId, roundTripTxId, 'Transaction ID should be consistent after round-trip'); + }); + + it('should allow signing BANDWIDTH transactions after round-trip', async () => { + // Build a BANDWIDTH transaction + const builder = (getBuilder('ttrx') as WrappedBuilder).getUnDelegateResourceTxBuilder(); + builder + .source({ address: PARTICIPANTS.from.address }) + .setReceiverAddress({ address: PARTICIPANTS.to.address }) + .block({ number: BLOCK_NUMBER, hash: BLOCK_HASH }) + .setBalance(UNDELEGATION_BALANCE) + .setResource(RESOURCE_BANDWIDTH); + + const tx = await builder.build(); + + // Round-trip and sign + const builder2 = getBuilder('ttrx').from(tx.toBroadcastFormat()); + builder2.sign({ key: PARTICIPANTS.from.pk }); + const signedTx = await builder2.build(); + + // Verify signature was added + assert.equal(signedTx.toJson().signature.length, 1, 'Transaction should have one signature'); + + // Verify resource is still BANDWIDTH + assert.equal( + signedTx.toJson().raw_data.contract[0].parameter.value.resource, + RESOURCE_BANDWIDTH, + 'Resource should still be BANDWIDTH after signing' + ); + }); + }); }); diff --git a/modules/sdk-coin-trx/test/unit/transactionBuilder/unfreezeBalanceTxBuilder.ts b/modules/sdk-coin-trx/test/unit/transactionBuilder/unfreezeBalanceTxBuilder.ts index 063301a08a..f1e89e8212 100644 --- a/modules/sdk-coin-trx/test/unit/transactionBuilder/unfreezeBalanceTxBuilder.ts +++ b/modules/sdk-coin-trx/test/unit/transactionBuilder/unfreezeBalanceTxBuilder.ts @@ -7,6 +7,7 @@ import { BLOCK_NUMBER, EXPIRATION, RESOURCE_ENERGY, + RESOURCE_BANDWIDTH, UNFROZEN_BALANCE, UNFREEZE_BALANCE_V2_CONTRACT, } from '../../resources'; @@ -320,4 +321,129 @@ describe('Tron UnfreezeBalanceV2 builder', function () { }); }); }); + + describe('BANDWIDTH serialization (protobuf3 compatibility)', () => { + it('should round-trip BANDWIDTH transactions correctly', async () => { + // Build a BANDWIDTH transaction + const builder = (getBuilder('ttrx') as WrappedBuilder).getUnfreezeBalanceV2TxBuilder(); + builder + .source({ address: PARTICIPANTS.custodian.address }) + .block({ number: BLOCK_NUMBER, hash: BLOCK_HASH }) + .setUnfreezeBalance(UNFROZEN_BALANCE) + .setResource(RESOURCE_BANDWIDTH); + + const tx = await builder.build(); + const txJson = tx.toJson(); + + // Verify the built transaction has BANDWIDTH resource + assert.equal( + txJson.raw_data.contract[0].parameter.value.resource, + RESOURCE_BANDWIDTH, + 'Built transaction should have BANDWIDTH resource' + ); + + // Round-trip: deserialize from broadcast format and rebuild + const builder2 = getBuilder('ttrx').from(tx.toBroadcastFormat()); + const tx2 = await builder2.build(); + const tx2Json = tx2.toJson(); + + // Verify resource is preserved after round-trip + assert.equal( + tx2Json.raw_data.contract[0].parameter.value.resource, + RESOURCE_BANDWIDTH, + 'Resource should be BANDWIDTH after round-trip from broadcast format' + ); + + // Round-trip: deserialize from JSON and rebuild + const builder3 = getBuilder('ttrx').from(tx.toJson()); + const tx3 = await builder3.build(); + const tx3Json = tx3.toJson(); + + // Verify resource is preserved after round-trip from JSON + assert.equal( + tx3Json.raw_data.contract[0].parameter.value.resource, + RESOURCE_BANDWIDTH, + 'Resource should be BANDWIDTH after round-trip from JSON' + ); + }); + + it('should round-trip ENERGY transactions correctly', async () => { + // Build an ENERGY transaction + const builder = (getBuilder('ttrx') as WrappedBuilder).getUnfreezeBalanceV2TxBuilder(); + builder + .source({ address: PARTICIPANTS.custodian.address }) + .block({ number: BLOCK_NUMBER, hash: BLOCK_HASH }) + .setUnfreezeBalance(UNFROZEN_BALANCE) + .setResource(RESOURCE_ENERGY); + + const tx = await builder.build(); + const txJson = tx.toJson(); + + // Verify the built transaction has ENERGY resource + assert.equal( + txJson.raw_data.contract[0].parameter.value.resource, + RESOURCE_ENERGY, + 'Built transaction should have ENERGY resource' + ); + + // Round-trip: deserialize from broadcast format and rebuild + const builder2 = getBuilder('ttrx').from(tx.toBroadcastFormat()); + const tx2 = await builder2.build(); + const tx2Json = tx2.toJson(); + + // Verify resource is preserved after round-trip + assert.equal( + tx2Json.raw_data.contract[0].parameter.value.resource, + RESOURCE_ENERGY, + 'Resource should be ENERGY after round-trip from broadcast format' + ); + }); + + it('should produce consistent transaction IDs for BANDWIDTH transactions', async () => { + // Build a BANDWIDTH transaction + const builder = (getBuilder('ttrx') as WrappedBuilder).getUnfreezeBalanceV2TxBuilder(); + builder + .source({ address: PARTICIPANTS.custodian.address }) + .block({ number: BLOCK_NUMBER, hash: BLOCK_HASH }) + .setUnfreezeBalance(UNFROZEN_BALANCE) + .setResource(RESOURCE_BANDWIDTH); + + const tx = await builder.build(); + const originalTxId = tx.toJson().txID; + + // Round-trip and verify txID is consistent + const builder2 = getBuilder('ttrx').from(tx.toBroadcastFormat()); + const tx2 = await builder2.build(); + const roundTripTxId = tx2.toJson().txID; + + assert.equal(originalTxId, roundTripTxId, 'Transaction ID should be consistent after round-trip'); + }); + + it('should allow signing BANDWIDTH transactions after round-trip', async () => { + // Build a BANDWIDTH transaction + const builder = (getBuilder('ttrx') as WrappedBuilder).getUnfreezeBalanceV2TxBuilder(); + builder + .source({ address: PARTICIPANTS.custodian.address }) + .block({ number: BLOCK_NUMBER, hash: BLOCK_HASH }) + .setUnfreezeBalance(UNFROZEN_BALANCE) + .setResource(RESOURCE_BANDWIDTH); + + const tx = await builder.build(); + + // Round-trip and sign + const builder2 = getBuilder('ttrx').from(tx.toBroadcastFormat()); + builder2.sign({ key: PARTICIPANTS.custodian.pk }); + const signedTx = await builder2.build(); + + // Verify signature was added + assert.equal(signedTx.toJson().signature.length, 1, 'Transaction should have one signature'); + + // Verify resource is still BANDWIDTH + assert.equal( + signedTx.toJson().raw_data.contract[0].parameter.value.resource, + RESOURCE_BANDWIDTH, + 'Resource should still be BANDWIDTH after signing' + ); + }); + }); }); From 1872bb18eca7d5505cd0861559585750f966a37a Mon Sep 17 00:00:00 2001 From: Abhijit Madhusudan Date: Wed, 21 Jan 2026 18:37:39 +0530 Subject: [PATCH 2/2] chore: exclude tar CVE GHSA-r6q2-hw4h-h46w in .iyarc Exclude the tar vulnerability instead of bumping version because: - Lerna requires tar v6, but fix only exists in v7.5.4+ - Forcing tar v7.x breaks lerna publishing - This is a race condition in tar's path reservation system Ticket: SC-5030 --- .iyarc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.iyarc b/.iyarc index 826b0ac640..9bed6f4399 100644 --- a/.iyarc +++ b/.iyarc @@ -4,4 +4,4 @@ # - This CVE affects archive EXTRACTION (unpacking malicious symlinks/hardlinks) # - Lerna only uses tar for PACKING GHSA-8qq5-rm4j-mr97 - +GHSA-r6q2-hw4h-h46w