Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add precheck for receivers account in sendRawTransactionCheck #3310

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/relay/src/lib/errors/JsonRpcError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,10 @@ export const predefined = {
code: -39013,
message: 'Invalid block range',
}),
RECEIVER_SIGNATURE_ENABLED: new JsonRpcError({
code: -32000,
message: "Operation is not supported when receiver's signature is enabled.",
}),
FILTER_NOT_FOUND: new JsonRpcError({
code: -32001,
message: 'Filter not found',
Expand Down
17 changes: 17 additions & 0 deletions packages/relay/src/lib/precheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export class Precheck {
this.value(parsedTx);
this.gasPrice(parsedTx, networkGasPriceInWeiBars, requestDetails);
this.balance(parsedTx, mirrorAccountInfo, requestDetails);
await this.receiverAccount(parsedTx, requestDetails);
}

/**
Expand Down Expand Up @@ -376,4 +377,20 @@ export class Precheck {
throw predefined.UNSUPPORTED_TRANSACTION_TYPE;
}
}

/**
* Checks if the receiver account exists and has receiver_sig_required set to true.
* @param {Transaction} tx - The transaction.
* @param {RequestDetails} requestDetails - The request details for logging and tracking.
*/
async receiverAccount(tx: Transaction, requestDetails: RequestDetails) {
nadezhdapopovaa marked this conversation as resolved.
Show resolved Hide resolved
if (tx.to) {
const verifyAccount = await this.mirrorNodeClient.getAccount(tx.to, requestDetails);

// When `receiver_sig_required` is set to true, the receiver's account must sign all incoming transactions.
if (verifyAccount && verifyAccount.receiver_sig_required) {
throw predefined.RECEIVER_SIGNATURE_ENABLED;
}
}
quiet-node marked this conversation as resolved.
Show resolved Hide resolved
}
}
9 changes: 9 additions & 0 deletions packages/relay/tests/lib/eth/eth_sendRawTransaction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ describe('@ethSendRawTransaction eth_sendRawTransaction spec', async function ()
let clock: any;
const accountAddress = '0x9eaee9E66efdb91bfDcF516b034e001cc535EB57';
const accountEndpoint = `accounts/${accountAddress}${NO_TRANSACTIONS}`;
const receiverAccountEndpoint = `accounts/${ACCOUNT_ADDRESS_1}${NO_TRANSACTIONS}`;
const gasPrice = '0xad78ebc5ac620000';
const transactionIdServicesFormat = '[email protected]';
const transactionId = '0.0.902-1684375868-230217103';
Expand Down Expand Up @@ -127,6 +128,13 @@ describe('@ethSendRawTransaction eth_sendRawTransaction spec', async function ()
balance: Hbar.from(100_000_000_000, HbarUnit.Hbar).to(HbarUnit.Tinybar),
},
};
const RECEIVER_ACCOUNT_RES = {
account: ACCOUNT_ADDRESS_1,
balance: {
balance: Hbar.from(1, HbarUnit.Hbar).to(HbarUnit.Tinybar),
},
receiver_sig_required: false,
};
const useAsyncTxProcessing = ConfigService.get('USE_ASYNC_TX_PROCESSING') as boolean;

beforeEach(() => {
Expand All @@ -135,6 +143,7 @@ describe('@ethSendRawTransaction eth_sendRawTransaction spec', async function ()
sdkClientStub = sinon.createStubInstance(SDKClient);
sinon.stub(hapiServiceInstance, 'getSDKClient').returns(sdkClientStub);
restMock.onGet(accountEndpoint).reply(200, ACCOUNT_RES);
restMock.onGet(receiverAccountEndpoint).reply(200, RECEIVER_ACCOUNT_RES);
restMock.onGet(networkExchangeRateEndpoint).reply(200, mockedExchangeRate);
});

Expand Down
3 changes: 2 additions & 1 deletion packages/relay/tests/lib/openrpc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ import {
overrideEnvsInMochaDescribe,
signedTransactionHash,
} from '../helpers';
import { NOT_FOUND_RES } from './eth/eth-config';
import { CONTRACT_RESULT_MOCK, NOT_FOUND_RES } from './eth/eth-config';

const logger = pino();
const registry = new Registry();
Expand Down Expand Up @@ -229,6 +229,7 @@ describe('Open RPC Specification', function () {
mock.onGet(`accounts/${defaultContractResults.results[1].from}?transactions=false`).reply(200);
mock.onGet(`accounts/${defaultContractResults.results[0].to}?transactions=false`).reply(200);
mock.onGet(`accounts/${defaultContractResults.results[1].to}?transactions=false`).reply(200);
mock.onGet(`accounts/${CONTRACT_RESULT_MOCK.from}?transactions=false`).reply(200, CONTRACT_RESULT_MOCK);
mock.onGet(`contracts/${defaultContractResults.results[0].from}`).reply(404, NOT_FOUND_RES);
mock.onGet(`contracts/${defaultContractResults.results[1].from}`).reply(404, NOT_FOUND_RES);
mock.onGet(`contracts/${defaultContractResults.results[0].to}`).reply(200);
Expand Down
46 changes: 46 additions & 0 deletions packages/relay/tests/lib/precheck.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -654,4 +654,50 @@ describe('Precheck', async function () {
expect(error.code).to.equal(predefined.UNSUPPORTED_TRANSACTION_TYPE.code);
});
});

describe('receiverAccount', async function () {
let parsedTx: Transaction;
let mirrorAccountTo: any;
const defaultNonce: number = 4;
const toAddress = ethers.Wallet.createRandom().address;

before(async () => {
const wallet = ethers.Wallet.createRandom();
const signed = await wallet.signTransaction({
...defaultTx,
from: wallet.address,
to: toAddress,
nonce: defaultNonce,
});

parsedTx = ethers.Transaction.from(signed);
});

it('should fail with signature required error', async function () {
mirrorAccountTo = {
receiver_sig_required: true,
};

mock.onGet(`accounts/${parsedTx.to}${transactionsPostFix}`).reply(200, mirrorAccountTo);

try {
await precheck.receiverAccount(parsedTx, requestDetails);
expectedError();
} catch (e: any) {
expect(e).to.exist;
expect(e.code).to.eq(-32000);
expect(e).to.eql(predefined.RECEIVER_SIGNATURE_ENABLED);
}
});

it('should accept check if signature required is set to false', async function () {
mirrorAccountTo = {
receiver_sig_required: false,
};

mock.onGet(`accounts/${parsedTx.to}${transactionsPostFix}`).reply(200, mirrorAccountTo);

expect(async () => await precheck.receiverAccount(parsedTx, requestDetails)).not.to.throw;
});
});
});
93 changes: 92 additions & 1 deletion packages/server/tests/acceptance/rpc_batch1.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@ import Constants from '@hashgraph/json-rpc-relay/dist/lib/constants';
// Errors and constants from local resources
import { predefined } from '@hashgraph/json-rpc-relay/dist/lib/errors/JsonRpcError';
import { RequestDetails } from '@hashgraph/json-rpc-relay/dist/lib/types';
import { FileInfo, FileInfoQuery, Hbar, TransferTransaction } from '@hashgraph/sdk';
import {
AccountCreateTransaction,
FileInfo,
FileInfoQuery,
Hbar,
PrivateKey,
TransferTransaction,
} from '@hashgraph/sdk';
import { expect } from 'chai';
import { ethers } from 'ethers';

Expand Down Expand Up @@ -1586,6 +1593,90 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () {
const error = predefined.NONCE_TOO_LOW(nonce, nonce + 1);
await Assertions.assertPredefinedRpcError(error, sendRawTransaction, true, relay, [signedTx, requestDetails]);
});

it('should fail "eth_sendRawTransaction" if receiver\'s account has receiver_sig_required enabled', async function () {
const newPrivateKey = PrivateKey.generateED25519();
const newAccount = await new AccountCreateTransaction()
.setKey(newPrivateKey.publicKey)
.setInitialBalance(100)
.setReceiverSignatureRequired(true)
.freezeWith(servicesNode.client)
.sign(newPrivateKey);

const transaction = await newAccount.execute(servicesNode.client);
const receipt = await transaction.getReceipt(servicesNode.client);

if (!receipt.accountId) {
throw new Error('Failed to create new account - accountId is null');
}

const toAddress = Utils.idToEvmAddress(receipt.accountId.toString());
const verifyAccount = await mirrorNode.get(`/accounts/${toAddress}`, requestId);

if (verifyAccount && !verifyAccount.account) {
verifyAccount == (await mirrorNode.get(`/accounts/${toAddress}`, requestId));
}
nadezhdapopovaa marked this conversation as resolved.
Show resolved Hide resolved

expect(verifyAccount.receiver_sig_required).to.be.true;

const tx = {
...defaultLegacyTransactionData,
chainId: Number(CHAIN_ID),
nonce: await accounts[0].wallet.getNonce(),
to: toAddress,
from: accounts[0].address,
};

const signedTx = await accounts[0].wallet.signTransaction(tx);

const error = predefined.RECEIVER_SIGNATURE_ENABLED;

await Assertions.assertPredefinedRpcError(error, sendRawTransaction, false, relay, [
signedTx,
requestDetails,
]);
});

it('should execute "eth_sendRawTransaction" if receiver\'s account has receiver_sig_required disabled', async function () {
const newPrivateKey = PrivateKey.generateED25519();
const newAccount = await new AccountCreateTransaction()
.setKey(newPrivateKey.publicKey)
.setInitialBalance(100)
.setReceiverSignatureRequired(false)
.freezeWith(servicesNode.client)
.sign(newPrivateKey);

const transaction = await newAccount.execute(servicesNode.client);
const receipt = await transaction.getReceipt(servicesNode.client);

if (!receipt.accountId) {
throw new Error('Failed to create new account - accountId is null');
}

const toAddress = Utils.idToEvmAddress(receipt.accountId.toString());
const verifyAccount = await mirrorNode.get(`/accounts/${toAddress}`, requestId);

if (verifyAccount && !verifyAccount.account) {
verifyAccount == (await mirrorNode.get(`/accounts/${toAddress}`, requestId));
}

expect(verifyAccount.receiver_sig_required).to.be.false;

const tx = {
...defaultLegacyTransactionData,
chainId: Number(CHAIN_ID),
nonce: await accounts[0].wallet.getNonce(),
to: toAddress,
from: accounts[0].address,
};

const signedTx = await accounts[0].wallet.signTransaction(tx);
const transactionHash = await relay.sendRawTransaction(signedTx, requestId);
const info = await mirrorNode.get(`/contracts/results/${transactionHash}`, requestId);

expect(info).to.exist;
expect(info.result).to.equal('SUCCESS');
});
});

it('@release should execute "eth_getTransactionByHash" for existing transaction', async function () {
Expand Down
Loading