Skip to content
Merged
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
13 changes: 2 additions & 11 deletions contracts/src/Ethscriptions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
24 changes: 7 additions & 17 deletions contracts/src/EthscriptionsERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
}
}
155 changes: 80 additions & 75 deletions contracts/src/EthscriptionsProver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,128 +2,133 @@
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";

/// @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 l2BlockNumber;
uint256 l2BlockTimestamp;
bytes32 l1BlockHash;
uint256 l1BlockNumber;
}

/// @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);

/// @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 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
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
// Capture the L1 block hash and number at the time of queuing
L1Block l1Block = L1Block(L1_BLOCK);
queuedProofInfo[txHash] = QueuedProof({
l2BlockNumber: block.number,
l2BlockTimestamp: block.timestamp,
l1BlockHash: l1Block.hash(),
l1BlockNumber: l1Block.number()
});
}
}

/// @notice Prove ethscription existence and metadata

/// @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);

// Create and send proof for current state with stored block info
_createAndSendProof(txHash, queuedProofInfo[txHash]);

// Clean up: remove from set and delete the proof info
queuedEthscriptions.remove(txHash);
delete queuedProofInfo[txHash];
}
}

/// @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 proofInfo The queued proof info containing block data
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);

// 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: block.number,
l2Timestamp: block.timestamp
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, block.number, block.timestamp);

emit EthscriptionDataProofSent(ethscriptionTxHash, proofInfo.l2BlockNumber, proofInfo.l2BlockTimestamp);
}

}
9 changes: 9 additions & 0 deletions contracts/src/L2/L1Block.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -80,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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: L1 Block Update Fails on Prover Error

The L1Block contract's setL1BlockValuesEcotone() function now calls flushAllProofs() without error handling. If the EthscriptionsProver reverts, this critical L1 block update will also revert, which could break the L2's ability to track L1 blocks. This removes a previous resilience mechanism that isolated prover failures.

Fix in Cursor Fix in Web

}
}
30 changes: 8 additions & 22 deletions contracts/test/EthscriptionsFailureHandling.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand All @@ -72,11 +70,6 @@ contract EthscriptionsFailureHandlingTest is TestSetup {
bytes revertData
);

event ProverFailed(
bytes32 indexed transactionHash,
bytes revertData
);

function setUp() public override {
super.setUp();

Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
Loading