···1+package abciapp
2+3+import (
4+ "bytes"
5+ "context"
6+ "slices"
7+ "time"
8+9+ abcitypes "github.com/cometbft/cometbft/abci/types"
10+ "github.com/palantir/stacktrace"
11+)
12+13+// InitChain implements [types.Application].
14+func (d *DIDPLCApplication) InitChain(context.Context, *abcitypes.RequestInitChain) (*abcitypes.ResponseInitChain, error) {
15+ // TODO
16+ return &abcitypes.ResponseInitChain{}, nil
17+}
18+19+// PrepareProposal implements [types.Application].
20+func (d *DIDPLCApplication) PrepareProposal(ctx context.Context, req *abcitypes.RequestPrepareProposal) (*abcitypes.ResponsePrepareProposal, error) {
21+ defer d.tree.Rollback()
22+23+ st := time.Now()
24+ acceptedTx := make([][]byte, 0, len(req.Txs))
25+ toProcess := req.Txs
26+ for {
27+ toTryNext := [][]byte{}
28+ for _, tx := range toProcess {
29+ result, err := processTx(ctx, d.plc, tx, req.Time, true)
30+ if err != nil {
31+ return nil, stacktrace.Propagate(err, "")
32+ }
33+34+ if result.Code == 0 {
35+ acceptedTx = append(acceptedTx, tx)
36+ } else {
37+ // if a transaction is invalid, it _might_ be because it depends on a transaction that's further up in the list
38+ // process it after all the others
39+ toTryNext = append(toTryNext, tx)
40+ }
41+ }
42+ if len(toProcess) == len(toTryNext) {
43+ // we made no progress in this iteration - all transactions left to process fail to do so
44+ // so they can't be depending on anything that would be included in this block, at this point
45+ // just continue while dropping the transactions that would never succeed in this block
46+ break
47+ }
48+ if time.Since(st) > 800*time.Millisecond {
49+ // this is taking too long, just continue with what's already in acceptedTx
50+ break
51+ }
52+ toProcess = toTryNext
53+ }
54+55+ return &abcitypes.ResponsePrepareProposal{Txs: acceptedTx}, nil
56+}
57+58+// ProcessProposal implements [types.Application].
59+func (d *DIDPLCApplication) ProcessProposal(ctx context.Context, req *abcitypes.RequestProcessProposal) (*abcitypes.ResponseProcessProposal, error) {
60+ // do not rollback tree in this method, in case the changes can be reused in FinalizeBlock
61+ if req.Height != d.tree.WorkingVersion() {
62+ // our tree went out of sync, this should never happen
63+ return &abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_REJECT}, nil
64+ }
65+66+ // if we return early, ensure we don't use incomplete results where we haven't voted ACCEPT
67+ d.lastProcessedProposalHash = nil
68+ d.lastProcessedProposalExecTxResults = nil
69+ defer func() {
70+ if d.lastProcessedProposalHash == nil {
71+ // we didn't vote ACCEPT
72+ // we could rollback only eventually on FinalizeBlock, but why wait - rollback now for safety
73+ d.tree.Rollback()
74+ }
75+ }()
76+77+ txResults := make([]*abcitypes.ExecTxResult, len(req.Txs))
78+ for i, tx := range req.Txs {
79+ result, err := processTx(ctx, d.plc, tx, req.Time, true)
80+ if err != nil {
81+ return nil, stacktrace.Propagate(err, "")
82+ }
83+ for _, c := range result.TreeChanges {
84+ _, err := d.tree.Set(c.Key, c.Value)
85+ if err != nil {
86+ return nil, stacktrace.Propagate(err, "")
87+ }
88+ }
89+ // when preparing a proposal, invalid transactions should have been discarded
90+ // so, if something doesn't succeed now, something has gone wrong and we should not vote in agreement of the proposal
91+ if result.Code != 0 {
92+ return &abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_REJECT}, nil
93+ }
94+95+ txResults[i] = &abcitypes.ExecTxResult{
96+ Code: result.Code,
97+ Data: result.Data,
98+ Log: result.Log,
99+ Info: result.Info,
100+ GasWanted: result.GasWanted,
101+ GasUsed: result.GasUsed,
102+ Events: result.Events,
103+ Codespace: result.Codespace,
104+ }
105+ }
106+107+ d.lastProcessedProposalHash = slices.Clone(req.Hash)
108+ d.lastProcessedProposalExecTxResults = txResults
109+110+ return &abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_ACCEPT}, nil
111+}
112+113+// ExtendVote implements [types.Application].
114+func (d *DIDPLCApplication) ExtendVote(context.Context, *abcitypes.RequestExtendVote) (*abcitypes.ResponseExtendVote, error) {
115+ // TODO
116+ return &abcitypes.ResponseExtendVote{}, nil
117+}
118+119+// VerifyVoteExtension implements [types.Application].
120+func (d *DIDPLCApplication) VerifyVoteExtension(context.Context, *abcitypes.RequestVerifyVoteExtension) (*abcitypes.ResponseVerifyVoteExtension, error) {
121+ // TODO
122+ return &abcitypes.ResponseVerifyVoteExtension{}, nil
123+}
124+125+// FinalizeBlock implements [types.Application].
126+func (d *DIDPLCApplication) FinalizeBlock(ctx context.Context, req *abcitypes.RequestFinalizeBlock) (*abcitypes.ResponseFinalizeBlock, error) {
127+ if bytes.Equal(req.Hash, d.lastProcessedProposalHash) && d.lastProcessedProposalExecTxResults != nil {
128+ // the block that was decided was the one we processed in ProcessProposal, and ProcessProposal processed successfully
129+ // reuse the uncommitted results
130+ return &abcitypes.ResponseFinalizeBlock{
131+ TxResults: d.lastProcessedProposalExecTxResults,
132+ AppHash: d.tree.WorkingHash(),
133+ }, nil
134+ }
135+ // a block other than the one we processed in ProcessProposal was decided
136+ // discard the current modified state, and process the decided block
137+ d.tree.Rollback()
138+139+ txResults := make([]*abcitypes.ExecTxResult, len(req.Txs))
140+ for i, tx := range req.Txs {
141+ result, err := processTx(ctx, d.plc, tx, req.Time, true)
142+ if err != nil {
143+ return nil, stacktrace.Propagate(err, "")
144+ }
145+ for _, c := range result.TreeChanges {
146+ _, err := d.tree.Set(c.Key, c.Value)
147+ if err != nil {
148+ return nil, stacktrace.Propagate(err, "")
149+ }
150+ }
151+ txResults[i] = &abcitypes.ExecTxResult{
152+ Code: result.Code,
153+ Data: result.Data,
154+ Log: result.Log,
155+ Info: result.Info,
156+ GasWanted: result.GasWanted,
157+ GasUsed: result.GasUsed,
158+ Events: result.Events,
159+ Codespace: result.Codespace,
160+ }
161+ }
162+163+ return &abcitypes.ResponseFinalizeBlock{
164+ TxResults: txResults,
165+ AppHash: d.tree.WorkingHash(),
166+ }, nil
167+}
168+169+// Commit implements [types.Application].
170+func (d *DIDPLCApplication) Commit(context.Context, *abcitypes.RequestCommit) (*abcitypes.ResponseCommit, error) {
171+ _, _, err := d.tree.SaveVersion()
172+ if err != nil {
173+ return nil, stacktrace.Propagate(err, "")
174+ }
175+176+ // TODO(later) consider whether we can set some RetainHeight in the response
177+ return &abcitypes.ResponseCommit{}, nil
178+}
···1+package abciapp
2+3+import (
4+ "context"
5+ "encoding/json"
6+ "time"
7+8+ "github.com/did-method-plc/go-didplc"
9+ "github.com/ipfs/go-cid"
10+ cbornode "github.com/ipfs/go-ipld-cbor"
11+ "github.com/palantir/stacktrace"
12+ "tangled.org/gbl08ma/didplcbft/plc"
13+)
14+15+type CreatePlcOpArguments struct {
16+ DID string `json:"did" refmt:"did"`
17+ Operation *didplc.OpEnum `refmt:"operation"`
18+}
19+20+func (CreatePlcOpArguments) ForAction() TransactionAction {
21+ return TransactionActionCreatePlcOp
22+}
23+24+func init() {
25+ cbornode.RegisterCborType(CreatePlcOpArguments{})
26+ cbornode.RegisterCborType(Transaction[CreatePlcOpArguments]{})
27+}
28+29+func processCreatePlcOpTx(ctx context.Context, p plc.PLC, txBytes []byte, atTime time.Time, execute bool) (*processResult, error) {
30+ tx, err := UnmarshalTransaction[CreatePlcOpArguments](txBytes)
31+ if err != nil {
32+ return &processResult{
33+ Code: 4000,
34+ Info: err.Error(),
35+ }, nil
36+ }
37+38+ // sadly didplc is really designed to unmarshal JSON, not CBOR
39+ // so JSON ends up being the lingua franca for operations inside our PLC implementation too
40+ // we also can't instance didplc.Operations directly from the CBOR unmarshaller (the MakeUnmarshalTransformFunc thing)
41+ // because the interface makes us lose data (it is not powerful enough to detect the type of a transaction, for instance)
42+ // so our PLC internals end up depending on OpEnum, too
43+ // the decision to use CBOR for the entire thing at the blockchain transaction level is:
44+ // - to make transactions more compact
45+ // - to have more of a canonical format for them (we specifically use the stable CBOR format already used by the PLC for signing)
46+47+ // there is one advantage to this approach: by ensuring we first unmarshal the operations into strongly defined types
48+ // (e.g. the OpEnum struct of the didplc package)
49+ // we avoid accepting malformed data like what happened in https://github.com/did-method-plc/did-method-plc/issues/71
50+ opBytes, err := json.Marshal(tx.Arguments.Operation)
51+ if err != nil {
52+ return nil, stacktrace.Propagate(err, "internal error")
53+ }
54+55+ var cid cid.Cid
56+ if execute {
57+ cid, err = p.ExecuteOperation(ctx, atTime, tx.Arguments.DID, opBytes)
58+ } else {
59+ err = p.ValidateOperation(ctx, plc.CommittedTreeVersion, atTime, tx.Arguments.DID, opBytes)
60+ }
61+ if err != nil {
62+ if code, ok := plc.InvalidOperationErrorCode(err); ok {
63+ return &processResult{
64+ Code: code,
65+ Info: err.Error(),
66+ }, nil
67+ }
68+ return nil, stacktrace.Propagate(err, "internal error")
69+ }
70+71+ return &processResult{
72+ TreeChanges: []treeChange{{
73+ Key: []byte(tx.Arguments.DID),
74+ Value: cid.Bytes(),
75+ }},
76+ Code: 0,
77+ }, nil
78+}
···1+#!/bin/sh
2+3+# Default to 4 nodes if no argument provided
4+NUM_NODES="${1:-4}"
5+6+# Validate input
7+if ! echo "$NUM_NODES" | grep -qE '^[0-9]+$'; then
8+ echo "Error: Number of nodes must be a positive integer"
9+ echo "Usage: $0 [number_of_nodes]"
10+ echo "Example: $0 7"
11+ exit 1
12+fi
13+14+if [ "$NUM_NODES" -lt 1 ]; then
15+ echo "Error: Number of nodes must be at least 1"
16+ exit 1
17+fi
18+19+echo "Starting testnet with $NUM_NODES nodes (preserving existing data)..."
20+21+# Check if testnet directory exists and has the expected number of nodes
22+if [ ! -d "testnet/node0" ]; then
23+ echo "Error: No existing testnet found. Run ./startfresh-testnet.sh first to create the testnet."
24+ exit 1
25+fi
26+27+# Count existing nodes
28+existing_nodes=0
29+for i in $(seq 0 99); do
30+ if [ -d "testnet/node$i" ]; then
31+ existing_nodes=$((existing_nodes + 1))
32+ else
33+ break
34+ fi
35+done
36+37+if [ "$NUM_NODES" -gt "$existing_nodes" ]; then
38+ echo "Error: Requested $NUM_NODES nodes but only $existing_nodes nodes exist in testnet/"
39+ echo "Run ./startfresh-testnet.sh $NUM_NODES to create additional nodes."
40+ exit 1
41+fi
42+43+echo "Found $existing_nodes existing nodes, starting first $NUM_NODES nodes..."
44+45+# Check if binary exists, build if needed
46+if [ ! -f "./didplcbft" ]; then
47+ echo "Binary not found, building didplcbft..."
48+ go build -trimpath
49+fi
50+51+# Array to store background process IDs
52+pids=""
53+54+# Cleanup function to kill all background processes
55+cleanup() {
56+ echo ""
57+ echo "Shutting down all nodes..."
58+59+ # Kill all background processes
60+ for pid in $pids; do
61+ if kill -0 "$pid" 2>/dev/null; then
62+ echo " Stopping node process $pid..."
63+ kill "$pid" 2>/dev/null
64+ fi
65+ done
66+67+ # Clean up temporary fifo files
68+ for i in $(seq 0 99); do
69+ rm -f "/tmp/didplcbft-node$i-stdout" "/tmp/didplcbft-node$i-stderr" 2>/dev/null
70+ done
71+72+ # Wait for all processes to terminate
73+ wait $pids 2>/dev/null
74+75+ echo "All nodes stopped."
76+ exit 0
77+}
78+79+# Set up signal traps
80+trap cleanup INT TERM EXIT
81+82+# Launch all nodes in parallel
83+echo "Launching $NUM_NODES nodes in parallel..."
84+85+for i in $(seq 0 $((NUM_NODES - 1))); do
86+ if [ -d "testnet/node$i" ]; then
87+ echo " Starting node$i..."
88+ mkfifo "/tmp/didplcbft-node$i-stdout" 2>/dev/null || true
89+ mkfifo "/tmp/didplcbft-node$i-stderr" 2>/dev/null || true
90+91+ # Start sed processes to prefix output
92+ sed "s/^/[node$i-stdout] /" < "/tmp/didplcbft-node$i-stdout" &
93+ sed "s/^/[node$i-stderr] /" < "/tmp/didplcbft-node$i-stderr" &
94+95+ # Start the didplcbft process with redirected output
96+ ./didplcbft --data-dir "testnet/node$i" > "/tmp/didplcbft-node$i-stdout" 2> "/tmp/didplcbft-node$i-stderr" &
97+ pid=$!
98+ pids="$pids $pid"
99+ echo " PID: $pid"
100+ else
101+ echo " Warning: node$i directory not found, skipping..."
102+ fi
103+done
104+105+echo ""
106+echo "All $NUM_NODES nodes are now running."
107+echo "Press Ctrl+C to stop all nodes."
108+echo ""
109+110+# Wait for all background processes
111+wait $pids
112+113+# If we reach here, all processes have terminated normally
114+echo "All nodes have terminated."
···1+#!/bin/sh
2+3+# Default to 4 nodes if no argument provided
4+NUM_NODES="${1:-4}"
5+6+# Validate input
7+if ! echo "$NUM_NODES" | grep -qE '^[0-9]+$'; then
8+ echo "Error: Number of nodes must be a positive integer"
9+ echo "Usage: $0 [number_of_nodes]"
10+ echo "Example: $0 7"
11+ exit 1
12+fi
13+14+if [ "$NUM_NODES" -lt 1 ]; then
15+ echo "Error: Number of nodes must be at least 1"
16+ exit 1
17+fi
18+19+echo "Setting up testnet with $NUM_NODES nodes..."
20+21+# Build the binary
22+echo "Building didplcbft binary..."
23+go build -trimpath
24+25+# Clean up existing testnet data
26+echo "Cleaning up existing testnet data..."
27+rm -r testnet/node*
28+29+# Generate testnet with specified number of nodes
30+echo "Generating testnet configuration for $NUM_NODES nodes..."
31+go run github.com/cometbft/cometbft/cmd/cometbft@v0.38.19 testnet --v "$((NUM_NODES - 1))" --n 1 --starting-ip-address 127.67.67.1 --config ./testnet/baseconfig.toml --o ./testnet
32+33+# Adjust RPC and P2P listen addresses for each node
34+echo "Configuring RPC and P2P addresses for $NUM_NODES nodes..."
35+36+for i in $(seq 0 $((NUM_NODES - 1))); do
37+ # Calculate RPC port (starting from 26100)
38+ rpc_port=$((26100 + i))
39+40+ # Calculate P2P IP address (127.67.67.1 + node_index)
41+ p2p_ip="127.67.67.$((1 + i))"
42+43+ echo " Configuring node$i (RPC: $rpc_port, P2P: $p2p_ip:26656)"
44+45+ # Adjust RPC listen address
46+ sed -i "s|^laddr = \"tcp://127.0.0.1:26657\"\$|laddr = \"tcp://127.0.0.1:$rpc_port\"|g" "testnet/node$i/config/config.toml"
47+48+ # Adjust P2P listen address
49+ sed -i "s|^laddr = \"tcp://0.0.0.0:26656\"\$|laddr = \"tcp://$p2p_ip:26656\"|g" "testnet/node$i/config/config.toml"
50+done
51+52+# Configure rpc_servers for the last node (the one that will be started manually)
53+last_node=$((NUM_NODES - 1))
54+echo "Configuring rpc_servers for node$last_node..."
55+56+# Build comma-separated list of RPC addresses for automatically started nodes
57+rpc_servers_list=""
58+for i in $(seq 0 $((NUM_NODES - 2))); do
59+ if [ -n "$rpc_servers_list" ]; then
60+ rpc_servers_list="$rpc_servers_list,"
61+ fi
62+ rpc_port=$((26100 + i))
63+ rpc_servers_list="${rpc_servers_list}tcp://127.0.0.1:$rpc_port"
64+done
65+66+# Replace empty rpc_servers configuration in the last node's config
67+echo " Setting rpc_servers = \"$rpc_servers_list\" for node$last_node"
68+sed -i "s|^rpc_servers = \"\"\$|rpc_servers = \"$rpc_servers_list\"|g" "testnet/node$last_node/config/config.toml"
69+70+# Enable state sync for the last node (the one that will be started manually)
71+echo " Enabling state sync for node$last_node"
72+sed -i '/\[statesync\]/,/enable = false/s/enable = false/enable = true/' "testnet/node$last_node/config/config.toml"
73+74+# Array to store background process IDs
75+pids=""
76+77+# Cleanup function to kill all background processes
78+cleanup() {
79+ echo ""
80+ echo "Shutting down all nodes..."
81+82+ # Kill all background processes
83+ for pid in $pids; do
84+ if kill -0 "$pid" 2>/dev/null; then
85+ echo " Stopping node process $pid..."
86+ kill "$pid" 2>/dev/null
87+ fi
88+ done
89+90+ # Clean up temporary fifo files
91+ for i in $(seq 0 99); do
92+ rm -f "/tmp/didplcbft-node$i-stdout" "/tmp/didplcbft-node$i-stderr" 2>/dev/null
93+ done
94+95+ # Wait for all processes to terminate
96+ wait $pids 2>/dev/null
97+98+ echo "All nodes stopped."
99+ exit 0
100+}
101+102+# Set up signal traps
103+trap cleanup INT TERM EXIT
104+105+# Launch all nodes except the last one (for testing later bringup)
106+nodes_to_start=$((NUM_NODES - 1))
107+echo "Launching $nodes_to_start nodes in parallel..."
108+109+for i in $(seq 0 $((nodes_to_start - 1))); do
110+ echo " Starting node$i..."
111+ mkfifo "/tmp/didplcbft-node$i-stdout" 2>/dev/null || true
112+ mkfifo "/tmp/didplcbft-node$i-stderr" 2>/dev/null || true
113+114+ # Start sed processes to prefix output
115+ sed "s/^/[node$i-stdout] /" < "/tmp/didplcbft-node$i-stdout" &
116+ sed "s/^/[node$i-stderr] /" < "/tmp/didplcbft-node$i-stderr" &
117+118+ # Start the didplcbft process with redirected output
119+ ./didplcbft --data-dir "testnet/node$i" > "/tmp/didplcbft-node$i-stdout" 2> "/tmp/didplcbft-node$i-stderr" &
120+ pid=$!
121+ pids="$pids $pid"
122+ echo " PID: $pid"
123+done
124+125+echo ""
126+echo "All $nodes_to_start nodes are now running."
127+echo "Note: Node $((NUM_NODES - 1)) is not started and can be launched later for testing."
128+echo "Press Ctrl+C to stop all running nodes."
129+echo ""
130+131+# Wait for all background processes
132+wait $pids
133+134+# If we reach here, all processes have terminated normally
135+echo "All nodes have terminated."