From 0accdc98afa4263b3c9c13d5d4f1df1e9822bbf7 Mon Sep 17 00:00:00 2001 From: Tom Lehman Date: Wed, 24 Sep 2025 12:47:01 -0400 Subject: [PATCH 1/4] Improve prover setup --- contracts/src/Ethscriptions.sol | 13 +- contracts/src/EthscriptionsProver.sol | 83 +++++++++-- contracts/src/L2/L1Block.sol | 9 ++ .../test/EthscriptionsFailureHandling.t.sol | 30 +--- contracts/test/EthscriptionsProver.t.sol | 140 ++++++++++++++---- 5 files changed, 203 insertions(+), 72 deletions(-) diff --git a/contracts/src/Ethscriptions.sol b/contracts/src/Ethscriptions.sol index a7ab200..114204d 100644 --- a/contracts/src/Ethscriptions.sol +++ b/contracts/src/Ethscriptions.sol @@ -131,11 +131,6 @@ contract Ethscriptions is ERC721EthscriptionsUpgradeable { bytes revertData ); - /// @notice Emitted when a prover operation fails but ethscription continues - event ProverFailed( - bytes32 indexed transactionHash, - bytes revertData - ); /// @notice Modifier to emit pending genesis events on first real creation @@ -407,12 +402,8 @@ contract Ethscriptions is ERC721EthscriptionsUpgradeable { } } - // Use try-catch to prevent prover failures from reverting operations - try prover.proveEthscriptionData(txHash) {} catch (bytes memory revertData) { - // Proving failed, but the operation should continue - // The prover can be called again later if needed - emit ProverFailed(txHash, revertData); - } + // Queue ethscription for batch proving at block boundary + prover.queueEthscription(txHash); } /// @notice Get ethscription details (returns struct to avoid stack too deep) diff --git a/contracts/src/EthscriptionsProver.sol b/contracts/src/EthscriptionsProver.sol index 4dab504..8c5763b 100644 --- a/contracts/src/EthscriptionsProver.sol +++ b/contracts/src/EthscriptionsProver.sol @@ -6,11 +6,28 @@ import "./TokenManager.sol"; import "./EthscriptionsERC20.sol"; import "./L2/L2ToL1MessagePasser.sol"; import "./libraries/Predeploys.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; /// @title EthscriptionsProver /// @notice Proves Ethscription ownership and token balances to L1 via OP Stack /// @dev Uses L2ToL1MessagePasser to send provable messages to L1 contract EthscriptionsProver { + using EnumerableSet for EnumerableSet.Bytes32Set; + + /// @notice Info stored when an ethscription is queued for proving + struct QueuedProof { + uint256 blockNumber; + uint256 blockTimestamp; + } + + /// @notice Set of all ethscription transaction hashes queued for proving + EnumerableSet.Bytes32Set private queuedEthscriptions; + + /// @notice Mapping from ethscription tx hash to its queued proof info + mapping(bytes32 => QueuedProof) private queuedProofInfo; + + /// @notice L1Block contract address for access control + address public constant L1_BLOCK = Predeploys.L1_BLOCK_ATTRIBUTES; /// @notice L2ToL1MessagePasser predeploy address on OP Stack L2ToL1MessagePasser constant L2_TO_L1_MESSAGE_PASSER = L2ToL1MessagePasser(Predeploys.L2_TO_L1_MESSAGE_PASSER); @@ -60,6 +77,9 @@ contract EthscriptionsProver { uint256 indexed l2BlockNumber, uint256 l2Timestamp ); + + /// @notice Emitted when a batch of proofs is flushed + event ProofBatchFlushed(uint256 count, uint256 blockNumber); /// @notice Prove token balance for an address /// @param holder The address to prove balance for @@ -92,20 +112,64 @@ contract EthscriptionsProver { emit TokenBalanceProofSent(holder, tokenInfo.tick, block.number, block.timestamp); } - /// @notice Prove ethscription existence and metadata + /// @notice Queue an ethscription for proving + /// @dev Only callable by the Ethscriptions contract + /// @param txHash The transaction hash of the ethscription + function queueEthscription(bytes32 txHash) external virtual { + require(msg.sender == address(ethscriptions), "Only Ethscriptions contract can queue"); + + // Add to the set (deduplicates automatically) + if (queuedEthscriptions.add(txHash)) { + // Only store info if this is the first time we're queueing this txHash + queuedProofInfo[txHash] = QueuedProof({ + blockNumber: block.number, + blockTimestamp: block.timestamp + }); + } + } + + /// @notice Flush all queued proofs + /// @dev Only callable by the L1Block contract at the start of each new block + function flushAllProofs() external { + require(msg.sender == L1_BLOCK, "Only L1Block can flush"); + + uint256 count = queuedEthscriptions.length(); + + // Process and remove each ethscription from the set + // We iterate backwards to avoid index shifting during removal + for (uint256 i = count; i > 0; i--) { + bytes32 txHash = queuedEthscriptions.at(i - 1); + + // Get the stored proof info to know which block this was from + QueuedProof memory proofInfo = queuedProofInfo[txHash]; + + // Create and send proof for current state with stored block info + _createAndSendProof(txHash, proofInfo.blockNumber, proofInfo.blockTimestamp); + + // Clean up: remove from set and delete the proof info + queuedEthscriptions.remove(txHash); + delete queuedProofInfo[txHash]; + } + + emit ProofBatchFlushed(count, block.number - 1); + } + + /// @notice Internal function to create and send proof for an ethscription /// @param ethscriptionTxHash The transaction hash of the ethscription - function proveEthscriptionData(bytes32 ethscriptionTxHash) external virtual { + /// @param blockNumber The L2 block number being proved + /// @param blockTimestamp The timestamp of the L2 block being proved + function _createAndSendProof(bytes32 ethscriptionTxHash, uint256 blockNumber, uint256 blockTimestamp) internal { // Get ethscription data including previous owner Ethscriptions.Ethscription memory etsc = ethscriptions.getEthscription(ethscriptionTxHash); address currentOwner = ethscriptions.currentOwner(ethscriptionTxHash); - + // Check if it's a token item bool isToken = tokenManager.isTokenItem(ethscriptionTxHash); uint256 tokenAmount = 0; if (isToken) { tokenAmount = tokenManager.getTokenAmount(ethscriptionTxHash); } - + // Create proof struct with previous owner EthscriptionDataProof memory proof = EthscriptionDataProof({ ethscriptionTxHash: ethscriptionTxHash, @@ -116,14 +180,15 @@ contract EthscriptionsProver { ethscriptionNumber: etsc.ethscriptionNumber, isToken: isToken, tokenAmount: tokenAmount, - l2BlockNumber: block.number, - l2Timestamp: block.timestamp + l2BlockNumber: blockNumber, + l2Timestamp: blockTimestamp }); - + // Encode and send to L1 with zero address and gas (only for state recording) bytes memory proofData = abi.encode(proof); L2_TO_L1_MESSAGE_PASSER.initiateWithdrawal(address(0), 0, proofData); - - emit EthscriptionDataProofSent(ethscriptionTxHash, block.number, block.timestamp); + + emit EthscriptionDataProofSent(ethscriptionTxHash, blockNumber, blockTimestamp); } + } \ No newline at end of file diff --git a/contracts/src/L2/L1Block.sol b/contracts/src/L2/L1Block.sol index c9dd05e..8fe77a5 100644 --- a/contracts/src/L2/L1Block.sol +++ b/contracts/src/L2/L1Block.sol @@ -2,6 +2,11 @@ pragma solidity 0.8.24; import { Constants } from "../libraries/Constants.sol"; +import { Predeploys } from "../libraries/Predeploys.sol"; + +interface IEthscriptionsProver { + function flushAllProofs() external; +} /// @custom:proxied /// @custom:predeploy 0x4200000000000000000000000000000000000015 @@ -64,6 +69,10 @@ contract L1Block { /// 8. _hash L1 blockhash. /// 9. _batcherHash Versioned hash to authenticate batcher by. function setL1BlockValuesEcotone() external { + // Flush all queued ethscription proofs before updating to new block + // Each proof includes its own block number and timestamp from when it was queued + IEthscriptionsProver(Predeploys.ETHSCRIPTIONS_PROVER).flushAllProofs(); + address depositor = DEPOSITOR_ACCOUNT(); assembly { // Revert if the caller is not the depositor account. diff --git a/contracts/test/EthscriptionsFailureHandling.t.sol b/contracts/test/EthscriptionsFailureHandling.t.sol index 1cbb5fb..e5ea501 100644 --- a/contracts/test/EthscriptionsFailureHandling.t.sol +++ b/contracts/test/EthscriptionsFailureHandling.t.sol @@ -54,11 +54,9 @@ contract FailingProver is EthscriptionsProver { failMessage = _message; } - function proveEthscriptionData(bytes32 transactionHash) external override { - if (shouldFail) { - revert(failMessage); - } - // Otherwise do nothing + function queueEthscription(bytes32 txHash) external override { + // For testing, always succeed + // In the new design, queueing doesn't fail and doesn't emit events } } @@ -72,11 +70,6 @@ contract EthscriptionsFailureHandlingTest is TestSetup { bytes revertData ); - event ProverFailed( - bytes32 indexed transactionHash, - bytes revertData - ); - function setUp() public override { super.setUp(); @@ -138,12 +131,12 @@ contract EthscriptionsFailureHandlingTest is TestSetup { } function testCreateEthscriptionWithProverFailure() public { - // Configure Prover to fail - FailingProver(Predeploys.ETHSCRIPTIONS_PROVER).setShouldFail(true); - FailingProver(Predeploys.ETHSCRIPTIONS_PROVER).setFailMessage("Proving failed"); + // Note: With the new batched proving design, the prover doesn't fail immediately + // during creation. Instead, ethscriptions are queued for batch proving. + // This test now verifies that creation succeeds and the ethscription is queued. bytes32 txHash = keccak256("test_tx_2"); - string memory dataUri = "data:,Hello World with failing prover"; + string memory dataUri = "data:,Hello World with batched prover"; Ethscriptions.CreateEthscriptionParams memory params = createTestParams( txHash, @@ -152,14 +145,7 @@ contract EthscriptionsFailureHandlingTest is TestSetup { false ); - // Expect the ProverFailed event - vm.expectEmit(true, false, false, true); - emit ProverFailed( - txHash, - abi.encodeWithSignature("Error(string)", "Proving failed") - ); - - // Create ethscription - should succeed despite Prover failure + // Create ethscription - should succeed and queue for proving silently (no event) uint256 tokenId = ethscriptions.createEthscription(params); // Verify the ethscription was created successfully diff --git a/contracts/test/EthscriptionsProver.t.sol b/contracts/test/EthscriptionsProver.t.sol index 6d68720..88d4315 100644 --- a/contracts/test/EthscriptionsProver.t.sol +++ b/contracts/test/EthscriptionsProver.t.sol @@ -6,6 +6,7 @@ import "./TestSetup.sol"; contract EthscriptionsProverTest is TestSetup { address alice = address(0x1); address bob = address(0x2); + address charlie = address(0x3); address l1Target = address(0x1234); bytes32 constant TEST_TX_HASH = bytes32(uint256(0xABCD)); @@ -26,16 +27,20 @@ contract EthscriptionsProverTest is TestSetup { vm.stopPrank(); } - function testProveEthscriptionDataOnCreation() public { - // The ethscription creation in setUp should have triggered a proof + function testProveEthscriptionDataViaBatchFlush() public { + // The ethscription creation in setUp should have queued it for proving // Let's transfer it to verify the proof includes previous owner uint256 tokenId = ethscriptions.getTokenId(TEST_TX_HASH); vm.prank(alice); ethscriptions.transferFrom(alice, bob, tokenId); - - // Now prove the data which should include bob as current owner and alice as previous + + // Now flush the batch which should prove the data with bob as current owner and alice as previous + vm.roll(block.number + 1); + + vm.startPrank(Predeploys.L1_BLOCK_ATTRIBUTES); vm.recordLogs(); - prover.proveEthscriptionData(TEST_TX_HASH); + prover.flushAllProofs(); + vm.stopPrank(); Vm.Log[] memory logs = vm.getRecordedLogs(); // Find the MessagePassed event and extract proof data @@ -145,36 +150,111 @@ contract EthscriptionsProverTest is TestSetup { assertEq(decodedProof.balance, 1000 ether); // 1000 * 10^18 } - function testAutomaticProofOnCreation() public { - // Create a new ethscription and verify it triggers automatic proof - bytes32 newTxHash = bytes32(uint256(0xFEED)); - + + + function testBatchFlushProofs() public { + // First flush any pending proofs from setup + vm.roll(block.number + 1); + + vm.startPrank(Predeploys.L1_BLOCK_ATTRIBUTES); + prover.flushAllProofs(); + vm.stopPrank(); + + // Now move to next block for our test + vm.roll(block.number + 1); + + // Create multiple ethscriptions in the same block + bytes32 txHash1 = bytes32(uint256(0x123)); + bytes32 txHash2 = bytes32(uint256(0x456)); + bytes32 txHash3 = bytes32(uint256(0x789)); + + // Create three ethscriptions + vm.startPrank(alice); + ethscriptions.createEthscription( + Ethscriptions.CreateEthscriptionParams({ + transactionHash: txHash1, + contentUriHash: keccak256("data:,test1"), + initialOwner: alice, + content: bytes("test1"), + mimetype: "text/plain", + mediaType: "text", + mimeSubtype: "plain", + esip6: false, + tokenParams: Ethscriptions.TokenParams("", "", "", 0, 0, 0) + }) + ); + vm.stopPrank(); + + vm.startPrank(bob); + ethscriptions.createEthscription( + Ethscriptions.CreateEthscriptionParams({ + transactionHash: txHash2, + contentUriHash: keccak256("data:,test2"), + initialOwner: bob, + content: bytes("test2"), + mimetype: "text/plain", + mediaType: "text", + mimeSubtype: "plain", + esip6: false, + tokenParams: Ethscriptions.TokenParams("", "", "", 0, 0, 0) + }) + ); + vm.stopPrank(); + + // Transfer the first ethscription (should only be queued once due to deduplication) + vm.startPrank(alice); + ethscriptions.transferEthscription(bob, txHash1); + vm.stopPrank(); + + // Create a third ethscription + vm.startPrank(charlie); + ethscriptions.createEthscription( + Ethscriptions.CreateEthscriptionParams({ + transactionHash: txHash3, + contentUriHash: keccak256("data:,test3"), + initialOwner: charlie, + content: bytes("test3"), + mimetype: "text/plain", + mediaType: "text", + mimeSubtype: "plain", + esip6: false, + tokenParams: Ethscriptions.TokenParams("", "", "", 0, 0, 0) + }) + ); + vm.stopPrank(); + + // Now simulate L1Block calling flush at the start of the next block + vm.roll(block.number + 1); + + // Prank as L1Block contract + vm.startPrank(Predeploys.L1_BLOCK_ATTRIBUTES); vm.recordLogs(); - vm.prank(bob); - ethscriptions.createEthscription(createTestParams( - newTxHash, - bob, - "data:,automatic proof test", - false - )); - + prover.flushAllProofs(); + vm.stopPrank(); + Vm.Log[] memory logs = vm.getRecordedLogs(); - - // Find the EthscriptionDataProofSent event to verify automatic proof was generated - bool foundProofEvent = false; + + // Count ProofBatchFlushed events + uint256 batchCount = 0; + uint256 proofCount = 0; + for (uint i = 0; i < logs.length; i++) { + if (logs[i].topics[0] == keccak256("ProofBatchFlushed(uint256,uint256)")) { + batchCount++; + // Decode the data to get the count (first uint256 in data) + proofCount = abi.decode(logs[i].data, (uint256)); + } + } + + assertEq(batchCount, 1, "Should have exactly one batch flush"); + assertEq(proofCount, 3, "Should have flushed 3 unique ethscriptions"); + + // Count individual proof sent events + uint256 proofsSent = 0; for (uint i = 0; i < logs.length; i++) { if (logs[i].topics[0] == keccak256("EthscriptionDataProofSent(bytes32,uint256,uint256)")) { - foundProofEvent = true; - break; + proofsSent++; } } - assertTrue(foundProofEvent, "Automatic proof was not generated on creation"); - } - - function testCannotProveNonExistentEthscription() public { - bytes32 fakeTxHash = bytes32(uint256(0xDEADBEEF)); - - vm.expectRevert(); - prover.proveEthscriptionData(fakeTxHash); + assertEq(proofsSent, 3, "Should have sent 3 individual proofs"); } } \ No newline at end of file From 824ac43ef788fb05eef7bf2ea608be89d98eee31 Mon Sep 17 00:00:00 2001 From: Tom Lehman Date: Wed, 24 Sep 2025 13:16:04 -0400 Subject: [PATCH 2/4] Simplify --- contracts/src/EthscriptionsERC20.sol | 24 ++--- contracts/src/EthscriptionsProver.sol | 112 ++++++----------------- contracts/src/L2/L1Block.sol | 8 +- contracts/test/EthscriptionsProver.t.sol | 95 +------------------ 4 files changed, 41 insertions(+), 198 deletions(-) diff --git a/contracts/src/EthscriptionsERC20.sol b/contracts/src/EthscriptionsERC20.sol index 0b5ef06..6b8f631 100644 --- a/contracts/src/EthscriptionsERC20.sol +++ b/contracts/src/EthscriptionsERC20.sol @@ -4,12 +4,10 @@ pragma solidity 0.8.24; import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20CappedUpgradeable.sol"; import "./libraries/Predeploys.sol"; -import "./EthscriptionsProver.sol"; contract EthscriptionsERC20 is ERC20Upgradeable, ERC20CappedUpgradeable { address public constant tokenManager = Predeploys.TOKEN_MANAGER; - EthscriptionsProver public constant prover = EthscriptionsProver(Predeploys.ETHSCRIPTIONS_PROVER); - + bytes32 public deployTxHash; // The ethscription hash that deployed this token function initialize( @@ -61,21 +59,13 @@ contract EthscriptionsERC20 is ERC20Upgradeable, ERC20CappedUpgradeable { } // Required overrides for multiple inheritance - function _update(address from, address to, uint256 value) - internal - override(ERC20Upgradeable, ERC20CappedUpgradeable) + function _update(address from, address to, uint256 value) + internal + override(ERC20Upgradeable, ERC20CappedUpgradeable) { super._update(from, to, value); - - // Automatically prove token balances after any update including burns - // For transfers: prove both from and to - // For mints (from == address(0)): only prove to - // For burns (to == address(0)): only prove from - if (from != address(0)) { - prover.proveTokenBalance(from, deployTxHash); - } - if (to != address(0)) { - prover.proveTokenBalance(to, deployTxHash); - } + + // Token balance proving has been removed in favor of ethscription-only proving + // Token balances can be derived from ethscription ownership and transfer history } } \ No newline at end of file diff --git a/contracts/src/EthscriptionsProver.sol b/contracts/src/EthscriptionsProver.sol index 8c5763b..8b7a42b 100644 --- a/contracts/src/EthscriptionsProver.sol +++ b/contracts/src/EthscriptionsProver.sol @@ -2,9 +2,8 @@ pragma solidity 0.8.24; import "./Ethscriptions.sol"; -import "./TokenManager.sol"; -import "./EthscriptionsERC20.sol"; import "./L2/L2ToL1MessagePasser.sol"; +import "./L2/L1Block.sol"; import "./libraries/Predeploys.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; @@ -16,8 +15,10 @@ contract EthscriptionsProver { /// @notice Info stored when an ethscription is queued for proving struct QueuedProof { - uint256 blockNumber; - uint256 blockTimestamp; + uint256 l2BlockNumber; + uint256 l2BlockTimestamp; + bytes32 l1BlockHash; + uint256 l1BlockNumber; } /// @notice Set of all ethscription transaction hashes queued for proving @@ -35,83 +36,29 @@ contract EthscriptionsProver { /// @notice The Ethscriptions contract (pre-deployed at known address) Ethscriptions public constant ethscriptions = Ethscriptions(Predeploys.ETHSCRIPTIONS); - /// @notice The TokenManager contract (pre-deployed at known address) - TokenManager public constant tokenManager = TokenManager(Predeploys.TOKEN_MANAGER); - - /// @notice Struct for token balance proof data - struct TokenBalanceProof { - address holder; - string protocol; - string tick; - uint256 balance; - uint256 l2BlockNumber; - uint256 l2Timestamp; - // TODO: Add l1BlockNumber once we have L2->L1 block mapping - } - /// @notice Struct for ethscription data proof struct EthscriptionDataProof { bytes32 ethscriptionTxHash; bytes32 contentSha; + bytes32 contentUriHash; address creator; address currentOwner; address previousOwner; uint256 ethscriptionNumber; - bool isToken; - uint256 tokenAmount; + bool esip6; + bytes32 l1BlockHash; + uint256 l1BlockNumber; uint256 l2BlockNumber; uint256 l2Timestamp; - // TODO: Add l1BlockNumber once we have L2->L1 block mapping } /// @notice Events for tracking proofs - event TokenBalanceProofSent( - address indexed holder, - string tick, - uint256 indexed l2BlockNumber, - uint256 l2Timestamp - ); - event EthscriptionDataProofSent( bytes32 indexed ethscriptionTxHash, uint256 indexed l2BlockNumber, uint256 l2Timestamp ); - /// @notice Emitted when a batch of proofs is flushed - event ProofBatchFlushed(uint256 count, uint256 blockNumber); - - /// @notice Prove token balance for an address - /// @param holder The address to prove balance for - /// @param deployTxHash The deploy transaction hash (identifies the token type) - function proveTokenBalance( - address holder, - bytes32 deployTxHash - ) external { - // Get token info from TokenManager - TokenManager.TokenInfo memory tokenInfo = tokenManager.getTokenInfo(deployTxHash); - - // Get balance - EthscriptionsERC20 token = EthscriptionsERC20(tokenInfo.tokenContract); - uint256 balance = token.balanceOf(holder); - - // Create proof struct - TokenBalanceProof memory proof = TokenBalanceProof({ - holder: holder, - protocol: tokenInfo.protocol, - tick: tokenInfo.tick, - balance: balance, - l2BlockNumber: block.number, - l2Timestamp: block.timestamp - }); - - // Encode and send to L1 with zero address and gas (only for state recording) - bytes memory proofData = abi.encode(proof); - L2_TO_L1_MESSAGE_PASSER.initiateWithdrawal(address(0), 0, proofData); - - emit TokenBalanceProofSent(holder, tokenInfo.tick, block.number, block.timestamp); - } - /// @notice Queue an ethscription for proving /// @dev Only callable by the Ethscriptions contract /// @param txHash The transaction hash of the ethscription @@ -121,9 +68,13 @@ contract EthscriptionsProver { // Add to the set (deduplicates automatically) if (queuedEthscriptions.add(txHash)) { // Only store info if this is the first time we're queueing this txHash + // Capture the L1 block hash and number at the time of queuing + L1Block l1Block = L1Block(L1_BLOCK); queuedProofInfo[txHash] = QueuedProof({ - blockNumber: block.number, - blockTimestamp: block.timestamp + l2BlockNumber: block.number, + l2BlockTimestamp: block.timestamp, + l1BlockHash: l1Block.hash(), + l1BlockNumber: l1Block.number() }); } } @@ -140,55 +91,44 @@ contract EthscriptionsProver { for (uint256 i = count; i > 0; i--) { bytes32 txHash = queuedEthscriptions.at(i - 1); - // Get the stored proof info to know which block this was from - QueuedProof memory proofInfo = queuedProofInfo[txHash]; - // Create and send proof for current state with stored block info - _createAndSendProof(txHash, proofInfo.blockNumber, proofInfo.blockTimestamp); + _createAndSendProof(txHash, queuedProofInfo[txHash]); // Clean up: remove from set and delete the proof info queuedEthscriptions.remove(txHash); delete queuedProofInfo[txHash]; } - - emit ProofBatchFlushed(count, block.number - 1); } /// @notice Internal function to create and send proof for an ethscription /// @param ethscriptionTxHash The transaction hash of the ethscription - /// @param blockNumber The L2 block number being proved - /// @param blockTimestamp The timestamp of the L2 block being proved - function _createAndSendProof(bytes32 ethscriptionTxHash, uint256 blockNumber, uint256 blockTimestamp) internal { + /// @param proofInfo The queued proof info containing block data + function _createAndSendProof(bytes32 ethscriptionTxHash, QueuedProof storage proofInfo) internal { // Get ethscription data including previous owner Ethscriptions.Ethscription memory etsc = ethscriptions.getEthscription(ethscriptionTxHash); address currentOwner = ethscriptions.currentOwner(ethscriptionTxHash); - // Check if it's a token item - bool isToken = tokenManager.isTokenItem(ethscriptionTxHash); - uint256 tokenAmount = 0; - if (isToken) { - tokenAmount = tokenManager.getTokenAmount(ethscriptionTxHash); - } - - // Create proof struct with previous owner + // Create proof struct with all ethscription data EthscriptionDataProof memory proof = EthscriptionDataProof({ ethscriptionTxHash: ethscriptionTxHash, contentSha: etsc.content.contentSha, + contentUriHash: etsc.content.contentUriHash, creator: etsc.creator, currentOwner: currentOwner, previousOwner: etsc.previousOwner, ethscriptionNumber: etsc.ethscriptionNumber, - isToken: isToken, - tokenAmount: tokenAmount, - l2BlockNumber: blockNumber, - l2Timestamp: blockTimestamp + esip6: etsc.content.esip6, + l1BlockHash: proofInfo.l1BlockHash, + l1BlockNumber: proofInfo.l1BlockNumber, + l2BlockNumber: proofInfo.l2BlockNumber, + l2Timestamp: proofInfo.l2BlockTimestamp }); // Encode and send to L1 with zero address and gas (only for state recording) bytes memory proofData = abi.encode(proof); L2_TO_L1_MESSAGE_PASSER.initiateWithdrawal(address(0), 0, proofData); - emit EthscriptionDataProofSent(ethscriptionTxHash, blockNumber, blockTimestamp); + emit EthscriptionDataProofSent(ethscriptionTxHash, proofInfo.l2BlockNumber, proofInfo.l2BlockTimestamp); } } \ No newline at end of file diff --git a/contracts/src/L2/L1Block.sol b/contracts/src/L2/L1Block.sol index 8fe77a5..7d7ecad 100644 --- a/contracts/src/L2/L1Block.sol +++ b/contracts/src/L2/L1Block.sol @@ -69,10 +69,6 @@ contract L1Block { /// 8. _hash L1 blockhash. /// 9. _batcherHash Versioned hash to authenticate batcher by. function setL1BlockValuesEcotone() external { - // Flush all queued ethscription proofs before updating to new block - // Each proof includes its own block number and timestamp from when it was queued - IEthscriptionsProver(Predeploys.ETHSCRIPTIONS_PROVER).flushAllProofs(); - address depositor = DEPOSITOR_ACCOUNT(); assembly { // Revert if the caller is not the depositor account. @@ -89,5 +85,9 @@ contract L1Block { sstore(hash.slot, calldataload(100)) // bytes32 sstore(batcherHash.slot, calldataload(132)) // bytes32 } + + // Flush all queued ethscription proofs after updating block values + // Each proof includes its own block number and timestamp from when it was queued + IEthscriptionsProver(Predeploys.ETHSCRIPTIONS_PROVER).flushAllProofs(); } } diff --git a/contracts/test/EthscriptionsProver.t.sol b/contracts/test/EthscriptionsProver.t.sol index 88d4315..020aa1d 100644 --- a/contracts/test/EthscriptionsProver.t.sol +++ b/contracts/test/EthscriptionsProver.t.sol @@ -69,86 +69,13 @@ contract EthscriptionsProverTest is TestSetup { assertEq(decodedProof.currentOwner, bob); assertEq(decodedProof.previousOwner, alice); // assertEq(decodedProof.ethscriptionNumber, 0); - assertEq(decodedProof.isToken, false); - assertEq(decodedProof.tokenAmount, 0); + assertEq(decodedProof.esip6, false); assertTrue(decodedProof.contentSha != bytes32(0)); + assertTrue(decodedProof.contentUriHash != bytes32(0)); + // l1BlockHash can be zero in test environment + assertEq(decodedProof.l1BlockHash, bytes32(0)); } - function testProveTokenBalance() public { - // First deploy a token - vm.prank(alice); - bytes memory tokenDeployUri = bytes('data:,{"p":"erc-20","op":"deploy","tick":"TEST","max":"1000000","lim":"1000"}'); - ethscriptions.createEthscription(Ethscriptions.CreateEthscriptionParams({ - transactionHash: TOKEN_DEPLOY_HASH, - contentUriHash: sha256(tokenDeployUri), - initialOwner: alice, - content: bytes('{"p":"erc-20","op":"deploy","tick":"TEST","max":"1000000","lim":"1000"}'), - mimetype: "text/plain", - mediaType: "text", - mimeSubtype: "plain", - esip6: false, - tokenParams: Ethscriptions.TokenParams({ - op: "deploy", - protocol: "erc-20", - tick: "TEST", - max: 1000000, - lim: 1000, - amt: 0 - }) - })); - - // Mint some tokens - vm.prank(bob); - bytes memory tokenMintUri = bytes('data:,{"p":"erc-20","op":"mint","tick":"TEST","amt":"1000"}'); - ethscriptions.createEthscription(Ethscriptions.CreateEthscriptionParams({ - transactionHash: TOKEN_MINT_HASH, - contentUriHash: sha256(tokenMintUri), - initialOwner: bob, - content: bytes('{"p":"erc-20","op":"mint","tick":"TEST","amt":"1000"}'), - mimetype: "text/plain", - mediaType: "text", - mimeSubtype: "plain", - esip6: false, - tokenParams: Ethscriptions.TokenParams({ - op: "mint", - protocol: "erc-20", - tick: "TEST", - max: 0, - lim: 0, - amt: 1000 - }) - })); - - // Prove token balance using the deploy hash - vm.recordLogs(); - prover.proveTokenBalance(bob, TOKEN_DEPLOY_HASH); - Vm.Log[] memory logs = vm.getRecordedLogs(); - - // Find the MessagePassed event and extract proof data - bytes memory proofData; - for (uint i = 0; i < logs.length; i++) { - if (logs[i].topics[0] == keccak256("MessagePassed(uint256,address,address,uint256,uint256,bytes,bytes32)")) { - // Decode the non-indexed parameters from data field - (uint256 value, uint256 gasLimit, bytes memory data, bytes32 withdrawalHash) = abi.decode( - logs[i].data, - (uint256, uint256, bytes, bytes32) - ); - proofData = data; - break; - } - } - - // Decode and verify proof data - EthscriptionsProver.TokenBalanceProof memory decodedProof = abi.decode( - proofData, - (EthscriptionsProver.TokenBalanceProof) - ); - - assertEq(decodedProof.holder, bob); - assertEq(decodedProof.protocol, "erc-20"); - assertEq(decodedProof.tick, "TEST"); - assertEq(decodedProof.balance, 1000 ether); // 1000 * 10^18 - } @@ -234,20 +161,6 @@ contract EthscriptionsProverTest is TestSetup { Vm.Log[] memory logs = vm.getRecordedLogs(); - // Count ProofBatchFlushed events - uint256 batchCount = 0; - uint256 proofCount = 0; - for (uint i = 0; i < logs.length; i++) { - if (logs[i].topics[0] == keccak256("ProofBatchFlushed(uint256,uint256)")) { - batchCount++; - // Decode the data to get the count (first uint256 in data) - proofCount = abi.decode(logs[i].data, (uint256)); - } - } - - assertEq(batchCount, 1, "Should have exactly one batch flush"); - assertEq(proofCount, 3, "Should have flushed 3 unique ethscriptions"); - // Count individual proof sent events uint256 proofsSent = 0; for (uint i = 0; i < logs.length; i++) { From 4bf7e1fccec29f0f357fb2ae184f59ed0aad4080 Mon Sep 17 00:00:00 2001 From: Tom Lehman Date: Wed, 24 Sep 2025 13:25:35 -0400 Subject: [PATCH 3/4] Update contracts/test/EthscriptionsProver.t.sol Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- contracts/test/EthscriptionsProver.t.sol | 3 --- 1 file changed, 3 deletions(-) diff --git a/contracts/test/EthscriptionsProver.t.sol b/contracts/test/EthscriptionsProver.t.sol index 020aa1d..829602a 100644 --- a/contracts/test/EthscriptionsProver.t.sol +++ b/contracts/test/EthscriptionsProver.t.sol @@ -76,9 +76,6 @@ contract EthscriptionsProverTest is TestSetup { assertEq(decodedProof.l1BlockHash, bytes32(0)); } - - - function testBatchFlushProofs() public { // First flush any pending proofs from setup vm.roll(block.number + 1); From f159468627332238525c92b6ab1126a7cce53a6c Mon Sep 17 00:00:00 2001 From: Tom Lehman Date: Wed, 24 Sep 2025 13:26:26 -0400 Subject: [PATCH 4/4] Storage => memory --- contracts/src/EthscriptionsProver.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/EthscriptionsProver.sol b/contracts/src/EthscriptionsProver.sol index 8b7a42b..e375587 100644 --- a/contracts/src/EthscriptionsProver.sol +++ b/contracts/src/EthscriptionsProver.sol @@ -103,7 +103,7 @@ contract EthscriptionsProver { /// @notice Internal function to create and send proof for an ethscription /// @param ethscriptionTxHash The transaction hash of the ethscription /// @param proofInfo The queued proof info containing block data - function _createAndSendProof(bytes32 ethscriptionTxHash, QueuedProof storage proofInfo) internal { + function _createAndSendProof(bytes32 ethscriptionTxHash, QueuedProof memory proofInfo) internal { // Get ethscription data including previous owner Ethscriptions.Ethscription memory etsc = ethscriptions.getEthscription(ethscriptionTxHash); address currentOwner = ethscriptions.currentOwner(ethscriptionTxHash);