From 295942bf6d1d421d2220c883e1cc7861c08833c9 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Sun, 1 Dec 2019 13:36:08 -0800 Subject: [PATCH] Initial Commit --- .gitignore | 1 + README.md | 11 +++++ api.go | 106 +++++++++++++++++++++++++++++++++++++++++++++++ api_test.go | 80 +++++++++++++++++++++++++++++++++++ client/client.go | 38 +++++++++++++++++ go.mod | 11 +++++ go.sum | 72 ++++++++++++++++++++++++++++++++ server/server.go | 77 ++++++++++++++++++++++++++++++++++ 8 files changed, 396 insertions(+) create mode 100644 README.md create mode 100644 api.go create mode 100644 api_test.go create mode 100644 client/client.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 server/server.go diff --git a/.gitignore b/.gitignore index e6a0c6e..0ff0169 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ config.json tokens.db +vendor/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..1471ed7 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# Zcash Token Service + +This repository contains a *prototype* implementation of a token service +(described in this [paper](./paper/)) and based on the privacypass implementation in Tapir. + +`client/client.go` implements the client portion, generating a set of tokens and sending these +to the zcash token service through an [encrypted memo](https://electriccoin.co/blog/encrypted-memo-field/) along with payment and a return address. + +`server/server.go` implements the server portion, polling zcash for new transactions, signing the +tokens it receives along with a proof and sending both back through an encrypted memo to the specified +return address. \ No newline at end of file diff --git a/api.go b/api.go new file mode 100644 index 0000000..2990603 --- /dev/null +++ b/api.go @@ -0,0 +1,106 @@ +package zcashtokenservice + +import ( + "cwtch.im/tapir/primitives/core" + "cwtch.im/tapir/primitives/privacypass" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "git.openprivacy.ca/openprivacy/libricochet-go/log" + "github.com/gtank/ristretto255" + "golang.org/x/crypto/sha3" + "strings" +) + +type Context struct { + Y *ristretto255.Element + tokens []*privacypass.Token + blindedTokens []privacypass.BlindedToken + transcript *core.Transcript + ct *privacypass.Token +} + +type ZcashTokenRequest struct { + BlindedTokens []privacypass.BlindedToken `json:"bts"` + ReturnAddress string `json:"ra"` + ConstraintToken []byte `json:"ct"` +} + +// RequestTokens generates a zcash uri +func RequestTokenURI(tokenServiceZcashAddress string, tokenServicePublicKey *ristretto255.Element, zcashReturnAddress string, constraintToken *privacypass.Token) (Context, string) { + + tokens, blinded := privacypass.GenerateBlindedTokenBatch(4) + + transcript := core.NewTranscript("zcash-token-service") + transcript.AddToTranscript(zcashReturnAddress, []byte(zcashReturnAddress)) + transcript.AddToTranscript("zcash-token-server", []byte(tokenServiceZcashAddress)) + + request := ZcashTokenRequest{BlindedTokens: blinded, ReturnAddress: zcashReturnAddress} + if constraintToken != nil { + transcript.AddToTranscript("constraint-token-t", constraintToken.GetT()) + request.ConstraintToken = constraintToken.GetT() + } + + data, _ := json.Marshal(request) + // NOTE: We are currently hardcoding this amount! + return Context{tokens: tokens, blindedTokens: blinded, Y: tokenServicePublicKey, transcript: transcript, ct: constraintToken}, fmt.Sprintf("zcash:%v?amt=%f&memo=%v", tokenServiceZcashAddress, 0.1, base64.StdEncoding.EncodeToString(data)) +} + +type ZcashTokenResponse struct { + privacypass.SignedBatchWithProof `json:"sb"` + Error error `json:",omitempty"` +} + +func ProcessRequest(request string, tokenServiceZcashAddress string, server *privacypass.TokenServer) (string, string) { + log.Infof("Request: [%x]", request) + data, err := base64.StdEncoding.DecodeString(request) + log.Infof("Err: %s", data) + if err == nil { + zcashTokenRequest := new(ZcashTokenRequest) + err = json.Unmarshal(data, zcashTokenRequest) + if err == nil { + transcript := core.NewTranscript("zcash-token-service") + transcript.AddToTranscript(zcashTokenRequest.ReturnAddress, []byte(zcashTokenRequest.ReturnAddress)) + transcript.AddToTranscript("zcash-token-server", []byte(tokenServiceZcashAddress)) + if len(zcashTokenRequest.ConstraintToken) == 0 { + signedBatchWithProof := server.SignBlindedTokenBatch(zcashTokenRequest.BlindedTokens, transcript) + data, _ := json.Marshal(ZcashTokenResponse{signedBatchWithProof, nil}) + return base64.StdEncoding.EncodeToString(data), zcashTokenRequest.ReturnAddress + } else { + transcript.AddToTranscript("constraint-token-t", zcashTokenRequest.ConstraintToken) + signedBatchWithProof := server.SignBlindedTokenBatchWithConstraint(zcashTokenRequest.BlindedTokens, zcashTokenRequest.ConstraintToken, transcript) + log.Debugf("Server Side Transcript: %s", transcript.OutputTranscriptToAudit()) + data, _ := json.Marshal(ZcashTokenResponse{signedBatchWithProof, nil}) + return base64.StdEncoding.EncodeToString(data), zcashTokenRequest.ReturnAddress + } + } + } + errorresponse, _ := json.Marshal(ZcashTokenResponse{privacypass.SignedBatchWithProof{}, err}) + return base64.StdEncoding.EncodeToString(errorresponse), "" +} + +func ProcessResponse(response string, ctx Context) ([]*privacypass.Token, error) { + response = strings.ReplaceAll(response, "\n", "") + response = strings.ReplaceAll(response, "\r", "") + data, err := base64.StdEncoding.DecodeString(response) + if err == nil { + zcashTokenResponse := new(ZcashTokenResponse) + err = json.Unmarshal(data, zcashTokenResponse) + if err == nil { + if ctx.ct == nil { + if privacypass.UnblindSignedTokenBatch(ctx.tokens, ctx.blindedTokens, zcashTokenResponse.SignedTokens, ctx.Y, zcashTokenResponse.Proof, ctx.transcript) { + return ctx.tokens, nil + } + } else { + Ht := sha3.Sum512(ctx.ct.GetT()) + T := new(ristretto255.Element).FromUniformBytes(Ht[:]) + if privacypass.UnblindSignedTokenBatch(ctx.tokens, append(ctx.blindedTokens, privacypass.BlindedToken{T}), append(zcashTokenResponse.SignedTokens, privacypass.SignedToken{ctx.ct.W}), ctx.Y, zcashTokenResponse.Proof, ctx.transcript) { + return ctx.tokens, nil + } + } + err = errors.New("failed to unblind signed tokens") + } + } + return nil, err +} diff --git a/api_test.go b/api_test.go new file mode 100644 index 0000000..0abae4c --- /dev/null +++ b/api_test.go @@ -0,0 +1,80 @@ +package zcashtokenservice + +import ( + "cwtch.im/tapir/primitives/privacypass" + "encoding/base64" + "git.openprivacy.ca/openprivacy/libricochet-go/log" + "net/url" + "testing" +) + +func TestRequestTokenURI(t *testing.T) { + + log.SetLevel(log.LevelDebug) + tokenServer := privacypass.NewTokenServer() + + ctx, uri := RequestTokenURI("zs1pjv7eneq9jshw0eyywpruv2cetl74sh84ymdnyv4c4vg8vl5k2qmlv0n7ye77g49lhqkg75v52f", tokenServer.Y, "zs1pjv7eneq9jshw0eyywpruv2cetl74sh84ymdnyv4c4vg8vl5k2qmlv0n7ye77g49lhqkg75v52f", nil) + + // Test that UI is Well Formed + t.Logf("URI: %v", uri) + url, err := url.Parse(uri) + if err != nil { + t.Fatalf("Cannot process uri %v", err) + } + + // Check that Request & Response are well formed + memo := url.Query().Get("memo") + if len(memo) > 512 { + t.Fatalf(" request memo is too long %v", len(memo)) + } + memostr, _ := base64.StdEncoding.DecodeString(memo) + t.Logf("request memo %s length is %v", memostr, len(memo)) + + response, _ := ProcessRequest(memo, "zs1pjv7eneq9jshw0eyywpruv2cetl74sh84ymdnyv4c4vg8vl5k2qmlv0n7ye77g49lhqkg75v52f", tokenServer) + + responsestr, _ := base64.StdEncoding.DecodeString(response) + if len(response) > 512 { + t.Fatalf("response memo is too long %s %v", responsestr, len(response)) + } + t.Logf("response memo length is %s %v", response, len(response)) + + tokens, err := ProcessResponse(response, ctx) + if err != nil { + t.Fatalf("Tokens were not signed: %v", err) + } + + // Test Tokens are Valid + for _, token := range tokens { + err = tokenServer.SpendToken(token.SpendToken([]byte("test")), []byte("test")) + if err == nil { + t.Logf("Spent token %v Successfully", token) + } else { + t.Errorf("Failed to spend token: %v", err) + } + } + + t.Logf("Testing with Constraint Token: %v", tokens[0]) + ctx, uri = RequestTokenURI("zs1pjv7eneq9jshw0eyywpruv2cetl74sh84ymdnyv4c4vg8vl5k2qmlv0n7ye77g49lhqkg75v52f", tokenServer.Y, "zs1pjv7eneq9jshw0eyywpruv2cetl74sh84ymdnyv4c4vg8vl5k2qmlv0n7ye77g49lhqkg75v52f", tokens[0]) + url, _ = url.Parse(uri) + memo = url.Query().Get("memo") + if len(memo) > 512 { + t.Fatalf(" request memo is too long %v", len(memo)) + } + response, _ = ProcessRequest(memo, "zs1pjv7eneq9jshw0eyywpruv2cetl74sh84ymdnyv4c4vg8vl5k2qmlv0n7ye77g49lhqkg75v52f", tokenServer) + tokens, err = ProcessResponse(response, ctx) + + if err != nil { + t.Errorf("Failed to unblind tokens: %v", err) + } + + // Test Tokens are Valid + for _, token := range tokens { + err = tokenServer.SpendToken(token.SpendToken([]byte("test")), []byte("test")) + if err == nil { + t.Logf("Spent token %v Successfully with Constraint Token", token) + } else { + t.Logf("Failed to spend token") + } + } + +} diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..ef13bc7 --- /dev/null +++ b/client/client.go @@ -0,0 +1,38 @@ +package main + +import ( + "bufio" + "fmt" + "git.openprivacy.ca/openprivacy/zcashtokenservice" + "github.com/gtank/ristretto255" + "os" +) + +func main() { + + // NOTE: If you want to run this prototype then you will need to edit all of these parameters to match + // your own setup. + clientZcashAddress := "zs1kfwveu7w2tl395lfql5rhrmewylk6fqp7386wjl3qc40rvxuak5gj450gafnztc62jdm6suy4ey" + tokenServiceZcashAddress := "zs1252tvdk394cph9pg28hpy45atzwf67xpctuedhtksac6d4a654x77akr022cx943rup5z7zsw8r" + publicKey := ristretto255.NewElement() + publicKey.UnmarshalText([]byte("IFS9viFSlzaYOpuuPAoYc8aUk7NKlgrjbVml5qjKJzE=")) + + ctx, uri := zcashtokenservice.RequestTokenURI(tokenServiceZcashAddress, publicKey, clientZcashAddress) + + fmt.Printf("Please use a zcash wallet to have these tokens signed: %v", uri) + + reader := bufio.NewReader(os.Stdin) + fmt.Print("\nPlease paste in the servers response: ") + response, _ := reader.ReadString('\n') + + tokens, err := zcashtokenservice.ProcessResponse(response, ctx) + + if err == nil { + fmt.Printf("\nSuccessfully signed tokens using Zcash!!:\n") + for _, token := range tokens { + fmt.Printf("Token: %x\n", token) + } + return + } + fmt.Printf("Error: %v", err) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f20528d --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module git.openprivacy.ca/openprivacy/zcashtokenservice + +go 1.13 + +require ( + cwtch.im/tapir v0.1.13 + git.openprivacy.ca/openprivacy/libricochet-go v1.0.6 + git.openprivacy.ca/openprivacy/zcashrpc v0.0.0-20191201220044-80615a955ce3 + github.com/gtank/ristretto255 v0.1.1 + golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..69a8997 --- /dev/null +++ b/go.sum @@ -0,0 +1,72 @@ +cwtch.im/tapir v0.1.10/go.mod h1:EuRYdVrwijeaGBQ4OijDDRHf7R2MDSypqHkSl5DxI34= +cwtch.im/tapir v0.1.11 h1:JLm1MIYq4VXKzhj68+P8OuVPllAU9U6G0DtUor2fbc4= +cwtch.im/tapir v0.1.11/go.mod h1:EuRYdVrwijeaGBQ4OijDDRHf7R2MDSypqHkSl5DxI34= +cwtch.im/tapir v0.1.12 h1:SUfaMlzIsLqNB+/1O3up/V4EKAAyynGPo2hixZYwsNo= +cwtch.im/tapir v0.1.12/go.mod h1:1fu4d+cMCepVaMm5vHrp4N/romdjKR+R6P8cvdRXYtQ= +cwtch.im/tapir v0.1.13 h1:gGAtFyRnxLrs1SQRAeVXiO5MG2DUUTE621aTQQEJN7U= +cwtch.im/tapir v0.1.13/go.mod h1:1fu4d+cMCepVaMm5vHrp4N/romdjKR+R6P8cvdRXYtQ= +cwtch.im/zcash2cwtch v0.0.0-20190914011329-5b125554715c h1:QwoUtw8QrZDiIwD2jr4q9GX8mug8bXrD+65+t0BEmEM= +cwtch.im/zcash2cwtch v0.0.0-20190914011329-5b125554715c/go.mod h1:kWpm1gL3HFfBUBf/3iLRapF72t3kQO2vY/dqq/PxrHA= +git.openprivacy.ca/openprivacy/libricochet-go v1.0.4 h1:GWLMJ5jBSIC/gFXzdbbeVz7fIAn2FTgW8+wBci6/3Ek= +git.openprivacy.ca/openprivacy/libricochet-go v1.0.4/go.mod h1:yMSG1gBaP4f1U+RMZXN85d29D39OK5s8aTpyVRoH5FY= +git.openprivacy.ca/openprivacy/libricochet-go v1.0.6 h1:5o4K2qn3otEE1InC5v5CzU0yL7Wl7DhVp4s8H3K6mXY= +git.openprivacy.ca/openprivacy/libricochet-go v1.0.6/go.mod h1:yMSG1gBaP4f1U+RMZXN85d29D39OK5s8aTpyVRoH5FY= +git.openprivacy.ca/openprivacy/zcashrpc v0.0.0-20191201220044-80615a955ce3 h1:D//38WC9EXDkcwZd/vPRSLqSdfVd4nQDN1KQBAcr4aw= +git.openprivacy.ca/openprivacy/zcashrpc v0.0.0-20191201220044-80615a955ce3/go.mod h1:gy826ODOMq16jtoxh6xB1YLqVrnRYLKf+8kBH/aWG9M= +github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= +github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= +github.com/cretz/bine v0.1.0 h1:1/fvhLE+fk0bPzjdO5Ci+0ComYxEMuB1JhM4X5skT3g= +github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f h1:8N8XWLZelZNibkhM1FuF+3Ad3YIbgirjdMiVA0eUkaM= +github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= +github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is= +github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= +github.com/gtank/ristretto255 v0.1.1-0.20191011164322-af147e8e15b6 h1:fYrfnLiiWLCPvmmKbH8AlOwZAtrV0QDKox1HsAEjygY= +github.com/gtank/ristretto255 v0.1.1-0.20191011164322-af147e8e15b6/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= +github.com/gtank/ristretto255 v0.1.1 h1:A+VVUhf73TS5HRfCnfMBqTBujkbwY3Fo8sRSFvL3cIg= +github.com/gtank/ristretto255 v0.1.1/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG9WsDpiCvZf1yKO7sz7scAjSlBa0= +github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +golang.org/x/crypto v0.0.0-20190128193316-c7b33c32a30b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f h1:R423Cnkcp5JABoeemiGEPlt9tHXFfw5kvc0yqlxRPWo= +golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..ef8b105 --- /dev/null +++ b/server/server.go @@ -0,0 +1,77 @@ +package main + +import ( + "cwtch.im/tapir/persistence" + "cwtch.im/tapir/primitives/privacypass" + "encoding/base64" + "encoding/json" + "git.openprivacy.ca/openprivacy/libricochet-go/log" + "git.openprivacy.ca/openprivacy/zcashrpc" + "git.openprivacy.ca/openprivacy/zcashtokenservice" + "io/ioutil" + "strings" + "time" +) + +type ZcashConfig struct { + Username string `json: "username"` + Password string `json: "password"` + ZAddress string `json: "zaddress"` +} + +type TapirServer struct { + tokenServer *privacypass.TokenServer + db persistence.Service +} + +func StartTapirServer() *TapirServer { + ts := new(TapirServer) + ts.db = new(persistence.BoltPersistence) + ts.db.Open("tokens.db") + ts.tokenServer = privacypass.NewTokenServerFromStore(ts.db) + ts.db.Setup([]string{"transactions"}) + return ts +} + +func (ts *TapirServer) Refresh() { + for { + configFile, _ := ioutil.ReadFile("config.json") + config := ZcashConfig{} + _ = json.Unmarshal(configFile, &config) + + zc := zcashrpc.NewLocalClient(config.Username, config.Password) + transactions, err := zc.ListReceivedTransactionsByAddress(config.ZAddress) + if err != nil { + log.Errorf("Error fetching zcash transactions: %v", err) + } + for _, transaction := range transactions { + exists, _ := ts.db.Check("transactions", transaction.TransactionID) + // Only process transactions we haven't seen before, and that meet the cost requirement + if !exists && transaction.Amount >= 0.1 { + log.Infof("Got a new transaction txid:%s, amount %f", transaction.TransactionID, transaction.Amount) + + decodedMemo, _ := transaction.Memo.Decode() + decodedMemoStr := strings.Trim(string(decodedMemo), "\000") + response, returnAddress := zcashtokenservice.ProcessRequest(decodedMemoStr, config.ZAddress, ts.tokenServer) + + if returnAddress != "" { + log.Infof("Sending Response: %v", response) + // We send a memo back with a very low memo cost (covered by the initial request) + zc.SendOne(config.ZAddress, returnAddress, response, 0.001) + } else { + error, err := base64.StdEncoding.DecodeString(response) + log.Infof("Error: %s %v", error, err) + } + ts.db.Persist("transactions", transaction.TransactionID, transaction) + } + } + time.Sleep(time.Second * 10) + } +} + +func main() { + log.SetLevel(log.LevelDebug) + ts := StartTapirServer() + log.Infof("Token Server Public Key: %v", ts.tokenServer.Y) + ts.Refresh() +}