Skip to content

feat: add LOOKUP_DELEGATED_ADDRESS precompile wrapper#6

Closed
pali101 wants to merge 5 commits intofilecoin-project:mainfrom
pali101:feat/lookup-delegated-address-precompile
Closed

feat: add LOOKUP_DELEGATED_ADDRESS precompile wrapper#6
pali101 wants to merge 5 commits intofilecoin-project:mainfrom
pali101:feat/lookup-delegated-address-precompile

Conversation

@pali101
Copy link
Copy Markdown
Contributor

@pali101 pali101 commented Jan 25, 2026

Summary

Implements the LOOKUP_DELEGATED_ADDRESS precompile wrapper (0xfe...02) to retrieve an actor's delegated address (f4) and extract the Ethereum-style address using optimized inline assembly.

Key Changes

  • Library: Added FVMLookupDelegatedAddress with:

    • lookupDelegatedAddress(uint64) → bytes: Retrieves the raw delegated address. Handles the "success with no data" case for actors without a delegated address.
    • lookupDelegatedAddressStrict(uint64) → bytes: Reverts with NoDelegatedAddress() if the lookup returns empty.
    • toEthAddress(bytes) → address: Validates the f410 format (length 22, prefix 0x040a) and extracts the last 20 bytes as a standard Ethereum address.
  • Mocks:

    • Added src/mocks/FVMLookupDelegatedAddress.sol which mirrors FVM behavior, including $uint64$ boundary checks.
    • Updated MockFVMTest.sol to etch the precompile and expose LOOKUP_ADDR for low-level testing.
  • Tests: Added LookupDelegatedAddress.t.sol with coverage, including:

    • Standard and Strict resolution.
    • Fuzzed Ethereum address extraction.
    • Boundary checks for Actor ID 0 and $max$ $uint64$.
    • Validation of invalid prefixes and incorrect lengths.

Closes #2


import {LOOKUP_DELEGATED_ADDRESS} from "./FVMPrecompiles.sol";

library FVMLookupDelegatedAddress {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

library FVMActor


// Update free memory pointer
// Round up to nearest 32-byte boundary
let newFmp := add(add(delegatedAddress, 0x20), and(add(returnSize, 0x1f), not(0x1f)))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

can we assume delegated addresses are all the same length?

uint256 actorIdFull = abi.decode(msg.data, (uint256));

// Validate that actor ID fits in u64 (max u64 = 2^64 - 1)
require(actorIdFull <= type(uint64).max, "Invalid actor ID: exceeds max u64");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do these errors match the ones in builtin-actors?

/// @notice Mock a delegated address lookup
/// @param actorId The actor ID
/// @param delegatedAddress The delegated address to return (empty bytes means no delegated address)
function mockLookupDelegatedAddress(uint64 actorId, bytes memory delegatedAddress) external {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

also provide a function to mock with solidity address.

using FVMLookupDelegatedAddress for bytes;

/// @notice Lookup the delegated address of an actor ID
function lookup(uint64 actorId) external view returns (bytes memory delegatedAddress) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

please deploy one of these to calibration and link it in the PR

// Handle execution failure
if iszero(success) {
mstore(0, errorSelector)
revert(0, 4)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this won't work because the error would be at 28..32 after mstore. Instead, revert(28, 4).

let success := staticcall(gas(), LOOKUP_DELEGATED_ADDRESS, fmp, 32, 0, 0)

// Handle execution failure
if iszero(success) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this possible?

/// @dev Validates that the address is 22 bytes and starts with 0x040a
/// @param delegatedAddress The delegated address (f410 format)
/// @return ethAddress The Ethereum-style address (last 20 bytes)
function toEthAddress(bytes memory delegatedAddress) internal pure returns (address ethAddress) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

move to separate library such as FVMAddress

assertEq(result, expected, "Extracted address should match");
}

function testToEthAddressInvalidLength() public {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

move toEthAddress tests to separate test file

// Shift right 240 bits (30 bytes) to get first 2 bytes
if iszero(eq(shr(240, firstWord), 0x040a)) {
mstore(0, errorSelector)
revert(0, 4)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

revert(28, 4)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I had to test this to be sure! It looks like because errorSelector is passed in as a Solidity bytes4, it actually gets left-aligned on the stack. That puts it at the very beginning of the memory word, making revert(0, 4) the correct offset here.

If we were using a raw Yul literal inside the assembly block, you'd be exactly right with revert(28, 4).

Quick test to confirm

revert(0, 4) correctly returns the selector, while revert(28, 4) returns zeros.

error PrecompileCallFailed();

// Reverts with 0xfd23ff64 (PrecompileCallFailed)
function rawStore() public pure returns (bytes32) {
    bytes4 selector = PrecompileCallFailed.selector;
    assembly ("memory-safe") {
        mstore(0, selector)
        revert(0,4) 
    }
}

// Reverts with 0x00000000
function rawStore28() public pure returns (bytes32) {
    bytes4 selector = PrecompileCallFailed.selector;
    assembly ("memory-safe") {
        mstore(0, selector)
        revert(28,4)
    }
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

That's pretty annoying because there is a difference in codesize and likely gas. Compare hardcoding the selector

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll update the code to use the hardcoded selector (0x8eb60a41) and revert(28, 4). Just a heads up, I'm moving all this work to a new branch and will raise a fresh PR with all the comments addressed by tonight.

@pali101
Copy link
Copy Markdown
Contributor Author

pali101 commented Mar 1, 2026

Closing in favor of #9.
Since PR #5 completely restructured the library into FVMActor and FVMAddress, it was much cleaner to open a fresh branch than to fight the merge conflicts here. All of the feedback from this thread (#6) has been implemented in the new PR.

@pali101 pali101 closed this Mar 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support precompile: LookupDelegatedAddress

2 participants