mirror of https://github.com/certusone/dc4bc.git
Merge branch 'master' into feat/bulletin-board
This commit is contained in:
commit
0eb587ca04
|
@ -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
137
README.md
|
@ -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
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
||||
}
|
|
@ -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))
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package config
|
||||
|
||||
const (
|
||||
// TODO: Move to machine level configs?
|
||||
ParticipantsMinCount = 3
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package dkg_commit_fsm
|
|
@ -0,0 +1 @@
|
|||
package dkg_deals_fsm
|
|
@ -0,0 +1 @@
|
|||
package dkg_proposal_fsm
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
1
go.mod
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue