+3
-9
README.md
+3
-9
README.md
···
26
26
- ...but export only works with timestamps (sequence-based export and websocket-based export is not implemented)
27
27
- Validation of operations (hopefully it's foolproof enough - needs revising, strengthening... and nullification might not be as simple as I made it out to be?)
28
28
- Snapshot-based sync for quickly bringing replicas up to date
29
+
- An initial, not very well tested, version of the "authoritative import" that gradually brings in operations from the official plc.directory
29
30
30
31
## What's yet to be implemented
31
32
32
-
- Actually syncing with the authoritative source, plc.directory (I am in the middle of this, and I am not liking how many doubts I have about how to approach it)
33
+
- Actually syncing with the authoritative source, plc.directory (in progress - fetching from the official directory is mostly implemented as indicated above; submitting blockchain happenings to the official directory is yet to be implemented)
33
34
- Spam/abuse prevention (typical blockchains do this with transaction fees and some lesser known ones - e.g. Nano - use proof of work, but this is not a cryptocurrency and PoW is a bit ewww)
34
35
- A process for nodes to become validators. Unless everyone agreed that it's best if the set of validators is centralized. I mean, it's not worse than the current state of plc.directory while still allowing people to easily have their own local synced mirror through the magic of CometBFT...
35
36
- Testing, testing, testing, validation, validation, validation...
···
72
73
73
74
You need to install Go, in case the go.mod and multiple *.go files didn't make it obvious.
74
75
75
-
To run a single node, you want to use `startfresh.sh`. To run multiple nodes, there's `startfresh-testnet.sh` but note that you'll have to go into the config.toml of every node **except one**, and add
76
-
77
-
```
78
-
[plc]
79
-
laddr = ""
80
-
```
81
-
82
-
to prevent the PLC API server from coming up on those nodes (since by default it'd try listening on the same port that's already used by one of the other replicas, and fail to launch).
76
+
To run a single node, you want to use `startfresh.sh`. To run multiple nodes, there's `startfresh-testnet.sh`. Note that using the created config files, only the first node will serve the PLC API.
83
77
84
78
The PLC API server listens on `127.0.0.1:28080` by default and other fun facts you could learn from reading config.go. There are other ports that the server listens on, one that exposes the CometBFT RPC API, and also the P2P ports for communication between the nodes.
85
79
+32
-27
abciapp/execution.go
+32
-27
abciapp/execution.go
···
7
7
"time"
8
8
9
9
abcitypes "github.com/cometbft/cometbft/abci/types"
10
+
cbornode "github.com/ipfs/go-ipld-cbor"
10
11
"github.com/palantir/stacktrace"
11
12
"github.com/samber/lo"
12
-
"tangled.org/gbl08ma/didplcbft/store"
13
13
)
14
14
15
15
// InitChain implements [types.Application].
···
22
22
func (d *DIDPLCApplication) PrepareProposal(ctx context.Context, req *abcitypes.RequestPrepareProposal) (*abcitypes.ResponsePrepareProposal, error) {
23
23
defer d.tree.Rollback()
24
24
25
+
if req.Height == 2 {
26
+
tx := Transaction[SetAuthoritativePlcArguments]{
27
+
Action: TransactionActionSetAuthoritativePlc,
28
+
Arguments: SetAuthoritativePlcArguments{
29
+
PLCURL: "https://plc.directory",
30
+
RestartImport: true,
31
+
},
32
+
}
33
+
34
+
out, err := cbornode.DumpObject(tx)
35
+
if err != nil {
36
+
return nil, stacktrace.Propagate(err, "")
37
+
}
38
+
39
+
req.Txs = append(req.Txs, out)
40
+
}
41
+
25
42
st := time.Now()
26
43
acceptedTx := make([][]byte, 0, len(req.Txs))
27
44
toProcess := req.Txs
···
33
50
return nil, stacktrace.Propagate(err, "")
34
51
}
35
52
36
-
if result.isAuthoritativeImportTransaction {
37
-
// this type of transaction is not meant to appear in the mempool,
38
-
// but maybe it's not impossible that a non-compliant node could have gossiped it to us?
39
-
// (not sure if CometBFT checks transactions coming from other peers against CheckTx)
40
-
continue
41
-
}
42
-
43
53
if result.Code == 0 {
44
54
acceptedTx = append(acceptedTx, tx)
45
55
} else {
···
69
79
70
80
if err == nil && len(maybeTx) != 0 {
71
81
totalSize := lo.SumBy(acceptedTx, func(tx []byte) int { return len(tx) })
72
-
// 4K safety margin
82
+
// 4 KB safety margin
73
83
if totalSize+len(maybeTx) < int(req.MaxTxBytes)-4096 {
74
84
// we have space to fit the import transaction
75
85
···
94
104
return &abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_REJECT}, nil
95
105
}
96
106
97
-
if req.Height == 1 {
98
-
tree, err := d.MutableTree()
99
-
if err != nil {
100
-
return nil, stacktrace.Propagate(err, "")
101
-
}
102
-
103
-
err = store.Tree.SetAuthoritativePLC(tree, "https://plc.directory")
104
-
if err != nil {
105
-
return nil, stacktrace.Propagate(err, "")
106
-
}
107
-
}
108
-
109
107
// if we return early, ensure we don't use incomplete results where we haven't voted ACCEPT
110
108
d.lastProcessedProposalHash = nil
111
109
d.lastProcessedProposalExecTxResults = nil
···
119
117
120
118
txResults := make([]*processResult, len(req.Txs))
121
119
for i, tx := range req.Txs {
122
-
result, err := processTx(ctx, d.transactionProcessorDependencies(), tx, req.Time, true)
120
+
result, action, processor, err := beginProcessTx(tx)
123
121
if err != nil {
124
122
return nil, stacktrace.Propagate(err, "")
125
123
}
124
+
if result.Code == 0 {
125
+
if action == TransactionActionAuthoritativeImport && i != len(req.Txs)-1 {
126
+
// if an Authoritative Import transaction is present on the block, it must be the last one
127
+
return &abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_REJECT}, nil
128
+
}
129
+
130
+
result, err = finishProcessTx(ctx, d.transactionProcessorDependencies(), processor, tx, req.Time, true)
131
+
if err != nil {
132
+
return nil, stacktrace.Propagate(err, "")
133
+
}
134
+
}
135
+
126
136
// when preparing a proposal, invalid transactions should have been discarded
127
137
// so, if something doesn't succeed now, something has gone wrong and we should not vote in agreement of the proposal
128
138
if result.Code != 0 {
129
-
return &abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_REJECT}, nil
130
-
}
131
-
132
-
if result.isAuthoritativeImportTransaction && i != len(req.Txs)-1 {
133
-
// if an Authoritative Import transaction is present on the block, it must be the last one
134
139
return &abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_REJECT}, nil
135
140
}
136
141
+15
-7
abciapp/mempool.go
+15
-7
abciapp/mempool.go
···
10
10
11
11
// CheckTx implements [types.Application].
12
12
func (d *DIDPLCApplication) CheckTx(ctx context.Context, req *abcitypes.RequestCheckTx) (*abcitypes.ResponseCheckTx, error) {
13
-
result, err := processTx(ctx, d.transactionProcessorDependencies(), req.Tx, time.Now(), false)
13
+
result, action, processor, err := beginProcessTx(req.Tx)
14
14
if err != nil {
15
15
return nil, stacktrace.Propagate(err, "")
16
16
}
17
-
if result.isAuthoritativeImportTransaction {
18
-
// this type of transaction is meant to be included only by validator nodes
19
-
return &abcitypes.ResponseCheckTx{
20
-
Code: 4002,
21
-
Info: "AuthoritativeImport transactions can only be introduced by validator nodes",
22
-
}, nil
17
+
if result.Code == 0 {
18
+
if action == TransactionActionAuthoritativeImport {
19
+
// this type of transaction is meant to be included only by validator nodes
20
+
return &abcitypes.ResponseCheckTx{
21
+
Code: 4002,
22
+
Info: "AuthoritativeImport transactions can only be introduced by validator nodes",
23
+
}, nil
24
+
}
25
+
26
+
result, err = finishProcessTx(ctx, d.transactionProcessorDependencies(), processor, req.Tx, time.Now(), false)
27
+
if err != nil {
28
+
return nil, stacktrace.Propagate(err, "")
29
+
}
23
30
}
31
+
24
32
return &abcitypes.ResponseCheckTx{
25
33
Code: result.Code,
26
34
Data: result.Data,
+28
-10
abciapp/tx.go
+28
-10
abciapp/tx.go
···
83
83
}
84
84
85
85
type processResult struct {
86
-
isAuthoritativeImportTransaction bool
87
-
commitSideEffects []func()
86
+
commitSideEffects []func()
88
87
89
88
Code uint32
90
89
Data []byte
···
109
108
}
110
109
}
111
110
112
-
func processTx(ctx context.Context, deps TransactionProcessorDependencies, txBytes []byte, atTime time.Time, execute bool) (*processResult, error) {
111
+
func beginProcessTx(txBytes []byte) (*processResult, TransactionAction, TransactionProcessor, error) {
113
112
if !IsTransactionSanitized(txBytes) {
114
113
return &processResult{
115
114
Code: 4000,
116
115
Info: "Transaction bytes do not follow canonical serialization format",
117
-
}, nil
116
+
}, "", nil, nil
118
117
}
119
118
var v map[string]interface{}
120
119
err := cbornode.DecodeInto(txBytes, &v)
···
122
121
return &processResult{
123
122
Code: 4001,
124
123
Info: "Invalid transaction",
125
-
}, nil
124
+
}, "", nil, nil
126
125
}
127
126
actionInterface, ok := v["action"]
128
127
if !ok {
129
128
return &processResult{
130
129
Code: 4001,
131
130
Info: "Unknown transaction action",
132
-
}, nil
131
+
}, "", nil, nil
133
132
}
134
-
action, ok := actionInterface.(string)
133
+
actionString, ok := actionInterface.(string)
135
134
if !ok {
136
135
return &processResult{
137
136
Code: 4001,
138
137
Info: "Unknown transaction action",
139
-
}, nil
138
+
}, "", nil, nil
140
139
}
141
140
142
-
processor, ok := knownActions[TransactionAction(action)]
141
+
action := TransactionAction(actionString)
142
+
143
+
processor, ok := knownActions[action]
143
144
if !ok {
144
145
return &processResult{
145
146
Code: 4001,
146
147
Info: "Unknown transaction action",
147
-
}, nil
148
+
}, "", nil, nil
148
149
}
149
150
151
+
return &processResult{}, action, processor, nil
152
+
}
153
+
154
+
func finishProcessTx(ctx context.Context, deps TransactionProcessorDependencies, processor TransactionProcessor, txBytes []byte, atTime time.Time, execute bool) (*processResult, error) {
150
155
result, err := processor(ctx, deps, txBytes, atTime, execute)
151
156
return result, stacktrace.Propagate(err, "")
152
157
}
158
+
159
+
func processTx(ctx context.Context, deps TransactionProcessorDependencies, txBytes []byte, atTime time.Time, execute bool) (*processResult, error) {
160
+
result, _, processor, err := beginProcessTx(txBytes)
161
+
if err != nil {
162
+
return nil, stacktrace.Propagate(err, "")
163
+
}
164
+
if result.Code != 0 {
165
+
return result, nil
166
+
}
167
+
168
+
result, err = finishProcessTx(ctx, deps, processor, txBytes, atTime, execute)
169
+
return result, stacktrace.Propagate(err, "")
170
+
}
-1
abciapp/tx_import.go
-1
abciapp/tx_import.go
+4
startfresh-testnet.sh
+4
startfresh-testnet.sh
···
47
47
48
48
# Adjust P2P listen address
49
49
sed -i "s|^laddr = \"tcp://0.0.0.0:26656\"\$|laddr = \"tcp://$p2p_ip:26656\"|g" "testnet/node$i/config/config.toml"
50
+
51
+
if [ "$i" -ne 0 ]; then
52
+
echo -e "\n[plc]\nladdr = \"\"" >> "testnet/node$i/config/config.toml"
53
+
fi
50
54
done
51
55
52
56
# Configure rpc_servers for the last node (the one that will be started manually)