PFP Pixel War is a collaborative game where the players use NFTs they own on L1 Ethereum mainnet to use them in L2 (Optimism Rollup) via Storage proofs and MUD. It's inspired from r/place by Reddit a 3.5-day experiment with 160 million pixel changes operated by over 10.5 million users, at an average pace of about 2 million pixels placed per hour.
ETHGLobal Autonomous world Hackathon Event
Our interpretation of an Autonomous World
- Trustless
- Collaborative and user generated
- Real time
- Our idea is easy to understand (simple showcase)
- Builds on existing assets (NFTs)
- Uses the network effect (target Web3 community who likes to flex their PFP)
Technical motivations
- Challenge to prove the ownership of an L1 NFT on L2
- Challenge to make this verifier a precompile in op-geth
- Discovery and Stress test the MUD framework
Objective
Prove on a Layer 2 that Bob owns a NFT on Layer 1.
You can have a look to the Pixel Wars Contracts README
We wrote a detailed technical article on how we did this.
The Ethereum state is a data structure (a modified Merkle Patricia Trie) which keeps all accounts (nonce, balance, codeHash, storageRoot) linked by hashes and reducible to a single root hash stored on the blockchain.
Solidity (contracts)
MUD (MUD client)
GO (this repo)
The verification of MPT is not available on EVM so we decided to make a precompile to simplify the verification of such trie on smartcontract. The cost to call our precompile MPT verifier is 30K gas.
You can have a look to the commit of our modification on op-geth
core/vm/contracts.go
We added a precompile to verify the MPT at address 0x92
common.BytesToAddress([]byte{0x92}): &mptVerify{},
We need to format our data to make it compatible with the native Geth Method
func (c *mptVerify) Run(input []byte) ([]byte, error) {
// [root <32 bytes>, key <32 bytes>, proof <32 bytes length><n bytes arrays prefixed with length>]
if len(input) < 96 {
return nil, errMptVerifyInvalidInput
}
uint256Ty, _ := abi.NewType("uint256", "", nil)
bytesArrTy, _ := abi.NewType("bytes[]", "", nil)
arguments := abi.Arguments{
abi.Argument{ Name: "root", Type: uint256Ty },
abi.Argument{ Name: "key", Type: uint256Ty },
abi.Argument{ Name: "proof", Type: bytesArrTy },
}
r, err := arguments.Unpack(input)
if err != nil {
return nil, errMptVerifyInvalidInput
}
root, _ := r[0].(*big.Int)
key, _ := r[1].(*big.Int)
proof, _ := r[2].([][]byte)
proofKv := mappingKeyValue{}
for _, step := range proof {
hash := crypto.Keccak256(step)
proofKv[string(hash)] = step
}
v, err := trie.VerifyProof(common.BigToHash(root), key.Bytes(), proofKv)
if err != nil {
return nil, errMptVerifyInvalidInput
}
var rlpDecoded []byte
if err = rlp.DecodeBytes(v, &rlpDecoded); err != nil {
return nil, err
}
return rlpDecoded, nil
}
About Optimism and Storage proof
The implementation of the Ethereum Merkle Patricia Trie was an interesting tech challenge. We deepened our understanding of EVM, precompiles and how the state is stored.
About MUD Framework
Really simplifies the work on the front-end Some issues with the public testnet indexer Great support from the MUD team Ultra-simplified smart-contract dev and management Binding between UI and contract state is realy cool
About EthGlobal
Top organization and Guidance / Top ressources
About life
Keep learning, Keep building :D
pnpm install
pnpm dev