Files
attesto-go/anchors_test.go
Codex 8781fa57d8 sdk(P1.5): on-chain anchor verification with zero heavy dependencies
verify_anchor_onchain / verifyAnchorOnchain / VerifyAnchorOnchain check an
anchor epoch against the chain itself in all three SDKs: one raw JSON-RPC
eth_call to the anchoring contract's getCommitment(batchId) comparing the
on-chain merkle root with the anchor's merkle_root, plus one
eth_getTransactionReceipt confirming status == 0x1 in the expected block.
The customer chooses the RPC endpoint — nothing asks Attesto to confirm
Attesto, and no web3/ethers dependency is added anywhere.

The getCommitment(string) selector (keccak256 first 4 bytes = a7b09e2a) is
pinned as a constant with the dynamic-string ABI encoding done manually;
a worked calldata example (computed once against web3 keccak) is asserted in
all three test suites, and APSProvenance.abi.json is copied into each SDK's
testdata with a test that flags the pinned selector for review if the ABI's
getCommitment signature ever changes. The contract address is read from the
anchor epoch's hashed payload (payload.contract_address).

Mocked-RPC tests cover match / root-mismatch / failed-tx / wrong-block /
missing-fields in each language with identical problem strings; a live test
against the production contract runs only when ATTESTO_LIVE_RPC_URL is set.
Go CLI gains `attesto anchors verify <id> --rpc-url <url>` (API fetch +
on-chain check in one step; existing get/remote-verify behavior unchanged).
READMEs updated per SDK.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 18:31:18 +02:00

132 lines
4.1 KiB
Go

package attesto
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
const (
anchorRoot = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
anchorTx = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
anchorBlock = int64(12345)
)
func anchorEpochFixture() map[string]any {
return map[string]any{
"anchorEpochId": "aep_demo",
"merkleRoot": "0x" + anchorRoot,
"txHash": anchorTx,
"blockNumber": float64(anchorBlock),
"anchorBatchId": "batch_demo",
"chainId": float64(137),
"payload": map[string]any{"contract_address": "0x" + strings.Repeat("d", 40)},
}
}
func mockRPC(t *testing.T, callRoot string, receipt map[string]any) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body struct {
Method string `json:"method"`
Params []any `json:"params"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
switch body.Method {
case "eth_call":
// (bytes32, uint32, string, uint64) — only the first word matters.
result := "0x" + callRoot + strings.Repeat("0", 192)
_ = json.NewEncoder(w).Encode(map[string]any{"jsonrpc": "2.0", "id": 1, "result": result})
case "eth_getTransactionReceipt":
_ = json.NewEncoder(w).Encode(map[string]any{"jsonrpc": "2.0", "id": 1, "result": receipt})
default:
w.WriteHeader(http.StatusBadRequest)
}
}))
}
func TestEncodeGetCommitmentCallMatchesPinnedExample(t *testing.T) {
want := "0xa7b09e2a" +
"0000000000000000000000000000000000000000000000000000000000000020" +
"000000000000000000000000000000000000000000000000000000000000000a" +
"62617463685f64656d6f00000000000000000000000000000000000000000000"
if got := EncodeGetCommitmentCall("batch_demo"); got != want {
t.Errorf("calldata = %s, want %s", got, want)
}
}
func TestABIStillDeclaresGetCommitmentString(t *testing.T) {
raw, err := os.ReadFile(filepath.Join("testdata", "APSProvenance.abi.json"))
if err != nil {
t.Fatal(err)
}
var abi []map[string]any
if err := json.Unmarshal(raw, &abi); err != nil {
t.Fatal(err)
}
for _, entry := range abi {
if entry["name"] == "getCommitment" {
inputs := entry["inputs"].([]any)
if len(inputs) != 1 || inputs[0].(map[string]any)["type"] != "string" {
t.Fatalf("getCommitment inputs changed: %v", inputs)
}
return
}
}
t.Fatal("getCommitment not found in ABI")
}
func TestAnchorVerifiesWhenChainMatches(t *testing.T) {
srv := mockRPC(t, anchorRoot, map[string]any{"status": "0x1", "blockNumber": "0x3039"})
defer srv.Close()
report := VerifyAnchorOnchain(context.Background(), anchorEpochFixture(), srv.URL, time.Second)
if !report.OK {
t.Errorf("expected ok, problems: %v", report.Problems)
}
}
func TestAnchorRootMismatch(t *testing.T) {
srv := mockRPC(t, strings.Repeat("e", 64), map[string]any{"status": "0x1", "blockNumber": "0x3039"})
defer srv.Close()
report := VerifyAnchorOnchain(context.Background(), anchorEpochFixture(), srv.URL, time.Second)
if report.OK || !containsProblem(report.Problems, "anchor merkle root mismatch") {
t.Errorf("problems: %v", report.Problems)
}
}
func TestAnchorFailedTransaction(t *testing.T) {
srv := mockRPC(t, anchorRoot, map[string]any{"status": "0x0", "blockNumber": "0x3039"})
defer srv.Close()
report := VerifyAnchorOnchain(context.Background(), anchorEpochFixture(), srv.URL, time.Second)
if !containsProblem(report.Problems, "anchor transaction failed") {
t.Errorf("problems: %v", report.Problems)
}
}
func TestAnchorWrongBlock(t *testing.T) {
srv := mockRPC(t, anchorRoot, map[string]any{"status": "0x1", "blockNumber": "0x3040"})
defer srv.Close()
report := VerifyAnchorOnchain(context.Background(), anchorEpochFixture(), srv.URL, time.Second)
if !containsProblem(report.Problems, "anchor transaction block mismatch") {
t.Errorf("problems: %v", report.Problems)
}
}
func containsProblem(problems []string, want string) bool {
for _, p := range problems {
if p == want {
return true
}
}
return false
}