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

[Unit test] role based key fee payer #59

Open
wants to merge 8 commits into
base: dev
Choose a base branch
from

Conversation

minminkikiki
Copy link

@minminkikiki minminkikiki commented Nov 10, 2024

Test Overview

  • A unit test that uses web3js-ext to create a feepayer key as a role-based account(RoleFeePayer), and checks whether transactions can be performed according to each role.

Reference

Understanding of the author, about Role-based Key

(If there was a misunderstanding, the test case itself could be wrong....)

  • Kaia blockchain can distinguish roles by RoleTransaction, RoleAccountUpdate, RoleFeePayer through RoleBasedKey, and can sign each transaction accordingly.
  • The types of transactions that can be signed with the key of the corresponding Role seem to be associated with each 'TxType' below.
    (e.g RoleAccountUpdate(Role) : AccountUpdate(TxType)
    RoleTransaction(Role) : ValueTransfer(TxType))
// Klaytn Type Enumeration
export enum TxType {
  // Basic
  ValueTransfer = 0x08,
  ValueTransferMemo = 0x10,
  AccountUpdate = 0x20,
  SmartContractDeploy = 0x28,
  SmartContractExecution = 0x30,
  Cancel = 0x38,

  // Fee Delegation
  FeeDelegatedValueTransfer = 0x09,
  FeeDelegatedValueTransferMemo = 0x11,
  FeeDelegatedAccountUpdate = 0x21,
  FeeDelegatedSmartContractDeploy = 0x29,
  FeeDelegatedSmartContractExecution = 0x31,
  FeeDelegatedCancel = 0x39,

  // Partial Fee Delegation
  FeeDelegatedValueTransferWithRatio = 0x0a,
  FeeDelegatedValueTransferMemoWithRatio = 0x12,
  FeeDelegatedAccountUpdateWithRatio = 0x22,
  FeeDelegatedSmartContractDeployWithRatio = 0x2a,
  FeeDelegatedSmartContractExecutionWithRatio = 0x32,
  FeeDelegatedCancelWithRatio = 0x3a,
}

  • Therefore, I will conduct a test to check whether each key performs its role properly by signing the transaction with AccountKeyRoleBased that matches each TxType.

Test Cases

  1. Before all tests, set up Role-based Key
before(async function () {
        console.log("\n--- Setting Role-based Key ---");
        const pub1 = getPublicKeyFromPrivate(senderRoleTransactionPriv);
        const pub2 = getPublicKeyFromPrivate(senderRoleAccountUpdatePriv);
        const pub3 = getPublicKeyFromPrivate(senderRoleFeePayerPriv);

        const updateTx = {
            type: TxType.AccountUpdate,
            from: senderAddr,
            gasLimit: 100000,
            key: {
                type: AccountKeyType.RoleBased,
                keys: [
                    { type: AccountKeyType.Public, key: pub1 },
                    { type: AccountKeyType.Public, key: pub2 },
                    { type: AccountKeyType.Public, key: pub3 }
                ]
            }
        };

        const signedUpdateTx = await roleAccountUpdate.signTransaction(updateTx);
        const receipt = await web3.eth.sendSignedTransaction(signedUpdateTx.rawTransaction);
        console.log("Account Updated:", receipt);
        assert.isNotNull(receipt.transactionHash, "Account update transaction should succeed");
    });
  1. Sending a normal transaction with RoleTransaction key
  • Signing transactions with 'RoleTransaction' key for 'TxType.ValueTransfer'
it("1. Sending a normal transaction with RoleTransaction key", async function () {
        const valueTx = {
            type: TxType.ValueTransfer,
            from: senderAddr,
            to: receiverAddr,
            value: toPeb("0.01"),
            gasLimit: 100000
        };

        const signedTx = await roleTransactionAccount.signTransaction(valueTx);
        const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
        console.log("RoleTransaction signedTx:", receipt.transactionHash);
        assert.isNotNull(receipt.transactionHash, "RoleTransaction transaction should succeed");
    });
  1. Attempting to sign a regular transaction with RoleFeePayer key (failure test):
  • Signing transactions with 'RoleFeePayer' key for 'TxType.ValueTransfer'
it("2. Attempting to sign a regular transaction with RoleFeePayer key (failure test)", async function () {
        const valueTx = {
            type: TxType.ValueTransfer,
            from: senderAddr,
            to: receiverAddr,
            value: toPeb("0.01"),
            gasLimit: 100000
        };

        try {
            const signedTx = await roleFeePayerAccount.signTransaction(valueTx);
            const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
            console.log("Unexpected Success - Transaction Hash:", receipt.transactionHash);
            assert.fail("RoleFeePayer key should not sign a regular transaction.");
        } catch (error: any) {
            console.log("Expected Error (RoleFeePayer):", error.message);
            assert.isTrue(true, "Error occurred as expected");
        }
    });
  1. Fee Delegated transaction signed by RoleFeePayer key (should succeed):
  • Signing transactions with 'RoleFeePayer' key for 'TxType.FeeDelegatedValueTransfer'
it("3. Fee Delegated transaction signed by RoleFeePayer key (should succeed)", async function () {
        console.log("\n--- Checking Balances ---");
        const senderBalance = await checkBalance(senderAddr);
        const feePayerBalance = await checkBalance(roleFeePayerAccount.address);

        assert.isAbove(senderBalance, 0.01, "Sender account must have enough balance.");
        assert.isAbove(feePayerBalance, 0.01, "FeePayer account must have enough balance.");

        const feeDelegatedTx = {
            type: TxType.FeeDelegatedValueTransfer,
            from: senderAddr,
            to: receiverAddr,
            value: toPeb("0.01"),
            gasLimit: 100000
        };

        const signedTx = await roleFeePayerAccount.signTransaction(feeDelegatedTx);
        const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
        console.log("Fee Delegated Transaction Hash:", receipt.transactionHash);
        assert.isNotNull(receipt.transactionHash, "RoleFeePayer transaction should succeed");
    });
  1. Attempting to sign Fee Delegated transaction with RoleTransaction key (failure test):
  • Signing transactions with 'RoleTransaction ' key for 'TxType.FeeDelegatedValueTransfer'
it("4. Attempting to sign Fee Delegated transaction with RoleTransaction key (should fail)", async function () {
        console.log("\n--- Checking Balances ---");
        const senderBalance = await checkBalance(senderAddr);

        assert.isAbove(senderBalance, 0.01, "Sender account should have sufficient balance");

        const feeDelegatedTx = {
            type: TxType.FeeDelegatedValueTransfer,
            from: senderAddr,
            to: receiverAddr,
            value: toPeb("0.01"),
            gasLimit: 100000
        };

        try {
            const signedTx = await roleTransactionAccount.signTransaction(feeDelegatedTx);
            const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
            console.log("Unexpected Success - Transaction Hash:", receipt.transactionHash);
            assert.fail("RoleTransaction key should not sign Fee Delegated transactions");
        } catch (error: any) {
            console.log("Expected Error (RoleTransaction as FeePayer):", error.message);
            assert.isTrue(true, "Error occurred as expected");
        }
    });

Test Results

image

  1. Sending a normal transaction with RoleTransaction key
  • Success, by signing with a key (RoleTransaction) that matches the role of the transaction (TxType.ValueTransfer)
  1. Attempting to sign a regular transaction with RoleFeePayer key (failure test):
  • Success, the transaction (TxType.ValueTransfer) is not signed with a key (RoleFeePayer) that matches the role of the transaction (TxType.ValueTransfer) (expected result)
  1. Fee Delegated transaction signed by RoleFeePayer key (should succeed):
  • Failure, An error occurred when the transaction (TxType.FeeDelegatedValueTransfer) must be signed with a key (RoleFeePayer) that matches the role of the transaction to succeed.
  1. Attempting to sign Fee Delegated transaction with RoleTransaction key (failure test):
  • Success, The expected failure result is due to not signing with a key (RoleTransaction) that matches the role of the transaction (TxType.FeeDelegatedValueTransfer).

Test Bug?

  1. Fee Delegated transaction signed by RoleFeePayer key (should succeed):
    image
  • I get an RLP-related error when signing a FeeDelegatedTranscation with RoleFeePayer.

@kjeom kjeom changed the base branch from kjeom-patch-1 to dev November 11, 2024 07:23
// 4) RLP Decoding
const { KlaytnTxFactory } = require("@kaiachain/web3js-ext");
const decoded = KlaytnTxFactory.fromRLP(signedTxByFeePayer.rawTransaction);
console.log("Decoded Transaction:", decoded);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kjeom
[What I Tried]

  1. Decoding (RLP) to Verify FeePayer Address
  • I attempted to check whether the actual FeePayer address matches my intended address by using KlaytnTxFactory.fromRLP(...) after signing the transaction twice (User first, then roleFeePayerAccount).
  • Once both signatures were complete, I decoded the final raw transaction, expecting to see the FeePayer field populated with my intended FeePayer address.
  1. Observation
  • However, after decoding, I found that the feePayer field was empty, suggesting that the FeePayer signature wasn’t reflected in the final transaction.
    image

[Question]
“If the feePayer field is empty in the decoded transaction, does that imply our two-step signing process is incorrect, or could there be a bug in how web3js-ext handles RoleBased FeePayer signatures?”

More specifically:

  • I called signTransaction twice (User → FeePayer), providing all necessary fields (type, from, to, value, gasLimit, gasPrice, nonce, chainId, feePayer, and senderRawTransaction).
  • Despite both signings appearing to succeed, the decoded result shows no FeePayer data.

[Additional Notes]

  • This issue arises when using RoleBased accounts for FeeDelegated transactions. Even though the second signing step looks successful (i.e., I get a new raw transaction), the final RLP does not contain the feePayer field.
  • I'm curious if this is related to a known RoleBased + FeeDelegated limitation or bug in web3js-ext...?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@minminkikiki Could you send the transaction to the network? and check whether the actual feePayer's address is the role-based account's address.

In this case, you need to make the account like following
legacy A : address A - private Key A
legacy B : address B - private Key B
role based C : address A - transaction key A, fee payer key B

Copy link
Author

@minminkikiki minminkikiki Feb 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kjeom I tried it as you told me, and the results are as follows.

legacy A(0x32e62d71311974a1c9e5f723c1750fa85d8a5b76) : address A - private Key A
legacy B(0x399294d64984a33cd30ddae0932df89c414941c4) : address B - private Key B
role based C : address A - transaction key A, fee payer key B
(legacy A/B were created using the kaia online tooklit for sending actual transactions)

[Expected Behavior]

  • Account Update: After updating legacyA to a RoleBased account, its RoleTransaction key is set to legacyA’s private key, and its RoleFeePayer key is set to legacyB’s private key. The account’s address remains legacyA.
  • FeeDelegated Transaction: When a FeeDelegatedValueTransfer transaction is sent:
    The transaction is first signed by legacyA (for the transaction data).
    Then it is signed by legacyB (for fee delegation), with the feePayer field set to legacyA’s address.
  • Final Outcome: The final transaction should show:
    The from field (and ideally the feePayer field) as legacyA’s address.
    The transferred value deducted from legacyA’s balance, and the gas fee paid by legacyB.

[Actual Behavior Observed]

  • The Account Update and Transaction fields correctly show legacyA’s address.
  • However, the feePayer field is either missing or does not match legacyA’s address.
    image
  • In some observations (via Scope Explorer), the feePayer field appears as an entirely different address (e.g., 0x54aef27ee84655cdaac7926bf28aff333a7c4d7a), which is neither legacyA(0x32e62d71311974a1c9e5f723c1750fa85d8a5b76) nor legacyB(0x399294d64984a33cd30ddae0932df89c414941c4).
    image
  • This suggests that the Role-Based Key mechanism is not correctly applying the fee delegation, potentially due to a bug in the signing or transaction construction process.

[Conclusion]

  • The feePayer field is not being correctly populated with legacyA’s address (the RoleBased account), and an unexpected or missing feePayer value is observed.

@minminkikiki minminkikiki requested a review from kjeom January 6, 2025 09:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants