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:
22
README.md
22
README.md
@@ -129,6 +129,28 @@ client, _ := attesto.NewClient(apiKey, attesto.WithHeadStore(attesto.NewFileHead
|
||||
client, _ = attesto.NewClient(apiKey, attesto.WithHeadStore(nil))
|
||||
```
|
||||
|
||||
## Verify anchors on-chain
|
||||
|
||||
`VerifyAnchorOnchain` checks an anchor epoch against the chain itself — one raw
|
||||
JSON-RPC `eth_call` to the anchoring contract's `getCommitment(batchId)`
|
||||
(comparing the on-chain merkle root) plus a transaction-receipt check (status,
|
||||
block). No web3 dependency; the RPC endpoint is yours, so this never asks
|
||||
Attesto to confirm Attesto.
|
||||
|
||||
```go
|
||||
anchor, _ := client.GetAnchorEpoch(ctx, "aep_...")
|
||||
report := attesto.VerifyAnchorOnchain(ctx, anchor, "https://polygon-rpc.example", 15*time.Second)
|
||||
if !report.OK {
|
||||
log.Fatalf("anchor failed on-chain verification: %v", report.Problems)
|
||||
}
|
||||
```
|
||||
|
||||
CLI equivalent (fetch + on-chain check in one step):
|
||||
|
||||
```bash
|
||||
attesto anchors verify aep_... --rpc-url https://polygon-rpc.example
|
||||
```
|
||||
|
||||
## Receiving Attesto webhooks
|
||||
|
||||
```go
|
||||
|
||||
172
anchors.go
Normal file
172
anchors.go
Normal file
@@ -0,0 +1,172 @@
|
||||
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}
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
attesto "git.rotz.ai/rotzmediagroup/attesto-v1/sdk/go"
|
||||
"git.rotz.ai/rotzmediagroup/attesto-v1/sdk/go/connectorkit"
|
||||
@@ -124,7 +125,7 @@ func (a *app) dispatch(ctx context.Context, args []string) error {
|
||||
case "witnesses":
|
||||
return a.witnesses(ctx, args[1:])
|
||||
case "anchors":
|
||||
return a.verifyableObject(ctx, "anchors", args[1:], attesto.VerifyAnchor, "/v2/anchors/")
|
||||
return a.anchors(ctx, args[1:])
|
||||
case "bundles":
|
||||
return a.bundles(ctx, args[1:])
|
||||
case "verify":
|
||||
@@ -410,6 +411,54 @@ func (a *app) verifyableObject(ctx context.Context, group string, args []string,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *app) anchors(ctx context.Context, args []string) error {
|
||||
// `anchors verify <anchor_epoch_id> --rpc-url <url>` chains an API fetch
|
||||
// with the on-chain check (eth_call getCommitment + tx receipt) against a
|
||||
// customer-chosen RPC endpoint. Without --rpc-url, all subcommands keep the
|
||||
// existing get / remote-verify behavior.
|
||||
if len(args) > 0 && args[0] == "verify" {
|
||||
rest := args[1:]
|
||||
positional := ""
|
||||
if len(rest) > 0 && !strings.HasPrefix(rest[0], "-") {
|
||||
positional, rest = rest[0], rest[1:]
|
||||
}
|
||||
fs := flag.NewFlagSet("anchors verify", flag.ContinueOnError)
|
||||
fs.SetOutput(a.err)
|
||||
id := fs.String("id", positional, "anchor epoch id")
|
||||
rpcURL := fs.String("rpc-url", "", "JSON-RPC endpoint for the anchor's chain")
|
||||
timeoutS := fs.Int("timeout-s", 15, "RPC timeout in seconds")
|
||||
file := fs.String("file", "", "proof object JSON file (remote verify mode)")
|
||||
publicKeyHex := fs.String("public-key-hex", "", "Ed25519 public key hex (remote verify mode)")
|
||||
if err := fs.Parse(rest); err != nil {
|
||||
return err
|
||||
}
|
||||
if *rpcURL == "" {
|
||||
fallback := []string{}
|
||||
if *file != "" {
|
||||
fallback = append(fallback, "--file", *file)
|
||||
}
|
||||
if *publicKeyHex != "" {
|
||||
fallback = append(fallback, "--public-key-hex", *publicKeyHex)
|
||||
}
|
||||
return a.remoteVerify(ctx, fallback, attesto.VerifyAnchor)
|
||||
}
|
||||
if *id == "" {
|
||||
return errors.New("anchor epoch id is required (positional or --id)")
|
||||
}
|
||||
client, err := a.systemClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
anchor, err := client.GetAnchorEpoch(ctx, *id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
report := attesto.VerifyAnchorOnchain(ctx, anchor, *rpcURL, time.Duration(*timeoutS)*time.Second)
|
||||
return a.write(report)
|
||||
}
|
||||
return a.verifyableObject(ctx, "anchors", args, attesto.VerifyAnchor, "/v2/anchors/")
|
||||
}
|
||||
|
||||
func (a *app) checkpoints(ctx context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return errors.New("checkpoints subcommand required")
|
||||
|
||||
172
testdata/APSProvenance.abi.json
vendored
Normal file
172
testdata/APSProvenance.abi.json
vendored
Normal file
@@ -0,0 +1,172 @@
|
||||
[
|
||||
{
|
||||
"inputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "constructor"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "batchId",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"name": "AlreadyCommitted",
|
||||
"type": "error"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "EmptyBatchId",
|
||||
"type": "error"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "NotOwner",
|
||||
"type": "error"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "string",
|
||||
"name": "batchId",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "merkleRoot",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint32",
|
||||
"name": "runCount",
|
||||
"type": "uint32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "string",
|
||||
"name": "model",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint64",
|
||||
"name": "blockTimestamp",
|
||||
"type": "uint64"
|
||||
}
|
||||
],
|
||||
"name": "RootCommitted",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "batchIds",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "batchId",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "merkleRoot",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "uint32",
|
||||
"name": "runCount",
|
||||
"type": "uint32"
|
||||
},
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "model",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"name": "commitRoot",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "getBatchCount",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "batchId",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"name": "getCommitment",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "merkleRoot",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "uint32",
|
||||
"name": "runCount",
|
||||
"type": "uint32"
|
||||
},
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "model",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"internalType": "uint64",
|
||||
"name": "blockTimestamp",
|
||||
"type": "uint64"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "owner",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user