diff --git a/build/ci.go b/build/ci.go index abb7c4997f..09d6db3253 100644 --- a/build/ci.go +++ b/build/ci.go @@ -47,6 +47,7 @@ package main import ( "bytes" "encoding/base64" + "encoding/json" "flag" "fmt" "log" @@ -710,11 +711,78 @@ func doArchive(cmdline []string) { if err := build.WriteArchive(alltools, allToolsArchiveFiles); err != nil { log.Fatal(err) } + + // Compute IPFS CIDs for archives and generate manifest + var artifacts []ipfsArtifact for _, archive := range []string{geth, alltools} { + cid, err := build.ComputeFileCID(archive) + if err != nil { + log.Printf("Warning: failed to compute CID for %s: %v", archive, err) + } else { + log.Printf("IPFS CID: %s -> %s", archive, cid.V1) + info, _ := os.Stat(archive) + artifacts = append(artifacts, ipfsArtifact{ + Name: archive, + Size: info.Size(), + CID: cid.V1, + Multihash: cid.Multihash, + }) + } if err := archiveUpload(archive, *upload, *signer, *signify); err != nil { log.Fatal(err) } } + + // Write and upload IPFS manifest + if len(artifacts) > 0 { + manifest := ipfsManifest{ + Version: version.Semantic, + Commit: env.Commit, + Date: time.Now().UTC().Format("2006-01-02"), + Artifacts: artifacts, + } + manifestFile := "ipfs-cids-" + basegeth + ".json" + if err := writeIPFSManifest(manifestFile, manifest); err != nil { + log.Printf("Warning: failed to write IPFS manifest: %v", err) + } else { + log.Printf("IPFS manifest: %s", manifestFile) + if *upload != "" { + auth := build.AzureBlobstoreConfig{ + Account: strings.Split(*upload, "/")[0], + Token: os.Getenv("AZURE_BLOBSTORE_TOKEN"), + Container: strings.SplitN(*upload, "/", 2)[1], + } + if err := build.AzureBlobstoreUpload(manifestFile, filepath.Base(manifestFile), auth); err != nil { + log.Printf("Warning: failed to upload IPFS manifest: %v", err) + } + } + } + } +} + +// ipfsArtifact represents a single release artifact with its IPFS CID. +type ipfsArtifact struct { + Name string `json:"name"` + Size int64 `json:"size"` + CID string `json:"cid"` // CIDv1 (bafkrei...) + Multihash string `json:"multihash"` // Base58 multihash (Qm...) +} + +// ipfsManifest contains IPFS CIDs for all release artifacts. +type ipfsManifest struct { + Version string `json:"version"` + Commit string `json:"commit"` + Date string `json:"date"` + Artifacts []ipfsArtifact `json:"artifacts"` +} + +// writeIPFSManifest writes the IPFS manifest to a JSON file. +func writeIPFSManifest(path string, manifest ipfsManifest) error { + data, err := json.MarshalIndent(manifest, "", " ") + if err != nil { + return err + } + return os.WriteFile(path, data, 0644) } func doKeeperArchive(cmdline []string) { diff --git a/internal/build/cid.go b/internal/build/cid.go new file mode 100644 index 0000000000..22d68fe539 --- /dev/null +++ b/internal/build/cid.go @@ -0,0 +1,127 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package build + +import ( + "crypto/sha256" + "encoding/base32" + "io" + "math/big" + "os" + "strings" +) + +// CID represents an IPFS Content Identifier for raw file content. +type CID struct { + // V1 is the CIDv1 with raw codec: bafkrei... (base32lower, 59 chars) + // This is the canonical format for raw binary content. + V1 string + + // Multihash is the raw SHA256 multihash (base58btc encoded): Qm... (46 chars) + // Note: This is NOT a valid CIDv0 for raw content (CIDv0 requires dag-pb codec). + // However, it's included for compatibility with tools that expect Qm... format. + // To get the actual content, use the V1 CID or convert: ipfs cid format -v 1 + Multihash string +} + +// ComputeFileCID computes the IPFS CID for a file's raw content. +// +// The CID is computed using SHA256 and the raw multicodec (0x55), which means +// the hash is of the file's exact bytes with no wrapping or chunking. +// +// Returns CIDv1 (bafkrei...) as the primary identifier, plus the base58-encoded +// multihash for compatibility with legacy tooling. +// +// Verify with: ipfs add --only-hash --raw-leaves -Q +func ComputeFileCID(path string) (*CID, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + return ComputeCID(f) +} + +// ComputeCID computes the IPFS CID from a reader's content. +func ComputeCID(r io.Reader) (*CID, error) { + h := sha256.New() + if _, err := io.Copy(h, r); err != nil { + return nil, err + } + digest := h.Sum(nil) + + // Build multihash: 0x12 (SHA256) + 0x20 (32 bytes length) + digest + multihash := make([]byte, 0, 34) + multihash = append(multihash, 0x12) // SHA256 multicodec + multihash = append(multihash, 0x20) // 32 bytes + multihash = append(multihash, digest...) + + // Base58-encoded multihash (Qm... format, for legacy compatibility) + mhBase58 := base58Encode(multihash) + + // CIDv1 = 'b' + base32lower(0x01 + 0x55 + multihash) + // 0x01 = CIDv1, 0x55 = raw multicodec + cidv1Bytes := make([]byte, 0, 36) + cidv1Bytes = append(cidv1Bytes, 0x01) // CID version 1 + cidv1Bytes = append(cidv1Bytes, 0x55) // raw codec + cidv1Bytes = append(cidv1Bytes, multihash...) + + encoded := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(cidv1Bytes) + cidv1 := "b" + strings.ToLower(encoded) + + return &CID{V1: cidv1, Multihash: mhBase58}, nil +} + +// base58Encode encodes bytes using Bitcoin's base58 alphabet. +// This is used for IPFS CIDv0 encoding. +func base58Encode(data []byte) string { + const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + + // Count leading zeros + var zeros int + for _, b := range data { + if b != 0 { + break + } + zeros++ + } + + // Convert to big integer + num := new(big.Int).SetBytes(data) + base := big.NewInt(58) + mod := new(big.Int) + + // Build result in reverse + var result []byte + for num.Sign() > 0 { + num.DivMod(num, base, mod) + result = append(result, alphabet[mod.Int64()]) + } + + // Add leading '1's for each leading zero byte + for i := 0; i < zeros; i++ { + result = append(result, '1') + } + + // Reverse the result + for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 { + result[i], result[j] = result[j], result[i] + } + + return string(result) +} diff --git a/internal/build/cid_test.go b/internal/build/cid_test.go new file mode 100644 index 0000000000..33063b5774 --- /dev/null +++ b/internal/build/cid_test.go @@ -0,0 +1,198 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package build + +import ( + "bytes" + "os" + "strings" + "testing" +) + +func TestBase58Encode(t *testing.T) { + tests := []struct { + input []byte + expected string + }{ + {[]byte{}, ""}, + {[]byte{0}, "1"}, + {[]byte{0, 0, 0}, "111"}, + {[]byte("Hello World!"), "2NEpo7TZRRrLZSi2U"}, + } + + for _, tt := range tests { + result := base58Encode(tt.input) + if result != tt.expected { + t.Errorf("base58Encode(%v) = %q, want %q", tt.input, result, tt.expected) + } + } +} + +func TestComputeCID(t *testing.T) { + tests := []struct { + name string + content []byte + wantV1Start string + wantMHStart string + wantV1Len int + wantMHLen int + }{ + { + name: "empty content", + content: []byte{}, + wantV1Start: "bafkrei", + wantMHStart: "Qm", + wantV1Len: 59, + wantMHLen: 46, + }, + { + name: "hello world", + content: []byte("hello world"), + wantV1Start: "bafkrei", + wantMHStart: "Qm", + wantV1Len: 59, + wantMHLen: 46, + }, + { + name: "binary content", + content: []byte{0x00, 0x01, 0x02, 0x03, 0xff, 0xfe, 0xfd}, + wantV1Start: "bafkrei", + wantMHStart: "Qm", + wantV1Len: 59, + wantMHLen: 46, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cid, err := ComputeCID(bytes.NewReader(tt.content)) + if err != nil { + t.Fatalf("ComputeCID() error = %v", err) + } + + // Check CIDv1 format + if !strings.HasPrefix(cid.V1, tt.wantV1Start) { + t.Errorf("V1 = %q, want prefix %q", cid.V1, tt.wantV1Start) + } + if len(cid.V1) != tt.wantV1Len { + t.Errorf("V1 length = %d, want %d", len(cid.V1), tt.wantV1Len) + } + + // Check multihash format + if !strings.HasPrefix(cid.Multihash, tt.wantMHStart) { + t.Errorf("Multihash = %q, want prefix %q", cid.Multihash, tt.wantMHStart) + } + if len(cid.Multihash) != tt.wantMHLen { + t.Errorf("Multihash length = %d, want %d", len(cid.Multihash), tt.wantMHLen) + } + + // CIDv1 should be lowercase + if cid.V1 != strings.ToLower(cid.V1) { + t.Errorf("V1 should be lowercase: %q", cid.V1) + } + }) + } +} + +func TestComputeCIDDeterministic(t *testing.T) { + content := []byte("deterministic test content") + + cid1, err := ComputeCID(bytes.NewReader(content)) + if err != nil { + t.Fatalf("ComputeCID() error = %v", err) + } + + cid2, err := ComputeCID(bytes.NewReader(content)) + if err != nil { + t.Fatalf("ComputeCID() error = %v", err) + } + + if cid1.V1 != cid2.V1 { + t.Errorf("V1 not deterministic: %q != %q", cid1.V1, cid2.V1) + } + if cid1.Multihash != cid2.Multihash { + t.Errorf("Multihash not deterministic: %q != %q", cid1.Multihash, cid2.Multihash) + } +} + +// TestKnownCID verifies against a known IPFS CID. +// Verified with: echo -n "hello" | ipfs add --only-hash --raw-leaves -Q +// Output: bafkreibm6jg3ux5qumhcn2b3flc3tyu6dmlb4xa7u5bf44yegnrjhc4yeq +func TestKnownCID(t *testing.T) { + content := []byte("hello") + cid, err := ComputeCID(bytes.NewReader(content)) + if err != nil { + t.Fatalf("ComputeCID() error = %v", err) + } + + // This is the CIDv1 for raw "hello" bytes + // Verified with: echo -n "hello" | ipfs add --only-hash --raw-leaves -Q + expectedV1 := "bafkreibm6jg3ux5qumhcn2b3flc3tyu6dmlb4xa7u5bf44yegnrjhc4yeq" + if cid.V1 != expectedV1 { + t.Errorf("V1 for 'hello' = %q, want %q", cid.V1, expectedV1) + } + + t.Logf("V1 (CIDv1): %s", cid.V1) + t.Logf("Multihash: %s", cid.Multihash) +} + +// TestEmptyContent verifies the CID for empty content. +// Verified with: echo -n "" | ipfs add --only-hash --raw-leaves -Q +// Output: bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku +func TestEmptyContent(t *testing.T) { + content := []byte{} + cid, err := ComputeCID(bytes.NewReader(content)) + if err != nil { + t.Fatalf("ComputeCID() error = %v", err) + } + + // This is the CIDv1 for empty content (SHA256 of nothing) + expectedV1 := "bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku" + if cid.V1 != expectedV1 { + t.Errorf("V1 for empty = %q, want %q", cid.V1, expectedV1) + } + + t.Logf("V1 (CIDv1): %s", cid.V1) + t.Logf("Multihash: %s", cid.Multihash) +} + +// TestReadmeFile verifies CID computation on an actual file in the repo. +// Run: ipfs add --only-hash --raw-leaves -Q ../../README.md +// to get the expected CID for comparison. +func TestReadmeFile(t *testing.T) { + // This test only runs if the README.md exists (it should in the repo) + path := "../../README.md" + if _, err := os.Stat(path); os.IsNotExist(err) { + t.Skip("README.md not found, skipping file test") + } + + cid, err := ComputeFileCID(path) + if err != nil { + t.Fatalf("ComputeFileCID() error = %v", err) + } + + // Just verify it produces valid-looking CIDs + if !strings.HasPrefix(cid.V1, "bafkrei") { + t.Errorf("V1 should start with bafkrei: %s", cid.V1) + } + if !strings.HasPrefix(cid.Multihash, "Qm") { + t.Errorf("Multihash should start with Qm: %s", cid.Multihash) + } + + t.Logf("README.md CIDv1: %s", cid.V1) + t.Logf("To verify: ipfs add --only-hash --raw-leaves -Q ../../README.md") +}