github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/rpcclient/notary/actor.go (about) 1 package notary 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "strings" 8 9 "github.com/nspcc-dev/neo-go/pkg/core/state" 10 "github.com/nspcc-dev/neo-go/pkg/core/transaction" 11 "github.com/nspcc-dev/neo-go/pkg/crypto/keys" 12 "github.com/nspcc-dev/neo-go/pkg/neorpc/result" 13 "github.com/nspcc-dev/neo-go/pkg/network/payload" 14 "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" 15 "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" 16 "github.com/nspcc-dev/neo-go/pkg/util" 17 "github.com/nspcc-dev/neo-go/pkg/vm" 18 "github.com/nspcc-dev/neo-go/pkg/vm/opcode" 19 "github.com/nspcc-dev/neo-go/pkg/wallet" 20 ) 21 22 // Actor encapsulates everything needed to create proper notary requests for 23 // assisted transactions. 24 type Actor struct { 25 // Actor is the main transaction actor, it has appropriate attributes and 26 // transaction modifiers to set ValidUntilBlock. Use it to create main 27 // transactions that have incomplete set of signatures. They can be 28 // signed (using available wallets), but can not be sent directly to the 29 // network. Instead of sending them to the network use Actor methods to 30 // wrap them into notary requests. 31 actor.Actor 32 // FbActor is the fallback transaction actor, it has two required signers 33 // and a set of attributes expected from a fallback transaction. It can 34 // be used to create _unsigned_ transactions with whatever actions 35 // required (but no additional attributes can be added). Signing them 36 // while technically possible (with notary contract signature missing), 37 // will lead to incorrect transaction because NotValidBefore and 38 // Conflicts attributes as well as ValidUntilBlock field can be 39 // correctly set only when some main transaction is available. 40 FbActor actor.Actor 41 42 fbScript []byte 43 reader *ContractReader 44 sender *wallet.Account 45 rpc RPCActor 46 } 47 48 // ActorOptions are used to influence main and fallback actors as well as the 49 // default Notarize behavior. 50 type ActorOptions struct { 51 // FbAttributes are additional attributes to be added into fallback 52 // transaction by an appropriate actor. Irrespective of this setting 53 // (which defaults to nil) NotaryAssisted, NotValidBefore and Conflicts 54 // attributes are always added. 55 FbAttributes []transaction.Attribute 56 // FbScript is the script to use in the Notarize convenience method, it 57 // defaults to a simple RET instruction (doing nothing). 58 FbScript []byte 59 // FbSigner is the second signer to be used for the fallback transaction. 60 // By default it's derived from the account and has None scope, it has 61 // to be a simple signature or deployed contract account, but this setting 62 // allows you to give it some other scope to be used in complex fallback 63 // scripts. 64 FbSigner actor.SignerAccount 65 // MainAttribtues are additional attributes to be added into main 66 // transaction by an appropriate actor. Irrespective of this setting 67 // (which defaults to nil) NotaryAssisted attribute is always added. 68 MainAttributes []transaction.Attribute 69 // MainCheckerModifier will be used by the main Actor when creating 70 // transactions. It defaults to using [actor.DefaultCheckerModifier] 71 // for result check and adds MaxNotValidBeforeDelta to the 72 // ValidUntilBlock transaction's field. Only override it if you know 73 // what you're doing. 74 MainCheckerModifier actor.TransactionCheckerModifier 75 // MainModifier will be used by the main Actor when creating 76 // transactions. By default it adds MaxNotValidBeforeDelta to the 77 // ValidUntilBlock transaction's field. Only override it if you know 78 // what you're doing. 79 MainModifier actor.TransactionModifier 80 } 81 82 // RPCActor is a set of methods required from RPC client to create Actor. 83 type RPCActor interface { 84 actor.RPCActor 85 86 SubmitP2PNotaryRequest(req *payload.P2PNotaryRequest) (util.Uint256, error) 87 } 88 89 // NewDefaultActorOptions returns the default Actor options. Internal functions 90 // of it need some data from the contract, so it should be added. 91 func NewDefaultActorOptions(reader *ContractReader, acc *wallet.Account) *ActorOptions { 92 opts := &ActorOptions{ 93 FbScript: []byte{byte(opcode.RET)}, 94 FbSigner: actor.SignerAccount{ 95 Signer: transaction.Signer{ 96 Account: acc.Contract.ScriptHash(), 97 Scopes: transaction.None, 98 }, 99 Account: acc, 100 }, 101 MainModifier: func(t *transaction.Transaction) error { 102 nvbDelta, err := reader.GetMaxNotValidBeforeDelta() 103 if err != nil { 104 return fmt.Errorf("can't get MaxNVBDelta: %w", err) 105 } 106 t.ValidUntilBlock += nvbDelta 107 return nil 108 }, 109 } 110 opts.MainCheckerModifier = func(r *result.Invoke, t *transaction.Transaction) error { 111 err := actor.DefaultCheckerModifier(r, t) 112 if err != nil { 113 return err 114 } 115 return opts.MainModifier(t) 116 } 117 return opts 118 } 119 120 // NewActor creates a new notary.Actor using the given RPC client, the set of 121 // signers for main transactions and the account that will sign notary requests 122 // (one plain signature or contract-based). The set of signers will be extended 123 // by the notary contract signer with the None scope (as required by the notary 124 // protocol) and all transactions created with the resulting Actor will get a 125 // NotaryAssisted attribute with appropriate number of keys specified 126 // (depending on signers). A fallback Actor will be created as well with the 127 // notary contract and simpleAcc signers and a full set of required fallback 128 // transaction attributes (NotaryAssisted, NotValidBefore and Conflicts). 129 func NewActor(c RPCActor, signers []actor.SignerAccount, simpleAcc *wallet.Account) (*Actor, error) { 130 return newTunedActor(c, signers, simpleAcc, nil) 131 } 132 133 // NewTunedActor is the same as NewActor, but allows to override the default 134 // options (see ActorOptions for details). Use with care. 135 func NewTunedActor(c RPCActor, signers []actor.SignerAccount, opts *ActorOptions) (*Actor, error) { 136 return newTunedActor(c, signers, opts.FbSigner.Account, opts) 137 } 138 139 func newTunedActor(c RPCActor, signers []actor.SignerAccount, simpleAcc *wallet.Account, opts *ActorOptions) (*Actor, error) { 140 if len(signers) < 1 { 141 return nil, errors.New("at least one signer (sender) is required") 142 } 143 var nKeys int 144 for _, sa := range signers { 145 if sa.Account.Contract == nil { 146 return nil, fmt.Errorf("empty contract for account %s", sa.Account.Address) 147 } 148 if sa.Account.Contract.Deployed { 149 continue 150 } 151 if vm.IsSignatureContract(sa.Account.Contract.Script) { 152 nKeys++ 153 continue 154 } 155 _, pubs, ok := vm.ParseMultiSigContract(sa.Account.Contract.Script) 156 if !ok { 157 return nil, fmt.Errorf("signer %s is not a contract- or signature-based", sa.Account.Address) 158 } 159 nKeys += len(pubs) 160 } 161 if nKeys > 255 { 162 return nil, fmt.Errorf("notary subsystem can't handle more than 255 signatures") 163 } 164 if simpleAcc.Contract == nil { 165 return nil, errors.New("bad simple account: no contract") 166 } 167 if !simpleAcc.CanSign() { 168 return nil, errors.New("bad simple account: can't sign") 169 } 170 if !vm.IsSignatureContract(simpleAcc.Contract.Script) && !simpleAcc.Contract.Deployed { 171 return nil, errors.New("bad simple account: neither plain signature, nor contract") 172 } 173 // Not reusing mainActor/fbActor for ContractReader to make requests a bit lighter. 174 reader := NewReader(invoker.New(c, nil)) 175 if opts == nil { 176 defOpts := NewDefaultActorOptions(reader, simpleAcc) 177 opts = defOpts 178 } 179 var notarySA = actor.SignerAccount{ 180 Signer: transaction.Signer{ 181 Account: Hash, 182 Scopes: transaction.None, 183 }, 184 Account: FakeContractAccount(Hash), 185 } 186 187 var mainSigners = make([]actor.SignerAccount, len(signers), len(signers)+1) 188 copy(mainSigners, signers) 189 mainSigners = append(mainSigners, notarySA) 190 191 mainOpts := actor.Options{ 192 Attributes: []transaction.Attribute{{ 193 Type: transaction.NotaryAssistedT, 194 Value: &transaction.NotaryAssisted{NKeys: uint8(nKeys)}, 195 }}, 196 CheckerModifier: opts.MainCheckerModifier, 197 Modifier: opts.MainModifier, 198 } 199 mainOpts.Attributes = append(mainOpts.Attributes, opts.MainAttributes...) 200 201 mainActor, err := actor.NewTuned(c, mainSigners, mainOpts) 202 if err != nil { 203 return nil, err 204 } 205 206 fbSigners := []actor.SignerAccount{notarySA, opts.FbSigner} 207 fbOpts := actor.Options{ 208 Attributes: []transaction.Attribute{{ 209 Type: transaction.NotaryAssistedT, 210 Value: &transaction.NotaryAssisted{NKeys: 0}, 211 }, { 212 // A stub, it has correct size, but the contents is to be filled per-request. 213 Type: transaction.NotValidBeforeT, 214 Value: &transaction.NotValidBefore{}, 215 }, { 216 // A stub, it has correct size, but the contents is to be filled per-request. 217 Type: transaction.ConflictsT, 218 Value: &transaction.Conflicts{}, 219 }}, 220 } 221 fbOpts.Attributes = append(fbOpts.Attributes, opts.FbAttributes...) 222 fbActor, err := actor.NewTuned(c, fbSigners, fbOpts) 223 if err != nil { 224 return nil, err 225 } 226 return &Actor{*mainActor, *fbActor, opts.FbScript, reader, simpleAcc, c}, nil 227 } 228 229 // Notarize is a simple wrapper for transaction-creating functions that allows to 230 // send any partially-signed transaction in a notary request with a fallback 231 // transaction created based on Actor settings and SendRequest adjustment rules. 232 // The values returned are main and fallback transaction hashes, ValidUntilBlock 233 // and error if any. 234 func (a *Actor) Notarize(mainTx *transaction.Transaction, err error) (util.Uint256, util.Uint256, uint32, error) { 235 var ( 236 // Just to simplify return values on error. 237 fbHash util.Uint256 238 mainHash util.Uint256 239 vub uint32 240 ) 241 if err != nil { 242 return mainHash, fbHash, vub, err 243 } 244 fbTx, err := a.FbActor.MakeUnsignedRun(a.fbScript, nil) 245 if err != nil { 246 return mainHash, fbHash, vub, err 247 } 248 return a.SendRequest(mainTx, fbTx) 249 } 250 251 // SendRequest creates and sends a notary request using the given main and 252 // fallback transactions. It accepts signed main transaction and unsigned fallback 253 // transaction that will be adjusted in its NotValidBefore and Conflicts 254 // attributes as well as ValidUntilBlock value. Conflicts is set to the main 255 // transaction hash, while NotValidBefore is set to the middle of current mainTx 256 // lifetime (between current block and ValidUntilBlock). The values returned are 257 // main and fallback transaction hashes, ValidUntilBlock and error if any. 258 func (a *Actor) SendRequest(mainTx *transaction.Transaction, fbTx *transaction.Transaction) (util.Uint256, util.Uint256, uint32, error) { 259 var ( 260 fbHash util.Uint256 261 mainHash = mainTx.Hash() 262 vub = mainTx.ValidUntilBlock 263 ) 264 if len(fbTx.Attributes) < 3 { 265 return mainHash, fbHash, vub, errors.New("invalid fallback: missing required attributes") 266 } 267 if fbTx.Attributes[1].Type != transaction.NotValidBeforeT { 268 return mainHash, fbHash, vub, errors.New("invalid fallback: NotValidBefore is missing where expected") 269 } 270 if fbTx.Attributes[2].Type != transaction.ConflictsT { 271 return mainHash, fbHash, vub, errors.New("invalid fallback: Conflicts is missing where expected") 272 } 273 height, err := a.GetBlockCount() 274 if err != nil { 275 return mainHash, fbHash, vub, err 276 } 277 // New values must be created to avoid overwriting originals via a pointer. 278 fbTx.Attributes[1].Value = &transaction.NotValidBefore{Height: (height + vub) / 2} 279 fbTx.Attributes[2].Value = &transaction.Conflicts{Hash: mainHash} 280 fbTx.ValidUntilBlock = vub 281 err = a.FbActor.Sign(fbTx) 282 if err != nil { 283 return mainHash, fbHash, vub, err 284 } 285 fbTx.Scripts[0].InvocationScript = append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, make([]byte, keys.SignatureLen)...) // Must be present. 286 return a.SendRequestExactly(mainTx, fbTx) 287 } 288 289 // SendRequestExactly accepts signed and completely prepared main and fallback 290 // transactions, creates a P2P notary request containing them, signs and sends 291 // it to the network. Caller takes full responsibility for transaction 292 // correctness in this case, use this method only if you know exactly that you 293 // need to override some of the other method's behavior and you can do it. The 294 // values returned are main and fallback transaction hashes, ValidUntilBlock 295 // and error if any. 296 func (a *Actor) SendRequestExactly(mainTx *transaction.Transaction, fbTx *transaction.Transaction) (util.Uint256, util.Uint256, uint32, error) { 297 var ( 298 fbHash = fbTx.Hash() 299 mainHash = mainTx.Hash() 300 vub = mainTx.ValidUntilBlock 301 ) 302 req := &payload.P2PNotaryRequest{ 303 MainTransaction: mainTx, 304 FallbackTransaction: fbTx, 305 } 306 req.Witness = transaction.Witness{ 307 InvocationScript: append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, a.sender.SignHashable(a.GetNetwork(), req)...), 308 VerificationScript: a.sender.GetVerificationScript(), 309 } 310 actualHash, err := a.rpc.SubmitP2PNotaryRequest(req) 311 if err != nil { 312 return mainHash, fbHash, vub, fmt.Errorf("failed to submit notary request: %w", err) 313 } 314 if !actualHash.Equals(fbHash) { 315 return mainHash, fbHash, vub, fmt.Errorf("sent and actual fallback tx hashes mismatch: %v vs %v", fbHash.StringLE(), actualHash.StringLE()) 316 } 317 return mainHash, fbHash, vub, nil 318 } 319 320 // Wait waits until main or fallback transaction will be accepted to the chain and returns 321 // the resulting application execution result or actor.ErrTxNotAccepted if both transactions 322 // failed to persist. Wait can be used if underlying Actor supports transaction awaiting, 323 // see actor.Actor and actor.Waiter documentation for details. Wait may be used as a wrapper 324 // for Notarize, SendRequest or SendRequestExactly. Notice that "already exists" or "already 325 // on chain" answers are not treated as errors by this routine because they mean that some 326 // of the transactions given might be already accepted or soon going to be accepted. These 327 // transactions can be waited for in a usual way potentially with positive result. 328 func (a *Actor) Wait(mainHash, fbHash util.Uint256, vub uint32, err error) (*state.AppExecResult, error) { 329 // #2248 will eventually remove this garbage from the code. 330 if err != nil && !(strings.Contains(strings.ToLower(err.Error()), "already exists") || strings.Contains(strings.ToLower(err.Error()), "already on chain")) { 331 return nil, err 332 } 333 return a.WaitAny(context.TODO(), vub, mainHash, fbHash) 334 }