ready.go 4.99 KB
Newer Older
Loïck Bonniot's avatar
Loïck Bonniot committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
package contract

import (
	"time"

	"dfss/dfssp/api"
	"dfss/dfssp/common"
	"dfss/dfssp/entities"
	"dfss/mgdb"
	"dfss/net"
	"golang.org/x/net/context"
	"gopkg.in/mgo.v2/bson"
)

// readySignal is the structure that is transmitted accross goroutines
type readySignal struct {
17 18 19 20 21
	ready        bool     // If true, this is the ready signal. If not, this is a new connection signal
	data         string   // Various data (CN or SignatureUUID)
	documentHash []byte   // Contract document SHA-512 hash
	chain        [][]byte // Only used to broadcast hash chain (signers hashes in order)
	sequence     []uint32 // Only used to broadcast signature sequence
Loïck Bonniot's avatar
Loïck Bonniot committed
22 23
}

Loïck Bonniot's avatar
Loïck Bonniot committed
24 25 26 27 28
// ReadySignTimeout is the delay users have to confirm the signature.
// A high value is not recommended, as there is no way to create any other signature on the same contract before the timeout
// if a client has connection issues.
var ReadySignTimeout = time.Minute

Loïck Bonniot's avatar
Loïck Bonniot committed
29
// ReadySign is the last job of the platform before the signature can occur.
30
// When a new client is ready, it joins a waitingGroup and waits for a master broadcast announcing that everybody is ready.
Loïck Bonniot's avatar
Loïck Bonniot committed
31 32 33 34 35
//
// Doing it this way is efficient in time, as only one goroutine deals with the database and do global checks.
func ReadySign(db *mgdb.MongoManager, rooms *common.WaitingGroupMap, ctx *context.Context, in *api.ReadySignRequest) *api.LaunchSignature {
	roomID := "ready_" + in.ContractUuid
	channel, _, first := rooms.Join(roomID)
Loïck Bonniot's avatar
Loïck Bonniot committed
36
	defer rooms.Unjoin(roomID, channel)
Loïck Bonniot's avatar
Loïck Bonniot committed
37

Loïck Bonniot's avatar
Loïck Bonniot committed
38
	cn := net.GetCN(ctx)
Loïck Bonniot's avatar
Loïck Bonniot committed
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
	// Check UUID
	if !bson.IsObjectIdHex(in.ContractUuid) {
		return &api.LaunchSignature{ErrorCode: &api.ErrorCode{Code: api.ErrorCode_INVARG}}
	}

	// If first in the room, create a goroutine for ready check.
	// It is absolutely thread safe thanks to a mutex applied on the `first` variable.
	if first {
		go masterReadyRoutine(db, rooms, in.ContractUuid)
	}

	// Broadcast identity
	rooms.Broadcast(roomID, &readySignal{data: cn})

	// Wait for ready signal
Loïck Bonniot's avatar
Loïck Bonniot committed
54
	timeout := time.After(ReadySignTimeout)
Loïck Bonniot's avatar
Loïck Bonniot committed
55 56 57 58 59 60 61 62 63 64 65 66
	for {
		select {
		case signal, ok := <-channel:
			if !ok {
				return &api.LaunchSignature{ErrorCode: &api.ErrorCode{Code: api.ErrorCode_INTERR}}
			}
			s := signal.(*readySignal)
			if s.ready {
				if len(s.data) > 0 {
					return &api.LaunchSignature{
						ErrorCode:     &api.ErrorCode{Code: api.ErrorCode_SUCCESS},
						SignatureUuid: s.data,
67
						DocumentHash:  s.documentHash,
Loïck Bonniot's avatar
Loïck Bonniot committed
68
						KeyHash:       s.chain,
69
						Sequence:      s.sequence,
Loïck Bonniot's avatar
Loïck Bonniot committed
70 71 72 73
					}
				} // data == "" means the contractUUID is bad
				return &api.LaunchSignature{ErrorCode: &api.ErrorCode{Code: api.ErrorCode_INVARG}}
			}
74 75
		case <-(*ctx).Done(): // Client's disconnection
			return &api.LaunchSignature{ErrorCode: &api.ErrorCode{Code: api.ErrorCode_INVARG}}
Loïck Bonniot's avatar
Loïck Bonniot committed
76
		case <-timeout: // Someone has not confirmed the signature within the delay
77
			return &api.LaunchSignature{ErrorCode: &api.ErrorCode{Code: api.ErrorCode_TIMEOUT, Message: "timeout for ready signal"}}
Loïck Bonniot's avatar
Loïck Bonniot committed
78 79 80 81 82 83 84 85 86 87
		}
	}

}

// masterReadyRoutine is a function to be started by the first signer ready as a goroutine.
// It will join the associated ready room and check ready status of each signer when a new signer signals its readiness.
func masterReadyRoutine(db *mgdb.MongoManager, rooms *common.WaitingGroupMap, contractUUID string) {
	roomID := "ready_" + contractUUID
	channel, oldMessages, _ := rooms.Join(roomID)
Loïck Bonniot's avatar
Loïck Bonniot committed
88
	defer rooms.Unjoin(roomID, channel)
Loïck Bonniot's avatar
Loïck Bonniot committed
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109

	// Push oldMessages into the channel.
	// It is safe as this sould be a very small slice (the room is just created).
	for _, v := range oldMessages {
		channel <- v
	}

	// Get contract signers from database
	fetch := entities.Contract{ID: bson.ObjectIdHex(contractUUID)}
	contract := entities.Contract{}
	err := db.Get("contracts").FindByID(fetch, &contract)
	if err != nil {
		rooms.Broadcast(roomID, &readySignal{
			ready: true,
			data:  "",
		}) // This represents a "error" response
		return
	}

	signersReady := make([]bool, len(contract.Signers))
	work := true
Loïck Bonniot's avatar
Loïck Bonniot committed
110
	timeout := time.After(ReadySignTimeout)
Loïck Bonniot's avatar
Loïck Bonniot committed
111 112 113 114 115 116 117 118 119 120
	for work {
		select {
		case signal, ok := <-channel:
			if !ok { // Channel closed, aborting everything
				return
			}
			cn := signal.(*readySignal).data
			ready := FindAndUpdatePendingSigner(cn, &signersReady, &contract.Signers)
			if ready {
				rooms.Broadcast(roomID, &readySignal{
121 122 123 124 125
					ready:        true,
					data:         bson.NewObjectId().Hex(),
					documentHash: contract.File.Hash,
					chain:        contract.GetHashChain(),
					sequence:     GenerateSignSequence(len(contract.Signers)),
Loïck Bonniot's avatar
Loïck Bonniot committed
126 127 128
				})
				work = false
			}
Loïck Bonniot's avatar
Loïck Bonniot committed
129
		case <-timeout:
Loïck Bonniot's avatar
Loïck Bonniot committed
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
			work = false
		}
	}

}

// FindAndUpdatePendingSigner is a utility function to return the state of current signers readiness.
// It has absolutely no interaction with the database.
func FindAndUpdatePendingSigner(mail string, signersReady *[]bool, signers *[]entities.Signer) (ready bool) {
	// Find an update ready status
	for i, s := range *signers {
		if s.Email == mail {
			(*signersReady)[i] = true
			break
		}
	}

	// Check if everyone is ready
	for _, s := range *signersReady {
149
		if !s {
Loïck Bonniot's avatar
Loïck Bonniot committed
150 151 152 153 154 155 156
			return
		}
	}

	ready = true
	return
}