Skip to content

Commit

Permalink
Merge branch '122-verify-payment-tx' into 'dev'
Browse files Browse the repository at this point in the history
add verifyPaymentTransaction function

Closes #122

See merge request ergo/rosen-bridge/rosen-chains!149
  • Loading branch information
vorujack committed Oct 27, 2024
2 parents 6c4f4f5 + 41facad commit 432f8a3
Show file tree
Hide file tree
Showing 13 changed files with 680 additions and 9 deletions.
9 changes: 9 additions & 0 deletions .changeset/sweet-garlics-greet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@rosen-chains/abstract-chain': major
'@rosen-chains/bitcoin': minor
'@rosen-chains/cardano': minor
'@rosen-chains/ergo': minor
'@rosen-chains/evm': minor
---

add verifyPaymentTransaction function which checks data consistency within a PaymentTransaction Object
9 changes: 9 additions & 0 deletions packages/abstract-chain/lib/AbstractChain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,15 @@ abstract class AbstractChain<TxType> {
* serializes the transaction of this chain into string
*/
protected abstract serializeTx: (tx: TxType) => string;

/**
* verifies consistency within the PaymentTransaction object
* @param transaction the PaymentTransaction
* @returns true if the transaction is verified
*/
abstract verifyPaymentTransaction: (
transaction: PaymentTransaction
) => Promise<boolean>;
}

export default AbstractChain;
1 change: 1 addition & 0 deletions packages/abstract-chain/tests/TestChain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class TestChain extends AbstractChain<string> {
getMinimumNativeToken = this.notImplemented;
PaymentTransactionFromJson = this.notImplemented;
rawTxToPaymentTransaction = this.notImplemented;
verifyPaymentTransaction = this.notImplemented;

generateMultipleTransactions = (
eventId: string,
Expand Down
1 change: 1 addition & 0 deletions packages/abstract-chain/tests/TestUtxoChain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class TestUtxoChain extends AbstractUtxoChain<string, string> {
getMinimumNativeToken = this.notImplemented;
PaymentTransactionFromJson = this.notImplemented;
rawTxToPaymentTransaction = this.notImplemented;
verifyPaymentTransaction = this.notImplemented;

getBoxInfo = (box: string): BoxInfo => {
throw Error('Not mocked');
Expand Down
50 changes: 50 additions & 0 deletions packages/chains/bitcoin/lib/BitcoinChain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,56 @@ class BitcoinChain extends AbstractUtxoChain<BitcoinTx, BitcoinUtxo> {
*/
protected unwrapBtc = (amount: bigint): RosenAmount =>
this.tokenMap.unwrapAmount(this.NATIVE_TOKEN_ID, amount, this.CHAIN);

/**
* verifies consistency within the PaymentTransaction object
* @param transaction the PaymentTransaction
* @returns true if the transaction is verified
*/
verifyPaymentTransaction = async (
transaction: PaymentTransaction
): Promise<boolean> => {
const psbt = Serializer.deserialize(transaction.txBytes);
const bitcoinTx = transaction as BitcoinTransaction;
const baseError = `Tx [${transaction.txId}] is not verified: `;

// verify txId
const txId = Transaction.fromBuffer(psbt.data.getTransaction()).getId();
if (transaction.txId !== txId) {
this.logger.warn(
baseError +
`Transaction id is inconsistent (expected [${transaction.txId}] found [${txId}])`
);
return false;
}

// verify inputUtxos
if (bitcoinTx.inputUtxos.length !== psbt.inputCount) {
this.logger.warn(
baseError +
`BitcoinTransaction object input counts is inconsistent [${bitcoinTx.inputUtxos.length} != ${psbt.inputCount}]`
);
return false;
}
for (let i = 0; i < psbt.inputCount; i++) {
const input = psbt.txInputs[i];
const txId = Buffer.from(input.hash).reverse().toString('hex');
const actualInputId = `${txId}.${input.index}`;
const bitcoinInput = JsonBigInt.parse(
bitcoinTx.inputUtxos[i]
) as BitcoinUtxo;
const expectedId = `${bitcoinInput.txId}.${bitcoinInput.index}`;
if (expectedId !== actualInputId) {
this.logger.warn(
baseError +
`Utxo id for input at index [${i}] is inconsistent [expected ${expectedId} found ${actualInputId}]`
);
return false;
}
}

return true;
};
}

export default BitcoinChain;
112 changes: 111 additions & 1 deletion packages/chains/bitcoin/tests/BitcoinChain.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -719,7 +719,7 @@ describe('BitcoinChain', () => {
const network = new TestBitcoinNetwork();

/**
* @target BitcoinChain.getBoxInfo should get box id and assets correctly
* @target BitcoinChain.getBoxInfo should get box info successfully
* @dependencies
* @scenario
* - mock a BitcoinUtxo with assets
Expand Down Expand Up @@ -830,4 +830,114 @@ describe('BitcoinChain', () => {
expect(result).toEqual(trackMap);
});
});

describe('verifyPaymentTransaction', () => {
const network = new TestBitcoinNetwork();

/**
* @target BitcoinChain.verifyPaymentTransaction should return true
* when data is consistent
* @dependencies
* @scenario
* - mock a BitcoinTransaction
* - run test
* - check returned value
* @expected
* - it should return true
*/
it('should return true when data is consistent', async () => {
// mock a BitcoinTransaction
const paymentTx = BitcoinTransaction.fromJson(
testData.transaction2PaymentTransaction
);

// run test
const bitcoinChain = testUtils.generateChainObject(network);
const result = await bitcoinChain.verifyPaymentTransaction(paymentTx);

// check returned value
expect(result).toEqual(true);
});

/**
* @target BitcoinChain.verifyPaymentTransaction should return false
* when transaction id is wrong
* @dependencies
* @scenario
* - mock a BitcoinTransaction with changed txId
* - run test
* - check returned value
* @expected
* - it should return false
*/
it('should return false when transaction id is wrong', async () => {
// mock a BitcoinTransaction with changed txId
const paymentTx = BitcoinTransaction.fromJson(
testData.transaction2PaymentTransaction
);
paymentTx.txId = testUtils.generateRandomId();

// run test
const bitcoinChain = testUtils.generateChainObject(network);
const result = await bitcoinChain.verifyPaymentTransaction(paymentTx);

// check returned value
expect(result).toEqual(false);
});

/**
* @target BitcoinChain.verifyPaymentTransaction should return false
* when number of utxos is wrong
* @dependencies
* @scenario
* - mock a BitcoinTransaction with less utxos
* - run test
* - check returned value
* @expected
* - it should return false
*/
it('should return false when number of utxos is wrong', async () => {
// mock a BitcoinTransaction with less utxos
const paymentTx = BitcoinTransaction.fromJson(
testData.transaction2PaymentTransaction
);
paymentTx.inputUtxos.pop();

// run test
const bitcoinChain = testUtils.generateChainObject(network);
const result = await bitcoinChain.verifyPaymentTransaction(paymentTx);

// check returned value
expect(result).toEqual(false);
});

/**
* @target BitcoinChain.verifyPaymentTransaction should return false
* when at least one of the utxos is wrong
* @dependencies
* @scenario
* - mock a BitcoinTransaction with changed utxo
* - run test
* - check returned value
* @expected
* - it should return false
*/
it('should return false when at least one of the utxos is wrong', async () => {
// mock a BitcoinTransaction with changed utxo
const paymentTx = BitcoinTransaction.fromJson(
testData.transaction2PaymentTransaction
);
paymentTx.inputUtxos[1] = JsonBigInt.stringify({
txId: testUtils.generateRandomId(),
index: 1,
});

// run test
const bitcoinChain = testUtils.generateChainObject(network);
const result = await bitcoinChain.verifyPaymentTransaction(paymentTx);

// check returned value
expect(result).toEqual(false);
});
});
});
56 changes: 56 additions & 0 deletions packages/chains/cardano/lib/CardanoChain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,62 @@ class CardanoChain extends AbstractUtxoChain<CardanoTx, CardanoUtxo> {
* serializes the transaction of this chain into string
*/
protected serializeTx = (tx: CardanoTx): string => JsonBigInt.stringify(tx);

/**
* verifies consistency within the PaymentTransaction object
* @param transaction the PaymentTransaction
* @returns true if the transaction is verified
*/
verifyPaymentTransaction = async (
transaction: PaymentTransaction
): Promise<boolean> => {
const tx = Serializer.deserialize(transaction.txBytes);
const cardanoTx = transaction as CardanoTransaction;
const baseError = `Tx [${transaction.txId}] is not verified: `;

// verify txId
const txId = Buffer.from(
CardanoWasm.hash_transaction(tx.body()).to_bytes()
).toString('hex');
if (transaction.txId !== txId) {
this.logger.warn(
baseError +
`Transaction ID is inconsistent (expected [${transaction.txId}] found [${txId}])`
);
return false;
}

// verify inputUtxos
const txInputs = tx.body().inputs();
if (cardanoTx.inputUtxos.length !== txInputs.len()) {
this.logger.warn(
baseError +
`CardanoTransaction object input counts is inconsistent [${
cardanoTx.inputUtxos.length
} != ${txInputs.len()}]`
);
return false;
}
for (let i = 0; i < txInputs.len(); i++) {
const input = txInputs.get(i);
const actualInputId = `${input
.transaction_id()
.to_hex()}.${input.index()}`;
const cardanoInput = JsonBigInt.parse(
cardanoTx.inputUtxos[i]
) as CardanoUtxo;
const expectedId = `${cardanoInput.txId}.${cardanoInput.index}`;
if (expectedId !== actualInputId) {
this.logger.warn(
baseError +
`BoxId for input at index [${i}] is inconsistent [expected ${expectedId} found ${actualInputId}]`
);
return false;
}
}

return true;
};
}

export default CardanoChain;
Loading

0 comments on commit 432f8a3

Please sign in to comment.