package attesto import ( "bytes" "context" "encoding/hex" "encoding/json" "net/http" "strconv" "strings" "time" ) // GetCommitmentSelector is the first 4 bytes of keccak256("getCommitment(string)"), // fixed by the contract ABI (APSProvenance.abi.json); pinned so no keccak // implementation is needed. const GetCommitmentSelector = "a7b09e2a" // EncodeGetCommitmentCall ABI-encodes getCommitment(string) calldata: selector, // then the single dynamic string argument (offset word, length word, padded bytes). func EncodeGetCommitmentCall(batchID string) string { raw := []byte(batchID) pad := (32 - len(raw)%32) % 32 body := make([]byte, 0, 64+len(raw)+pad) offset := make([]byte, 32) offset[31] = 0x20 length := make([]byte, 32) for i, v := 31, len(raw); v > 0; i, v = i-1, v>>8 { length[i] = byte(v & 0xff) } body = append(body, offset...) body = append(body, length...) body = append(body, raw...) body = append(body, make([]byte, pad)...) return "0x" + GetCommitmentSelector + hex.EncodeToString(body) } func stripHex(value string) string { return strings.TrimPrefix(strings.ToLower(strings.TrimSpace(value)), "0x") } func rpcCall(ctx context.Context, httpClient *http.Client, rpcURL, method string, params []any) (json.RawMessage, string) { payload, err := json.Marshal(map[string]any{ "jsonrpc": "2.0", "id": 1, "method": method, "params": params, }) if err != nil { return nil, "rpc " + method + " failed" } req, err := http.NewRequestWithContext(ctx, http.MethodPost, rpcURL, bytes.NewReader(payload)) if err != nil { return nil, "rpc " + method + " failed" } req.Header.Set("Content-Type", "application/json") resp, err := httpClient.Do(req) if err != nil { return nil, "rpc " + method + " failed" } defer resp.Body.Close() var envelope struct { Result json.RawMessage `json:"result"` Error json.RawMessage `json:"error"` } if resp.StatusCode != http.StatusOK || json.NewDecoder(resp.Body).Decode(&envelope) != nil { return nil, "rpc " + method + " failed" } if len(envelope.Error) > 0 && string(envelope.Error) != "null" { return nil, "rpc " + method + " failed" } if len(envelope.Result) == 0 { return nil, "rpc " + method + " failed" } return envelope.Result, "" } func anchorField(epoch map[string]any, keys ...string) (string, bool) { for _, key := range keys { if value, ok := epoch[key].(string); ok && value != "" { return value, true } } return "", false } // VerifyAnchorOnchain verifies an anchor epoch against the chain itself via raw // JSON-RPC: one eth_call to getCommitment(batchId) comparing the on-chain merkle // root with the anchor's merkle_root, plus one eth_getTransactionReceipt // confirming the anchoring transaction succeeded in the expected block. The // anchor object is accepted in snake_case or camelCase; the contract address is // read from the epoch's hashed payload. rpcURL is customer-chosen — this // function never talks to Attesto. func VerifyAnchorOnchain(ctx context.Context, anchorEpoch map[string]any, rpcURL string, timeout time.Duration) VerifyReport { problems := make([]string, 0) payload, _ := anchorEpoch["payload"].(map[string]any) merkleRoot, okRoot := anchorField(anchorEpoch, "merkle_root", "merkleRoot") txHash, okTx := anchorField(anchorEpoch, "tx_hash", "txHash") batchID, okBatch := anchorField(anchorEpoch, "anchor_batch_id", "anchorBatchId") contractAddress, okAddr := anchorField(anchorEpoch, "contract_address", "contractAddress") if !okAddr && payload != nil { contractAddress, okAddr = anchorField(payload, "contract_address") } blockNumber := int64(-1) switch v := anchorEpoch["block_number"].(type) { case float64: blockNumber = int64(v) case int64: blockNumber = v default: switch v := anchorEpoch["blockNumber"].(type) { case float64: blockNumber = int64(v) case int64: blockNumber = v } } if !okRoot || !okTx || !okBatch || !okAddr || blockNumber < 0 { return VerifyReport{ Kind: "anchor-onchain", OK: false, Problems: []string{"anchor epoch is missing required fields"}, } } if timeout <= 0 { timeout = 15 * time.Second } httpClient := &http.Client{Timeout: timeout} callResult, problem := rpcCall(ctx, httpClient, rpcURL, "eth_call", []any{ map[string]any{"to": contractAddress, "data": EncodeGetCommitmentCall(batchID)}, "latest", }) if problem != "" { problems = append(problems, problem) } else { var returned string if json.Unmarshal(callResult, &returned) != nil { problems = append(problems, "rpc eth_call failed") } else { // Return tuple is (bytes32 merkleRoot, uint32, string, uint64); // the root is the first 32-byte word. onchain := stripHex(returned) if len(onchain) < 64 || onchain[:64] != stripHex(merkleRoot) { problems = append(problems, "anchor merkle root mismatch") } } } receiptResult, problem := rpcCall(ctx, httpClient, rpcURL, "eth_getTransactionReceipt", []any{txHash}) if problem != "" { problems = append(problems, problem) } else if string(receiptResult) == "null" { problems = append(problems, "anchor transaction not found") } else { var receipt struct { Status string `json:"status"` BlockNumber string `json:"blockNumber"` } if json.Unmarshal(receiptResult, &receipt) != nil { problems = append(problems, "anchor transaction not found") } else { if receipt.Status != "0x1" { problems = append(problems, "anchor transaction failed") } mined, err := strconv.ParseInt(stripHex(receipt.BlockNumber), 16, 64) if err != nil || mined != blockNumber { problems = append(problems, "anchor transaction block mismatch") } } } return VerifyReport{Kind: "anchor-onchain", OK: len(problems) == 0, Problems: problems} }