Merge branch 'master' into feat/bulletin-board

This commit is contained in:
programmer10110 2020-07-29 17:44:56 +03:00
commit 0eb587ca04
18 changed files with 1152 additions and 46 deletions

View File

@ -0,0 +1,71 @@
## DKG
### Conference call
It's presumed participants can use a separate secure communication channel (let's call it Conference Call) to establish initial parameters: the set of participants, their identities and public authentification keys, the nature and connection parameters of a bulletin board and so on.
### Participants
N participants, having a hot (connected to the network) node and a cold (airgapped) node. Participants all have two pair of keys: auth keys and encryption keys. PubAuthKey_i, PrivAuthKey_i, PubEncKey_i, PrivEncKey_i respectively Participant_i. Each participant also have a secret seed used to generate DKG messages.
Auth keys are stored on the hot node, encryption keys and a seed are stored on a cold node.
### Bulletin Board
The core communication/storage primitive for dc4bc is a bulletin board - a simple append-only log that can be accesed by all the participants and allows posting authentificated messages and polling for posted messages. We need BB to have two functions:
- post(message, signature)
- getMessages(offset = 0)
- returns a list of all messages posted after the first <offset> one
This allows us to establish communication primitives:
- broadcast(message) by Participant_i:
post(message, signature(message, PrivAuthKey_i))
- private_message(message, Participant_j):
encrypted_message = { "to" : Participant_j, "message": encrypt(message, PubEncKey_j)}
broadcast(encrypted_message)
Bulletin board can be constructed using a trusted centralized service a-la github/amazon, using public blockchain, or using a consensus between participants to establish a private blockchain. Anyway, it should be abstracted away in the client and signer both and easily switchable.
Bulletin board is only available on a hot node.
### Secure Channel
There is a secure comminication channel between a hot node and a cold node between each participant. We expect it to be a dead simple QR-code based asynchronous messaging protocol, but it can be something more complicated eventually, e.g. USB connection to the HSM. It's got two primitive functions:
- h2c_message(message) - send a message from hot node to cold node, returns message hash
- await_c2h_reply(hash(message)) - wait for reply from cold node
## DKG Process
1. Using a Conference Call, participants establish: the set of participants, public keys for authentfication and encryption, the nature and connection parameters of a bulletin board, step timeouts, threshold number.
2. Any participant broadc ast a DKG Startup Message, that contains the set of participants, and public keys for authentfication and encryption. Hash of that message later is used as a unique id of a DKG (used in messages to differentiate between multiple parallel DKGs if needed).
3. All participants broadcast their agreement to participate in this particular DKG within the agreed upon step timeout.
4. When all participants agree, every participant asks a cold node to publish a commit:
1. message_hash = h2c_message(<start DKG with DKG_hash xxx, number of participants X, threshold Y>)
2. broadcast(await_c2h_reply(message_hash))
5. When all participants publish a commit, every participant:
1. h2c_message(<all commits>)
2. message_hash = h2c_message(<send deals>)
3. deals = await_c2h_reply(message_hash)
4. for participant in participants:
1. direct_message(participant, deal[participant])
6. When a pariticipant has recieved all the deals:
1. They reconstruct the public key from the deals and broadcast it
7. If everyone broadcasts the same reconstructed public key, DKG completed successfully
If at any point something goes wrong (timeout reached, the deal is invalid, public key is not recinstucted equally, some of participants complain using a Conference Call) the DKG is aborted.
## Signature process
1. Any paricipant broadcast a message to sign upon.
2. All other participants signal their willingness to sign by broadcasting agreemen to sign that message.
3. When enough (>= threshold) participants broadcasted an agreement, every participant:
1. message_hash = h2c_message(<send a partial signature for message "message" for threshold public key "key">)
2. broadcast(await_c2h_reply(message_hash))
4. When enough (>= threshold) participants broadcasted a partial signature, threshold signature is reconstructed.
5. Someone broadcasts a partial signature.
If not enough participants signal their willingness to sign within a timeout or signal their rejection to sign, signature process is aborted.
We organize logic in the hot node as a set of simple state machines that change state only by external trigger, such as CLI command, message from cold node, or a new message on Bulletin Board. That way it can be easily tested and audited.

137
README.md
View File

@ -1,3 +1,84 @@
# dc4bc: distributed custody for the beacon chain
The goal of ths project is to make a simple, secure framework to generate and use threshold signatures for infrequent financial transactions over Ethereum 2.0 Beacon Chain (BLS on BLS12-381 curve). dc4bc only deals with key generation and the signature process with all the user-side logic offloaded to applications using dc4bc as a service or an API.
For a better key management, we presume that, when used in production, private encryption keys and threshold siganture related secrets reside in an airgapped machine or an HSM. For a better auditablity and testability, network protocol logic is implemented as a set of finite state machines that change state deterministically in response to a a stream of outside events.
The main and, for now, only network communication primitive we use is a shared bulletin board in form of an authetnicated append-only log. Different implementations of that log could be a shared file (for local development or testing), a trusted network service (e.g. Amazon S3 bucket), a federated blockchain between protocol participants or a public blockchain.
## Moving parts
### Participants
N participants, having a hot (connected to the network) node and a cold (airgapped) node. Participants all have two pair of keys: auth keys and encryption keys. PubAuthKey_i, PrivAuthKey_i, PubEncKey_i, PrivEncKey_i respectively Participant_i. Each participant also have a secret seed used to generate DKG messages.
Auth keys are stored on the hot node, encryption keys and a seed are stored on a cold node.
### Conference call
It's presumed participants can use a separate secure communication channel (let's call it Conference Call) to establish initial parameters: the set of participants, their identities and public authentification keys, the nature and connection parameters of a bulletin board and so on.
### Bulletin Board
The core communication/storage primitive for dc4bc is a bulletin board - a simple authenticated append-only log that can be accesed by all the participants and allows posting authentificated messages and polling for posted messages. We need BB to have two functions:
- post(message, signature)
- getMessages(offset = 0)
- returns a list of all messages posted after the first <offset> one
This allows us to establish communication primitives:
- broadcast(message) by Participant_i:
post(message, signature(message, PrivAuthKey_i))
- private_message(message, Participant_j):
encrypted_message = { "to" : Participant_j, "message": encrypt(message, PubEncKey_j)}
broadcast(encrypted_message)
Bulletin board can be constructed using a trusted centralized service a-la github/amazon, using public blockchain, or using a consensus between participants to establish a private blockchain. Anyway, it should be abstracted away in the client and signer both and easily switchable.
Bulletin board is only available on a hot node.
### Secure Channel
There is a secure comminication channel between a hot node and a cold node between each participant. We expect it to be a dead simple QR-code based asynchronous messaging protocol, but it can be something more complicated eventually, e.g. USB connection to the HSM. It's got two primitive functions:
- h2c_message(message) - send a message from hot node to cold node, returns message hash
- await_c2h_reply(hash(message)) - wait for reply from cold node
## DKG Process
1. Using a Conference Call, participants establish: the set of participants, public keys for authentfication and encryption, the nature and connection parameters of a bulletin board, step timeouts, threshold number.
2. Any participant broadcasts a DKG Startup Message, that contains the set of participants, and public keys for authentfication and encryption. Hash of that message later is used as a unique id of a DKG (used in messages to differentiate between multiple parallel DKGs if needed).
3. All participants broadcast their agreement to participate in this particular DKG within the agreed upon step timeout.
4. When all participants agree, every participant asks a cold node to publish a commit:
1. message_hash = h2c_message(<start DKG with DKG_hash xxx, number of participants X, threshold Y>)
2. broadcast(await_c2h_reply(message_hash))
5. When all participants publish a commit, every participant:
1. h2c_message(<all commits>)
2. message_hash = h2c_message(<send deals>)
3. deals = await_c2h_reply(message_hash)
4. for participant in participants:
1. direct_message(participant, deal[participant])
6. When a pariticipant has recieved all the deals:
1. They reconstruct the public key from the deals and broadcast it
7. If everyone broadcasts the same reconstructed public key, DKG completed successfully
If at any point something goes wrong (timeout reached, the deal is invalid, public key is not recinstucted equally, some of participants complain using a Conference Call) the DKG is aborted.
## Signature process
1. Any paricipant broadcast a message to sign upon.
2. All other participants signal their willingness to sign by broadcasting agreemen to sign that message.
3. When enough (>= threshold) participants broadcasted an agreement, every participant:
1. message_hash = h2c_message(<send a partial signature for message "message" for threshold public key "key">)
2. broadcast(await_c2h_reply(message_hash))
4. When enough (>= threshold) participants broadcasted a partial signature, threshold signature is reconstructed.
5. Someone broadcasts a partial signature.
If not enough participants signal their willingness to sign within a timeout or signal their rejection to sign, signature process is aborted.
We organize logic in the hot node as a set of simple state machines that change state only by external trigger, such as CLI command, message from cold node, or a new message on Bulletin Board. That way it can be easily tested and audited.
### Overview
Participants start with a pair of communication keys and aim to collectively produce a threshold BLS key pair. The three main components of the process are:
@ -40,52 +121,16 @@ The expected DKG workflow goes as follows:
8. When the participants decide to distribute profits, they get their partial signature from the airgapped machine and send it to the storage; after the required number of partial signatures is supplied, the collective signarute can be recovered.
### The Storage
The Storage will be a gRPC server written in Go and should implement the following interface:
* Method1()
* Method2()
The following libraries will be used for the required functionality:
1. Lib1
2. Lib2
3. Lib3
## Roadmap
### The Client
The Client will be a gRPC client written in Go, and should implement the following interface:
* Method1()
* Method2()
The following libraries will be used for the required functionality:
1. Lib1
2. Lib2
3. Lib3
### The Airgapped Machine
The Airgapped Machine will be written in Go and should implement the following interface:
* Method1()
* Method2()
The following libraries will be used for the required functionality:
1. Lib1
2. Lib2
3. Lib3
### Roadmap
1. The components as described above will be first mocked, implementing the specified interfaces.
2. The Storage will be implemented, using a suitable key-value database and an interface wrapping the DB operations.
3. The Client will be implemented, sending mocked messages to the storage and reading responses from it.
4. The DKG library for the Arcade project will be adopted for our needs (mostly refactoring, interface simplification and more unit tests).
5. The Airgapped Machine will be implemented in 4 steps:
* The DKG part will be implemented using Arcade's refactored codebase;
* This intermediate implementation will be used as a library by the Client to simplify testing;
* A Docker infrastructure will be implemented to automatically test the DKG on a local machine;
* The collective signing part will be implemented for the Airgapped Machine as a library call, with tests using the Docker infrastructure;
* The QR-code communication protocol will be implemented for the Airgapped Machine;
* The Airgapped Machine code will be removed from the client.
1. DKG prototype using a kyber lib for cryptography and modified dkglib from Arcade project for DKG, and a local file for append log.
2. Unit test harness and overall architecture documentation
3. Threshold signature FSM along with tests and docs
4. Integration test harness, happy path scenario, CI pipeline
5. Network-based append log
6. Airgapped machine communication via QR codes
7. Integration with the production DKG library
8. E2E test harness with full eth1->beacon chain scenario
9. Final clean-up and documanetation

View File

@ -0,0 +1,97 @@
package main
import (
"fmt"
"github.com/looplab/fsm"
)
func main() {
signatureProposalFSM := fsm.NewFSM(
"idle",
fsm.Events{
{Name: "proposal_spotted", Src: []string{"idle"}, Dst: "validate_proposal"},
{Name: "proposal_valid", Src: []string{"validate_proposal"}, Dst: "proposed"},
{Name: "proposal_invalid", Src: []string{"validate_proposal"}, Dst: "idle"},
{Name: "recieve_yay", Src: []string{"proposed"}, Dst: "process_yay"},
{Name: "receive_nay", Src: []string{"proposed"}, Dst: "process_nay"},
{Name: "send_nay", Src: []string{"proposed"}, Dst: "proposed"},
{Name: "send_yay", Src: []string{"proposed"}, Dst: "proposed"},
{Name: "enough_yays", Src: []string{"process_yay"}, Dst: "signing"},
{Name: "enough_nays", Src: []string{"process_nay"}, Dst: "abort"},
{Name: "not_enough_yays", Src: []string{"process_yay"}, Dst: "proposed"},
{Name: "not_enough_nays", Src: []string{"process_nay"}, Dst: "proposed"},
},
fsm.Callbacks{},
)
fmt.Print(fsm.Visualize(signatureProposalFSM))
signatureConstructFSM := fsm.NewFSM(
"idle",
fsm.Events{
{Name: "request_airgapped_sig", Src: []string{"signing"}, Dst: "signing"},
{Name: "transmit_airgapped_sig", Src: []string{"signing"}, Dst: "signing"},
{Name: "receive_sig", Src: []string{"signing"}, Dst: "process_sig"},
{Name: "enough_signature_shares", Src: []string{"process_sig"}, Dst: "reconstruct_signature"},
{Name: "not_enough_signature_shares", Src: []string{"process_sig"}, Dst: "signing"},
{Name: "signature_reconstucted", Src: []string{"reconstruct_signature"}, Dst: "publish_signature"},
{Name: "signature_published", Src: []string{"publish_signature"}, Dst: "fin"},
{Name: "failed_to_reconstuct_signature", Src: []string{"reconstruct_signature"}, Dst: "signing"},
},
fsm.Callbacks{},
)
fmt.Print(fsm.Visualize(signatureConstructFSM))
DkgProposeFSM := fsm.NewFSM(
"idle",
fsm.Events{
{Name: "proposal_spotted", Src: []string{"idle"}, Dst: "validate_proposal"},
{Name: "proposal_valid", Src: []string{"validate_proposal"}, Dst: "proposed"},
{Name: "proposal_invalid", Src: []string{"validate_proposal"}, Dst: "idle"},
{Name: "recieve_yay", Src: []string{"proposed"}, Dst: "process_yay"},
{Name: "receive_nay", Src: []string{"proposed"}, Dst: "abort"},
{Name: "send_nay", Src: []string{"proposed"}, Dst: "proposed"},
{Name: "send_yay", Src: []string{"proposed"}, Dst: "proposed"},
{Name: "not_enough_yays", Src: []string{"process_yay"}, Dst: "proposed"},
{Name: "all_yays", Src: []string{"process_yay"}, Dst: "dkg_commitments"},
{Name: "timeout", Src: []string{"proposed"}, Dst: "abort"},
},
fsm.Callbacks{},
)
fmt.Print(fsm.Visualize(DkgProposeFSM))
DkgCommitFSM := fsm.NewFSM(
"dkg_commitments",
fsm.Events{
{Name: "request_airgapped_commitment", Src: []string{"dkg_commitments"}, Dst: "dkg_commitments"},
{Name: "transmit_airgapped_commitment", Src: []string{"dkg_commitments"}, Dst: "dkg_commitments"},
{Name: "recieve_commitment", Src: []string{"dkg_commitments"}, Dst: "process_commitment"},
{Name: "invalid_commitment", Src: []string{"process_commitment"}, Dst: "abort"},
{Name: "all_commitments", Src: []string{"process_commitment"}, Dst: "dkg_deals"},
{Name: "not_enough_commitments", Src: []string{"process_commitment"}, Dst: "dkg_commitments"},
{Name: "timeout", Src: []string{"dkg_commitments"}, Dst: "abort"},
},
fsm.Callbacks{},
)
fmt.Print(fsm.Visualize(DkgCommitFSM))
DkgDealsFSM := fsm.NewFSM(
"dkg_deals",
fsm.Events{
{Name: "pass_commitements_and_request_airgapped_deals", Src: []string{"dkg_deals"}, Dst: "dkg_deals"},
{Name: "transmit_airgapped_deals", Src: []string{"dkg_deals"}, Dst: "dkg_deals"},
{Name: "transmit_airgapped_error", Src: []string{"dkg_deals"}, Dst: "abort"},
{Name: "recieve_deal", Src: []string{"dkg_deals"}, Dst: "process_deal"},
{Name: "not_my_deal", Src: []string{"process_deal"}, Dst: "dkg_deals"},
{Name: "invalid_deal", Src: []string{"process_deal"}, Dst: "abort"},
{Name: "enough_deals", Src: []string{"process_deal"}, Dst: "dkg_construct_tss"},
{Name: "not_enough_deals", Src: []string{"process_deal"}, Dst: "dkg_deals"},
{Name: "pass_deals_and_request_airgapped_public_key", Src: []string{"dkg_construct_tss"}, Dst: "dkg_construct_tss"},
{Name: "transmit_airgapped_public_key", Src: []string{"dkg_construct_tss"}, Dst: "fin"},
{Name: "transmit_airgapped_error", Src: []string{"dkg_construct_tss"}, Dst: "abort"},
{Name: "timeout", Src: []string{"dkg_deals"}, Dst: "abort"},
},
fsm.Callbacks{},
)
fmt.Print(fsm.Visualize(DkgDealsFSM))
}

34
fsm/cmd/test/test.go Normal file
View File

@ -0,0 +1,34 @@
package main
import (
"github.com/p2p-org/dc4bc/fsm/state_machines"
"github.com/p2p-org/dc4bc/fsm/types/requests"
"log"
)
func main() {
fsmMachine, err := state_machines.New([]byte{})
log.Println(fsmMachine, err)
resp, dump, err := fsmMachine.Do(
"proposal_init",
"d8a928b2043db77e340b523547bf16cb4aa483f0645fe0a290ed1f20aab76257",
requests.ProposalParticipantsListRequest{
{
"John Doe",
[]byte("pubkey123123"),
},
{
"Crypto Billy",
[]byte("pubkey456456"),
},
{
"Matt",
[]byte("pubkey789789"),
},
},
)
log.Println("Response", resp)
log.Println("Err", err)
log.Println("Dump", string(dump))
}

6
fsm/config/config.go Normal file
View File

@ -0,0 +1,6 @@
package config
const (
// TODO: Move to machine level configs?
ParticipantsMinCount = 3
)

308
fsm/fsm/fsm.go Normal file
View File

@ -0,0 +1,308 @@
package fsm
import (
"errors"
"sync"
)
//
// fsmInstance, err := fsm.New(scope)
// if err != nil {
// log.Println(err)
// return
// }
//
// fsmInstance.Do(event, args)
//
// Temporary global finish state for deprecating operations
const (
StateGlobalIdle = "__idle"
StateGlobalDone = "__done"
)
// FSMResponse returns result for processing with client events
type FSMResponse struct {
// Returns machine execution result state
State string
// Must be cast, according to mapper event_name->response_type
Data interface{}
}
type FSM struct {
name string
initialState string
currentState string
// May be mapping must require pair source + event?
transitions map[trKey]*trEvent
callbacks Callbacks
initialEvent string
// Finish states, for switch machine or fin,
// These states cannot be linked as SrcState in this machine
finStates map[string]bool
// stateMu guards access to the currentState state.
stateMu sync.RWMutex
// eventMu guards access to State() and Transition().
eventMu sync.Mutex
}
// Transition key source + dst
type trKey struct {
source string
event string
}
// Transition lightweight event description
type trEvent struct {
dstState string
isInternal bool
}
type EventDesc struct {
Name string
SrcState []string
// Dst state changes after callback
DstState string
// Internal events, cannot be emitted from external call
IsInternal bool
}
type Callback func(event string, args ...interface{}) (interface{}, error)
type Callbacks map[string]Callback
// TODO: Exports
func MustNewFSM(name, initial string, events []EventDesc, callbacks map[string]Callback) *FSM {
// Add validation, chains building
if name == "" {
panic("name cannot be empty")
}
if initial == "" {
panic("initialState state cannot be empty")
}
// to remove
if len(events) == 0 {
panic("cannot init fsm with empty events")
}
f := &FSM{
name: name,
currentState: initial,
initialState: initial,
transitions: make(map[trKey]*trEvent),
finStates: make(map[string]bool),
callbacks: make(map[string]Callback),
}
allEvents := make(map[string]bool)
// Required for find finStates
allSources := make(map[string]bool)
allStates := make(map[string]bool)
// Validate events
for _, event := range events {
if event.Name == "" {
panic("cannot init empty event")
}
// TODO: Check transition when all events added
if len(event.SrcState) == 0 {
panic("event must have min one source available state")
}
if event.DstState == "" {
panic("event dest cannot be empty, use StateGlobalDone for finish or external state")
}
if _, ok := allEvents[event.Name]; ok {
panic("duplicate event")
}
allEvents[event.Name] = true
allStates[event.DstState] = true
for _, sourceState := range event.SrcState {
tKey := trKey{
sourceState,
event.Name,
}
if sourceState == StateGlobalDone {
panic("StateGlobalDone cannot set as source state")
}
if _, ok := f.transitions[tKey]; ok {
panic("duplicate dst for pair `source + event`")
}
f.transitions[tKey] = &trEvent{event.DstState, event.IsInternal}
// For using provider, event must use with IsGlobal = true
if sourceState == initial {
if f.initialEvent != "" {
panic("machine entry event already exist")
}
f.initialEvent = event.Name
}
allSources[sourceState] = true
}
}
if len(allStates) < 2 {
panic("machine must contain at least two states")
}
// Validate callbacks
for event, callback := range callbacks {
if event == "" {
panic("callback name cannot be empty")
}
if _, ok := allEvents[event]; !ok {
panic("callback has no event")
}
f.callbacks[event] = callback
}
for state := range allStates {
if state == StateGlobalIdle {
continue
}
// Exit states cannot be a source in this machine
if _, exists := allSources[state]; !exists || state == StateGlobalDone {
f.finStates[state] = true
}
}
if len(f.finStates) == 0 {
panic("cannot initialize machine without final states")
}
return f
}
func (f *FSM) Do(event string, args ...interface{}) (resp *FSMResponse, err error) {
f.eventMu.Lock()
defer f.eventMu.Unlock()
trEvent, ok := f.transitions[trKey{f.currentState, event}]
if !ok {
return nil, errors.New("cannot execute event for this state")
}
if trEvent.isInternal {
return nil, errors.New("event is internal")
}
resp = &FSMResponse{
State: f.State(),
}
if callback, ok := f.callbacks[event]; ok {
resp.Data, err = callback(event, args...)
// Do not try change state on error
if err != nil {
return resp, err
}
}
err = f.setState(event)
return
}
// State returns the currentState state of the FSM.
func (f *FSM) State() string {
f.stateMu.RLock()
defer f.stateMu.RUnlock()
return f.currentState
}
// setState allows the user to move to the given state from currentState state.
// The call does not trigger any callbacks, if defined.
func (f *FSM) setState(event string) error {
f.stateMu.Lock()
defer f.stateMu.Unlock()
trEvent, ok := f.transitions[trKey{f.currentState, event}]
if !ok {
return errors.New("cannot change state")
}
f.currentState = trEvent.dstState
return nil
}
func (f *FSM) Name() string {
return f.name
}
func (f *FSM) InitialState() string {
return f.initialState
}
// Check entry event for available emitting as global entry event
func (f *FSM) GlobalInitialEvent() (event string) {
if initialEvent, exists := f.transitions[trKey{StateGlobalIdle, f.initialEvent}]; exists {
if !initialEvent.isInternal {
event = f.initialEvent
}
}
return
}
func (f *FSM) EntryEvent() (event string) {
if entryEvent, exists := f.transitions[trKey{f.initialState, f.initialEvent}]; exists {
if !entryEvent.isInternal {
event = f.initialEvent
}
}
return
}
func (f *FSM) EventsList() (events []string) {
if len(f.transitions) > 0 {
for trKey, trEvent := range f.transitions {
if !trEvent.isInternal {
events = append(events, trKey.event)
}
}
}
return
}
func (f *FSM) StatesList() (states []string) {
allStates := map[string]bool{}
if len(f.transitions) > 0 {
for trKey, _ := range f.transitions {
allStates[trKey.source] = true
}
}
if len(allStates) > 0 {
for state := range allStates {
states = append(states, state)
}
}
return
}
func (f *FSM) IsFinState(state string) bool {
_, exists := f.finStates[state]
return exists
}

150
fsm/fsm_pool/fsm_pool.go Normal file
View File

@ -0,0 +1,150 @@
package fsm_pool
import (
"errors"
"github.com/p2p-org/dc4bc/fsm/fsm"
)
type IStateMachine interface {
// Returns machine state from scope dump
// For nil argument returns fsm with process initiation
// Get() IStateMachine
Name() string
InitialState() string
// Process event
Do(event string, args ...interface{}) (*fsm.FSMResponse, error)
GlobalInitialEvent() string
EventsList() []string
StatesList() []string
IsFinState(state string) bool
}
type FSMMapper map[string]IStateMachine
type FSMRouteMapper map[string]string
type FSMPoolProvider struct {
fsmInitialEvent string
// Pool mapper by names
mapper FSMMapper
events FSMRouteMapper
states FSMRouteMapper
}
func Init(machines ...IStateMachine) *FSMPoolProvider {
if len(machines) == 0 {
panic("cannot initialize empty pool")
}
p := &FSMPoolProvider{
mapper: make(FSMMapper),
events: make(FSMRouteMapper),
states: make(FSMRouteMapper),
}
allInitStatesMap := make(map[string]string)
// Fill up mapper
for _, machine := range machines {
if machine == nil {
panic("machine not initialized, got nil")
}
machineName := machine.Name()
if machineName == "" {
panic("machine name cannot be empty")
}
if _, exists := p.mapper[machineName]; exists {
panic("duplicate machine name")
}
allInitStatesMap[machine.InitialState()] = machineName
machineEvents := machine.EventsList()
for _, event := range machineEvents {
if _, exists := p.events[event]; exists {
panic("duplicate public event")
}
p.events[event] = machineName
}
// Setup entry event for machines pool if available
if initialEvent := machine.GlobalInitialEvent(); initialEvent != "" {
if p.fsmInitialEvent != "" {
panic("duplicate entry event initialization")
}
p.fsmInitialEvent = initialEvent
}
p.mapper[machineName] = machine
}
// Second iteration, all initial states filled up
// Fill up states with initial and exit states checking
for _, machine := range machines {
machineName := machine.Name()
machineStates := machine.StatesList()
for _, state := range machineStates {
if machine.IsFinState(state) {
// If state is initial for another machine,
if initMachineName, exists := allInitStatesMap[state]; exists {
p.states[allInitStatesMap[state]] = initMachineName
continue
}
}
if name, exists := p.states[state]; exists && name != machineName {
panic("duplicate state for machines")
}
p.states[state] = machineName
}
}
if p.fsmInitialEvent == "" {
panic("machines pool entry event not set")
}
return p
}
func (p *FSMPoolProvider) EntryPointMachine() (IStateMachine, error) {
// StateGlobalIdle
// TODO: Short code
entryStateMachineName := p.events[p.fsmInitialEvent]
machine, exists := p.mapper[entryStateMachineName]
if !exists || machine == nil {
return nil, errors.New("cannot init machine with entry point")
}
return machine, nil
}
func (p *FSMPoolProvider) MachineByEvent(event string) (IStateMachine, error) {
eventMachineName := p.events[event]
machine, exists := p.mapper[eventMachineName]
if !exists || machine == nil {
return nil, errors.New("cannot init machine for event")
}
return machine, nil
}
func (p *FSMPoolProvider) MachineByState(state string) (IStateMachine, error) {
eventMachineName := p.states[state]
machine, exists := p.mapper[eventMachineName]
if !exists || machine == nil {
return nil, errors.New("cannot init machine for state")
}
return machine, nil
}

View File

@ -0,0 +1 @@
package dkg_commit_fsm

View File

@ -0,0 +1 @@
package dkg_deals_fsm

View File

@ -0,0 +1 @@
package dkg_proposal_fsm

View File

@ -0,0 +1,13 @@
package internal
type MachineStatePayload struct {
ProposalPayload ProposalConfirmationPrivateQuorum
SigningPayload map[string]interface{}
}
// Using combine response for modify data with chain
// User value or pointer? How about memory state?
type MachineCombinedResponse struct {
Response interface{}
Payload *MachineStatePayload
}

View File

@ -0,0 +1,17 @@
package internal
import "time"
type ProposalParticipantPrivate struct {
// Public title for address, such as name, nickname, organization
Title string
PublicKey []byte
// For validation user confirmation: sign(InvitationSecret, PublicKey) => user
InvitationSecret string
ConfirmedAt *time.Time
}
// Unique alias for map iteration - Public Key Fingerprint
// Excludes array merge and rotate operations
type ProposalConfirmationPrivateQuorum map[string]ProposalParticipantPrivate

View File

@ -0,0 +1,98 @@
package state_machines
import (
"encoding/json"
"errors"
"github.com/p2p-org/dc4bc/fsm/fsm"
"github.com/p2p-org/dc4bc/fsm/fsm_pool"
"github.com/p2p-org/dc4bc/fsm/state_machines/internal"
"github.com/p2p-org/dc4bc/fsm/state_machines/signature_construct_fsm"
"github.com/p2p-org/dc4bc/fsm/state_machines/signature_proposal_fsm"
)
// Is machine state scope dump will be locked?
type FSMDump struct {
Id string
State string
Payload internal.MachineStatePayload
}
type FSMInstance struct {
machine fsm_pool.IStateMachine
dump *FSMDump
}
var (
fsmPoolProvider *fsm_pool.FSMPoolProvider
)
func init() {
fsmPoolProvider = fsm_pool.Init(
signature_proposal_fsm.New(),
signature_construct_fsm.New(),
)
}
func New(data []byte) (*FSMInstance, error) {
var err error
i := &FSMInstance{}
if len(data) == 0 {
i.InitDump()
i.machine, err = fsmPoolProvider.EntryPointMachine()
return i, err // Create machine
}
err = i.dump.Unmarshal(data)
if err != nil {
return nil, errors.New("cannot read machine dump")
}
i.machine, err = fsmPoolProvider.MachineByState(i.dump.State)
return i, err
}
func (i *FSMInstance) Do(event string, args ...interface{}) (*fsm.FSMResponse, []byte, error) {
// Provide payload as first argument ever
result, err := i.machine.Do(event, append([]interface{}{i.dump.Payload}, args...)...)
// On route errors result will be nil
if result != nil {
// Proxying combined response, separate payload and data
if result.Data != nil {
if r, ok := result.Data.(internal.MachineCombinedResponse); ok {
i.dump.Payload = *r.Payload
result.Data = r.Response
} else {
return nil, []byte{}, errors.New("cannot cast callback response")
}
}
i.dump.State = result.State
}
dump, dumpErr := i.dump.Marshal()
if dumpErr != nil {
return result, []byte{}, err
}
return result, dump, err
}
func (i *FSMInstance) InitDump() {
if i.dump == nil {
i.dump = &FSMDump{
State: fsm.StateGlobalIdle,
}
}
}
// TODO: Add encryption
func (d *FSMDump) Marshal() ([]byte, error) {
return json.Marshal(d)
}
// TODO: Add decryption
func (d *FSMDump) Unmarshal(data []byte) error {
return json.Unmarshal(data, d)
}

View File

@ -0,0 +1,39 @@
package signature_construct_fsm
import (
"github.com/p2p-org/dc4bc/fsm/fsm"
"github.com/p2p-org/dc4bc/fsm/fsm_pool"
)
const (
fsmName = "signature_construct_fsm"
stateConstructorEntryPoint = "process_sig"
awaitConstructor = "validate_process_sig" // waiting participants
eventInitSignatureConstructor = "process_sig_init"
eventInitSignatureFinishTmp = "process_sig_fin"
)
type SignatureConstructFSM struct {
*fsm.FSM
}
func New() fsm_pool.IStateMachine {
machine := &SignatureConstructFSM{}
machine.FSM = fsm.MustNewFSM(
fsmName,
stateConstructorEntryPoint,
[]fsm.EventDesc{
// {Name: "", SrcState: []string{""}, DstState: ""},
// Init
{Name: eventInitSignatureConstructor, SrcState: []string{stateConstructorEntryPoint}, DstState: awaitConstructor},
{Name: eventInitSignatureFinishTmp, SrcState: []string{awaitConstructor}, DstState: "dkg_proposal_fsm"},
},
fsm.Callbacks{},
)
return machine
}

View File

@ -0,0 +1,98 @@
package signature_proposal_fsm
import (
"errors"
"github.com/p2p-org/dc4bc/fsm/state_machines/internal"
"github.com/p2p-org/dc4bc/fsm/types/requests"
"github.com/p2p-org/dc4bc/fsm/types/responses"
"log"
)
// init -> awaitingConfirmations
// args: payload, signing id, participants list
func (s *SignatureProposalFSM) actionInitProposal(event string, args ...interface{}) (response interface{}, err error) {
var payload internal.MachineStatePayload
// Init proposal
log.Println("I'm actionInitProposal")
if len(args) < 3 {
err = errors.New("payload and signing id required and participants list required")
return
}
if len(args) > 3 {
err = errors.New("too many arguments")
return
}
payload, ok := args[0].(internal.MachineStatePayload)
if !ok {
err = errors.New("cannot cast payload")
return
}
signingId, ok := args[1].(string)
if !ok {
err = errors.New("cannot cast signing id, awaiting string value")
return
}
if len(signingId) < signingIdLen {
err = errors.New("signing id to short ")
return
}
request, ok := args[2].(requests.ProposalParticipantsListRequest)
if !ok {
err = errors.New("cannot cast participants list")
return
}
if err = request.Validate(); err != nil {
return
}
payload.ProposalPayload = make(internal.ProposalConfirmationPrivateQuorum)
for _, participant := range request {
participantId := createFingerprint(&participant.PublicKey)
secret, err := generateRandomString(32)
if err != nil {
return nil, errors.New("cannot generateRandomString")
}
payload.ProposalPayload[participantId] = internal.ProposalParticipantPrivate{
Title: participant.Title,
PublicKey: participant.PublicKey,
InvitationSecret: secret,
ConfirmedAt: nil,
}
}
/*s.state = &fsm_pool.FSMachine{
Id: signingId,
State: stateAwaitProposalConfirmation,
}
s.state.Payload.ProposalPayload = &privateParticipantsList*/
return internal.MachineCombinedResponse{
Response: responses.ProposalParticipantInvitationsResponse{},
Payload: &payload,
}, nil
}
//
func (s *SignatureProposalFSM) actionConfirmProposalByParticipant(event string, args ...interface{}) (response interface{}, err error) {
log.Println("I'm actionConfirmProposalByParticipant")
return
}
func (s *SignatureProposalFSM) actionDeclineProposalByParticipant(event string, args ...interface{}) (response interface{}, err error) {
log.Println("I'm actionDeclineProposalByParticipant")
return
}
func (s *SignatureProposalFSM) actionValidateProposal(event string, args ...interface{}) (response interface{}, err error) {
log.Println("I'm actionValidateProposal")
return
}

View File

@ -0,0 +1,58 @@
package signature_proposal_fsm
import (
"crypto/sha256"
"encoding/base64"
"github.com/p2p-org/dc4bc/fsm/state_machines/internal"
"github.com/p2p-org/dc4bc/fsm/types/responses"
"math/rand"
)
// Request and response mutators
func ProposalParticipantsQuorumToResponse(list *internal.ProposalConfirmationPrivateQuorum) responses.ProposalParticipantInvitationsResponse {
var response responses.ProposalParticipantInvitationsResponse
for quorumId, parcipant := range *list {
response = append(response, &responses.ProposalParticipantInvitationEntryResponse{
Title: parcipant.Title,
PubKeyFingerprint: quorumId,
// TODO: Add encryption
EncryptedInvitation: parcipant.InvitationSecret,
})
}
return response
}
// Common functions
func createFingerprint(data *[]byte) string {
hash := sha256.Sum256(*data)
return base64.StdEncoding.EncodeToString(hash[:])
}
// https://blog.questionable.services/article/generating-secure-random-numbers-crypto-rand/
// GenerateRandomBytes returns securely generated random bytes.
// It will return an error if the system's secure random
// number generator fails to function correctly, in which
// case the caller should not continue.
func generateRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
// Note that err == nil only if we read len(b) bytes.
if err != nil {
return nil, err
}
return b, nil
}
// GenerateRandomString returns a URL-safe, base64 encoded
// securely generated random string.
// It will return an error if the system's secure random
// number generator fails to function correctly, in which
// case the caller should not continue.
func generateRandomString(s int) (string, error) {
b, err := generateRandomBytes(s)
return base64.URLEncoding.EncodeToString(b), err
}

View File

@ -0,0 +1,68 @@
package signature_proposal_fsm
import (
"github.com/p2p-org/dc4bc/fsm/fsm"
"github.com/p2p-org/dc4bc/fsm/fsm_pool"
)
const (
fsmName = "signature_proposal_fsm"
signingIdLen = 32
stateAwaitProposalConfirmation = "validate_proposal" // waiting participants
stateValidationCanceledByParticipant = "validation_canceled_by_participant"
stateValidationCanceledByTimeout = "validation_canceled_by_timeout"
stateProposed = "proposed"
eventInitProposal = "proposal_init"
eventConfirmProposal = "proposal_confirm_by_participant"
eventDeclineProposal = "proposal_decline_by_participant"
eventValidateProposal = "proposal_validate"
eventSetProposalValidated = "proposal_set_validated"
eventSetValidationCanceledByTimeout = "proposal_canceled_timeout"
eventSwitchProposedToSigning = "switch_state_to_signing"
)
type SignatureProposalFSM struct {
*fsm.FSM
}
func New() fsm_pool.IStateMachine {
machine := &SignatureProposalFSM{}
machine.FSM = fsm.MustNewFSM(
fsmName,
fsm.StateGlobalIdle,
[]fsm.EventDesc{
// {Name: "", SrcState: []string{""}, DstState: ""},
// Init
{Name: eventInitProposal, SrcState: []string{fsm.StateGlobalIdle}, DstState: stateAwaitProposalConfirmation},
// Validate by participants
{Name: eventConfirmProposal, SrcState: []string{stateAwaitProposalConfirmation}, DstState: stateAwaitProposalConfirmation},
// Is decline event should auto change state to default, or it process will initiated by client (external emit)?
// Now set for external emitting.
{Name: eventDeclineProposal, SrcState: []string{stateAwaitProposalConfirmation}, DstState: stateValidationCanceledByParticipant},
{Name: eventValidateProposal, SrcState: []string{stateAwaitProposalConfirmation}, DstState: stateAwaitProposalConfirmation},
// eventProposalValidate internal or from client?
// yay
// Exit point
{Name: eventSetProposalValidated, SrcState: []string{stateAwaitProposalConfirmation}, DstState: "process_sig", IsInternal: true},
// nan
{Name: eventSetValidationCanceledByTimeout, SrcState: []string{stateAwaitProposalConfirmation}, DstState: stateValidationCanceledByTimeout, IsInternal: true},
},
fsm.Callbacks{
eventInitProposal: machine.actionInitProposal,
eventConfirmProposal: machine.actionConfirmProposalByParticipant,
eventDeclineProposal: machine.actionDeclineProposalByParticipant,
eventValidateProposal: machine.actionValidateProposal,
},
)
return machine
}

1
go.mod
View File

@ -5,6 +5,7 @@ go 1.13
require (
github.com/google/uuid v1.1.1
github.com/juju/fslock v0.0.0-20160525022230-4d5c94c67b4b
github.com/looplab/fsm v0.1.0
github.com/makiuchi-d/gozxing v0.0.0-20190830103442-eaff64b1ceb7
github.com/mattn/go-gtk v0.0.0-20191030024613-af2e013261f5
github.com/mattn/go-pointer v0.0.0-20190911064623-a0a44394634f // indirect