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>
This commit is contained in:
131
anchors_test.go
Normal file
131
anchors_test.go
Normal file
@@ -0,0 +1,131 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user