Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cross chain merkle trees #1911

Merged
merged 30 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from 9 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
71 changes: 51 additions & 20 deletions contracts/generated/ManagementContract/ManagementContract.go

Large diffs are not rendered by default.

15 changes: 8 additions & 7 deletions contracts/src/management/ManagementContract.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ contract ManagementContract is Initializable, OwnableUpgradeable {
MerkleTreeMessageBus.IMerkleTreeMessageBus public merkleMessageBus;
mapping(bytes32 =>bool) public isWithdrawalSpent;

bytes32 public lastBatchHash;

function initialize() public initializer {
__Ownable_init(msg.sender);
lastBatchSeqNo = 0;
Expand All @@ -76,18 +78,20 @@ contract ManagementContract is Initializable, OwnableUpgradeable {
}
}

function addCrossChainMessagesRoot(bytes32 root, bytes32 blockHash, uint256 blockNum, bytes[] memory crossChainHashes, bytes calldata signature) external {
function addCrossChainMessagesRoot(bytes32 _lastBatchHash, bytes32 blockHash, uint256 blockNum, bytes[] memory crossChainHashes, bytes calldata signature) external {
if (block.number > blockNum + 255) {
revert("Block binding too old");
}

if ((blockhash(blockNum) != blockHash)) {
// revert(string(abi.encodePacked("Invalid block binding:", Strings.toString(block.number),":", Strings.toString(uint256(blockHash)), ":", Strings.toString(uint256(blockhash(blockNum))))));
revert(string(abi.encodePacked("Invalid block binding:", Strings.toString(block.number),":", Strings.toString(uint256(blockHash)), ":", Strings.toString(uint256(blockhash(blockNum))))));
}

address enclaveID = ECDSA.recover(keccak256(abi.encode(root, blockHash, blockNum, crossChainHashes)), signature);
address enclaveID = ECDSA.recover(keccak256(abi.encode(_lastBatchHash, blockHash, blockNum, crossChainHashes)), signature);
require(attested[enclaveID], "enclaveID not attested"); //todo: only sequencer, rather than everyone who has attested.

lastBatchHash = _lastBatchHash;

for(uint256 i = 0; i < crossChainHashes.length; i++) {
merkleMessageBus.addStateRoot(bytes32(crossChainHashes[i]), block.timestamp); //todo: change the activation time.
}
Expand All @@ -102,17 +106,14 @@ contract ManagementContract is Initializable, OwnableUpgradeable {
}

// solc-ignore-next-line unused-param
function AddRollup(Structs.MetaRollup calldata r, string calldata _rollupData, Structs.HeaderCrossChainData calldata crossChainData) public {
// TODO: Add a check that ensures the cross messages are coming from the correct fork using block hashes.

function AddRollup(Structs.MetaRollup calldata r, string calldata _rollupData, Structs.HeaderCrossChainData calldata) public {
address enclaveID = ECDSA.recover(r.Hash, r.Signature);
// revert if the EnclaveID is not attested
require(attested[enclaveID], "enclaveID not attested");
// revert if the EnclaveID is not permissioned as a sequencer
require(sequencerEnclave[enclaveID], "enclaveID not a sequencer");

AppendRollup(r);
pushCrossChainMessages(crossChainData);
}

// InitializeNetworkSecret kickstarts the network secret, can only be called once
Expand Down
12 changes: 6 additions & 6 deletions go/common/crosschain.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import (
)

type ExtCrossChainBundle struct {
StateRootHash gethcommon.Hash
Signature []byte
L1BlockHash gethcommon.Hash
L1BlockNum *big.Int
CrossChainHashes [][]byte
LastBatchHash gethcommon.Hash
Signature []byte
L1BlockHash gethcommon.Hash // The block hash that's expected to be canonical on signature submission
L1BlockNum *big.Int // The number of the block that has the block hash. This is used to verify the block hash.
CrossChainRootHashes [][]byte // The CrossChainRoots of the batches that are being submitted
}

func (bundle ExtCrossChainBundle) HashPacked() common.Hash {
Expand All @@ -37,7 +37,7 @@ func (bundle ExtCrossChainBundle) HashPacked() common.Hash {
},
}

bytes, err := args.Pack(bundle.StateRootHash, bundle.L1BlockHash, bundle.L1BlockNum, bundle.CrossChainHashes)
bytes, err := args.Pack(bundle.LastBatchHash, bundle.L1BlockHash, bundle.L1BlockNum, bundle.CrossChainRootHashes)
if err != nil {
panic(err)
}
Expand Down
6 changes: 3 additions & 3 deletions go/common/headers.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ type BatchHeader struct {
CrossChainMessages []MessageBus.StructsCrossChainMessage `json:"crossChainMessages"`
LatestInboundCrossChainHash common.Hash `json:"inboundCrossChainHash"` // The block hash of the latest block that has been scanned for cross chain messages.
LatestInboundCrossChainHeight *big.Int `json:"inboundCrossChainHeight"` // The block height of the latest block that has been scanned for cross chain messages.
TransfersTree common.Hash `json:"transfersTree"` // This is a merkle tree of all of the outbound value transfers for the MainNet
CrossChainRoot common.Hash `json:"crossChainTreeHash"` // This is a merkle tree of all of the outbound value transfers for the MainNet
StefanIliev545 marked this conversation as resolved.
Show resolved Hide resolved
CrossChainTree SerializedCrossChainTree `json:"crossChainTree"`
Copy link
Collaborator

Choose a reason for hiding this comment

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

why does this have to be in a serialised form here? You can serialise it during the encoding

Copy link
Collaborator

Choose a reason for hiding this comment

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

And it's not clear why it's needed, since you have the CrossChainMessages list already, and it can be calculated from them

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, I'm confused as to why we are storing the CrossChainMessages, but not the ValueTransfers

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This tree contains everything. The old list will be removed in the follow up PR along with the old logic.

}

Expand Down Expand Up @@ -93,7 +93,7 @@ func (b *BatchHeader) MarshalJSON() ([]byte, error) {
b.CrossChainMessages,
b.LatestInboundCrossChainHash,
(*hexutil.Big)(b.LatestInboundCrossChainHeight),
b.TransfersTree,
b.CrossChainRoot,
b.CrossChainTree,
})
}
Expand Down Expand Up @@ -122,7 +122,7 @@ func (b *BatchHeader) UnmarshalJSON(data []byte) error {
b.CrossChainMessages = dec.CrossChainMessages
b.LatestInboundCrossChainHash = dec.LatestInboundCrossChainHash
b.LatestInboundCrossChainHeight = (*big.Int)(dec.LatestInboundCrossChainHeight)
b.TransfersTree = dec.TransfersTree
b.CrossChainRoot = dec.TransfersTree
b.CrossChainTree = dec.CrossChainTree
return nil
}
Expand Down
6 changes: 5 additions & 1 deletion go/common/host/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,18 @@ type L1Publisher interface {
// PublishSecretResponse will create and publish a secret response tx to the management contract - fire and forget we don't wait for receipt
PublishSecretResponse(secretResponse *common.ProducedSecretResponse) error

PublishCrossChainBundle(bundle *common.ExtCrossChainBundle)
// PublishCrossChainBundle will create and publish a cross-chain bundle tx to the management contract
PublishCrossChainBundle(bundle *common.ExtCrossChainBundle) error

FetchLatestSeqNo() (*big.Int, error)

// GetImportantContracts returns a (cached) record of addresses of the important network contracts
GetImportantContracts() map[string]gethcommon.Address
// ResyncImportantContracts will fetch the latest important contracts from the management contract, update the cache
ResyncImportantContracts() error

// GetBundleRangeFromManagementContract returns the range of batches for which to build a bundle
GetBundleRangeFromManagementContract() (*big.Int, *big.Int, error)
}

// L2BatchRepository provides an interface for the host to request L2 batch data (live-streaming and historical)
Expand Down
4 changes: 2 additions & 2 deletions go/common/rpc/converters.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ func ToBatchHeaderMsg(header *common.BatchHeader) *generated.BatchHeaderMsg {
GasUsed: header.GasUsed,
Time: header.Time,
BaseFee: baseFee,
TransferTree: header.TransfersTree.Bytes(),
TransferTree: header.CrossChainRoot.Bytes(),
Coinbase: header.Coinbase.Bytes(),
CrossChainMessages: ToCrossChainMsgs(header.CrossChainMessages),
LatestInboundCrossChainHash: header.LatestInboundCrossChainHash.Bytes(),
Expand Down Expand Up @@ -197,7 +197,7 @@ func FromBatchHeaderMsg(header *generated.BatchHeaderMsg) *common.BatchHeader {
GasLimit: header.GasLimit,
GasUsed: header.GasUsed,
Time: header.Time,
TransfersTree: gethcommon.BytesToHash(header.TransferTree),
CrossChainRoot: gethcommon.BytesToHash(header.TransferTree),
BaseFee: big.NewInt(0).SetUint64(header.BaseFee),
Coinbase: gethcommon.BytesToAddress(header.Coinbase),
CrossChainMessages: FromCrossChainMsgs(header.CrossChainMessages),
Expand Down
4 changes: 2 additions & 2 deletions go/enclave/components/batch_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ func (executor *batchExecutor) CreateGenesisState(
Number: big.NewInt(int64(0)),
SequencerOrderNo: big.NewInt(int64(common.L2GenesisSeqNo)), // genesis batch has seq number 1
ReceiptHash: types.EmptyRootHash,
TransfersTree: types.EmptyRootHash,
CrossChainRoot: types.EmptyRootHash,
Time: timeNow,
Coinbase: coinbase,
BaseFee: baseFee,
Expand Down Expand Up @@ -387,7 +387,7 @@ func (executor *batchExecutor) populateOutboundCrossChainData(ctx context.Contex
executor.logger.Info("[CrossChain] adding messages to batch")
}
batch.Header.CrossChainMessages = crossChainMessages
batch.Header.TransfersTree = xchainHash
batch.Header.CrossChainRoot = xchainHash

executor.logger.Trace(fmt.Sprintf("Added %d cross chain messages to batch.",
len(batch.Header.CrossChainMessages)), log.CmpKey, log.CrossChainCmp)
Expand Down
4 changes: 2 additions & 2 deletions go/enclave/crosschain/block_message_extractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func (m *blockMessageExtractor) getCrossChainMessages(block *common.L1Block, rec
}

// Retrieves the relevant logs from the message bus.
logs, err := filterLogsFromReceipts(receipts, m.GetBusAddress(), []*gethcommon.Hash{&CrossChainEventID})
logs, err := filterLogsFromReceipts(receipts, m.GetBusAddress(), &CrossChainEventID)
if err != nil {
m.logger.Error("Error encountered when filtering receipt logs.", log.ErrKey, err)
return make(common.CrossChainMessages, 0), err
Expand All @@ -138,7 +138,7 @@ func (m *blockMessageExtractor) getValueTransferMessages(receipts common.L1Recei
}

// Retrieves the relevant logs from the message bus.
logs, err := filterLogsFromReceipts(receipts, m.GetBusAddress(), []*gethcommon.Hash{&ValueTransferEventID})
logs, err := filterLogsFromReceipts(receipts, m.GetBusAddress(), &ValueTransferEventID)
if err != nil {
m.logger.Error("Error encountered when filtering receipt logs.", log.ErrKey, err)
return make(common.ValueTransferEvents, 0), err
Expand Down
18 changes: 8 additions & 10 deletions go/enclave/crosschain/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,15 @@ func receiptsHash(receipts types.Receipts) string {
}

// filterLogsFromReceipts - filters the receipts for logs matching address, if provided and topic if provided.
func filterLogsFromReceipts(receipts types.Receipts, address *gethcommon.Address, topics []*gethcommon.Hash) ([]types.Log, error) {
func filterLogsFromReceipts(receipts types.Receipts, address *gethcommon.Address, topic *gethcommon.Hash) ([]types.Log, error) {
logs := make([]types.Log, 0)

for _, receipt := range receipts {
if receipt.Status == 0 {
continue
}

logsForReceipt, err := filterLogsFromReceipt(receipt, address, topics)
logsForReceipt, err := filterLogsFromReceipt(receipt, address, topic)
if err != nil {
return logs, err
}
Expand All @@ -74,8 +74,8 @@ func filterLogsFromReceipts(receipts types.Receipts, address *gethcommon.Address
return logs, nil
}

// filterLogsFromReceipt - filters the receipt for logs matching address, if provided and topic if provided.
func filterLogsFromReceipt(receipt *types.Receipt, address *gethcommon.Address, topics []*gethcommon.Hash) ([]types.Log, error) {
// filterLogsFromReceipt - filters the receipt for logs matching address, if provided and matching any of the provided topics.
func filterLogsFromReceipt(receipt *types.Receipt, address *gethcommon.Address, topic *gethcommon.Hash) ([]types.Log, error) {
logs := make([]types.Log, 0)

if receipt == nil {
Expand All @@ -89,10 +89,8 @@ func filterLogsFromReceipt(receipt *types.Receipt, address *gethcommon.Address,
shouldSkip = true
}

for _, topic := range topics {
if topic != nil && log.Topics[0] != *topic {
shouldSkip = true
}
if topic != nil && log.Topics[0] != *topic {
shouldSkip = true
}

if shouldSkip {
Expand Down Expand Up @@ -164,7 +162,7 @@ func (mb MerkleBatches) Len() int {

func (mb MerkleBatches) EncodeIndex(index int, w *bytes.Buffer) {
batch := mb[index]
if err := rlp.Encode(w, batch.Header.TransfersTree); err != nil {
if err := rlp.Encode(w, batch.Header.CrossChainRoot); err != nil {
panic(err)
}
}
Expand All @@ -173,7 +171,7 @@ func (mb MerkleBatches) ForMerkleTree() [][]interface{} {
values := make([][]interface{}, 0)
for _, batch := range mb {
val := []interface{}{
batch.Header.TransfersTree,
batch.Header.CrossChainRoot,
}
values = append(values, val)
}
Expand Down
4 changes: 2 additions & 2 deletions go/enclave/crosschain/message_bus_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func (m *MessageBusManager) GenerateMessageBusDeployTx() (*common.L2Tx, error) {

// ExtractLocalMessages - Finds relevant logs in the receipts and converts them to cross chain messages.
func (m *MessageBusManager) ExtractOutboundMessages(ctx context.Context, receipts common.L2Receipts) (common.CrossChainMessages, error) {
logs, err := filterLogsFromReceipts(receipts, m.messageBusAddress, []*gethcommon.Hash{&CrossChainEventID})
logs, err := filterLogsFromReceipts(receipts, m.messageBusAddress, &CrossChainEventID)
if err != nil {
m.logger.Error("Error extracting logs from L2 message bus!", log.ErrKey, err)
return make(common.CrossChainMessages, 0), err
Expand All @@ -127,7 +127,7 @@ func (m *MessageBusManager) ExtractOutboundMessages(ctx context.Context, receipt

// ExtractOutboundTransfers - Finds relevant logs in the receipts and converts them to cross chain messages.
func (m *MessageBusManager) ExtractOutboundTransfers(_ context.Context, receipts common.L2Receipts) (common.ValueTransferEvents, error) {
logs, err := filterLogsFromReceipts(receipts, m.messageBusAddress, []*gethcommon.Hash{&ValueTransferEventID})
logs, err := filterLogsFromReceipts(receipts, m.messageBusAddress, &ValueTransferEventID)
if err != nil {
m.logger.Error("Error extracting logs from L2 message bus!", log.ErrKey, err)
return make(common.ValueTransferEvents, 0), err
Expand Down
1 change: 0 additions & 1 deletion go/enclave/enclave.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,6 @@ func NewEnclave(
}
}

// todo - this does not query atomically. If stuff reorgs we might get weird exports.
func (e *enclaveImpl) ExportCrossChainData(ctx context.Context, fromSeqNo uint64, toSeqNo uint64) (*common.ExtCrossChainBundle, common.SystemError) {
return e.Sequencer().ExportCrossChainData(ctx, fromSeqNo, toSeqNo)
}
Expand Down
60 changes: 14 additions & 46 deletions go/enclave/nodetype/sequencer.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,12 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/ten-protocol/go-ten/go/common/errutil"
"github.com/ten-protocol/go-ten/go/common/measure"
"github.com/ten-protocol/go-ten/go/enclave/crosschain"
"github.com/ten-protocol/go-ten/go/enclave/evm/ethchainadapter"
"github.com/ten-protocol/go-ten/go/enclave/storage"
"github.com/ten-protocol/go-ten/go/enclave/txpool"

"github.com/ten-protocol/go-ten/go/common/compression"

smt "github.com/FantasyJony/openzeppelin-merkle-tree-go/standard_merkle_tree"
gethcommon "github.com/ethereum/go-ethereum/common"
gethlog "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
Expand Down Expand Up @@ -480,65 +478,35 @@ func (s *sequencer) Close() error {
}

func (s *sequencer) ExportCrossChainData(ctx context.Context, fromSeqNo uint64, toSeqNo uint64) (*common.ExtCrossChainBundle, error) {
canonicalBatchesInRollup := make([]*core.Batch, 0)
for i := fromSeqNo; i <= toSeqNo; i++ {
batch, err := s.storage.FetchBatchBySeqNo(ctx, i)
if err != nil {
return nil, err
}

l1BlockHash := batch.Header.L1Proof
block, err := s.storage.FetchBlock(ctx, l1BlockHash)
if err != nil {
return nil, err
}

canonicalBlock, err := s.storage.FetchCanonicaBlockByHeight(ctx, block.Header().Number)
if err != nil {
return nil, err
}

if canonicalBlock.Hash().Cmp(block.Hash()) != 0 {
continue
}

// Only add batches that point to canonical blocks.
canonicalBatchesInRollup = append(canonicalBatchesInRollup, batch)
canonicalBatches, err := s.storage.FetchCanonicalBatchesBetween((ctx), fromSeqNo, toSeqNo)
if err != nil {
return nil, err
}

if len(canonicalBatchesInRollup) == 0 {
if len(canonicalBatches) == 0 {
return nil, fmt.Errorf("no batches found for export of cross chain data")
}

// build a merkle tree of all the batches that are valid for the canonical L1 chain
// The proof is double inclusion - one for the message being in the batch's tree and one for

smtValues := crosschain.MerkleBatches(canonicalBatchesInRollup).ForMerkleTree()

tree, err := smt.Of(smtValues, []string{smt.SOL_BYTES32})
if err != nil {
return nil, err
}

batchesHash := tree.GetRoot()
blockHash := canonicalBatches[len(canonicalBatches)-1].Header.L1Proof
batchHash := canonicalBatches[len(canonicalBatches)-1].Header.Hash()

block, err := s.blockProcessor.GetHead(ctx)
block, err := s.storage.FetchBlock(ctx, blockHash)
if err != nil {
return nil, err
}

crossChainHashes := make([][]byte, 0)
for _, batch := range canonicalBatchesInRollup {
if batch.Header.TransfersTree != gethcommon.BigToHash(gethcommon.Big0) {
crossChainHashes = append(crossChainHashes, batch.Header.TransfersTree.Bytes())
for _, batch := range canonicalBatches {
if batch.Header.CrossChainRoot != gethcommon.BigToHash(gethcommon.Big0) {
crossChainHashes = append(crossChainHashes, batch.Header.CrossChainRoot.Bytes())
}
}

bundle := &common.ExtCrossChainBundle{
StateRootHash: gethcommon.BytesToHash(batchesHash),
L1BlockHash: block.Hash(),
L1BlockNum: big.NewInt(0).Set(block.Header().Number),
CrossChainHashes: crossChainHashes,
LastBatchHash: batchHash, // unused for now.
L1BlockHash: block.Hash(),
L1BlockNum: big.NewInt(0).Set(block.Header().Number),
CrossChainRootHashes: crossChainHashes,
} //todo: check fromSeqNo

err = s.signCrossChainBundle(bundle)
Expand Down
4 changes: 4 additions & 0 deletions go/enclave/storage/enclavedb/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ func ReadNonCanonicalBatches(ctx context.Context, db *sql.DB, startAtSeq uint64,
return fetchBatches(ctx, db, " where b.sequence>=? and b.sequence <=? and b.is_canonical=false order by b.sequence", startAtSeq, endSeq)
}

func ReadCanonicalBatches(ctx context.Context, db *sql.DB, startAtSeq uint64, endSeq uint64) ([]*core.Batch, error) {
return fetchBatches(ctx, db, " where b.sequence>=? and b.sequence <=? and b.is_canonical=true order by b.sequence", startAtSeq, endSeq)
}

// todo - is there a better way to write this query?
func ReadCurrentHeadBatch(ctx context.Context, db *sql.DB) (*core.Batch, error) {
return fetchBatch(ctx, db, " where b.is_canonical=true and b.is_executed=true and b.height=(select max(b1.height) from batch b1 where b1.is_canonical=true and b1.is_executed=true)")
Expand Down
Loading
Loading