Загрузка данных
// 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)
}