diff --git a/execution/errors.go b/execution/errors.go new file mode 100644 index 0000000..2241b86 --- /dev/null +++ b/execution/errors.go @@ -0,0 +1,154 @@ +package execution + +import ( + "fmt" + + "github.com/BlocSoc-iitr/selene/consensus/consensus_core" + "github.com/BlocSoc-iitr/selene/consensus/types" + "github.com/ethereum/go-ethereum/accounts/abi" +) + +// ExecutionError represents various execution-related errors +type ExecutionError struct { + Kind string + Details interface{} +} + +func (e *ExecutionError) Error() string { + switch e.Kind { + case "InvalidAccountProof": + return fmt.Sprintf("invalid account proof for string: %v", e.Details) + case "InvalidStorageProof": + details := e.Details.([]interface{}) + return fmt.Sprintf("invalid storage proof for string: %v, slot: %v", details[0], details[1]) + case "CodeHashMismatch": + details := e.Details.([]interface{}) + return fmt.Sprintf("code hash mismatch for string: %v, found: %v, expected: %v", details[0], details[1], details[2]) + case "ReceiptRootMismatch": + return fmt.Sprintf("receipt root mismatch for tx: %v", e.Details) + case "MissingTransaction": + return fmt.Sprintf("missing transaction for tx: %v", e.Details) + case "NoReceiptForTransaction": + return fmt.Sprintf("could not prove receipt for tx: %v", e.Details) + case "MissingLog": + details := e.Details.([]interface{}) + return fmt.Sprintf("missing log for transaction: %v, index: %v", details[0], details[1]) + case "TooManyLogsToProve": + details := e.Details.([]interface{}) + return fmt.Sprintf("too many logs to prove: %v, current limit is: %v", details[0], details[1]) + case "IncorrectRpcNetwork": + return "execution RPC is for the incorrect network" + case "InvalidBaseGasFee": + details := e.Details.([]interface{}) + return fmt.Sprintf("Invalid base gas fee selene %v vs rpc endpoint %v at block %v", details[0], details[1], details[2]) + case "InvalidGasUsedRatio": + details := e.Details.([]interface{}) + return fmt.Sprintf("Invalid gas used ratio of selene %v vs rpc endpoint %v at block %v", details[0], details[1], details[2]) + case "BlockNotFoundError": + return fmt.Sprintf("Block %v not found", e.Details) + case "EmptyExecutionPayload": + return "Selene Execution Payload is empty" + case "InvalidBlockRange": + details := e.Details.([]interface{}) + return fmt.Sprintf("User query for block %v but selene oldest block is %v", details[0], details[1]) + default: + return "unknown execution error" + } +} + +// Helper functions to create specific ExecutionError instances +func NewInvalidAccountProofError(address types.Address) error { + return &ExecutionError{"InvalidAccountProof", address} +} + +func NewInvalidStorageProofError(address types.Address, slot consensus_core.Bytes32) error { + return &ExecutionError{"InvalidStorageProof", []interface{}{address, slot}} +} + +func NewCodeHashMismatchError(address types.Address, found consensus_core.Bytes32, expected consensus_core.Bytes32) error { + return &ExecutionError{"CodeHashMismatch", []interface{}{address, found, expected}} +} + +func NewReceiptRootMismatchError(tx consensus_core.Bytes32) error { + return &ExecutionError{"ReceiptRootMismatch", tx} +} + +func NewMissingTransactionError(tx consensus_core.Bytes32) error { + return &ExecutionError{"MissingTransaction", tx} +} + +func NewNoReceiptForTransactionError(tx consensus_core.Bytes32) error { + return &ExecutionError{"NoReceiptForTransaction", tx} +} + +func NewMissingLogError(tx consensus_core.Bytes32, index uint64) error { + return &ExecutionError{"MissingLog", []interface{}{tx, index}} +} + +func NewTooManyLogsToProveError(count int, limit int) error { + return &ExecutionError{"TooManyLogsToProve", []interface{}{count, limit}} +} + +func NewIncorrectRpcNetworkError() error { + return &ExecutionError{"IncorrectRpcNetwork", nil} +} + +func NewInvalidBaseGasFeeError(selene uint64, rpc uint64, block uint64) error { + return &ExecutionError{"InvalidBaseGasFee", []interface{}{selene, rpc, block}} +} + +func NewInvalidGasUsedRatioError(seleneRatio float64, rpcRatio float64, block uint64) error { + return &ExecutionError{"InvalidGasUsedRatio", []interface{}{seleneRatio, rpcRatio, block}} +} + +func NewBlockNotFoundError(block uint64) error { + return &ExecutionError{"BlockNotFoundError", block} +} + +func NewEmptyExecutionPayloadError() error { + return &ExecutionError{"EmptyExecutionPayload", nil} +} + +func NewInvalidBlockRangeError(queryBlock uint64, oldestBlock uint64) error { + return &ExecutionError{"InvalidBlockRange", []interface{}{queryBlock, oldestBlock}} +} + +// EvmError represents EVM-related errors +type EvmError struct { + Kind string + Details interface{} +} + +func (e *EvmError) Error() string { + switch e.Kind { + case "Revert": + return fmt.Sprintf("execution reverted: %v", e.Details) + case "Generic": + return fmt.Sprintf("evm error: %v", e.Details) + case "RpcError": + return fmt.Sprintf("rpc error: %v", e.Details) + default: + return "unknown evm error" + } +} + +// Helper functions for creating specific EVM errors +func NewRevertError(data []byte) error { + return &EvmError{"Revert", data} +} + +func NewGenericError(message string) error { + return &EvmError{"Generic", message} +} + +func NewRpcError(report error) error { + return &EvmError{"RpcError", report} +} + +func DecodeRevertReason(data []byte) string { + reason, err := abi.UnpackRevert(data) + if err != nil { + reason = string(err.Error()) + } + return reason +} diff --git a/execution/proof.go b/execution/proof.go new file mode 100644 index 0000000..549e7ec --- /dev/null +++ b/execution/proof.go @@ -0,0 +1,186 @@ +package execution + +import ( + "bytes" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/rlp" + "golang.org/x/crypto/sha3" +) + +// EIP1186AccountProofResponse for account proof encoding +type EIP1186AccountProofResponse struct { + Nonce uint64 + Balance *big.Int + StorageHash [32]byte + CodeHash [32]byte +} + +func VerifyProof(proof [][]byte, root []byte, path []byte, value []byte) (bool, error) { + expectedHash := root + pathOffset := 0 + + for i, node := range proof { + if !bytes.Equal(expectedHash, keccak256(node)) { + return false, nil + } + + var nodeList [][]byte + if err := rlp.DecodeBytes(node, &nodeList); err != nil { + fmt.Println("Error decoding node:", err) + return false, err + } + + if len(nodeList) == 17 { + if i == len(proof)-1 { + // exclusion proof + nibble := getNibble(path, pathOffset) + if len(nodeList[nibble]) == 0 && isEmptyValue(value) { + return true, nil + } + } else { + nibble := getNibble(path, pathOffset) + expectedHash = nodeList[nibble] + pathOffset++ + } + } else if len(nodeList) == 2 { + if i == len(proof)-1 { + // exclusion proof + if !pathsMatch(nodeList[0], skipLength(nodeList[0]), path, pathOffset) && isEmptyValue(value) { + return true, nil + } + + // inclusion proof + if bytes.Equal(nodeList[1], value) { + return pathsMatch(nodeList[0], skipLength(nodeList[0]), path, pathOffset), nil + } + } else { + nodePath := nodeList[0] + prefixLength := sharedPrefixLength(path, pathOffset, nodePath) + if prefixLength < len(nodePath)*2-skipLength(nodePath) { + // Proof shows a divergent path , but we're not at the leaf yet + return false, nil + } + pathOffset += prefixLength + expectedHash = nodeList[1] + } + } else { + return false, nil + } + } + + return false, nil +} + +func pathsMatch(p1 []byte, s1 int, p2 []byte, s2 int) bool { + len1 := len(p1)*2 - s1 + len2 := len(p2)*2 - s2 + + if len1 != len2 { + return false + } + + for offset := 0; offset < len1; offset++ { + n1 := getNibble(p1, s1+offset) + n2 := getNibble(p2, s2+offset) + if n1 != n2 { + return false + } + } + + return true +} + +// dead code +func GetRestPath(p []byte, s int) string { + var ret string + for i := s; i < len(p)*2; i++ { + n := getNibble(p, i) + ret += fmt.Sprintf("%01x", n) + } + return ret +} + +func isEmptyValue(value []byte) bool { + emptyAccount := Account{ + Nonce: 0, + Balance: big.NewInt(0), + StorageHash: [32]byte{0x56, 0xe8, 0x1f, 0x17, 0x1b, 0xcc, 0x55, 0xa6, 0xff, 0x83, 0x45, 0xe6, 0x92, 0xc0, 0xf8, 0x6e, 0x5b, 0x48, 0xe0, 0x1b, 0x99, 0x6c, 0xad, 0xc0, 0x01, 0x62, 0x2f, 0xb5, 0xe3, 0x63, 0xb4, 0x21}, + CodeHash: [32]byte{0xc5, 0xd2, 0x46, 0x01, 0x86, 0xf7, 0x23, 0x3c, 0x92, 0x7e, 0x7d, 0xb2, 0xdc, 0xc7, 0x03, 0xc0, 0xe5, 0x00, 0xb6, 0x53, 0xca, 0x82, 0x27, 0x3b, 0x7b, 0xfa, 0xd8, 0x04, 0x5d, 0x85, 0xa4, 0x70}, + } + + encodedEmptyAccount, _ := rlp.EncodeToBytes(emptyAccount) + + isEmptySlot := len(value) == 1 && value[0] == 0x80 + isEmptyAccount := bytes.Equal(value, encodedEmptyAccount) + + return isEmptySlot || isEmptyAccount +} + +func sharedPrefixLength(path []byte, pathOffset int, nodePath []byte) int { + skipLength := skipLength(nodePath) + + len1 := min(len(nodePath)*2-skipLength, len(path)*2-pathOffset) + prefixLen := 0 + + for i := 0; i < len1; i++ { + pathNibble := getNibble(path, i+pathOffset) + nodePathNibble := getNibble(nodePath, i+skipLength) + if pathNibble != nodePathNibble { + break + } + prefixLen++ + } + + return prefixLen +} + +func skipLength(node []byte) int { + if len(node) == 0 { + return 0 + } + + nibble := getNibble(node, 0) + switch nibble { + case 0, 2: + return 2 + case 1, 3: + return 1 + default: + return 0 + } +} + +func getNibble(path []byte, offset int) byte { + byteVal := path[offset/2] + if offset%2 == 0 { + return byteVal >> 4 + } + return byteVal & 0xF +} + +func keccak256(data []byte) []byte { + hash := sha3.NewLegacyKeccak256() + hash.Write(data) + return hash.Sum(nil) +} + +func EncodeAccount(proof *EIP1186AccountProofResponse) ([]byte, error) { + account := Account{ + Nonce: proof.Nonce, + Balance: proof.Balance, + StorageHash: proof.StorageHash, + CodeHash: proof.CodeHash, + } + + return rlp.EncodeToBytes(account) +} + +// Make a generic function for it +func min(a, b int) int { + if a < b { + return a + } + return b +}