···8899 abcitypes "github.com/cometbft/cometbft/abci/types"
1010 "github.com/palantir/stacktrace"
1111+ "github.com/samber/lo"
1112)
12131314// InitChain implements [types.Application].
···1920// PrepareProposal implements [types.Application].
2021func (d *DIDPLCApplication) PrepareProposal(ctx context.Context, req *abcitypes.RequestPrepareProposal) (*abcitypes.ResponsePrepareProposal, error) {
2122 defer d.tree.Rollback()
2323+2424+ deps := TransactionProcessorDependencies{
2525+ plc: d.plc,
2626+ tree: d,
2727+ }
22282329 st := time.Now()
2430 acceptedTx := make([][]byte, 0, len(req.Txs))
···2632 for {
2733 toTryNext := [][]byte{}
2834 for _, tx := range toProcess {
2929- result, err := processTx(ctx, d.plc, tx, req.Time, true)
3535+ result, err := processTx(ctx, deps, tx, req.Time, true)
3036 if err != nil {
3137 return nil, stacktrace.Propagate(err, "")
3238 }
33394040+ if result.IsAuthoritativeImportTransaction {
4141+ // this type of transaction is not meant to appear in the mempool,
4242+ // but maybe it's not impossible that a non-compliant node could have gossiped it to us?
4343+ // (not sure if CometBFT checks transactions coming from other peers against CheckTx)
4444+ continue
4545+ }
4646+3447 if result.Code == 0 {
3548 acceptedTx = append(acceptedTx, tx)
3649 } else {
···3952 toTryNext = append(toTryNext, tx)
4053 }
4154 }
4242- if len(toProcess) == len(toTryNext) {
5555+ if len(toProcess) >= len(toTryNext) {
4356 // we made no progress in this iteration - all transactions left to process fail to do so
4457 // so they can't be depending on anything that would be included in this block, at this point
4558 // just continue while dropping the transactions that would never succeed in this block
···5265 toProcess = toTryNext
5366 }
54676868+ totalSize := lo.SumBy(acceptedTx, func(tx []byte) int { return len(tx) })
6969+ if totalSize < int(req.MaxTxBytes)-4096 {
7070+ // we have space to fit an import transaction
7171+ // TODO
7272+ }
7373+5574 return &abcitypes.ResponsePrepareProposal{Txs: acceptedTx}, nil
5675}
5776···7493 }
7594 }()
76959696+ deps := TransactionProcessorDependencies{
9797+ plc: d.plc,
9898+ tree: d,
9999+ }
100100+77101 txResults := make([]*abcitypes.ExecTxResult, len(req.Txs))
78102 for i, tx := range req.Txs {
7979- result, err := processTx(ctx, d.plc, tx, req.Time, true)
103103+ result, err := processTx(ctx, deps, tx, req.Time, true)
80104 if err != nil {
81105 return nil, stacktrace.Propagate(err, "")
82106 }
···86110 return &abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_REJECT}, nil
87111 }
88112113113+ if result.IsAuthoritativeImportTransaction && i != len(req.Txs)-1 {
114114+ // if an Authoritative Import transaction is present on the block, it must be the last one
115115+ return &abcitypes.ResponseProcessProposal{Status: abcitypes.ResponseProcessProposal_REJECT}, nil
116116+ }
117117+89118 txResults[i] = &abcitypes.ExecTxResult{
90119 Code: result.Code,
91120 Data: result.Data,
···130159 // discard the current modified state, and process the decided block
131160 d.tree.Rollback()
132161162162+ deps := TransactionProcessorDependencies{
163163+ plc: d.plc,
164164+ tree: d,
165165+ }
166166+133167 txResults := make([]*abcitypes.ExecTxResult, len(req.Txs))
134168 for i, tx := range req.Txs {
135135- result, err := processTx(ctx, d.plc, tx, req.Time, true)
169169+ result, err := processTx(ctx, deps, tx, req.Time, true)
136170 if err != nil {
137171 return nil, stacktrace.Propagate(err, "")
138172 }
···2233import (
44 "context"
55+ "encoding/hex"
66+ "net/url"
57 "time"
6899+ "github.com/did-method-plc/go-didplc"
710 cbornode "github.com/ipfs/go-ipld-cbor"
811 "github.com/palantir/stacktrace"
912 "tangled.org/gbl08ma/didplcbft/plc"
1313+ "tangled.org/gbl08ma/didplcbft/store"
1014)
11151616+var TransactionActionSetAuthoritativePlc = registerTransactionAction[SetAuthoritativePlcArguments]("SetAuthoritativePlc", processSetAuthoritativePlcTx)
1717+1818+type SetAuthoritativePlcArguments struct {
1919+ PLCURL string `json:"plcURL" refmt:"plcURL"`
2020+ RestartImport bool `json:"restartImport" refmt:"restartImport"`
2121+}
2222+2323+func (SetAuthoritativePlcArguments) ForAction() TransactionAction {
2424+ return TransactionActionSetAuthoritativePlc
2525+}
2626+2727+func init() {
2828+ cbornode.RegisterCborType(SetAuthoritativePlcArguments{})
2929+ cbornode.RegisterCborType(Transaction[SetAuthoritativePlcArguments]{})
3030+}
3131+3232+func processSetAuthoritativePlcTx(ctx context.Context, deps TransactionProcessorDependencies, txBytes []byte, atTime time.Time, execute bool) (*processResult, error) {
3333+ tx, err := UnmarshalTransaction[SetAuthoritativePlcArguments](txBytes)
3434+ if err != nil {
3535+ return &processResult{
3636+ Code: 4000,
3737+ Info: err.Error(),
3838+ }, nil
3939+ }
4040+4141+ // TODO this transaction must somehow validate that whoever submitted it has the permission to change this
4242+ // Does it even make sense to keep this operation type as something submitted via the mempool in the long run,
4343+ // or would it be tied to some sort of proposal/participation system, where the validators submit this operation type in response to some on-chain trigger?
4444+4545+ // A simple solution in the short term might be to just validate a "simple" public-private signature + an expiry timestamp (to prevent replay attacks)
4646+ // which would both be part of the SetAuthoritativePlcArguments. Very centralized, but very straightforward
4747+ // (the public key would be part of the config or even hardcoded for good measure)
4848+4949+ if tx.Arguments.PLCURL != "" {
5050+ parsed, err := url.Parse(tx.Arguments.PLCURL)
5151+ if err != nil || parsed.Scheme != "https" {
5252+ return &processResult{
5353+ Code: 4100,
5454+ Info: "Malformed Authoritative PLC URL",
5555+ }, nil
5656+ }
5757+ }
5858+5959+ if execute {
6060+ tree, err := deps.tree.MutableTree()
6161+ if err != nil {
6262+ return nil, stacktrace.Propagate(err, "")
6363+ }
6464+ err = store.Tree.SetAuthoritativePLC(tree, tx.Arguments.PLCURL)
6565+ if err != nil {
6666+ return nil, stacktrace.Propagate(err, "")
6767+ }
6868+6969+ if tx.Arguments.RestartImport {
7070+ err = store.Tree.SetAuthoritativeImportProgress(tree, 0)
7171+ if err != nil {
7272+ return nil, stacktrace.Propagate(err, "")
7373+ }
7474+ }
7575+ }
7676+7777+ return &processResult{
7878+ Code: 0,
7979+ }, nil
8080+}
8181+8282+var TransactionActionAuthoritativeImport = registerTransactionAction[AuthoritativeImportArguments]("AuthoritativeImport", processAuthoritativeImportTx)
8383+1284type AuthoritativeImportArguments struct {
1313- Cursor string `json:"cursor" refmt:"cursor"`
8585+ PLCURL string `json:"plcURL" refmt:"plcURL"`
8686+ Cursor uint64 `json:"cursor" refmt:"cursor"`
8787+ Count uint64 `json:"count" refmt:"count"`
1488 Hash string `json:"hash" refmt:"hash"`
1589}
1690···2397 cbornode.RegisterCborType(Transaction[AuthoritativeImportArguments]{})
2498}
25992626-func processAuthoritativeImportTx(ctx context.Context, p plc.PLC, txBytes []byte, atTime time.Time, execute bool) (*processResult, error) {
2727- // TODO
2828- return nil, stacktrace.NewError("not implemented")
100100+func processAuthoritativeImportTx(ctx context.Context, deps TransactionProcessorDependencies, txBytes []byte, atTime time.Time, execute bool) (*processResult, error) {
101101+ tx, err := UnmarshalTransaction[AuthoritativeImportArguments](txBytes)
102102+ if err != nil {
103103+ return &processResult{
104104+ Code: 4000,
105105+ Info: err.Error(),
106106+ }, nil
107107+ }
108108+109109+ roTree, err := deps.tree.ImmutableTree(plc.CommittedTreeVersion)
110110+ if err != nil {
111111+ return nil, stacktrace.Propagate(err, "")
112112+ }
113113+114114+ expectedPlcUrl, err := store.Tree.AuthoritativePLC(roTree)
115115+ if err != nil {
116116+ return nil, stacktrace.Propagate(err, "")
117117+ }
118118+119119+ if expectedPlcUrl != tx.Arguments.PLCURL {
120120+ return &processResult{
121121+ Code: 4110,
122122+ Info: "Unexpected Authoritative PLC URL",
123123+ }, nil
124124+ }
125125+126126+ expectedCursor, err := store.Tree.AuthoritativeImportProgress(roTree)
127127+ if err != nil {
128128+ return nil, stacktrace.Propagate(err, "")
129129+ }
130130+131131+ if expectedCursor != tx.Arguments.Cursor {
132132+ return &processResult{
133133+ Code: 4111,
134134+ Info: "Unexpected import cursor",
135135+ }, nil
136136+ }
137137+138138+ // TODO this shouldn't be happening synchronously! We should always be ahead of the next transaction!
139139+ // or at the very least it should only happen once (e.g. when processing the proposal) and then we should cache until it expires or until we actually commit
140140+ operations, newCursor, err := fetchExportedBatchFromAuthoritativeSource(ctx, expectedPlcUrl, expectedCursor, tx.Arguments.Count)
141141+ if err != nil {
142142+ // returning an actual error like this means "consensus failure". Probably not the best way to deal with this, we would rather drop the transaction if not all nodes can fetch the same thing
143143+ // TODO investigate
144144+ return nil, stacktrace.Propagate(err, "")
145145+ }
146146+147147+ expectedHashBytes, err := computeLogEntriesHash(operations)
148148+ if err != nil {
149149+ return nil, stacktrace.Propagate(err, "")
150150+ }
151151+152152+ if hex.EncodeToString(expectedHashBytes) != tx.Arguments.Hash {
153153+ return &processResult{
154154+ Code: 4112,
155155+ Info: "Unexpected import hash",
156156+ }, nil
157157+ }
158158+159159+ if execute {
160160+ tree, err := deps.tree.MutableTree()
161161+ if err != nil {
162162+ return nil, stacktrace.Propagate(err, "")
163163+ }
164164+165165+ var client didplc.Client
166166+ for _, entry := range operations {
167167+ err := deps.plc.ImportOperationFromAuthoritativeSource(ctx, entry, func() ([]didplc.LogEntry, error) {
168168+ // TODO Oh NOOOOOOO! This is not deterministic
169169+ // (fetched at different times, the audit log might grow, therefore we'll fetch and insert more ops, and change the apphash)
170170+ // we need to either limit how much audit log we return (only doable if how much was fetched for each op was part of the tx, ugh)
171171+ // or (probably preferred approach) make it so that the ImportOperationFromAuthoritativeSource / ReplaceHistory function only replaces up until the CID that's being imported, and no further ops
172172+ // Even then there is a problem: what if the nullified status changes between imports :dizzy_face:
173173+ // (can the nullified status change for the ops that are being imported only up until CID? Need to think)
174174+ e, err := client.AuditLog(ctx, entry.DID)
175175+ return e, stacktrace.Propagate(err, "")
176176+ })
177177+ if err != nil {
178178+ return nil, stacktrace.Propagate(err, "")
179179+ }
180180+ }
181181+ err = store.Tree.SetAuthoritativeImportProgress(tree, newCursor)
182182+ if err != nil {
183183+ return nil, stacktrace.Propagate(err, "")
184184+ }
185185+ }
186186+187187+ // TODO finish implementation
188188+ // 1. if execute is true: actually import the operations
189189+ // 2. if execute is true: update AuthoritativeImportProgress
190190+191191+ return &processResult{
192192+ IsAuthoritativeImportTransaction: true,
193193+ Code: 0,
194194+ }, stacktrace.NewError("not implemented")
29195}