qkc/types: add logs and receipts - #34
Conversation
| return hexutil.UnmarshalFixedText("Bloom", input, b[:]) | ||
| } | ||
|
|
||
| func CreateBloom(receipts Receipts) Bloom { |
There was a problem hiding this comment.
Can we only keep the CreateBloom function and remove other Bloom related code since it is the same as geth
|
|
||
| // Log represents a contract log event. These events are generated by the LOG opcode and | ||
| // stored/indexed by the node. | ||
| type Log struct { |
There was a problem hiding this comment.
I think the key design question is whether the Python master/slave serialization format requires us to introduce qkc/types.Log as a new general-purpose log model.
Modern geth already uses *core/types.Log throughout StateDB, EVM execution, receipt processing, filtering, indexing, and related APIs. If we replace it with qkc/types.Log, or propagate a parallel log model into these layers, we will need changes or conversions across many geth call sites. This would also increase the maintenance cost of future geth updates.
Reusing core/types.Log should not change the QKC receipt root: geth’s RLP encoding of an individual log contains only Address, Topics, and Data, which matches the log payload used by QKC receipt RLP. QKC can still keep its own receipt type for QKC-specific receipt fields such as contract_address and contract_full_shard_key.
The place where QKC does need a different representation is the Python master/slave protocol. Python expects the following qkc/serialize schema:
recipient, topics, data,
block_number, block_hash, tx_idx, tx_hash, log_idx
This schema differs from the fields and types in core/types.Log. However, a different cluster serialization format does not necessarily require a different execution-layer model. We can define serialization-only cluster types at the master/slave RPC boundary.
This is also how pyquarkchain is structured today. It has separate classes for the two roles:
quarkchain.evm.messages.Log / Receipt
EVM and receipt-trie objects
quarkchain.core.Log / TransactionReceipt
query and master/slave serialization objects
In MinorBlock.get_receipt(), Python reads an evm.messages.Receipt, converts each EVM log with Log.create_from_eth_log(), and returns a core.TransactionReceipt containing core.Log objects. The converted object is then used by the cluster RPC response.
We can mirror that boundary in Go:
qkc/types.Receipt
Logs: []*core/types.Log
|
| toClusterTransactionReceipt()
v
ClusterTransactionReceipt
Logs: []*ClusterLog
|
| qkc/serialize
v
Python master
The cluster types could look approximately like:
type ClusterLog struct {
Recipient common.Address
Topics []common.Hash
Data []byte `bytesizeofslicelen:"4"`
BlockNumber uint64
BlockHash common.Hash
TxIndex uint32
TxHash common.Hash
Index uint32
}
type ClusterTransactionReceipt struct {
Success []byte
GasUsed uint64
PrevGasUsed uint64
Bloom coretypes.Bloom
ContractAddress account.Address
Logs []*ClusterLog `bytesizeofslicelen:"4"`
}Introducing only ClusterLog would not be sufficient because receipt serialization recursively serializes its Logs field. The cluster layer should convert the complete object graph before calling qkc/serialize:
internal QKC receipt
-> convert every core/types.Log to ClusterLog
-> construct ClusterTransactionReceipt
-> construct GetTransactionReceiptResponse
-> qkc/serialize
GetLogResponse would similarly convert its []*core/types.Log into []*ClusterLog.
This boundary also gives us an explicit place to:
- Omit geth-only fields such as
BlockTimestampandRemoved. - Convert geth’s
uintindexes to the Python protocol’suint32. - Check for index overflow.
- Preserve Python’s exact field order and length-prefix rules.
There are also two concrete compatibility issues in the current implementation.
First, qkc/types.Log does not implement an explicit QKC Serialize method. qkc/serialize therefore encodes it through reflection, making the Go struct declaration order part of the wire protocol.
The current Go order is:
Recipient, Topics, Data, BlockNumber, TxHash, TxIndex, BlockHash, Index
Python expects:
recipient, topics, data, block_number, block_hash, tx_idx, tx_hash, log_idx
TxHash and BlockHash currently occupy the opposite wire positions, so the encoded bytes are not compatible with the Python master. A test can miss this if it uses the same value for both hashes.
Second, the current receiptSer starts with:
TxHash common.Hashbut Python core.TransactionReceipt.FIELDS does not contain tx_hash; its first field is success. The current receipt encoding therefore has an extra leading 32 bytes and is also incompatible with Python.
Could we keep core/types.Log as the internal log model and move the Python-specific receipt/log representation to explicitly named cluster types? The current receiptSer could be refactored into ClusterTransactionReceipt, with the extra TxHash removed and its Logs field changed to []*ClusterLog.
It would also be useful to add Python-generated golden-vector tests using different TxHash and BlockHash values, plus a complete TransactionReceipt vector, to verify byte-for-byte compatibility.
| // Receipt represents the results of a transaction. | ||
| type Receipt struct { | ||
| // Consensus fields | ||
| PostState []byte `json:"root"` |
There was a problem hiding this comment.
QKC mainnet receipts do not use a 32-byte post-state here. pyquarkchain.mk_receipt always writes 0x01 for success and empty bytes for failure, and the Python master treats only 0x01 as success. Can we remove PostState from the normal QKC model, or clearly isolate it as legacy-only, and update the golden tests to cover real success and failure values? The current 32-byte test value would be reported as failed by the master.
|
|
||
| // Implementation fields (don't reorder!) | ||
| TxHash common.Hash `json:"transactionHash" gencodec:"required"` | ||
| ContractAddress account.Recipient `json:"contractAddress"` |
There was a problem hiding this comment.
ContractAddress and ContractFullShardKey are included by receiptRLP, so they affect the receipt trie root and are QKC consensus fields. Can we move them under Consensus fields? Only TxHash and GasUsed are derived/implementation fields here.
| "github.com/ethereum/go-ethereum/rlp" | ||
| ) | ||
|
|
||
| //go:generate gencodec -type Receipt -field-override receiptMarshaling -out gen_receipt_json.go |
There was a problem hiding this comment.
receiptMarshaling currently has no effect because gen_receipt_json.go is not in this PR, and go generate ./qkc/types fails because gencodec is not on PATH. Please use the repository pattern go run github.com/fjl/gencodec ... and commit the generated file, or remove this directive and override if a JSON codec is not needed.
There was a problem hiding this comment.
I checked both pyquarkchain and goquarkchain. Neither implementation marshals the QKC TransactionReceipt type directly to JSON. Pyquarkchain uses receipt_encoder to build a dictionary, while goquarkchain uses ReceiptEncoder or constructs a map[string]interface{} in the RPC layer. Goquarkchain contains the same generator directive, but it also has no generated receipt JSON file, and I could not find any direct json.Marshal or json.Unmarshal use of this type.
Since there is no current consumer for a generated JSON codec, could we remove the generator directive, receiptMarshaling, the now-unused hexutil import, and the gencodec tags? If direct JSON serialization is needed later, it can be added together with its actual caller and generated implementation.
| ContractFullShardKey *Uint32 | ||
| } | ||
|
|
||
| type receiptStorageRLP struct { |
There was a problem hiding this comment.
Which storage format is this intended to preserve? It matches old goquarkchain's full receipt storage, but this PR has no consumer or storage golden test; pyquarkchain stores consensus receipts in the receipt trie, and modern geth uses a different minimal storage format. Can we defer this until the consumer and schema are defined, or add the intended consumer and a compatibility vector?
| } | ||
|
|
||
| // Serialize serialize the QKC minor block. | ||
| func (r *Receipt) Serialize(w *[]byte) error { |
There was a problem hiding this comment.
Receipt is the internal receipt model, while ClusterTransactionReceipt represents the Python master/slave wire format. They have different purposes.
Currently, Receipt.Serialize and Receipt.Deserialize hide the conversion between these two types. This makes the internal Receipt depend on the cluster protocol and makes it unclear which format is being serialized.
Since the cluster RPC is not implemented in this PR, could we:
- Remove
SerializeandDeserializefromReceipt. - Keep wire serialization only on
ClusterTransactionReceipt. - Add explicit conversion functions between the two types:
NewClusterTransactionReceipt(*Receipt) (*ClusterTransactionReceipt, error)
(*ClusterTransactionReceipt).ToReceipt() (*Receipt, error)When the cluster RPC is added, it can call these conversions at the RPC boundary. This keeps the internal receipt independent of the Python wire format and makes the serialization format explicit.
| return nil, err | ||
| } | ||
| return &ClusterTransactionReceipt{ | ||
| Success: status, |
There was a problem hiding this comment.
status points to the global receiptStatusSuccessfulRLP slice. Assigning it directly to the public Success field exposes the same underlying bytes, so clusterReceipt.Success[0] = 2 would also change future receipt status encoding.
Could we copy it here?
Success: common.CopyBytes(status),| return err | ||
| } | ||
| r.CumulativeGasUsed, r.Bloom, r.Logs, r.ContractAddress, r.ContractFullShardKey = dec.CumulativeGasUsed, | ||
| dec.Bloom, dec.Logs, common.BytesToAddress(dec.ContractAddress), dec.ContractFullShardKey.GetValue() |
There was a problem hiding this comment.
dec.ContractAddress is an unrestricted []byte, and common.BytesToAddress accepts any length by padding or truncating it. Python receipt RLP accepts only an empty value or exactly 20 bytes. As written, values with lengths such as 1, 19, 21, or 32 bytes are accepted and normalized, so decoding and re-encoding changes the receipt bytes.
Could we validate that len(dec.ContractAddress) is either 0 or common.AddressLength before converting it?
| BlockHash: log.BlockHash, | ||
| TxIndex: uint32(log.TxIndex), | ||
| TxHash: log.TxHash, | ||
| Index: uint32(log.Index), |
There was a problem hiding this comment.
coretypes.Log.Index and Python Log.log_idx have different meanings. Geth sets Log.Index to the global log index in the block, while pyquarkchain sets log_idx from enumerate(receipt.logs), so it starts from 0 for every receipt.
For example, if transaction 0 emits two logs, the first log of transaction 1 has Index 2 in geth but log_idx 0 in Python. Copying log.Index here will therefore return a different value to the Python master.
Could we pass the QKC per-receipt log index into NewClusterLog explicitly, or set it from the position in r.Logs inside NewClusterTransactionReceipt? A test with logs from a second transaction would cover this difference.
625320b to
f666e52
Compare
92c7c91 to
1d9f03d
Compare
3rd PR for QuarkChain/pm#155.