Загрузка данных


// metacmp сравнивает CBOR-метаданные solc в creation-байткоде (--bin)
// и в runtime-коде контракта (eth_getCode).
//
// Использование:
//
//	metacmp -bin Contract.bin -code code.hex
//	metacmp -bin Contract.bin -rpc http://127.0.0.1:8545 -addr 0xABC...
//
// Код возврата: 0 — метаданные совпали, 1 — не совпали, 2 — ошибка.
package main

import (
	"bytes"
	"encoding/hex"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"math/big"
	"net/http"
	"os"
	"strings"
)

// Metadata — содержимое CBOR-блока, который solc дописывает в конец runtime-кода.
type Metadata struct {
	Offset       int    // смещение начала CBOR-блока в байткоде
	Raw          []byte // сырые байты CBOR-карты (без 2 байт длины)
	IPFS         []byte // multihash метаданных (34 байта), ключ "ipfs"
	Bzzr0, Bzzr1 []byte // swarm-хеш у старых версий solc
	Solc         string // версия компилятора
	Experimental bool
}

func (m Metadata) String() string {
	var parts []string
	if m.IPFS != nil {
		parts = append(parts, "ipfs="+base58(m.IPFS))
	}
	if m.Bzzr0 != nil {
		parts = append(parts, "bzzr0="+hex.EncodeToString(m.Bzzr0))
	}
	if m.Bzzr1 != nil {
		parts = append(parts, "bzzr1="+hex.EncodeToString(m.Bzzr1))
	}
	if m.Solc != "" {
		parts = append(parts, "solc="+m.Solc)
	}
	if m.Experimental {
		parts = append(parts, "experimental")
	}
	return strings.Join(parts, ", ")
}

// ExtractMetadata читает CBOR-блок метаданных solc из конца байткода.
// solc кладёт его в самый конец runtime-кода, а последние 2 байта — его длина.
// В --bin блок тоже стоит в конце: контракты не создают другие контракты,
// поэтому чужого байткода с метаданными внутри --bin нет.
func ExtractMetadata(code []byte) (Metadata, error) {
	if len(code) < 2 {
		return Metadata{}, errors.New("байткод слишком короткий")
	}
	n := int(code[len(code)-2])<<8 | int(code[len(code)-1])
	start := len(code) - 2 - n
	if n == 0 || start < 0 {
		return Metadata{}, errors.New("в конце байткода нет метаданных solc (--metadata-hash none или к --bin дописаны аргументы конструктора?)")
	}
	m, parsed, err := parseMap(code[start : len(code)-2])
	if err != nil || parsed != n || (m.IPFS == nil && m.Bzzr0 == nil && m.Bzzr1 == nil) {
		return Metadata{}, errors.New("в конце байткода нет валидных метаданных solc")
	}
	m.Offset = start
	m.Raw = code[start : len(code)-2]
	return m, nil
}

// --- минимальный CBOR-декодер, достаточный для метаданных solc ---

var errCBOR = errors.New("bad cbor")

// readHead читает заголовок CBOR-элемента: major type и аргумент.
func readHead(b []byte) (major byte, arg uint64, n int, err error) {
	if len(b) == 0 {
		return 0, 0, 0, errCBOR
	}
	major, info := b[0]>>5, b[0]&0x1f
	switch {
	case info < 24:
		return major, uint64(info), 1, nil
	case info == 24 && len(b) >= 2:
		return major, uint64(b[1]), 2, nil
	case info == 25 && len(b) >= 3:
		return major, uint64(b[1])<<8 | uint64(b[2]), 3, nil
	}
	return 0, 0, 0, errCBOR
}

func parseMap(b []byte) (Metadata, int, error) {
	var m Metadata
	major, count, pos, err := readHead(b)
	if err != nil || major != 5 {
		return m, 0, errCBOR
	}
	for k := uint64(0); k < count; k++ {
		// ключ — текстовая строка
		mj, l, n, err := readHead(b[pos:])
		if err != nil || mj != 3 || pos+n+int(l) > len(b) {
			return m, 0, errCBOR
		}
		key := string(b[pos+n : pos+n+int(l)])
		pos += n + int(l)

		// значение — байтовая/текстовая строка или bool
		if pos >= len(b) {
			return m, 0, errCBOR
		}
		if b[pos] == 0xf4 || b[pos] == 0xf5 {
			if key == "experimental" {
				m.Experimental = b[pos] == 0xf5
			}
			pos++
			continue
		}
		mj, l, n, err = readHead(b[pos:])
		if err != nil || (mj != 2 && mj != 3) || pos+n+int(l) > len(b) {
			return m, 0, errCBOR
		}
		val := b[pos+n : pos+n+int(l)]
		pos += n + int(l)

		switch key {
		case "ipfs":
			m.IPFS = val
		case "bzzr0":
			m.Bzzr0 = val
		case "bzzr1":
			m.Bzzr1 = val
		case "solc":
			if mj == 3 { // prerelease-версии пишутся строкой
				m.Solc = string(val)
			} else if len(val) == 3 {
				m.Solc = fmt.Sprintf("%d.%d.%d", val[0], val[1], val[2])
			}
		}
	}
	return m, pos, nil
}

// --- base58 (для отображения IPFS CID вида Qm...) ---

const b58alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"

func base58(b []byte) string {
	x := new(big.Int).SetBytes(b)
	base, mod := big.NewInt(58), new(big.Int)
	var out []byte
	for x.Sign() > 0 {
		x.DivMod(x, base, mod)
		out = append(out, b58alphabet[mod.Int64()])
	}
	for _, c := range b {
		if c != 0 {
			break
		}
		out = append(out, '1')
	}
	for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
		out[i], out[j] = out[j], out[i]
	}
	return string(out)
}

// --- ввод ---

func decodeHex(s string) ([]byte, error) {
	s = strings.TrimSpace(s)
	s = strings.TrimPrefix(strings.TrimPrefix(s, "0x"), "0X")
	if strings.Contains(s, "__") {
		return nil, errors.New("в байткоде есть плейсхолдеры библиотек __$...$__ — подставьте адреса библиотек")
	}
	return hex.DecodeString(s)
}

func readHexFile(path string) ([]byte, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}
	return decodeHex(string(data))
}

func getCode(rpcURL, addr string) ([]byte, error) {
	req, _ := json.Marshal(map[string]any{
		"jsonrpc": "2.0", "id": 1, "method": "eth_getCode",
		"params": []any{addr, "latest"},
	})
	resp, err := http.Post(rpcURL, "application/json", bytes.NewReader(req))
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var r struct {
		Result string `json:"result"`
		Error  *struct {
			Message string `json:"message"`
		} `json:"error"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
		return nil, err
	}
	if r.Error != nil {
		return nil, errors.New(r.Error.Message)
	}
	return decodeHex(r.Result)
}

func main() {
	binPath := flag.String("bin", "", "файл с выводом solc --bin")
	codePath := flag.String("code", "", "файл с результатом eth_getCode (hex)")
	rpcURL := flag.String("rpc", "", "URL ноды (вместо -code)")
	addr := flag.String("addr", "", "адрес контракта (вместе с -rpc)")
	flag.Parse()

	fail := func(err error) {
		fmt.Fprintln(os.Stderr, "ошибка:", err)
		os.Exit(2)
	}

	if *binPath == "" || (*codePath == "") == (*rpcURL == "" || *addr == "") {
		flag.Usage()
		os.Exit(2)
	}

	bin, err := readHexFile(*binPath)
	if err != nil {
		fail(fmt.Errorf("bin: %w", err))
	}

	var code []byte
	if *codePath != "" {
		code, err = readHexFile(*codePath)
	} else {
		code, err = getCode(*rpcURL, *addr)
	}
	if err != nil {
		fail(fmt.Errorf("code: %w", err))
	}
	if len(code) == 0 {
		fail(errors.New("по адресу нет кода"))
	}

	binMeta, err := ExtractMetadata(bin)
	if err != nil {
		fail(fmt.Errorf("bin: %w", err))
	}
	codeMeta, err := ExtractMetadata(code)
	if err != nil {
		fail(fmt.Errorf("code: %w", err))
	}

	fmt.Printf("bin:  %s\n", binMeta)
	fmt.Printf("code: %s\n", codeMeta)

	if bytes.Equal(binMeta.Raw, codeMeta.Raw) {
		fmt.Println("результат: СОВПАДАЕТ — те же исходники, версия solc и настройки компиляции")
		return
	}
	fmt.Println("результат: НЕ СОВПАДАЕТ")
	os.Exit(1)
}