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>
132 lines
4.1 KiB
Go
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
|
|
}
|