From e3f52828c76463caafdf938eee5db458a7a700c5 Mon Sep 17 00:00:00 2001 From: Julink Date: Tue, 23 Jul 2024 16:35:56 +0200 Subject: [PATCH] feat: make e2e L1Resolver tests work on local stack --- .../linea-ccip-gateway/src/L2ProofService.ts | 6 + packages/linea-ens-resolver/.mocharc.json | 9 +- packages/linea-ens-resolver/package.json | 4 +- .../test/testL1Resolver.spec.ts | 256 ++++----- .../test/testL1ResolverLocal.spec.ts | 488 ++++++++++++++++++ packages/linea-ens-resolver/test/utils.ts | 72 +++ pnpm-lock.yaml | 26 +- 7 files changed, 684 insertions(+), 177 deletions(-) create mode 100644 packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts create mode 100644 packages/linea-ens-resolver/test/utils.ts diff --git a/packages/linea-ccip-gateway/src/L2ProofService.ts b/packages/linea-ccip-gateway/src/L2ProofService.ts index 40a41cc37..bc9103d6e 100644 --- a/packages/linea-ccip-gateway/src/L2ProofService.ts +++ b/packages/linea-ccip-gateway/src/L2ProofService.ts @@ -90,6 +90,12 @@ export class L2ProofService implements IProofService { ): Promise { try { let proof = await this.helper.getProofs(blockNo, address, slots); + if (!proof.accountProof) { + throw `No account proof on contract ${address} for block number ${blockNo}`; + } + if (proof.storageProofs.length === 0) { + throw `No storage proofs on contract ${address} for block number ${blockNo}`; + } proof = this.checkStorageInitialized(proof); return AbiCoder.defaultAbiCoder().encode( [ diff --git a/packages/linea-ens-resolver/.mocharc.json b/packages/linea-ens-resolver/.mocharc.json index 48586d7d7..6eb3b2b78 100644 --- a/packages/linea-ens-resolver/.mocharc.json +++ b/packages/linea-ens-resolver/.mocharc.json @@ -1,7 +1,8 @@ { "require": "ts-node/register", "loader": "ts-node/esm", - "extensions": ["ts", "tsx"], - "spec": ["test/**/*.spec.*"], - "watch-files": ["src"] -} + "extensions": [ + "ts", + "tsx" + ] +} \ No newline at end of file diff --git a/packages/linea-ens-resolver/package.json b/packages/linea-ens-resolver/package.json index 3ee6b29b4..0072ea955 100644 --- a/packages/linea-ens-resolver/package.json +++ b/packages/linea-ens-resolver/package.json @@ -3,8 +3,8 @@ "version": "1.0.0", "description": "L1 contracts to resolve Linea ENS domains stored on Linea from L1", "scripts": { - "test": "hardhat compile && cd ../linea-ccip-gateway && npm run build && cd ../linea-ens-resolver && mocha test/testL1Resolver.spec.ts --timeout 120000 --exit", - "test:local-stack": "hardhat compile && cd ../linea-ccip-gateway && npm run build && cd ../linea-ens-resolver && mocha test/testL1ResolverLocalStack.spec.ts --timeout 120000 --exit", + "test": "hardhat compile && cd ../linea-ccip-gateway && npm run build && cd ../linea-ens-resolver && mocha test/testL1Resolver.spec.ts --timeout 300000 --exit", + "test:local": "hardhat compile && cd ../linea-ccip-gateway && npm run build && cd ../linea-ens-resolver && mocha test/testL1ResolverLocal.spec.ts --timeout 300000 --exit", "compile": "hardhat compile", "clean": "rm -fr artifacts cache node_modules typechain-types" }, diff --git a/packages/linea-ens-resolver/test/testL1Resolver.spec.ts b/packages/linea-ens-resolver/test/testL1Resolver.spec.ts index 4d94dea59..d741ac24d 100644 --- a/packages/linea-ens-resolver/test/testL1Resolver.spec.ts +++ b/packages/linea-ens-resolver/test/testL1Resolver.spec.ts @@ -1,9 +1,19 @@ import { makeL2Gateway } from "linea-ccip-gateway"; import { Server } from "@chainlink/ccip-read-server"; +import { HardhatEthersProvider } from "@nomicfoundation/hardhat-ethers/internal/hardhat-ethers-provider"; +import { HardhatEthersHelpers } from "@nomicfoundation/hardhat-ethers/types"; import { expect } from "chai"; -import { AbiCoder, Contract, JsonRpcProvider, ethers as ethersT } from "ethers"; +import { + AbiCoder, + BrowserProvider, + Contract, + JsonRpcProvider, + Signer, + ethers as ethersT, +} from "ethers"; import { FetchRequest } from "ethers"; import { ethers } from "hardhat"; +import { EthereumProvider } from "hardhat/types"; import request from "supertest"; import packet from "dns-packet"; import { @@ -19,10 +29,6 @@ import { retValueTest, retValueLongTest, } from "./testData"; -import { HardhatEthersProvider } from "@nomicfoundation/hardhat-ethers/internal/hardhat-ethers-provider"; -import { HardhatEthersHelpers } from "@nomicfoundation/hardhat-ethers/types"; -import { EthereumProvider } from "hardhat/types"; - const labelhash = (label) => ethers.keccak256(ethers.toUtf8Bytes(label)); const encodeName = (name) => "0x" + packet.name.encode(name).toString("hex"); const domainName = "linea-test"; @@ -30,18 +36,10 @@ const baseDomain = `${domainName}.eth`; const node = ethers.namehash(baseDomain); const encodedname = encodeName(baseDomain); -// Account 1 on L1 "FOR LOCAL DEV ONLY - DO NOT REUSE THESE KEYS ELSEWHERE" -const SIGNER_L1_PK = - "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a"; -// Account 1 on L1 "FOR LOCAL DEV ONLY - DO NOT REUSE THESE KEYS ELSEWHERE" -const SIGNER_L2_PK = - "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63"; - -const REGISTRANT_ADDR = "0xFE3B557E8Fb62b89F4916B721be55cEb828dBd73"; - -const SUB_DOMAIN = "testpoh.linea-test.eth"; -const subDomainNode = ethers.namehash(SUB_DOMAIN); -const encodedSubDomain = encodeName(SUB_DOMAIN); +const registrantAddr = "0x4a8e79E5258592f208ddba8A8a0d3ffEB051B10A"; +const subDomain = "testpoh.linea-test.eth"; +const subDomainNode = ethers.namehash(subDomain); +const encodedSubDomain = encodeName(subDomain); const EMPTY_ADDRESS = "0x0000000000000000000000000000000000000000"; const EMPTY_BYTES32 = @@ -52,14 +50,6 @@ const PROOF_ENCODING_PADDING = const ACCEPTED_L2_BLOCK_RANGE_LENGTH = 86400; -const ROLLUP_CONTRACT_ADDR = "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9"; - -const L1_NODE_URL = "http://localhost:8445/"; -const L1_CHAIN_ID = 31648428; -const L2_NODE_URL = "http://localhost:8845/"; -const L2_CHAIN_ID = 1337; -const SHOMEI_NODE_URL = "http://localhost:8889/"; - type ethersObj = typeof ethersT & Omit & { provider: Omit & { @@ -68,7 +58,6 @@ type ethersObj = typeof ethersT & }; declare module "hardhat/types/runtime" { - //@ts-ignore const ethers: ethersObj; interface HardhatRuntimeEnvironment { ethers: ethersObj; @@ -76,54 +65,61 @@ declare module "hardhat/types/runtime" { } describe("Crosschain Resolver", () => { - let l1Provider: JsonRpcProvider; + let l1Provider: BrowserProvider; let l2Provider: JsonRpcProvider; + let l1SepoliaProvider: JsonRpcProvider; + let signer: Signer; let verifier: Contract; let target: Contract; - let l2Resolver: Contract; + let l2contract: Contract; let ens: Contract; let wrapper: Contract; let baseRegistrar: Contract; let rollup: Contract; - let signerL1, - signerL2, - signerL1Address, - signerL2Address, - l2ResolverAddress, - wrapperAddress; + let signerAddress, l2ResolverAddress, wrapperAddress; before(async () => { - l1Provider = new ethers.JsonRpcProvider(L1_NODE_URL, L1_CHAIN_ID, { - staticNetwork: true, - }); - - l2Provider = new ethers.JsonRpcProvider(L2_NODE_URL, L2_CHAIN_ID, { - staticNetwork: true, - }); - signerL1 = new ethers.Wallet(SIGNER_L1_PK, l1Provider); - signerL1Address = await signerL1.getAddress(); - - signerL2 = new ethers.Wallet(SIGNER_L2_PK, l2Provider); - signerL2Address = await signerL2.getAddress(); - - rollup = await ethers.getContractAt( - "RollupMock", - ROLLUP_CONTRACT_ADDR, - signerL1 + // Hack to get a 'real' ethers provider from hardhat. The default `HardhatProvider` + // doesn't support CCIP-read. + l1Provider = new ethers.BrowserProvider(ethers.provider._hardhatProvider); + // Those test work only with a specific contract deployed on linea sepolia + l2Provider = new ethers.JsonRpcProvider( + "https://rpc.sepolia.linea.build/", + 59140, + { + staticNetwork: true, + } ); - - const shomeiNode = new ethers.JsonRpcProvider( - SHOMEI_NODE_URL, - L1_CHAIN_ID, + // We need this provider to get the latest L2BlockNumber along with the the linea state root hash + l1SepoliaProvider = new ethers.JsonRpcProvider( + "https://gateway.tenderly.co/public/sepolia", + 11155111, { staticNetwork: true, } ); + signer = await l1Provider.getSigner(0); + signerAddress = await signer.getAddress(); + + const Rollup = await ethers.getContractFactory("RollupMock", signer); + + // We query the latest block number and state root hash on the actual L1 sepolia chain + // because otherwise if we hard code a block number and state root hash the test is no longer + // working after a while as linea_getProof stops working for older blocks + const rollupSepolia = new ethers.Contract( + "0xB218f8A4Bc926cF1cA7b3423c154a0D627Bdb7E5", + Rollup.interface, + l1SepoliaProvider + ); + const currentL2BlockNumber = await rollupSepolia.currentL2BlockNumber(); + const stateRootHash = + await rollupSepolia.stateRootHashes(currentL2BlockNumber); + rollup = await Rollup.deploy(currentL2BlockNumber, stateRootHash); + const gateway = makeL2Gateway( l1Provider as unknown as JsonRpcProvider, l2Provider, - await rollup.getAddress(), - shomeiNode + await rollup.getAddress() ); const server = new Server(); gateway.add(server); @@ -148,75 +144,56 @@ describe("Crosschain Resolver", () => { }, }; }); - - const ensFactory = await ethers.getContractFactory("ENSRegistry", signerL1); + const ensFactory = await ethers.getContractFactory("ENSRegistry", signer); ens = await ensFactory.deploy(); - await ens.waitForDeployment(); - const ensAddress = await ens.getAddress(); const baseRegistrarFactory = await ethers.getContractFactory( "BaseRegistrarImplementation", - signerL1 + signer ); baseRegistrar = await baseRegistrarFactory.deploy( ensAddress, ethers.namehash("eth") ); - await baseRegistrar.waitForDeployment(); - const baseRegistrarAddress = await baseRegistrar.getAddress(); - let tx = await baseRegistrar.addController(signerL1Address); - await tx.wait(); - + await baseRegistrar.addController(signerAddress); const metaDataserviceFactory = await ethers.getContractFactory( "StaticMetadataService", - signerL1 + signer ); const metaDataservice = await metaDataserviceFactory.deploy( "https://ens.domains" ); - await metaDataservice.waitForDeployment(); - const metaDataserviceAddress = await metaDataservice.getAddress(); const reverseRegistrarFactory = await ethers.getContractFactory( "ReverseRegistrar", - signerL1 + signer ); const reverseRegistrar = await reverseRegistrarFactory.deploy(ensAddress); - await reverseRegistrar.waitForDeployment(); - const reverseRegistrarAddress = await reverseRegistrar.getAddress(); - tx = await ens.setSubnodeOwner( + await ens.setSubnodeOwner( EMPTY_BYTES32, labelhash("reverse"), - signerL1 + signerAddress ); - await tx.wait(); - - tx = await ens.setSubnodeOwner( + await ens.setSubnodeOwner( ethers.namehash("reverse"), labelhash("addr"), reverseRegistrarAddress ); - await tx.wait(); - - tx = await ens.setSubnodeOwner( + await ens.setSubnodeOwner( EMPTY_BYTES32, labelhash("eth"), baseRegistrarAddress ); - await tx.wait(); - - tx = await baseRegistrar.register( + await baseRegistrar.register( labelhash(domainName), - signerL1Address, + signerAddress, 100000000 ); - await tx.wait(); - const publicResolverFactory = await ethers.getContractFactory( "PublicResolver", - signerL1 + signer ); const publicResolver = await publicResolverFactory.deploy( ensAddress, @@ -224,35 +201,31 @@ describe("Crosschain Resolver", () => { "0x0000000000000000000000000000000000000000", reverseRegistrarAddress ); - await publicResolver.waitForDeployment(); - const publicResolverAddress = await publicResolver.getAddress(); - tx = await reverseRegistrar.setDefaultResolver(publicResolverAddress); - await tx.wait(); + await reverseRegistrar.setDefaultResolver(publicResolverAddress); const wrapperFactory = await ethers.getContractFactory( "NameWrapper", - signerL1 + signer ); + await l1Provider.send("evm_mine", []); wrapper = await wrapperFactory.deploy( ensAddress, baseRegistrarAddress, metaDataserviceAddress ); - await wrapper.waitForDeployment(); - wrapperAddress = await wrapper.getAddress(); + const impl = await ethers.getContractFactory("PublicResolver", signer); + l2ResolverAddress = "0x28F15B034f9744d43548ac64DCE04ed77BdBd832"; - const Mimc = await ethers.getContractFactory("Mimc", signerL1); + const Mimc = await ethers.getContractFactory("Mimc", signer); const mimc = await Mimc.deploy(); - await mimc.waitForDeployment(); const SparseMerkleProof = await ethers.getContractFactory( "SparseMerkleProof", - { libraries: { Mimc: await mimc.getAddress() }, signer: signerL1 } + { libraries: { Mimc: await mimc.getAddress() }, signer } ); const sparseMerkleProof = await SparseMerkleProof.deploy(); - await sparseMerkleProof.waitForDeployment(); const verifierFactory = await ethers.getContractFactory( "LineaSparseProofVerifier", @@ -260,42 +233,17 @@ describe("Crosschain Resolver", () => { libraries: { SparseMerkleProof: await sparseMerkleProof.getAddress(), }, - signer: signerL1, + signer, } ); verifier = await verifierFactory.deploy( ["test:"], await rollup.getAddress() ); - await verifier.waitForDeployment(); - - const impl = await ethers.getContractFactory( - "DelegatableResolver", - signerL2 - ); - const implContract = await impl.deploy(); - await implContract.waitForDeployment(); - - const testL2Factory = await ethers.getContractFactory( - "DelegatableResolverFactory", - signerL2 - ); - const l2factoryContract = await testL2Factory.deploy( - await implContract.getAddress() - ); - await l2factoryContract.waitForDeployment(); - - tx = await l2factoryContract.create(await signerL2.getAddress()); - await tx.wait(); - - const logs = await l2factoryContract.queryFilter("NewDelegatableResolver"); - //@ts-ignore - const [resolver] = logs[0].args; - l2ResolverAddress = resolver; const l1ResolverFactory = await ethers.getContractFactory( "L1Resolver", - signerL1 + signer ); const verifierAddress = await verifier.getAddress(); target = await l1ResolverFactory.deploy( @@ -305,21 +253,19 @@ describe("Crosschain Resolver", () => { "https://api.studio.thegraph.com/query/69290/ens-linea-sepolia/version/latest", 59141 ); - await target.waitForDeployment(); - - l2Resolver = impl.attach(l2ResolverAddress); - tx = await l2Resolver["setAddr(bytes32,address)"]( - subDomainNode, - REGISTRANT_ADDR + // Mine an empty block so we have something to prove against + await l1Provider.send("evm_mine", []); + l2contract = new ethers.Contract( + l2ResolverAddress, + impl.interface, + l2Provider ); - await tx.wait(); }); it("should not allow non owner to set target", async () => { const incorrectname = encodeName("notowned.eth"); try { - const tx = await target.setTarget(incorrectname, l2ResolverAddress); - await tx.wait(); + await target.setTarget(incorrectname, l2ResolverAddress); throw "Should have reverted"; } catch (e) { expect(e.reason).equal("Not authorized to set target for this node"); @@ -330,48 +276,42 @@ describe("Crosschain Resolver", () => { }); it("should allow owner to set target", async () => { - const tx = await target.setTarget(encodedname, signerL1Address); - await tx.wait(); + await target.setTarget(encodedname, signerAddress); const result = await target.getTarget(encodeName(baseDomain)); - expect(result[1]).to.equal(signerL1Address); + expect(result[1]).to.equal(signerAddress); }); it("subname should get target of its parent", async () => { - const tx = await target.setTarget(encodedname, signerL1Address); - await tx.wait(); + await target.setTarget(encodedname, signerAddress); const result = await target.getTarget(encodedSubDomain); expect(result[0]).to.equal(subDomainNode); - expect(result[1]).to.equal(signerL1Address); + expect(result[1]).to.equal(signerAddress); }); it("should allow wrapped owner to set target", async () => { const label = "wrapped"; const tokenId = labelhash(label); - let tx = await baseRegistrar.setApprovalForAll(wrapperAddress, true); - await tx.wait(); - tx = await baseRegistrar.register(tokenId, signerL1Address, 100000000); - await tx.wait(); - tx = await wrapper.wrapETH2LD( + await baseRegistrar.setApprovalForAll(wrapperAddress, true); + await baseRegistrar.register(tokenId, signerAddress, 100000000); + await wrapper.wrapETH2LD( label, - signerL1Address, + signerAddress, 0, // CAN_DO_EVERYTHING EMPTY_ADDRESS ); - await tx.wait(); const wrappedtname = encodeName(`${label}.eth`); - tx = await target.setTarget(wrappedtname, l2ResolverAddress); - await tx.wait(); + await target.setTarget(wrappedtname, l2ResolverAddress); const encodedname = encodeName(`${label}.eth`); const result = await target.getTarget(encodedname); expect(result[1]).to.equal(l2ResolverAddress); }); it("should resolve empty ETH Address", async () => { - let tx = await target.setTarget(encodedname, l2ResolverAddress); - await tx.wait(); + await target.setTarget(encodedname, l2ResolverAddress); const addr = "0x0000000000000000000000000000000000000000"; - const result = await l2Resolver["addr(bytes32)"](node); + const result = await l2contract["addr(bytes32)"](node); expect(result).to.equal(addr); + await l1Provider.send("evm_mine", []); const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); const calldata = i.encodeFunctionData("addr", [node]); @@ -384,8 +324,8 @@ describe("Crosschain Resolver", () => { it("should resolve ETH Address", async () => { await target.setTarget(encodedname, l2ResolverAddress); - const result = await l2Resolver["addr(bytes32)"](subDomainNode); - expect(ethers.getAddress(result)).to.equal(REGISTRANT_ADDR); + const result = await l2contract["addr(bytes32)"](subDomainNode); + expect(ethers.getAddress(result)).to.equal(registrantAddr); await l1Provider.send("evm_mine", []); const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); @@ -395,7 +335,7 @@ describe("Crosschain Resolver", () => { }); const decoded = i.decodeFunctionResult("addr", result2); expect(ethers.getAddress(decoded[0])).to.equal( - ethers.getAddress(REGISTRANT_ADDR) + ethers.getAddress(registrantAddr) ); }); @@ -452,7 +392,7 @@ describe("Crosschain Resolver", () => { it("should revert when the functions's selector is invalid", async () => { await target.setTarget(encodedname, l2ResolverAddress); const addr = "0x0000000000000000000000000000000000000000"; - const result = await l2Resolver["addr(bytes32)"](node); + const result = await l2contract["addr(bytes32)"](node); expect(result).to.equal(addr); await l1Provider.send("evm_mine", []); const i = new ethers.Interface([ @@ -472,7 +412,7 @@ describe("Crosschain Resolver", () => { it("should revert if the calldata is too short", async () => { await target.setTarget(encodedname, l2ResolverAddress); const addr = "0x0000000000000000000000000000000000000000"; - const result = await l2Resolver["addr(bytes32)"](node); + const result = await l2contract["addr(bytes32)"](node); expect(result).to.equal(addr); await l1Provider.send("evm_mine", []); const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); diff --git a/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts b/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts new file mode 100644 index 000000000..736620357 --- /dev/null +++ b/packages/linea-ens-resolver/test/testL1ResolverLocal.spec.ts @@ -0,0 +1,488 @@ +import { makeL2Gateway } from "linea-ccip-gateway"; +import { Server } from "@chainlink/ccip-read-server"; +import { expect } from "chai"; +import { Contract, JsonRpcProvider, ethers as ethersT } from "ethers"; +import { FetchRequest } from "ethers"; +import { ethers } from "hardhat"; +import request from "supertest"; +import packet from "dns-packet"; +import { HardhatEthersProvider } from "@nomicfoundation/hardhat-ethers/internal/hardhat-ethers-provider"; +import { HardhatEthersHelpers } from "@nomicfoundation/hardhat-ethers/types"; +import { EthereumProvider } from "hardhat/types"; +import { + deployContract, + getAndIncreaseFeeData, + sendTransactionsWithInterval, + waitForL2BlockNumberFinalized, + waitForLatestL2BlockNumberFinalizedToChange, +} from "./utils"; + +const labelhash = (label) => ethers.keccak256(ethers.toUtf8Bytes(label)); +const encodeName = (name) => "0x" + packet.name.encode(name).toString("hex"); +const domainName = "linea-test"; +const baseDomain = `${domainName}.eth`; +const node = ethers.namehash(baseDomain); +const encodedname = encodeName(baseDomain); +const contenthash = + "0xe3010170122029f2d17be6139079dc48696d1f582a8530eb9805b561eda517e22a892c7e3f1f"; +const testAddr = "0x76a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac"; +const coinType = 0; + +// Account 1 on L1 "FOR LOCAL DEV ONLY - DO NOT REUSE THESE KEYS ELSEWHERE" +const SIGNER_L1_PK = + "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a"; +// Account 1 on L1 "FOR LOCAL DEV ONLY - DO NOT REUSE THESE KEYS ELSEWHERE" +const SIGNER_L2_PK = + "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63"; + +const REGISTRANT_ADDR = "0xFE3B557E8Fb62b89F4916B721be55cEb828dBd73"; + +const SUB_DOMAIN = "testpoh.linea-test.eth"; +const subDomainNode = ethers.namehash(SUB_DOMAIN); +const encodedSubDomain = encodeName(SUB_DOMAIN); + +const EMPTY_ADDRESS = "0x0000000000000000000000000000000000000000"; +const EMPTY_BYTES32 = + "0x0000000000000000000000000000000000000000000000000000000000000000"; + +const ROLLUP_CONTRACT_ADDR = "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9"; + +const L1_NODE_URL = "http://localhost:8445/"; +const L1_CHAIN_ID = 31648428; +const L2_NODE_URL = "http://localhost:8845/"; +const L2_CHAIN_ID = 1337; +const SHOMEI_NODE_URL = "http://localhost:8889/"; + +type ethersObj = typeof ethersT & + Omit & { + provider: Omit & { + _hardhatProvider: EthereumProvider; + }; + }; + +declare module "hardhat/types/runtime" { + //@ts-ignore + const ethers: ethersObj; + interface HardhatRuntimeEnvironment { + ethers: ethersObj; + } +} + +// These tests need to be run along the Linea local stack running +describe("Crosschain Resolver Local", () => { + let l1Provider: JsonRpcProvider; + let l2Provider: JsonRpcProvider; + let verifier: Contract; + let target: Contract; + let l2Resolver: Contract; + let wrapper: Contract; + let baseRegistrar: Contract; + let rollup: Contract; + let signerL1, + signerL2, + signerL1Address, + signerL2Address, + l2ResolverAddress, + wrapperAddress; + let lastSetupTxBlockNumber: number; + let sendTransactionsPromise: NodeJS.Timeout; + + before(async () => { + // Setup providers and signers + l1Provider = new ethers.JsonRpcProvider(L1_NODE_URL, L1_CHAIN_ID, { + staticNetwork: true, + }); + l2Provider = new ethers.JsonRpcProvider(L2_NODE_URL, L2_CHAIN_ID, { + staticNetwork: true, + }); + signerL1 = new ethers.Wallet(SIGNER_L1_PK, l1Provider); + signerL1Address = await signerL1.getAddress(); + signerL2 = new ethers.Wallet(SIGNER_L2_PK, l2Provider); + signerL2Address = await signerL2.getAddress(); + rollup = await ethers.getContractAt( + "RollupMock", + ROLLUP_CONTRACT_ADDR, + signerL1 + ); + + // Setup CCIP Gateway + const shomeiNode = new ethers.JsonRpcProvider( + SHOMEI_NODE_URL, + L2_CHAIN_ID, + { + staticNetwork: true, + } + ); + const gateway = makeL2Gateway( + l1Provider as unknown as JsonRpcProvider, + l2Provider, + await rollup.getAddress(), + shomeiNode + ); + const server = new Server(); + gateway.add(server); + const app = server.makeApp("/"); + const getUrl = FetchRequest.createGetUrlFunc(); + ethers.FetchRequest.registerGetUrl(async (req: FetchRequest) => { + if (req.url != "test:") return getUrl(req); + + const r = request(app).post("/"); + if (req.hasBody()) { + r.set("Content-Type", "application/json").send( + ethers.toUtf8String(req.body) + ); + } + const response = await r; + return { + statusCode: response.statusCode, + statusMessage: response.ok ? "OK" : response.statusCode.toString(), + body: ethers.toUtf8Bytes(JSON.stringify(response.body)), + headers: { + "Content-Type": "application/json", + }, + }; + }); + + // Deploy and configure contracts + const ens = await deployContract("ENSRegistry", signerL1); + baseRegistrar = await deployContract( + "BaseRegistrarImplementation", + signerL1, + await ens.getAddress(), + ethers.namehash("eth") + ); + const baseRegistrarAddress = await baseRegistrar.getAddress(); + await baseRegistrar.addController(signerL1Address).then((tx) => tx.wait()); + const metaDataservice = await deployContract( + "StaticMetadataService", + signerL1, + "" + ); + const reverseRegistrar = await deployContract( + "ReverseRegistrar", + signerL1, + await ens.getAddress() + ); + const reverseRegistrarAddress = await reverseRegistrar.getAddress(); + + await ens + .setSubnodeOwner(EMPTY_BYTES32, labelhash("reverse"), signerL1) + .then((tx) => tx.wait()); + await ens + .setSubnodeOwner( + ethers.namehash("reverse"), + labelhash("addr"), + reverseRegistrarAddress + ) + .then((tx) => tx.wait()); + await ens + .setSubnodeOwner(EMPTY_BYTES32, labelhash("eth"), baseRegistrarAddress) + .then((tx) => tx.wait()); + await baseRegistrar + .register(labelhash(domainName), signerL1Address, 100000000) + .then((tx) => tx.wait()); + + const publicResolver = await deployContract( + "PublicResolver", + signerL1, + await ens.getAddress(), + EMPTY_ADDRESS, + EMPTY_ADDRESS, + await reverseRegistrar.getAddress() + ); + const publicResolverAddress = await publicResolver.getAddress(); + await reverseRegistrar + .setDefaultResolver(publicResolverAddress) + .then((tx) => tx.wait()); + + wrapper = await deployContract( + "NameWrapper", + signerL1, + await ens.getAddress(), + await baseRegistrar.getAddress(), + await metaDataservice.getAddress() + ); + wrapperAddress = await wrapper.getAddress(); + + const mimc = await deployContract("Mimc", signerL1); + const SparseMerkleProof = await ethers.getContractFactory( + "SparseMerkleProof", + { libraries: { Mimc: await mimc.getAddress() }, signer: signerL1 } + ); + const sparseMerkleProof = await SparseMerkleProof.deploy(); + await sparseMerkleProof.waitForDeployment(); + const verifierFactory = await ethers.getContractFactory( + "LineaSparseProofVerifier", + { + libraries: { + SparseMerkleProof: await sparseMerkleProof.getAddress(), + }, + signer: signerL1, + } + ); + verifier = await verifierFactory.deploy( + ["test:"], + await rollup.getAddress() + ); + await verifier.waitForDeployment(); + + const implContract = await deployContract("DelegatableResolver", signerL2); + const l2factoryContract = await deployContract( + "DelegatableResolverFactory", + signerL2, + await implContract.getAddress() + ); + await l2factoryContract + .create(await signerL2.getAddress()) + .then((tx) => tx.wait()); + const logs = await l2factoryContract.queryFilter("NewDelegatableResolver"); + //@ts-ignore + l2ResolverAddress = logs[0].args[0]; + + target = await deployContract( + "L1Resolver", + signerL1, + await verifier.getAddress(), + await ens.getAddress(), + wrapperAddress, + "", + 59141 + ); + + const delegatableResolverImpl = await ethers.getContractFactory( + "DelegatableResolver", + signerL2 + ); + l2Resolver = delegatableResolverImpl.attach(l2ResolverAddress); + await l2Resolver["setAddr(bytes32,address)"]( + subDomainNode, + REGISTRANT_ADDR + ).then((tx) => tx.wait()); + + await l2Resolver["setAddr(bytes32,uint256,bytes)"]( + subDomainNode, + coinType, + testAddr + ).then((tx) => tx.wait()); + + await l2Resolver + .setText(subDomainNode, "name", "test.eth") + .then((tx) => tx.wait()); + + const tx = await l2Resolver.setContenthash(subDomainNode, contenthash); + + const txReceipt = await tx.wait(); + lastSetupTxBlockNumber = txReceipt.blockNumber; + + // Generate activity on Linea to make finalization events happen + const [maxPriorityFeePerGas, maxFeePerGas] = getAndIncreaseFeeData( + await l2Provider.getFeeData() + ); + sendTransactionsPromise = sendTransactionsWithInterval( + signerL2, + { + to: signerL2Address, + value: ethers.parseEther("0.0001"), + maxPriorityFeePerGas, + maxFeePerGas, + }, + 1_000 + ); + }); + + it("should revert when querying L1Resolver and the currentL2BlockNumber is older than the L2 block number we are fetching the data from", async () => { + const currentL2BlockNumberFinalized = await rollup.currentL2BlockNumber({ + blockTag: "finalized", + }); + expect(currentL2BlockNumberFinalized < lastSetupTxBlockNumber); + await target + .setTarget(encodedname, l2ResolverAddress) + .then((tx) => tx.wait()); + const result = await l2Resolver["addr(bytes32)"](subDomainNode); + expect(ethers.getAddress(result)).to.equal(REGISTRANT_ADDR); + + const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); + const calldata = i.encodeFunctionData("addr", [subDomainNode]); + try { + await target.resolve(encodedSubDomain, calldata, { + enableCcipRead: true, + }); + throw "Should have reverted"; + } catch (e) { + expect(e.shortMessage).contain( + `error encountered during CCIP fetch: "Internal server error: No storage proofs on contract ${l2ResolverAddress}` + ); + } + }); + + it("should not allow non owner to set target", async () => { + const incorrectname = encodeName("notowned.eth"); + try { + await target + .setTarget(incorrectname, l2ResolverAddress) + .then((tx) => tx.wait()); + throw "Should have reverted"; + } catch (e) { + expect(e.reason).equal("Not authorized to set target for this node"); + } + + const result = await target.getTarget(incorrectname); + expect(result[1]).to.equal(EMPTY_ADDRESS); + }); + + it("should allow owner to set target", async () => { + await target + .setTarget(encodedname, signerL1Address) + .then((tx) => tx.wait()); + const result = await target.getTarget(encodeName(baseDomain)); + expect(result[1]).to.equal(signerL1Address); + }); + + it("subname should get target of its parent", async () => { + await target + .setTarget(encodedname, signerL1Address) + .then((tx) => tx.wait()); + const result = await target.getTarget(encodedSubDomain); + expect(result[0]).to.equal(subDomainNode); + expect(result[1]).to.equal(signerL1Address); + }); + + it("should allow wrapped owner to set target", async () => { + const label = "wrapped"; + const tokenId = labelhash(label); + await baseRegistrar + .setApprovalForAll(wrapperAddress, true) + .then((tx) => tx.wait()); + await baseRegistrar + .register(tokenId, signerL1Address, 100000000) + .then((tx) => tx.wait()); + await wrapper + .wrapETH2LD( + label, + signerL1Address, + 0, // CAN_DO_EVERYTHING + EMPTY_ADDRESS + ) + .then((tx) => tx.wait()); + const wrappedtname = encodeName(`${label}.eth`); + await target + .setTarget(wrappedtname, l2ResolverAddress) + .then((tx) => tx.wait()); + const encodedname = encodeName(`${label}.eth`); + const result = await target.getTarget(encodedname); + expect(result[1]).to.equal(l2ResolverAddress); + }); + + it("should resolve empty ETH Address", async () => { + // Wait for the latest L2 finalized block to more recent than the L2 block number we are fetching the data from + await waitForL2BlockNumberFinalized(rollup, lastSetupTxBlockNumber, 5000); + + let tx = await target + .setTarget(encodedname, l2ResolverAddress) + .then((tx) => tx.wait()); + const result = await l2Resolver["addr(bytes32)"](node); + expect(result).to.equal(EMPTY_ADDRESS); + + const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); + const calldata = i.encodeFunctionData("addr", [node]); + const result2 = await target.resolve(encodedname, calldata, { + enableCcipRead: true, + }); + const decoded = i.decodeFunctionResult("addr", result2); + expect(decoded[0]).to.equal(EMPTY_ADDRESS); + }); + + it("should resolve ETH Address", async () => { + const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); + const calldata = i.encodeFunctionData("addr", [subDomainNode]); + const result2 = await target.resolve(encodedSubDomain, calldata, { + enableCcipRead: true, + }); + const decoded = i.decodeFunctionResult("addr", result2); + expect(ethers.getAddress(decoded[0])).to.equal( + ethers.getAddress(REGISTRANT_ADDR) + ); + }); + + it("should resolve non ETH Address", async () => { + const i = new ethers.Interface([ + "function addr(bytes32,uint256) returns(bytes)", + ]); + const calldata = i.encodeFunctionData("addr", [subDomainNode, coinType]); + const result2 = await target.resolve(encodedSubDomain, calldata, { + enableCcipRead: true, + }); + const decoded = i.decodeFunctionResult("addr", result2); + expect(decoded[0]).to.equal(testAddr); + }); + + it("should resolve text record", async () => { + const i = new ethers.Interface([ + "function text(bytes32,string) returns(string)", + ]); + const calldata = i.encodeFunctionData("text", [subDomainNode, "name"]); + const result2 = await target.resolve(encodedSubDomain, calldata, { + enableCcipRead: true, + }); + const decoded = i.decodeFunctionResult("text", result2); + expect(decoded[0]).to.equal("test.eth"); + }); + + it("should resolve contenthash", async () => { + const i = new ethers.Interface([ + "function contenthash(bytes32) returns(bytes)", + ]); + const calldata = i.encodeFunctionData("contenthash", [subDomainNode]); + const result2 = await target.resolve(encodedSubDomain, calldata, { + enableCcipRead: true, + }); + const decoded = i.decodeFunctionResult("contenthash", result2); + expect(decoded[0]).to.equal(contenthash); + }); + + it("should revert when the functions's selector is invalid", async () => { + const i = new ethers.Interface([ + "function unknown(bytes32) returns(address)", + ]); + const calldata = i.encodeFunctionData("unknown", [node]); + try { + await target.resolve(encodedname, calldata, { + enableCcipRead: true, + }); + throw "Should have reverted"; + } catch (error) { + expect(error.reason).to.equal("invalid selector"); + } + }); + + it("should revert if the calldata is too short", async () => { + const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); + const calldata = "0x"; + try { + await target.resolve(encodedname, calldata, { + enableCcipRead: true, + }); + throw "Should have reverted"; + } catch (error) { + expect(error.reason).to.equal("param data too short"); + } + }); + + it("should not revert when querying L1Resolver right after a finalization has occured", async () => { + waitForLatestL2BlockNumberFinalizedToChange(rollup, 2000); + + const i = new ethers.Interface(["function addr(bytes32) returns(address)"]); + const calldata = i.encodeFunctionData("addr", [subDomainNode]); + const result2 = await target.resolve(encodedSubDomain, calldata, { + enableCcipRead: true, + }); + const decoded = i.decodeFunctionResult("addr", result2); + expect(ethers.getAddress(decoded[0])).to.equal( + ethers.getAddress(REGISTRANT_ADDR) + ); + }); + + after(async () => { + clearInterval(sendTransactionsPromise); + }); +}); diff --git a/packages/linea-ens-resolver/test/utils.ts b/packages/linea-ens-resolver/test/utils.ts new file mode 100644 index 000000000..9972b3dba --- /dev/null +++ b/packages/linea-ens-resolver/test/utils.ts @@ -0,0 +1,72 @@ +import { + BigNumberish, + Contract, + FeeData, + TransactionRequest, + Wallet, +} from "ethers"; +import { ethers } from "hardhat"; +import { setTimeout } from "timers/promises"; + +export function sendTransactionsWithInterval( + signer: Wallet, + transactionRequest: TransactionRequest, + pollingInterval: number +) { + return setInterval(async function () { + const tx = await signer.sendTransaction(transactionRequest); + await tx.wait(); + }, pollingInterval); +} + +export function getAndIncreaseFeeData( + feeData: FeeData +): [BigNumberish, BigNumberish, BigNumberish] { + const maxPriorityFeePerGas = BigInt( + (parseFloat(feeData.maxPriorityFeePerGas!.toString()) * 1.1).toFixed(0) + ); + const maxFeePerGas = BigInt( + (parseFloat(feeData.maxFeePerGas!.toString()) * 1.1).toFixed(0) + ); + const gasPrice = BigInt( + (parseFloat(feeData.gasPrice!.toString()) * 1.1).toFixed(0) + ); + return [maxPriorityFeePerGas, maxFeePerGas, gasPrice]; +} + +export async function waitForL2BlockNumberFinalized( + rollup: Contract, + afterBlockNo: number, + pollingInterval: number +) { + let currentL2BlockNumberFinalized; + do { + currentL2BlockNumberFinalized = await rollup.currentL2BlockNumber({ + blockTag: "finalized", + }); + await setTimeout(pollingInterval); + } while (currentL2BlockNumberFinalized < afterBlockNo); +} + +export async function waitForLatestL2BlockNumberFinalizedToChange( + rollup: Contract, + pollingInterval: number +) { + const currentL2BlockNumber = await rollup.currentL2BlockNumber(); + let newL2BlockNumber; + do { + newL2BlockNumber = await rollup.currentL2BlockNumber(); + await setTimeout(pollingInterval); + } while (currentL2BlockNumber === newL2BlockNumber); +} + +export const deployContract = async ( + name: string, + provider: Wallet, + ...args: any[] +) => { + const factory = await ethers.getContractFactory(name, provider); + const contract = await factory.deploy(...args); + await contract.waitForDeployment(); + return contract; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bbde1be2d..ed71e7ad1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16844,7 +16844,7 @@ snapshots: '@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.11.1(bufferutil@4.0.8)(utf-8-validate@5.0.10))(hardhat@2.22.2(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10))': dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) ethers: 6.11.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) hardhat: 2.22.2(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10) lodash.isequal: 4.5.0 @@ -16865,7 +16865,7 @@ snapshots: '@nomicfoundation/ignition-core': 0.15.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@nomicfoundation/ignition-ui': 0.15.1 chalk: 4.1.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) fs-extra: 10.1.0 hardhat: 2.22.2(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10) prompts: 2.4.2 @@ -16946,7 +16946,7 @@ snapshots: '@ethersproject/address': 5.7.0 cbor: 8.1.0 chalk: 2.4.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) hardhat: 2.22.2(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10) lodash.clonedeep: 4.5.0 semver: 6.3.1 @@ -16961,7 +16961,7 @@ snapshots: '@ethersproject/address': 5.7.0 cbor: 8.1.0 chalk: 2.4.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) hardhat: 2.22.2(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.4.5))(typescript@5.4.5)(utf-8-validate@5.0.10) lodash.clonedeep: 4.5.0 semver: 6.3.1 @@ -16975,7 +16975,7 @@ snapshots: '@ethersproject/address': 5.6.1 '@nomicfoundation/solidity-analyzer': 0.1.1 cbor: 9.0.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) ethers: 6.13.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) fs-extra: 10.1.0 immer: 10.0.2 @@ -19890,7 +19890,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -23443,7 +23443,7 @@ snapshots: follow-redirects@1.15.6(debug@4.3.4): optionalDependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) for-each@0.3.3: dependencies: @@ -24052,7 +24052,7 @@ snapshots: axios: 0.21.4(debug@4.3.4) chalk: 4.1.2 chokidar: 3.6.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) enquirer: 2.4.1 ethers: 5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) form-data: 4.0.0 @@ -24217,7 +24217,7 @@ snapshots: chalk: 2.4.2 chokidar: 3.6.0 ci-info: 2.0.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) enquirer: 2.4.1 env-paths: 2.2.1 ethereum-cryptography: 1.2.0 @@ -24298,7 +24298,7 @@ snapshots: uuid: 8.3.2 ws: 7.5.9(bufferutil@4.0.8)(utf-8-validate@5.0.10) optionalDependencies: - ts-node: 10.9.2(@types/node@18.19.31)(typescript@5.4.5) + ts-node: 10.9.2(@types/node@20.11.20)(typescript@5.4.5) typescript: 5.4.5 transitivePeerDependencies: - bufferutil @@ -24513,7 +24513,7 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -29836,7 +29836,7 @@ snapshots: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) fast-safe-stringify: 2.1.1 form-data: 4.0.0 formidable: 2.1.2 @@ -30544,7 +30544,7 @@ snapshots: typechain@8.3.2(typescript@5.4.5): dependencies: '@types/prettier': 2.7.3 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) fs-extra: 7.0.1 glob: 7.1.7 js-sha3: 0.8.0