Skip to content

fix: isLongZeroAddress for non-zero shards and realms #3056

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

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
31 changes: 24 additions & 7 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,18 +119,35 @@ export function isStringOrUint8Array(variable) {
}

/**
* Takes an address as `Uint8Array` and returns whether or not this is a long-zero address
* Takes an address as `Uint8Array` and returns whether or not this represents a Hiero account ID
* converted to a solidity address format (rather than a native Ethereum address).
*
* @param {Uint8Array} address
* @returns {boolean}
* An account ID in solidity address format has:
* - First 4 bytes (0-3) = shard
* - Next 8 bytes (4-11) = realm
* - Last 8 bytes (12-19) = account number
*
* This function checks if the middle bytes (4-11) follow the expected pattern
* for a Hiero realm number, which helps differentiate from Ethereum addresses.
*
* @param {Uint8Array} address - The 20-byte address to check
* @returns {boolean} - True if this represents a Hiero account ID, false if it's likely an Ethereum address
*/
export function isLongZeroAddress(address) {
for (let i = 0; i < 12; i++) {
if (address[i] != 0) {
return false;
// Check if the realm bytes (4-11) follow expected pattern
// In a Hiero account ID, these bytes will typically have leading zeros
// and represent a valid realm number
let hasNonZeroBytes = false;
for (let i = 4; i < 8; i++) {
if (address[i] !== 0) {
hasNonZeroBytes = true;
break;
}
}
return true;

// If first 4 bytes are zero and no non-zero bytes found in first half of realm,
// this is likely a Hiero account ID
return !hasNonZeroBytes;
}

/**
Expand Down
114 changes: 114 additions & 0 deletions test/unit/AccountId.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { expect } from "chai";
import BigNumber from "bignumber.js";
import EvmAddress from "../../src/EvmAddress.js";
import { AccountId, PublicKey, Long, PrivateKey } from "../../src/index.js";
import { isLongZeroAddress } from "../../src/util.js";
import * as hex from "../../src/encoding/hex.js";

describe("AccountId", function () {
it("constructors", function () {
Expand Down Expand Up @@ -320,4 +322,116 @@ describe("AccountId", function () {
);
}
});
it("should identify Hiero account IDs with zero shard and realm", function () {
// Create a typical Hiero account address with zeros in shard and realm
const address = new Uint8Array(20);
// Set some non-zero bytes in the account number portion (last 8 bytes)
address[12] = 1;
address[19] = 255;

expect(isLongZeroAddress(address)).to.be.true;
});

it("should identify Hiero account IDs with non-zero shard", function () {
const address = new Uint8Array(20);
// Set non-zero shard (first 4 bytes)
address[0] = 1;
// Keep realm bytes (4-11) as zeros
// Set some non-zero account number
address[12] = 1;

expect(isLongZeroAddress(address)).to.be.true;
});

it("should identify Ethereum addresses", function () {
// Test with a typical Ethereum address
const ethAddress = "0x742d35Cc6634C0532925a3b844Bc454e4438f44e";
const bytes = hex.decode(ethAddress.slice(2)); // Remove '0x' prefix

expect(isLongZeroAddress(bytes)).to.be.false;
});

it("should identify Hiero account IDs with large realm values", function () {
const accountId = new AccountId(1, 50000, 3);
const solAddress = accountId.toSolidityAddress();
const bytes = hex.decode(solAddress);

expect(isLongZeroAddress(bytes)).to.be.true;
});

it("should handle edge case with all zero bytes", function () {
const address = new Uint8Array(20); // All bytes are 0
expect(isLongZeroAddress(address)).to.be.true;
});
it("should handle long-zero format addresses", function () {
// Create an address that represents a Hiero account ID (0.0.5)
const accountId = new AccountId(0, 0, 5);
const solAddress = accountId.toSolidityAddress();

// Convert it back using fromEvmAddress
const result = AccountId.fromEvmAddress(0, 0, solAddress);

// Should reconstruct the original account ID
expect(result.shard.toNumber()).to.equal(0);
expect(result.realm.toNumber()).to.equal(0);
expect(result.num.toNumber()).to.equal(5);
expect(result.evmAddress).to.be.null;
});

it("should handle native EVM addresses", function () {
const evmAddress = "0x742d35Cc6634C0532925a3b844Bc454e4438f44e";
const shard = 1;
const realm = 2;

const result = AccountId.fromEvmAddress(shard, realm, evmAddress);

// For EVM addresses, should store shard/realm and keep the EVM address
expect(result.shard.toNumber()).to.equal(shard);
expect(result.realm.toNumber()).to.equal(realm);
expect(result.num.toNumber()).to.equal(0);
expect(result.evmAddress).to.not.be.null;
expect(result.evmAddress.toString()).to.equal(
evmAddress.slice(2).toLowerCase(),
);
});

it("should handle non-zero shard and realm with long-zero format", function () {
// Create an address that represents a Hiero account ID (1.2.5)
const accountId = new AccountId(1, 2, 5);
const solAddress = accountId.toSolidityAddress();

// Convert it back using fromEvmAddress
const result = AccountId.fromEvmAddress(1, 2, solAddress);

// Should reconstruct the original account ID
expect(result.shard.toNumber()).to.equal(1);
expect(result.realm.toNumber()).to.equal(2);
expect(result.num.toNumber()).to.equal(5);
expect(result.evmAddress).to.be.null;
});

it("should accept both string and EvmAddress objects", function () {
const evmAddressStr = "0x742d35Cc6634C0532925a3b844Bc454e4438f44e";
const evmAddressObj = EvmAddress.fromString(evmAddressStr);

const result1 = AccountId.fromEvmAddress(1, 2, evmAddressStr);
const result2 = AccountId.fromEvmAddress(1, 2, evmAddressObj);

// Both methods should produce equivalent results
expect(result1.toString()).to.equal(result2.toString());
expect(result1.evmAddress.toString()).to.equal(
result2.evmAddress.toString(),
);
});

it("should handle very large account numbers in long-zero format", function () {
// Create an address with a large account number
const accountId = new AccountId(0, 0, Long.fromString("123456789"));
const solAddress = accountId.toSolidityAddress();

const result = AccountId.fromEvmAddress(0, 0, solAddress);

expect(result.num.toString()).to.equal("123456789");
expect(result.evmAddress).to.be.null;
});
});
78 changes: 78 additions & 0 deletions test/unit/ContractId.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,82 @@ describe("ContractId", function () {

expect(contractId).to.deep.equal(contractIdFromAddress);
});
it("should handle long-zero format addresses", function () {
// Create a contract address that represents a Hiero contract (0.0.5)
const contractId = new ContractId(0, 0, 5);
const solAddress = contractId.toSolidityAddress();

// Convert it back using fromEvmAddress
const result = ContractId.fromEvmAddress(0, 0, solAddress);

// Should reconstruct the original contract ID
expect(result.shard.toNumber()).to.equal(0);
expect(result.realm.toNumber()).to.equal(0);
expect(result.num.toNumber()).to.equal(5);
expect(result.evmAddress).to.be.null;
});

it("should handle native EVM addresses", function () {
const evmAddress = "742d35Cc6634C0532925a3b844Bc454e4438f44e";
const shard = 1;
const realm = 2;

const result = ContractId.fromEvmAddress(shard, realm, evmAddress);

// For EVM addresses, should store shard/realm and keep the EVM address
expect(result.shard.toNumber()).to.equal(shard);
expect(result.realm.toNumber()).to.equal(realm);
expect(result.num.toNumber()).to.equal(0);
expect(result.evmAddress).to.not.be.null;
expect(hex.encode(result.evmAddress)).to.equal(
evmAddress.toLowerCase(),
);
});

it("should handle non-zero shard and realm with long-zero format", function () {
// Create a contract address that represents a Hiero contract (1.2.5)
const contractId = new ContractId(1, 2, 5);
const solAddress = contractId.toSolidityAddress();

// Convert it back using fromEvmAddress
const result = ContractId.fromEvmAddress(1, 2, solAddress);

// Should reconstruct the original contract ID
expect(result.shard.toNumber()).to.equal(1);
expect(result.realm.toNumber()).to.equal(2);
expect(result.num.toNumber()).to.equal(5);
expect(result.evmAddress).to.be.null;
});

it("should handle addresses with 0x prefix", function () {
const evmAddress = "0x742d35Cc6634C0532925a3b844Bc454e4438f44e";
const result = ContractId.fromEvmAddress(1, 2, evmAddress);

expect(hex.encode(result.evmAddress)).to.equal(
evmAddress.slice(2).toLowerCase(),
);
});

it("should handle very large contract numbers in long-zero format", function () {
// Create a contract with a large number
const contractId = new ContractId(0, 0, Long.fromString("123456789"));
const solAddress = contractId.toSolidityAddress();

const result = ContractId.fromEvmAddress(0, 0, solAddress);

expect(result.num.toString()).to.equal("123456789");
expect(result.evmAddress).to.be.null;
});

it("should handle negative shard/realm values", function () {
const evmAddress = "742d35Cc6634C0532925a3b844Bc454e4438f44e";

expect(() => {
ContractId.fromEvmAddress(-1, 0, evmAddress);
}).to.throw();

expect(() => {
ContractId.fromEvmAddress(0, -1, evmAddress);
}).to.throw();
});
});