diff --git a/agent.go b/agent.go index 821787d..bb21cb2 100644 --- a/agent.go +++ b/agent.go @@ -3,6 +3,7 @@ package agent import ( "encoding/binary" "encoding/hex" + "errors" "fmt" "net/url" "reflect" @@ -28,11 +29,7 @@ var icp0, _ = url.Parse("https://icp0.io/") func effectiveCanisterID(canisterId principal.Principal, args []any) principal.Principal { // If the canisterId is not aaaaa-aa (encoded as empty byte array), return it. - if len(canisterId.Raw) > 0 { - return canisterId - } - - if(len(args) < 1) { + if 0 < len(canisterId.Raw) || len(args) < 1 { return canisterId } @@ -140,9 +137,8 @@ func (a Agent) Call(canisterID principal.Principal, methodName string, args []an return err } ecID := effectiveCanisterID(canisterID, args) - a.logger.Printf("[AGENT] CALL %s (ecid: %s) %x", canisterID, ecID, a.identity.PublicKey(), methodName) - - if _, err := a.call(ecID, data); err != nil { + a.logger.Printf("[AGENT] CALL %s %s (%x)", canisterID, methodName, *requestID) + if _, err := a.call(canisterID, data); err != nil { return err } @@ -262,7 +258,7 @@ func (a Agent) Query(canisterID principal.Principal, methodName string, args []a // RequestStatus returns the status of the request with the given ID. func (a Agent) RequestStatus(ecID principal.Principal, requestID RequestID) ([]byte, hashtree.Node, error) { - a.logger.Printf("[AGENT] REQUEST STATUS %x", requestID[:]); + a.logger.Printf("[AGENT] REQUEST STATUS %s %x", ecID, requestID) path := []hashtree.Label{hashtree.Label("request_status"), requestID[:]} c, err := a.readStateCertificate(ecID, [][]hashtree.Label{path}) if err != nil { @@ -284,6 +280,11 @@ func (a Agent) RequestStatus(ecID principal.Principal, requestID RequestID) ([]b return nil, nil, err } status, err := hashtree.NewHashTree(node).Lookup(append(path, hashtree.Label("status"))...) + var lookupError hashtree.LookupError + if errors.As(err, &lookupError) && lookupError.Type == hashtree.LookupResultAbsent { + // The status might not be available immediately, since the request is still being processed. + return nil, nil, nil + } if err != nil { return nil, nil, err } @@ -309,7 +310,7 @@ func (a Agent) poll(ecID principal.Principal, requestID RequestID, delay, timeou for { select { case <-ticker.C: - a.logger.Printf("[AGENT] POLL (reqID: %x) ", requestID[:]) + a.logger.Printf("[AGENT] POLL %s %x", ecID, requestID) data, node, err := a.RequestStatus(ecID, requestID) if err != nil { return nil, err @@ -329,7 +330,6 @@ func (a Agent) poll(ecID principal.Principal, requestID RequestID, delay, timeou } return nil, fmt.Errorf("(%d) %s", uint64FromBytes(code), string(message)) case "replied": - fmt.Println(node) replied, err := hashtree.NewHashTree(node).Lookup(append(path, hashtree.Label("reply"))...) if err != nil { return nil, fmt.Errorf("no reply found") diff --git a/agent_test.go b/agent_test.go index 37f2068..25a8b67 100644 --- a/agent_test.go +++ b/agent_test.go @@ -186,6 +186,6 @@ func TestICPLedger_queryBlocks(t *testing.T) { type testLogger struct{} -func (t testLogger) Printf(format string, v ...interface{}) { +func (t testLogger) Printf(format string, v ...any) { fmt.Printf("[TEST]"+format+"\n", v...) } diff --git a/certification/hashtree/errors.go b/certification/hashtree/errors.go index 0b2608b..829d07a 100644 --- a/certification/hashtree/errors.go +++ b/certification/hashtree/errors.go @@ -17,37 +17,45 @@ func pathToString(path []Label) string { return sb.String() } +// LookupError is an error that occurs during a lookup. type LookupError struct { + // Type is the type of the lookup result. Type LookupResultType - Path string + // Path is the path that was looked up. + Path []Label + // Index is the index in the path where the error occurred. + Index int } // NewLookupAbsentError returns a new LookupError with type LookupResultAbsent. -func NewLookupAbsentError(path ...Label) LookupError { +func NewLookupAbsentError(path []Label, index int) LookupError { return LookupError{ - Type: LookupResultAbsent, - Path: pathToString(path), + Type: LookupResultAbsent, + Path: path, + Index: index, } } // NewLookupError returns a new LookupError with type LookupResultError. -func NewLookupError(path ...Label) LookupError { +func NewLookupError(path []Label, index int) LookupError { return LookupError{ - Type: LookupResultError, - Path: pathToString(path), + Type: LookupResultError, + Path: path, + Index: index, } } // NewLookupUnknownError returns a new LookupError with type LookupResultUnknown. -func NewLookupUnknownError(path ...Label) LookupError { +func NewLookupUnknownError(path []Label, index int) LookupError { return LookupError{ - Type: LookupResultUnknown, - Path: pathToString(path), + Type: LookupResultUnknown, + Path: path, + Index: index, } } func (l LookupError) Error() string { - return fmt.Sprintf("lookup error (path: %q): %s", l.Path, l.error()) + return fmt.Sprintf("lookup error (path: %q) at %q: %s", pathToString(l.Path), l.Path[l.Index], l.error()) } func (l LookupError) error() string { diff --git a/certification/hashtree/hashtree.go b/certification/hashtree/hashtree.go index de6d043..9cb7838 100644 --- a/certification/hashtree/hashtree.go +++ b/certification/hashtree/hashtree.go @@ -17,12 +17,12 @@ func (t HashTree) Digest() [32]byte { // Lookup looks up a path in the hash tree. func (t HashTree) Lookup(path ...Label) ([]byte, error) { - return lookupPath(t.Root, path...) + return lookupPath(t.Root, path, 0) } // LookupSubTree looks up a path in the hash tree and returns the sub-tree. func (t HashTree) LookupSubTree(path ...Label) (Node, error) { - return lookupSubTree(t.Root, path...) + return lookupSubTree(t.Root, path, 0) } // MarshalCBOR marshals a hash tree. diff --git a/certification/hashtree/hashtree_test.go b/certification/hashtree/hashtree_test.go index 39017df..11e62a7 100644 --- a/certification/hashtree/hashtree_test.go +++ b/certification/hashtree/hashtree_test.go @@ -34,10 +34,14 @@ func TestHashTree_Lookup(t *testing.T) { for _, i := range []int{0, 1} { if _, err := tree.Lookup(Label(fmt.Sprintf("label %d", i))); !errors.As(err, &lookupError) || lookupError.Type != LookupResultAbsent { t.Fatalf("unexpected lookup result") + } else if e := lookupError.Error(); e != fmt.Sprintf(`lookup error (path: "label %d") at "label %d": not found, not present in the tree`, i, i) { + t.Fatalf("unexpected error message: %s", e) } } if _, err := tree.Lookup(Label("label 2")); !errors.As(err, &lookupError) || lookupError.Type != LookupResultUnknown { t.Fatalf("unexpected lookup result") + } else if e := lookupError.Error(); e != `lookup error (path: "label 2") at "label 2": not found, could be pruned` { + t.Fatalf("unexpected error message: %s", e) } if v, err := tree.Lookup(Label("label 3")); err != nil { t.Fatalf("unexpected lookup result") @@ -49,6 +53,8 @@ func TestHashTree_Lookup(t *testing.T) { for _, i := range []int{4, 5, 6} { if _, err := tree.Lookup(Label(fmt.Sprintf("label %d", i))); !errors.As(err, &lookupError) || lookupError.Type != LookupResultAbsent { t.Fatalf("unexpected lookup result") + } else if e := lookupError.Error(); e != fmt.Sprintf(`lookup error (path: "label %d") at "label %d": not found, not present in the tree`, i, i) { + t.Fatalf("unexpected error message: %s", e) } } }) @@ -84,6 +90,8 @@ func TestHashTree_Lookup(t *testing.T) { for _, i := range []int{0, 1, 2} { if _, err := tree.Lookup(Label(fmt.Sprintf("label %d", i))); !errors.As(err, &lookupError) || lookupError.Type != LookupResultAbsent { t.Fatalf("unexpected lookup result") + } else if e := lookupError.Error(); e != fmt.Sprintf(`lookup error (path: "label %d") at "label %d": not found, not present in the tree`, i, i) { + t.Fatalf("unexpected error message: %s", e) } } if v, err := tree.Lookup(Label("label 3")); err != nil { @@ -96,10 +104,14 @@ func TestHashTree_Lookup(t *testing.T) { for _, i := range []int{4, 5} { if _, err := tree.Lookup(Label(fmt.Sprintf("label %d", i))); !errors.As(err, &lookupError) || lookupError.Type != LookupResultAbsent { t.Fatalf("unexpected lookup result") + } else if e := lookupError.Error(); e != fmt.Sprintf(`lookup error (path: "label %d") at "label %d": not found, not present in the tree`, i, i) { + t.Fatalf("unexpected error message: %s", e) } } if _, err := tree.Lookup(Label("label 6")); !errors.As(err, &lookupError) || lookupError.Type != LookupResultUnknown { t.Fatalf("unexpected lookup result") + } else if e := lookupError.Error(); e != `lookup error (path: "label 6") at "label 6": not found, could be pruned` { + t.Fatalf("unexpected error message: %s", e) } }) } diff --git a/certification/hashtree/lookup.go b/certification/hashtree/lookup.go index c084f19..6990290 100644 --- a/certification/hashtree/lookup.go +++ b/certification/hashtree/lookup.go @@ -4,44 +4,44 @@ import ( "bytes" ) -func lookupPath(n Node, path ...Label) ([]byte, error) { +func lookupPath(n Node, path []Label, idx int) ([]byte, error) { switch { - case len(path) == 0: + case len(path) == 0 || len(path) == idx: switch n := n.(type) { case Leaf: return n, nil case nil, Empty: - return nil, NewLookupAbsentError() + return nil, NewLookupAbsentError(path, idx-1) case Pruned: - return nil, NewLookupUnknownError() + return nil, NewLookupUnknownError(path, idx-1) default: // Labeled, Fork - return nil, NewLookupError() + return nil, NewLookupError(path, idx-1) } default: - switch l := lookupLabel(n, path[0]); l.Type { + switch l := lookupLabel(n, path[idx]); l.Type { case lookupLabelResultFound: - return lookupPath(l.Node, path[1:]...) + return lookupPath(l.Node, path, idx+1) case lookupLabelResultUnknown: - return nil, NewLookupUnknownError(path...) + return nil, NewLookupUnknownError(path, idx) default: - return nil, NewLookupAbsentError(path...) + return nil, NewLookupAbsentError(path, idx) } } } -func lookupSubTree(n Node, path ...Label) (Node, error) { +func lookupSubTree(n Node, path []Label, idx int) (Node, error) { switch { - case len(path) == 0: + case len(path) == 0 || len(path) == idx: return n, nil default: - switch l := lookupLabel(n, path[0]); l.Type { + switch l := lookupLabel(n, path[idx]); l.Type { case lookupLabelResultFound: - return lookupSubTree(l.Node, path[1:]...) + return lookupSubTree(l.Node, path, idx+1) case lookupLabelResultUnknown: - return nil, NewLookupUnknownError(path...) + return nil, NewLookupUnknownError(path, idx) default: - return nil, NewLookupAbsentError(path...) + return nil, NewLookupAbsentError(path, idx) } } } diff --git a/certification/hashtree/lookup_test.go b/certification/hashtree/lookup_test.go new file mode 100644 index 0000000..3f91125 --- /dev/null +++ b/certification/hashtree/lookup_test.go @@ -0,0 +1,44 @@ +package hashtree_test + +import ( + "bytes" + "errors" + "github.com/aviate-labs/agent-go/certification/hashtree" + "testing" +) + +func TestHashTree_Lookup_absent(t *testing.T) { + tree := hashtree.NewHashTree(hashtree.Labeled{ + Label: hashtree.Label("a"), + Tree: hashtree.Labeled{ + Label: hashtree.Label("b"), + Tree: hashtree.Labeled{ + Label: hashtree.Label("c"), + Tree: hashtree.Fork{ + LeftTree: hashtree.Labeled{ + Label: hashtree.Label("d0"), + }, + RightTree: hashtree.Labeled{ + Label: hashtree.Label("d1"), + Tree: hashtree.Leaf("d"), + }, + }, + }, + }, + }) + var lookupError hashtree.LookupError + if _, err := tree.Lookup(hashtree.Label("a"), hashtree.Label("b"), hashtree.Label("c0"), hashtree.Label("d0")); !errors.As(err, &lookupError) || lookupError.Type != hashtree.LookupResultAbsent { + t.Fatalf("unexpected lookup result") + } + if _, err := tree.Lookup(hashtree.Label("a"), hashtree.Label("b"), hashtree.Label("c"), hashtree.Label("d0")); !errors.As(err, &lookupError) || lookupError.Type != hashtree.LookupResultAbsent { + t.Fatalf("unexpected lookup result") + } + + v, err := tree.Lookup(hashtree.Label("a"), hashtree.Label("b"), hashtree.Label("c"), hashtree.Label("d1")) + if err != nil { + t.Fatalf("unexpected lookup result") + } + if !bytes.Equal(v, hashtree.Label("d")) { + t.Fatalf("unexpected node value") + } +} diff --git a/ic/dfx.json b/ic/dfx.json index 97d0c02..230265d 100644 --- a/ic/dfx.json +++ b/ic/dfx.json @@ -3,6 +3,10 @@ "assetstorage": { "type": "motoko", "main": "assetstorage/actor.mo" + }, + "ic0": { + "type": "motoko", + "main": "ic/actor.mo" } } } diff --git a/ic/ic_test.go b/ic/ic_test.go index eb213ae..07ce1c0 100644 --- a/ic/ic_test.go +++ b/ic/ic_test.go @@ -4,7 +4,9 @@ import ( "encoding/json" "fmt" "github.com/aviate-labs/agent-go" + "github.com/aviate-labs/agent-go/ic" "github.com/aviate-labs/agent-go/ic/assetstorage" + ic0 "github.com/aviate-labs/agent-go/ic/ic" "github.com/aviate-labs/agent-go/principal" "net/url" "os" @@ -19,11 +21,11 @@ func TestModules(t *testing.T) { if err != nil { t.Skip(err) } - var networksConfig map[string]map[string]string + var networksConfig networkConfig if err := json.Unmarshal(rawNetworksConfig, &networksConfig); err != nil { t.Fatal(err) } - host, err := url.Parse(fmt.Sprintf("http://%s", networksConfig["local"]["bind"])) + host, err := url.Parse(fmt.Sprintf("http://%s", networksConfig.Local.Bind)) if err != nil { t.Fatal(err) } @@ -33,7 +35,7 @@ func TestModules(t *testing.T) { if err != nil { t.Skip(err) } - start := exec.Command(dfxPath, "start", "--background", "--clean", "--artificial-delay=10") + start := exec.Command(dfxPath, "start", "--background", "--clean") if err := start.Start(); err != nil { t.Fatal(err) } @@ -60,18 +62,59 @@ func TestModules(t *testing.T) { t.Fatal(err) } + config := agent.Config{ + ClientConfig: &agent.ClientConfig{Host: host}, + FetchRootKey: true, + Logger: new(localLogger), + } + t.Run("assetstorage", func(t *testing.T) { cId, _ := principal.Decode(m["assetstorage"]["local"]) - a, err := assetstorage.NewAgent(cId, agent.Config{ - ClientConfig: &agent.ClientConfig{Host: host}, - FetchRootKey: true, - }) + a, err := assetstorage.NewAgent(cId, config) if err != nil { t.Fatal(err) } if _, err := a.ApiVersion(); err != nil { t.Error(err) } + + if err := a.Authorize(principal.AnonymousID); err != nil { + t.Fatal(err) + } + }) + + t.Run("management canister", func(t *testing.T) { + controller := principal.AnonymousID + addController := exec.Command(dfxPath, "canister", "update-settings", "--add-controller", controller.String(), "ic0") + if out, err := addController.CombinedOutput(); err != nil { + t.Fatal(sanitizeOutput(out)) + } + + getContollers := exec.Command(dfxPath, "canister", "info", "ic0") + out, err := getContollers.CombinedOutput() + if err != nil { + t.Fatal(sanitizeOutput(out)) + } + if !strings.Contains(string(out), controller.String()) { + t.Error("controller not added") + } + + cId, _ := principal.Decode(m["ic0"]["local"]) + a, err := ic0.NewAgent(ic.MANAGEMENT_CANISTER_PRINCIPAL, config) + if err != nil { + t.Fatal(err) + } + + if err := a.UpdateSettings(ic0.UpdateSettingsArgs{ + CanisterId: cId, + Settings: ic0.CanisterSettings{ + Controllers: &[]principal.Principal{ + principal.AnonymousID, + }, + }, + }); err != nil { + t.Error(err) + } }) } @@ -83,3 +126,15 @@ func sanitizeOutput(out []byte) string { } return s } + +type localLogger struct{} + +func (l localLogger) Printf(format string, v ...any) { + fmt.Printf("[LOCAL]"+format+"\n", v...) +} + +type networkConfig struct { + Local struct { + Bind string `json:"bind"` + } `json:"local"` +} diff --git a/ic/testdata/networks.json b/ic/testdata/networks.json index 5a407fc..0b6b072 100644 --- a/ic/testdata/networks.json +++ b/ic/testdata/networks.json @@ -1,5 +1,9 @@ { "local": { - "bind": "127.0.0.1:8080" + "bind": "127.0.0.1:8080", + "type": "ephemeral", + "replica": { + "subnet_type": "system" + } } } diff --git a/logger.go b/logger.go index dc339c0..c713f50 100644 --- a/logger.go +++ b/logger.go @@ -1,9 +1,9 @@ package agent type Logger interface { - Printf(format string, v ...interface{}) + Printf(format string, v ...any) } type defaultLogger struct{} -func (l defaultLogger) Printf(format string, v ...interface{}) {} +func (l defaultLogger) Printf(format string, v ...any) {} diff --git a/mock/replica.go b/mock/replica.go index 408ec7e..63441f2 100644 --- a/mock/replica.go +++ b/mock/replica.go @@ -108,9 +108,9 @@ func (r *Replica) handleCanister(writer http.ResponseWriter, canisterId, typ str _, _ = writer.Write([]byte("expected call request")) return } - requestId := agent.NewRequestID(req) - requestIdHex := hex.EncodeToString(requestId[:]) - r.Requests[requestIdHex] = req + requestID := agent.NewRequestID(req) + requestIDHex := hex.EncodeToString(requestID[:]) + r.Requests[requestIDHex] = req writer.WriteHeader(http.StatusAccepted) case "query": if req.Type != agent.RequestTypeQuery { @@ -161,12 +161,12 @@ func (r *Replica) handleCanister(writer http.ResponseWriter, canisterId, typ str _, _ = writer.Write([]byte("expected request_status")) return } - requestId := req.Paths[0][1] - requestIdHex := hex.EncodeToString(requestId) - req, ok := r.Requests[requestIdHex] + requestID := req.Paths[0][1] + requestIDHex := hex.EncodeToString(requestID) + req, ok := r.Requests[requestIDHex] if !ok { writer.WriteHeader(http.StatusNotFound) - _, _ = writer.Write([]byte("request not found: " + requestIdHex)) + _, _ = writer.Write([]byte("request not found: " + requestIDHex)) return } @@ -195,7 +195,7 @@ func (r *Replica) handleCanister(writer http.ResponseWriter, canisterId, typ str LeftTree: hashtree.Labeled{ Label: []byte("request_status"), Tree: hashtree.Labeled{ - Label: requestId, + Label: requestID, Tree: hashtree.Fork{ LeftTree: hashtree.Labeled{ Label: []byte("reply"), diff --git a/request.go b/request.go index aaff522..e3355fc 100644 --- a/request.go +++ b/request.go @@ -3,13 +3,12 @@ package agent import ( "bytes" "crypto/sha256" - "math/big" - "sort" - "github.com/aviate-labs/agent-go/certification/hashtree" "github.com/aviate-labs/agent-go/identity" "github.com/aviate-labs/agent-go/principal" "github.com/aviate-labs/leb128" + "math/big" + "sort" "github.com/fxamacker/cbor/v2" ) diff --git a/request_test.go b/request_test.go index 21636ae..ecc2ebd 100644 --- a/request_test.go +++ b/request_test.go @@ -42,6 +42,32 @@ func TestNewRequestID(t *testing.T) { })); h != "ea01a9c3d3830db108e0a87995ea0d4183dc9c6e51324e9818fced5c57aa64f5" { t.Error(h) } + + if h := fmt.Sprintf("%x", agent.NewRequestID(agent.Request{ + Type: agent.RequestTypeCall, + Sender: principal.AnonymousID, + IngressExpiry: 1711532558242940000, + CanisterID: principal.Principal{Raw: make([]byte, 0)}, // aaaaa-aa + MethodName: "update_settings", + Arguments: []byte{ + // ic0.UpdateSettingsArgs{ + // CanisterId: "bkyz2-fmaaa-aaaaa-qaaaq-cai", + // Settings: ic0.CanisterSettings{ + // Controllers: &[]principal.Principal{ + // principal.AnonymousID, + // }, + // }, + // } + 0x44, 0x49, 0x44, 0x4c, 0x06, 0x6e, 0x7d, 0x6d, 0x68, 0x6e, 0x01, 0x6c, 0x05, 0xc0, 0xcf, 0xf2, + 0x71, 0x00, 0xd7, 0xe0, 0x9b, 0x90, 0x02, 0x02, 0x80, 0xad, 0x98, 0x8a, 0x04, 0x00, 0xde, 0xeb, + 0xb5, 0xa9, 0x0e, 0x00, 0xa8, 0x82, 0xac, 0xc6, 0x0f, 0x00, 0x6e, 0x78, 0x6c, 0x03, 0xb3, 0xc4, + 0xb1, 0xf2, 0x04, 0x68, 0xe3, 0xf9, 0xf5, 0xd9, 0x08, 0x03, 0xca, 0x99, 0x98, 0xb4, 0x0d, 0x04, + 0x01, 0x05, 0x01, 0x0a, 0x80, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, + 0x01, 0x01, 0x01, 0x04, 0x00, 0x00, 0x00, 0x00, + }, + })); h != "3599fd3f4505a6ec44429dddff35a3e1338d9d28c64444cf4632df427d83d3cf" { + t.Error(h) + } } func TestRequestID_Sign(t *testing.T) {