package polygon import ( "bytes" "context" "errors" "fmt" "math" "math/big" "strings" "sync" libcommon "github.com/ledgerwatch/erigon-lib/common" "github.com/ledgerwatch/erigon-lib/common/hexutility" "github.com/ledgerwatch/erigon/accounts/abi/bind" "github.com/ledgerwatch/erigon/cl/merkle_tree" "github.com/ledgerwatch/erigon/cmd/devnet/devnet" "github.com/ledgerwatch/erigon/cmd/devnet/requests" "github.com/ledgerwatch/erigon/core/types" "github.com/ledgerwatch/erigon/crypto" "github.com/ledgerwatch/erigon/params/networkname" "github.com/ledgerwatch/erigon/rlp" "github.com/ledgerwatch/erigon/rpc" "github.com/ledgerwatch/erigon/turbo/jsonrpc" "github.com/ledgerwatch/erigon/turbo/trie" ) var ErrTokenIndexOutOfRange = errors.New("Index is grater than the number of tokens in transaction") type ProofGenerator struct { heimdall *Heimdall } func NewProofGenerator() *ProofGenerator { return &ProofGenerator{} } func (pg *ProofGenerator) NodeCreated(ctx context.Context, node devnet.Node) { if pg.heimdall == nil { if strings.HasPrefix(node.Name(), "bor") { if network := devnet.CurrentNetwork(ctx); network != nil { for _, service := range network.Services { if heimdall, ok := service.(*Heimdall); ok { pg.heimdall = heimdall } } } } } } func (pg *ProofGenerator) NodeStarted(ctx context.Context, node devnet.Node) { } func (pg *ProofGenerator) Start(ctx context.Context) error { return nil } func (pg *ProofGenerator) Stop() { } func (pg *ProofGenerator) GenerateExitPayload(ctx context.Context, burnTxHash libcommon.Hash, eventSignature libcommon.Hash, tokenIndex int) ([]byte, error) { logger := devnet.Logger(ctx) if pg.heimdall == nil || pg.heimdall.rootChainBinding == nil { return nil, fmt.Errorf("ProofGenerator not initialized") } logger.Info("Checking for checkpoint status", "hash", burnTxHash) isCheckpointed, err := pg.isCheckPointed(ctx, burnTxHash) if err != nil { return nil, fmt.Errorf("Error getting burn transaction: %w", err) } if !isCheckpointed { return nil, fmt.Errorf("Burn transaction has not been checkpointed yet") } // build payload for exit result, err := pg.buildPayloadForExit(ctx, burnTxHash, eventSignature, tokenIndex) if err != nil { if errors.Is(err, ErrTokenIndexOutOfRange) { return nil, fmt.Errorf("Block not included: %w", err) } return nil, fmt.Errorf("Null receipt received") } if len(result) == 0 { return nil, fmt.Errorf("Null result received") } return result, nil } func (pg *ProofGenerator) getChainBlockInfo(ctx context.Context, burnTxHash libcommon.Hash) (uint64, uint64, error) { childNode := devnet.SelectBlockProducer(devnet.WithCurrentNetwork(ctx, networkname.BorDevnetChainName)) var wg sync.WaitGroup var lastChild *big.Int var burnTransaction *jsonrpc.RPCTransaction var err [2]error wg.Add(1) go func() { defer wg.Done() lastChild, err[0] = pg.heimdall.rootChainBinding.GetLastChildBlock(&bind.CallOpts{}) }() wg.Add(1) go func() { defer wg.Done() burnTransaction, err[1] = childNode.GetTransactionByHash(burnTxHash) }() wg.Wait() for _, err := range err { if err != nil { return 0, 0, err } } return lastChild.Uint64(), burnTransaction.BlockNumber.Uint64(), nil } // lastchild block is greater equal to transacton block number; func (pg *ProofGenerator) isCheckPointed(ctx context.Context, burnTxHash libcommon.Hash) (bool, error) { lastChildBlockNum, burnTxBlockNum, err := pg.getChainBlockInfo(ctx, burnTxHash) if err != nil { return false, err } return lastChildBlockNum >= burnTxBlockNum, nil } func (pg *ProofGenerator) buildPayloadForExit(ctx context.Context, burnTxHash libcommon.Hash, logEventSig libcommon.Hash, index int) ([]byte, error) { node := devnet.SelectBlockProducer(ctx) if node == nil { return nil, fmt.Errorf("No node available") } if index < 0 { return nil, fmt.Errorf("Index must not negative") } var receipt *types.Receipt var block *requests.Block // step 1 - Get Block number from transaction hash lastChildBlockNum, txBlockNum, err := pg.getChainBlockInfo(ctx, burnTxHash) if err != nil { return nil, err } if lastChildBlockNum < txBlockNum { return nil, fmt.Errorf("Burn transaction has not been checkpointed as yet") } // step 2- get transaction receipt from txhash and // block information from block number var wg sync.WaitGroup var errs [2]error wg.Add(1) go func() { defer wg.Done() receipt, errs[0] = node.GetTransactionReceipt(burnTxHash) }() go func() { defer wg.Done() block, errs[1] = node.GetBlockByNumber(rpc.AsBlockNumber(txBlockNum), true) }() wg.Wait() for _, err := range errs { if err != nil { return nil, err } } // step 3 - get information about block saved in parent chain // step 4 - build block proof var rootBlockNumber uint64 var start, end uint64 rootBlockNumber, start, end, err = pg.getRootBlockInfo(txBlockNum) if err != nil { return nil, err } blockProof, err := getBlockProof(node, txBlockNum, start, end) if err != nil { return nil, err } // step 5- create receipt proof receiptProof, err := getReceiptProof(receipt, block, node, nil) if err != nil { return nil, err } // step 6 - encode payload, convert into hex if index > 0 { logIndices := getAllLogIndices(logEventSig, receipt) if index >= len(logIndices) { return nil, ErrTokenIndexOutOfRange } return encodePayload( rootBlockNumber, blockProof, txBlockNum, block.Time, block.TxHash, block.ReceiptHash, getReceiptBytes(receipt), // rlp encoded receiptProof.parentNodes, receiptProof.path, logIndices[index]), nil } logIndex := getLogIndex(logEventSig, receipt) if logIndex < 0 { return nil, fmt.Errorf("Log not found in receipt") } return encodePayload( rootBlockNumber, blockProof, txBlockNum, block.Time, block.TxHash, block.ReceiptHash, getReceiptBytes(receipt), // rlp encoded receiptProof.parentNodes, receiptProof.path, logIndex), nil } func encodePayload(headerNumber uint64, buildBlockProof string, blockNumber uint64, timestamp uint64, transactionsRoot libcommon.Hash, receiptsRoot libcommon.Hash, receipt []byte, receiptParentNodes [][]byte, path []byte, logIndex int) []byte { parentNodesBytes, _ := rlp.EncodeToBytes(receiptParentNodes) bytes, _ := rlp.EncodeToBytes( []interface{}{ headerNumber, buildBlockProof, blockNumber, timestamp, hexutility.Encode(transactionsRoot[:]), hexutility.Encode(receiptsRoot[:]), hexutility.Encode(receipt), hexutility.Encode(parentNodesBytes), hexutility.Encode(append([]byte{0}, path...)), logIndex, }) return bytes } type receiptProof struct { blockHash libcommon.Hash parentNodes [][]byte root []byte path []byte value interface{} } func getReceiptProof(receipt *types.Receipt, block *requests.Block, node devnet.Node, receipts []*types.Receipt) (*receiptProof, error) { stateSyncTxHash := types.ComputeBorTxHash(block.Number.Uint64(), block.Hash) receiptsTrie := trie.New(trie.EmptyRoot) if len(receipts) == 0 { var wg sync.WaitGroup var lock sync.Mutex errs := make([]error, len(block.Transactions)) for i, transaction := range block.Transactions { if transaction.Hash == stateSyncTxHash { // ignore if tx hash is bor state-sync tx continue } hash := transaction.Hash wg.Add(1) go func(i int) { defer wg.Done() receipt, errs[i] = node.GetTransactionReceipt(hash) path, _ := rlp.EncodeToBytes(receipt.TransactionIndex) rawReceipt := getReceiptBytes(receipt) lock.Lock() defer lock.Unlock() receiptsTrie.Update(path, rawReceipt) }(i) } wg.Wait() for _, err := range errs { if err != nil { return nil, err } } } else { for _, receipt := range receipts { path, _ := rlp.EncodeToBytes(receipt.TransactionIndex) rawReceipt := getReceiptBytes(receipt) receiptsTrie.Update(path, rawReceipt) } } path, _ := rlp.EncodeToBytes(receipt.TransactionIndex) result, ok := receiptsTrie.Get(path) if !ok { return nil, fmt.Errorf("Node does not contain the key") } var nodeValue any if isTypedReceipt(receipt) { nodeValue = result } else { rlp.DecodeBytes(result, nodeValue) } return &receiptProof{ blockHash: receipt.BlockHash, parentNodes: nil, //TODO - not sure how to get this result.stack.map(s => s.raw()), root: block.ReceiptHash[:], path: path, value: nodeValue, }, nil } func getBlockProof(node devnet.Node, txBlockNum, startBlock, endBlock uint64) (string, error) { proofs, err := getFastMerkleProof(node, txBlockNum, startBlock, endBlock) if err != nil { return "", err } return hexutility.Encode(bytes.Join(proofs, []byte{})), nil } func getFastMerkleProof(node devnet.Node, blockNumber, startBlock, endBlock uint64) ([][]byte, error) { merkleTreeDepth := int(math.Ceil(math.Log2(float64(endBlock - startBlock + 1)))) // We generate the proof root down, whereas we need from leaf up var reversedProof [][]byte offset := startBlock targetIndex := blockNumber - offset leftBound := uint64(0) rightBound := endBlock - offset // console.log("Searching for", targetIndex); for depth := 0; depth < merkleTreeDepth; depth += 1 { nLeaves := uint64(2) << (merkleTreeDepth - depth) // The pivot leaf is the last leaf which is included in the left subtree pivotLeaf := leftBound + nLeaves/2 - 1 if targetIndex > pivotLeaf { // Get the root hash to the merkle subtree to the left newLeftBound := pivotLeaf + 1 // eslint-disable-next-line no-await-in-loop subTreeMerkleRoot, err := node.GetRootHash(offset+leftBound, offset+pivotLeaf) if err != nil { return nil, err } reversedProof = append(reversedProof, subTreeMerkleRoot[:]) leftBound = newLeftBound } else { // Things are more complex when querying to the right. // Root hash may come some layers down so we need to build a full tree by padding with zeros // Some trees may be completely empty var newRightBound uint64 if rightBound <= pivotLeaf { newRightBound = rightBound } else { newRightBound = pivotLeaf } // Expect the merkle tree to have a height one less than the current layer expectedHeight := merkleTreeDepth - (depth + 1) if rightBound <= pivotLeaf { // Tree is empty so we repeatedly hash zero to correct height subTreeMerkleRoot := recursiveZeroHash(expectedHeight) reversedProof = append(reversedProof, subTreeMerkleRoot[:]) } else { // Height of tree given by RPC node subTreeHeight := int(math.Ceil(math.Log2(float64(rightBound - pivotLeaf)))) // Find the difference in height between this and the subtree we want heightDifference := expectedHeight - subTreeHeight // For every extra layer we need to fill 2*n leaves filled with the merkle root of a zero-filled Merkle tree // We need to build a tree which has heightDifference layers // The first leaf will hold the root hash as returned by the RPC remainingNodesHash, err := node.GetRootHash(offset+pivotLeaf+1, offset+rightBound) if err != nil { return nil, err } // The remaining leaves will hold the merkle root of a zero-filled tree of height subTreeHeight leafRoots := recursiveZeroHash(subTreeHeight) // Build a merkle tree of correct size for the subtree using these merkle roots leaves := make([][]byte, 2< childBlockNumber { // childBlockNumber was checkpointed before this header end = mid - 1 } else if headerEnd < childBlockNumber { // childBlockNumber was checkpointed after this header start = mid + 1 } } return ans * checkPointInterval, nil } func isTypedReceipt(receipt *types.Receipt) bool { return receipt.Status != 0 && receipt.Type != 0 } func getReceiptBytes(receipt *types.Receipt) []byte { buffer := &bytes.Buffer{} receipt.EncodeRLP(buffer) return buffer.Bytes() }