Skip to content

Commit

Permalink
Add mock replica.
Browse files Browse the repository at this point in the history
  • Loading branch information
q-uint committed May 18, 2023
1 parent caa0cd2 commit 7eb96a3
Show file tree
Hide file tree
Showing 20 changed files with 482 additions and 63 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
coverage.out
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
.PHONY: test test-ledger gen gen-ic fmt
.PHONY: test test-cover test-ledger gen gen-ic fmt

test:
go test -v -cover ./...


test-cover:
go test -v -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

test-ledger:
cd ic; dfx start --background --clean
cd ic/testdata; dfx deploy --no-wallet
Expand Down
29 changes: 19 additions & 10 deletions agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package agent

import (
"encoding/binary"
"encoding/hex"
"fmt"
"net/url"
"time"
Expand Down Expand Up @@ -41,10 +42,11 @@ type Agent struct {
client Client
identity identity.Identity
ingressExpiry time.Duration
rootKey []byte
}

// New returns a new Agent based on the given configuration.
func New(cfg Config) Agent {
func New(cfg Config) (*Agent, error) {
if cfg.IngressExpiry == 0 {
cfg.IngressExpiry = 10 * time.Second
}
Expand All @@ -59,11 +61,21 @@ func New(cfg Config) Agent {
if cfg.ClientConfig != nil {
ccfg = *cfg.ClientConfig
}
return Agent{
client: NewClient(ccfg),
client := NewClient(ccfg)
rootKey, _ := hex.DecodeString(certificate.RootKey)
if cfg.FetchRootKey {
status, err := client.Status()
if err != nil {
return nil, err
}
rootKey = status.RootKey
}
return &Agent{
client: client,
identity: id,
ingressExpiry: cfg.IngressExpiry,
}
rootKey: rootKey,
}, nil
}

// Call calls a method on a canister and unmarshals the result into the given values.
Expand Down Expand Up @@ -230,18 +242,14 @@ func (a Agent) RequestStatus(canisterID principal.Principal, requestID RequestID
if err := cbor.Unmarshal(c, &state); err != nil {
return nil, nil, err
}
status, err := a.client.Status() // TODO: fetch status once.
if err != nil {
return nil, nil, err
}
cert, err := certificate.New(canisterID, status.RootKey[len(status.RootKey)-96:], c)
cert, err := certificate.New(canisterID, a.rootKey[len(a.rootKey)-96:], c)
if err != nil {
return nil, nil, err
}
if err := cert.Verify(); err != nil {
return nil, nil, err
}
node, err := certificate.DeserializeNode(state["tree"].([]interface{}))
node, err := certificate.DeserializeNode(state["tree"].([]any))
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -343,4 +351,5 @@ type Config struct {
Identity identity.Identity
IngressExpiry time.Duration
ClientConfig *ClientConfig
FetchRootKey bool
}
4 changes: 2 additions & 2 deletions agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (

func Example_anonymous_query() {
ledgerID, _ := principal.Decode("ryjl3-tyaaa-aaaaa-aaaba-cai")
a := agent.New(agent.Config{})
a, _ := agent.New(agent.Config{})
args, err := candid.EncodeValueString("record { account = \"9523dc824aa062dcd9c91b98f4594ff9c6af661ac96747daef2090b7fe87037d\" }")
if err != nil {
fmt.Println(err)
Expand All @@ -26,7 +26,7 @@ func Example_query() {
publicKey, privateKey, _ := ed25519.GenerateKey(rand.Reader)
id, _ := identity.NewEd25519Identity(publicKey, privateKey)
ledgerID, _ := principal.Decode("ryjl3-tyaaa-aaaaa-aaaba-cai")
a := agent.New(agent.Config{
a, _ := agent.New(agent.Config{
Identity: id,
})
args, err := candid.EncodeValueString("record { account = \"9523dc824aa062dcd9c91b98f4594ff9c6af661ac96747daef2090b7fe87037d\" }")
Expand Down
9 changes: 9 additions & 0 deletions certificate/bls/bls.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ func PublicKeyFromHexString(s string) (*PublicKey, error) {
return &pub, pub.DeserializeHexStr(s)
}

type SecretKey = bls.SecretKey

// NewSecretKeyByCSPRNG returns a new SecretKey generated by CSPRNG.
func NewSecretKeyByCSPRNG() *SecretKey {
var sk bls.SecretKey
sk.SetByCSPRNG()
return &sk
}

type Signature = bls.Sign

// SignatureFromBytes returns a Signature from a byte slice.
Expand Down
33 changes: 33 additions & 0 deletions certificate/bls/bls_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,43 @@
package bls

import (
"encoding/hex"
"testing"
)

func TestSecretKey(t *testing.T) {
sk := NewSecretKeyByCSPRNG()
s := sk.Sign("hello")
if !s.Verify(sk.GetPublicKey(), "hello") {
t.Error()
}
}

func TestVerify(t *testing.T) {
// SOURCE: https://github.com/dfinity/agent-js/blob/5214dc1fc4b9b41f023a88b1228f04d2f2536987/packages/bls-verify/src/index.test.ts#L101
publicKeyHex := "a7623a93cdb56c4d23d99c14216afaab3dfd6d4f9eb3db23d038280b6d5cb2caaee2a19dd92c9df7001dede23bf036bc0f33982dfb41e8fa9b8e96b5dc3e83d55ca4dd146c7eb2e8b6859cb5a5db815db86810b8d12cee1588b5dbf34a4dc9a5"
publicKeyRaw, _ := hex.DecodeString(publicKeyHex)
publicKey, err := PublicKeyFromBytes(publicKeyRaw)
if err != nil {
t.Fatal(err)
}

signatureHex := "b89e13a212c830586eaa9ad53946cd968718ebecc27eda849d9232673dcd4f440e8b5df39bf14a88048c15e16cbcaabe"
signatureHexRaw, _ := hex.DecodeString(signatureHex)
signature, err := SignatureFromBytes(signatureHexRaw)
if err != nil {
t.Fatal(err)
}

if signature.Verify(publicKey, "bye") {
t.Error()
}
if !signature.Verify(publicKey, "hello") {
t.Error()
}
}

func TestVerify_hex(t *testing.T) {
// SOURCE: https://github.com/dfinity/agent-js/blob/5214dc1fc4b9b41f023a88b1228f04d2f2536987/packages/bls-verify/src/index.test.ts#L101
publicKeyHex := "a7623a93cdb56c4d23d99c14216afaab3dfd6d4f9eb3db23d038280b6d5cb2caaee2a19dd92c9df7001dede23bf036bc0f33982dfb41e8fa9b8e96b5dc3e83d55ca4dd146c7eb2e8b6859cb5a5db815db86810b8d12cee1588b5dbf34a4dc9a5"
publicKey, err := PublicKeyFromHexString(publicKeyHex)
Expand Down
5 changes: 2 additions & 3 deletions certificate/certificate.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package certificate

import (
"encoding/hex"
"fmt"
"github.com/aviate-labs/agent-go/certificate/bls"
"github.com/aviate-labs/agent-go/principal"
Expand Down Expand Up @@ -42,7 +41,7 @@ func New(canisterID principal.Principal, rootKey []byte, certificate []byte) (*C

// Verify verifies the certificate.
func (c Certificate) Verify() error {
signature, err := bls.SignatureFromHexString(hex.EncodeToString(c.cert.Signature))
signature, err := bls.SignatureFromBytes(c.cert.Signature)
if err != nil {
return err
}
Expand All @@ -51,7 +50,7 @@ func (c Certificate) Verify() error {
return err
}
rootHash := c.cert.Tree.Digest()
message := append(domainSeparator("ic-state-root"), rootHash[:]...)
message := append(DomainSeparator("ic-state-root"), rootHash[:]...)
if !signature.Verify(publicKey, string(message)) {
return fmt.Errorf("signature verification failed")
}
Expand Down
18 changes: 9 additions & 9 deletions certificate/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ import (
"github.com/fxamacker/cbor/v2"
)

func Serialize(node Node) ([]byte, error) {
return cbor.Marshal(serialize(node))
}

func domainSeparator(t string) []byte {
func DomainSeparator(t string) []byte {
return append(
[]byte{uint8(len(t))},
[]byte(t)...,
)
}

func Serialize(node Node) ([]byte, error) {
return cbor.Marshal(serialize(node))
}

func serialize(node Node) []any {
switch n := node.(type) {
case Empty:
Expand Down Expand Up @@ -52,7 +52,7 @@ func serialize(node Node) []any {
type Empty struct{}

func (e Empty) Reconstruct() [32]byte {
return sha256.Sum256(domainSeparator("ic-hashtree-empty"))
return sha256.Sum256(DomainSeparator("ic-hashtree-empty"))
}

func (e Empty) String() string {
Expand All @@ -68,7 +68,7 @@ func (f Fork) Reconstruct() [32]byte {
l := f.LeftTree.Reconstruct()
r := f.RightTree.Reconstruct()
return sha256.Sum256(append(
domainSeparator("ic-hashtree-fork"),
DomainSeparator("ic-hashtree-fork"),
append(l[:], r[:]...)...,
))
}
Expand All @@ -91,7 +91,7 @@ type Labeled struct {
func (l Labeled) Reconstruct() [32]byte {
t := l.Tree.Reconstruct()
return sha256.Sum256(append(
domainSeparator("ic-hashtree-labeled"),
DomainSeparator("ic-hashtree-labeled"),
append(l.Label, t[:]...)...,
))
}
Expand All @@ -104,7 +104,7 @@ type Leaf []byte

func (l Leaf) Reconstruct() [32]byte {
return sha256.Sum256(append(
domainSeparator("ic-hashtree-leaf"),
DomainSeparator("ic-hashtree-leaf"),
l...,
))
}
Expand Down
37 changes: 32 additions & 5 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,25 @@ func (c Client) Status() (*Status, error) {
}

func (c Client) call(canisterID principal.Principal, data []byte) ([]byte, error) {
return c.post("call", canisterID, data, 202)
u := c.url(fmt.Sprintf("/api/v2/canister/%s/call", canisterID.Encode()))
resp, err := c.client.Post(u, "application/cbor", bytes.NewBuffer(data))
if err != nil {
return nil, err
}
switch resp.StatusCode {
case http.StatusAccepted:
return io.ReadAll(resp.Body)
case http.StatusOK:
body, _ := io.ReadAll(resp.Body)
var err preprocessingError
if err := cbor.Unmarshal(body, &err); err != nil {
return nil, err
}
return nil, fmt.Errorf("(%d) %s: %s", err.RejectCode, err.Message, err.ErrorCode)
default:
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("(%d) %s: %s", resp.StatusCode, resp.Status, body)
}
}

func (c Client) get(path string) ([]byte, error) {
Expand All @@ -48,14 +66,14 @@ func (c Client) get(path string) ([]byte, error) {
return io.ReadAll(resp.Body)
}

func (c Client) post(path string, canisterID principal.Principal, data []byte, statusCorePass int) ([]byte, error) {
func (c Client) post(path string, canisterID principal.Principal, data []byte) ([]byte, error) {
u := c.url(fmt.Sprintf("/api/v2/canister/%s/%s", canisterID.Encode(), path))
resp, err := c.client.Post(u, "application/cbor", bytes.NewBuffer(data))
if err != nil {
return nil, err
}
switch resp.StatusCode {
case statusCorePass:
case http.StatusOK:
return io.ReadAll(resp.Body)
default:
body, _ := io.ReadAll(resp.Body)
Expand All @@ -64,11 +82,11 @@ func (c Client) post(path string, canisterID principal.Principal, data []byte, s
}

func (c Client) query(canisterID principal.Principal, data []byte) ([]byte, error) {
return c.post("query", canisterID, data, 200)
return c.post("query", canisterID, data)
}

func (c Client) readState(canisterID principal.Principal, data []byte) ([]byte, error) {
return c.post("read_state", canisterID, data, 200)
return c.post("read_state", canisterID, data)
}

func (c Client) url(p string) string {
Expand All @@ -81,3 +99,12 @@ func (c Client) url(p string) string {
type ClientConfig struct {
Host *url.URL
}

type preprocessingError struct {
// The reject code.
RejectCode uint64 `cbor:"reject_code"`
// A textual diagnostic message.
Message string `cbor:"reject_message"`
// An optional implementation-specific textual error code.
ErrorCode string `cbor:"error_code"`
}
14 changes: 9 additions & 5 deletions gen/templates/agent.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,20 @@ type {{ .Name }} = {{ .Type }}

// Agent is a client for the "{{ .CanisterName }}" canister.
type Agent struct {
a agent.Agent
a *agent.Agent
canisterId principal.Principal
}

// NewAgent creates a new agent for the "{{ .CanisterName }}" canister.
func NewAgent(canisterId principal.Principal, config agent.Config) Agent {
return Agent{
a: agent.New(config),
canisterId: canisterId,
func NewAgent(canisterId principal.Principal, config agent.Config) (*Agent, error) {
a, err := agent.New(config)
if err != nil {
return nil, err
}
return &Agent{
a: a,
canisterId: canisterId,
}, nil
}
{{- range .Methods }}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
github.com/di-wu/parser v0.3.0
github.com/fxamacker/cbor/v2 v2.4.0
github.com/herumi/bls-go-binary v1.28.2
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc
)

require github.com/x448/float16 v0.8.4 // indirect
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ github.com/herumi/bls-go-binary v1.28.2 h1:F0AezsC0M1a9aZjk7g0l2hMb1F56Xtpfku97p
github.com/herumi/bls-go-binary v1.28.2/go.mod h1:O4Vp1AfR4raRGwFeQpr9X/PQtncEicMoOe6BQt1oX0Y=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo=
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU=
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
14 changes: 9 additions & 5 deletions ic/assetstorage/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,20 @@ import (

// Agent is a client for the "assetstorage" canister.
type Agent struct {
a agent.Agent
a *agent.Agent
canisterId principal.Principal
}

// NewAgent creates a new agent for the "assetstorage" canister.
func NewAgent(canisterId principal.Principal, config agent.Config) Agent {
return Agent{
a: agent.New(config),
canisterId: canisterId,
func NewAgent(canisterId principal.Principal, config agent.Config) (*Agent, error) {
a, err := agent.New(config)
if err != nil {
return nil, err
}
return &Agent{
a: a,
canisterId: canisterId,
}, nil
}

// ApiVersion calls the "api_version" method on the "assetstorage" canister.
Expand Down
Loading

0 comments on commit 7eb96a3

Please sign in to comment.