Skip to content
This repository has been archived by the owner on Aug 2, 2022. It is now read-only.

Contract API support for generating asynchronous transactions #175

Closed
wanderingbort opened this issue Aug 15, 2017 · 12 comments
Closed

Contract API support for generating asynchronous transactions #175

wanderingbort opened this issue Aug 15, 2017 · 12 comments

Comments

@wanderingbort
Copy link
Contributor

Related to #174

This task is for implementation of the contract API and related support for producing GeneratedTransaction for asynchronous calls to other contract methods.

@wanderingbort wanderingbort added this to the Test Network Release milestone Aug 15, 2017
@wanderingbort wanderingbort self-assigned this Aug 15, 2017
@wanderingbort
Copy link
Contributor Author

wanderingbort commented Aug 30, 2017

Existing Stubs

At the moment we have a stub from several months ago which defines this intrinsic in the wasm-vm:

int send(void *tx_buffer, int tx_buffer_size);

It seems that the idea was to serialize a whole transaction into the proper packed form and then pass it to the native code.
PROS:

  • dirt simple ABI

CONS:

  • guarantees that the wasm must understand fc::pack binary format for things like vector
  • probably requires some concept of malloc or at least alloca for message sizes that cannot be determined at compile time

Managed Transaction Alternative

alternatively, if we make a slightly more complex api. We may be able to simplify what have to implement in the more restrictive wasm-vm environment

typedef uint32_t TransactionHandle;
typedef uint64_t FuncName;
 
// create a transaction and auto-determine refBlockPrefix, refBlockNum and Expiration
TransactionHandle transaction_create(AccountName *scopes, int scope_count);

// add a message to the pending transaction
void transaction_add_message(TransactionHandle trx, AccountName code, FuncName type, PermissionName* permissions, int permission_count, void* data, int data_size);

// publish transaction (defaults to async, but can be sync)
void transaction_send(TransactionHandle trx, int immediate = 0);

PROS:

  • unless the data for a message is dynamically sized, we may not have to re-implement fc::pack in wasm-vm OR provide malloc, alloca, etc .
  • parallels readMessage which is currently given a raw buffer which must be de-serialized in the VM
  • provides some insulation for maintaining backwards compatibility as the native data structures and procedures are abstracted
  • less native-side de/re-serialization of things to check validity of things like transaction::scope, message::code and message::authorization

CONS:

  • fat ABI (more like a C-style library than intrinsics)
  • retaining the transaction on the native side requires more book keeping in the application context
Other thoughts:

in the above API, scope is set once when the transaction is created. It may be more developer friendly to have an API endpoint to add them ad-hoc (as you collect messages and realize you need more scopes):

// add a scope to the pending transaction
void transaction_add_scope(TransactionHandle trx, AccountName scope);

@bytemaster
Copy link
Contributor

A middle ground is to remove the handle all together and only allow one "under construction transaction".

There is a security vulnerability in the second approach that allows someone to allocate unbounded memory (almost) by looping over transaction_create or transaction_add_message.

We must be very careful regarding any API that allocates resources.

I think we can support both APIs. The first API is maximally flexible, the second one may have edge cases that are not predictable.

@wanderingbort
Copy link
Contributor Author

I am comfortable with the single transaction mode.

With the API that stands, I think its reasonable to limit the number of pending transactions to some small number and the "total serialized size" to some reasonable amount. Each call to transaction_add_message could enforce that the total serialized transaction doesn't exceed the configured max.

I think we can support both APIs. The first API is maximally flexible, the second one may have edge cases that are not predictable.

I would say that is "cheap at twice the price" but in this instance, pushing enough code into the wasm-vm to serialize eos::types::Transaction properly is a decent amount of work as it is and I would argue it offers no flexibility.

None of the data structures managed by the alternative API are flexible from the point-of-view of a contract. The contents of Message::data is still every bit as flexible and it represents the sum-total of the contract-level flexibility offered in the existing set of contract facing APIs.

I would say that interface is more fragile because it needs to be compiled with an awareness of the block-level "envelope" types which are only indirectly accessed by other contract facing APIs. For instance, we have abstracted access to Message::code through AccountName currentCode();

@wanderingbort
Copy link
Contributor Author

after further review and beginning to implement, I would like to amend my proposal as follows:

  • Due to the existing APIs like the database layer which abstract iteration over variably sized arrays and have no "batched" or variably sized array input aside from a logically singular data element, I have removed any dependency on being able to construct a dynamically sized array in the contract code.
    • This is further motivated by our current lack of heap and stack allocation, and our current lack of a valid STL containers implementation that genuinely make variably sized arrays harder.
    • This affects mostly the concept of scopes and permissions
  • because we have a variably sized array (permissions) in a variably sized array (messages), i propose to allow only 1 "pending message" per transaction, which must be pushed or dropped before adding another
    • This gives us tighter control over resource consumption as every entrypoint can monitor the projected size of the transaction and throw an error when the producer-controlled limit is exceeded. This may make this api "slower" however, the trade for safety is worthwhile.
    • This also maps well to a C++ API which can use future containers etc to iteratively construct messages with permissions without having to first flatten them into contiguous memory
  • I have maintained that multiple transactions can be in construction at a time because I think this will be useful. The native side will enforce that only a very small number of constructing transactions is allowed until such time as we are convinced this is not a security threat.
  • I have adopted the terms "inline" and "deferred" to replace "sync" and "async" in the documentation and API as I feel they are more descriptive of the reality of these transactions
    • for security reasons, I don't think we want to allow the avenue for opaque re-entrancy and/or recursive wasm-vm contexts that would occur if sending a "synchronous" transaction actually processed synchronously. Instead, we would probably queue it for execution immediately after this transaction/call guaranteeing to the contract that this call will complete in its entirety before any foreign code is run. Thus, "inline". I considered "immediate" but worried that it would imply processing before returning execution to the caller.
    • likewise, asynchronous has become an overburdened concept in the modern programming vernacular and is usually associated with some "response" event or callback which we don't have. "deferred" is less burdened and does not currently imply any form of response.

There result is the following proposed C API:

   typedef uint32_t TransactionHandle;
   #define InvalidTransactionHandle (0xFFFFFFFFUL)
   #define SendInline (1)
   #define SendDeferred (0)

   TransactionHandle transactionCreate();
   void transactionAddScope(TransactionHandle trx, AccountName scope, int readOnly = 0);
   void transactionSetMessageDestination(TransactionHandle trx, AccountName code, FuncName type);
   void transactionAddMessagePermission(TransactionHandle trx, AccountName account, PermissionName permission);
   void transactionPushMessage(TransactionHandle trx, void* data, int size);
   void transactionResetMessage(TransactionHandle trx);
   void transactionSend(TransactionHandle trx, int mode = SendDeferred);
   void transactionDrop(TransactionHandle trx);

I am still working on the C++ api, but it, at least, adds type safety to the message payload. It will probably also provide an easier avenue for constructing transactions in a more declarative way.

@bytemaster
Copy link
Contributor

The point of a transaction is to enable multiple messages that depend on each-other to be applied atomically. I think that we should not use the "current" lack of malloc as an excuse to assume that a dev couldn't implement it. In fact, it is being implemented (in progress) right now.

So I think we can assume up to N pending trx with up to M pending messages within each trx and a maximum temporary memory allocation of 64KB.

I also like using inline (for in same transaction) and deferred to imply executed at block producers discretion.

@wanderingbort
Copy link
Contributor Author

I think you misunderstand the concern with malloc et al. These transaction can have multiple messages, scopes and permissions. They are fully featured.

this is valid for creating a transaction with 3 messages (albeit I skipped things like setting permissions etc);

TransactionHandle trx = transactionCreate();
transactionPushMessage(trx, ...);
transactionPushMessage(trx, ...);
transactionPushMessage(trx, ...);
transactionSend(trx, ...);

The question is who manages the variably sized temporaries while the transaction is in construction. The contract or the system.

For now, the contract ecosystem is not very supportive of contracts managing temporaries because these don't lend themselves well to being WebAssembly locals. The usual suspects for managing variably sized things are not supported yet (heap allocation, stack allocation).

If we had a system that accepted AccountName *scopes, int num_scopes instead of using functions to push scopes in, you would have to create locals of worst case size AccountName pending_scopes[10] and then use as much as you need up to the limit you self imposed but then the system may also impose limits on you once you hand that data off.

Sure we will eventually have a full ecosystem but, for now, this API is more consistent with the database and message APIs which abstract native types behind functions and, in my opinion, safer than the alternative of passing pointers and promises that the pointer is a valid array of the size the caller claims.

@bytemaster
Copy link
Contributor

This looks reasonable to me. Just keep in mind that there is function call overhead with your API that doesn't exist with other.

At this point something that works good enough for EOS Dawn 1.0 is all that is necessary.

@wanderingbort
Copy link
Contributor Author

As I get deeper into this, I think we may want to make some large changes to this system post EOS Dawn 1.0.

Here are my thoughts:

  • Flatten the processed transaction "tree" that forms from inline transactions creating additional inline transactions
    • this improves parse-ability for partial nodes later
    • it makes the code flow very easily for doing things like capping the maximum number of inline transactions from a single deferred/user-generated transaction and/or timing.
    • it makes the ordering of replay explicit and obvious whereas a tree can be either breadth or depth first.
    • Deferred Transaction IDs can be easily implied depending on which schema we go with.
      • (parent_txid, index) or (block-hash,cycle,thread,tx-index,index)
      • this saves ~32 bytes per deferred transaction in the block.
      • without flattening these id schemas end up with a long tail that can be up to the maximum recursive depth of transaction generation.
  • allow this API to be "no-op" during partial/replay where the artifacts of calling this API will be known in the block
  • Consider removing the concept of "inline transactions" for "implicit messages".
    • Encapsulating these messages within transactions is redundant, they are already all in the context of their parent transaction and must abide by all the rules of that transaction. Unless there is a compelling reason to limit the scope or readScope to a subset of the parent's every "inline transaction" message could be appended into the parent transaction's messages and be functionally equivalent.
    • this would effectively flatten the features of inline transactions as suggested above
    • Deferred Transactions would still exist and remain largely unchanged

@bytemaster
Copy link
Contributor

After call with wanderingbort here is the conclusion:

  1. the API for inline messages will not allow for creation of inline transactions (just pushing messages)
  2. in the backend we will aggregate all inline messages into one inline transaction and execute it
  3. in the future we may update the processed transaction output to remove the 1 redundant header, but that is an implementation detail
  4. we will probably update the processed transaction output to make the null case more efficient (array of optional ).

Priority:

  1. can developers build apps with the function
  2. api stability
  3. blockchain data-structure stability

wanderingbort added a commit to wanderingbort/eos that referenced this issue Sep 7, 2017
wanderingbort added a commit to wanderingbort/eos that referenced this issue Sep 7, 2017
wanderingbort added a commit to wanderingbort/eos that referenced this issue Sep 7, 2017
first swipe at processing thereof, need tests badly

ref EOSIO#175
wanderingbort added a commit to wanderingbort/eos that referenced this issue Sep 7, 2017
wanderingbort added a commit to wanderingbort/eos that referenced this issue Sep 7, 2017
wanderingbort added a commit to wanderingbort/eos that referenced this issue Sep 7, 2017
wanderingbort added a commit to wanderingbort/eos that referenced this issue Sep 7, 2017
wanderingbort added a commit to wanderingbort/eos that referenced this issue Sep 8, 2017
exposed many bugs in the contract code and producer
fixed what I found

ref EOSIO#175
wanderingbort added a commit to wanderingbort/eos that referenced this issue Sep 8, 2017
wanderingbort added a commit to wanderingbort/eos that referenced this issue Sep 8, 2017
wanderingbort added a commit to wanderingbort/eos that referenced this issue Sep 8, 2017
wanderingbort added a commit to wanderingbort/eos that referenced this issue Sep 8, 2017
@wanderingbort
Copy link
Contributor Author

ok, reflecting the comments from @bytemaster and our call the "final" API for this issue/PR is:

TransactionHandle transactionCreate();
void transactionRequireScope(TransactionHandle trx, AccountName scope, int readOnly = 0);
void transactionAddMessage(TransactionHandle trx, MessageHandle msg);
void transactionSend(TransactionHandle trx);
void transactionDrop(TransactionHandle trx);

MessageHandle messageCreate(AccountName code, FuncName type, void const* data, int size);
void messageRequirePermission(MessageHandle msg, AccountName account, PermissionName permission);
void messageSend(MessageHandle msg);
void messageDrop(MessageHandle msg);

for c++ there are wrappers manage handles for you and expose type same payloads for messages.

test contract

There is a simple "proxy" contract as part of the tests. It runs in the "Slow tests". This allows you to install code on one account (the proxy) and set another account (the destination) as the "owner". Any transfer of EOS or currency will be relayed to the "owner" automatically in a deferred transaction. This couldn't be done conveniently with inline messages because it would require that the original sender of the transaction know the final "owner" account and list them in the scope parameter.

The owner isn't private persay, but requiring a payer to inspect the contract state is silly. This could be extended to pay dividends of any eos or currency trasfer to a set of owners in proportion to their "shares" but that is left as an exercise to the reader

this contract is not meant to be safe or really representative It is merely an example of a contract producing a deferred transaction because inline messaging was insufficient. At the very least, it should probably enforce that only the owner can set a different owner 😄

other notes

Many of the limits of the current system are driven by the producer voting. For now they default to:

  • 4KB max for any generated message
  • 64KB max for any generated Transaction
  • 4 maximum recursive inline message call depth
  • 10ms maximum total transaction processing time
    • currently this is only checked for inline transactions but we can make the check more granular and cover things like lots of messages, notifies, etc etc

For now there is a cludge for permissioning until I or @bytemaster address #113
This cludge is that code is implicitly allowed to send things for its own account with a level of "code". In the future, we can remove this cludge because users will be able to delegate a permission to their contract code and should do so appropriately.

wanderingbort added a commit to wanderingbort/eos that referenced this issue Sep 8, 2017
@wanderingbort
Copy link
Contributor Author

closed by #389

ljrprocc pushed a commit to bithacks-tech/myeosio that referenced this issue Jul 4, 2018
@shahofblah
Copy link

WRT #175 (comment)

Instead, we would probably queue it for execution immediately after this transaction/call guaranteeing to the contract that this call will complete in its entirety before any foreign code is run. Thus, "inline". I considered "immediate" but worried that it would imply processing before returning execution to the caller.

I believe that 'immediate' fits better than 'inline', as 'inline' implies that the line of code calling that action is replaced by the action code. Immediate is at least semantically unambiguous in the sense of how long it takes to return a result, with the only ambiguity being when the action is scheduled. 'Inline' implies it is scheduled at the moment of its call which is unambiguously wrong.

Also, the official docs probably also need an update as they say

Inline communication takes the form of requesting other actions that need to be executed as part of the calling action...These can effectively be thought of as nested transactions within the calling transaction

If the actions are executed sequentially, they are neither a part of the calling action nor nested transactions.

NorseGaud pushed a commit that referenced this issue Jul 30, 2019
# This is the 1st commit message:

various improvements

# This is the commit message #2:

new hash

# This is the commit message #3:

fix for script path

# This is the commit message #4:

fixes

# This is the commit message #5:

fixes

# This is the commit message #6:

fixes

# This is the commit message #7:

fixes

# This is the commit message #8:

fixes

# This is the commit message #9:

fixes

# This is the commit message #10:

fixes

# This is the commit message #11:

fixes

# This is the commit message #12:

fixes

# This is the commit message #13:

fixes

# This is the commit message #14:

fixes

# This is the commit message #15:

fixes

# This is the commit message #16:

fixes

# This is the commit message #17:

fixes

# This is the commit message #18:

fixes

# This is the commit message #19:

fixes

# This is the commit message #20:

fixes

# This is the commit message #21:

fixes

# This is the commit message #22:

fixes

# This is the commit message #23:

fixes

# This is the commit message #24:

fixes

# This is the commit message #25:

fixes

# This is the commit message #26:

testing

# This is the commit message #27:

testing

# This is the commit message #28:

testing

# This is the commit message #29:

testing

# This is the commit message #30:

testing

# This is the commit message #31:

testing

# This is the commit message #32:

testing

# This is the commit message #33:

testing

# This is the commit message #34:

testing

# This is the commit message #35:

testing

# This is the commit message #36:

testing

# This is the commit message #37:

testing

# This is the commit message #38:

testing

# This is the commit message #39:

testing

# This is the commit message #40:

testing

# This is the commit message #41:

testing

# This is the commit message #42:

testing

# This is the commit message #43:

testing

# This is the commit message #44:

fixes

# This is the commit message #45:

fixes

# This is the commit message #46:

fixes

# This is the commit message #47:

fixes

# This is the commit message #48:

fixes

# This is the commit message #49:

fixes

# This is the commit message #50:

fixes

# This is the commit message #51:

fixes

# This is the commit message #52:

fixes

# This is the commit message #53:

fixes

# This is the commit message #54:

fixes

# This is the commit message #55:

fixes

# This is the commit message #56:

fixes

# This is the commit message #57:

fixes

# This is the commit message #58:

fixes

# This is the commit message #59:

fixes

# This is the commit message #60:

fixes

# This is the commit message #61:

fixes

# This is the commit message #62:

fixes

# This is the commit message #63:

fixes

# This is the commit message #64:

fixes

# This is the commit message #65:

fixes

# This is the commit message #66:

fixes

# This is the commit message #67:

fixes

# This is the commit message #68:

fixes

# This is the commit message #69:

fixes

# This is the commit message #70:

fixes

# This is the commit message #71:

fixes

# This is the commit message #72:

fixes

# This is the commit message #73:

fixes

# This is the commit message #74:

fixes

# This is the commit message #75:

fixes

# This is the commit message #76:

fixes

# This is the commit message #77:

fixes

# This is the commit message #78:

fixes

# This is the commit message #79:

more testing

# This is the commit message #80:

testing

# This is the commit message #81:

fixes

# This is the commit message #82:

fixes

# This is the commit message #83:

fixes

# This is the commit message #84:

fixes

# This is the commit message #85:

fixes

# This is the commit message #86:

fixes

# This is the commit message #87:

fixes

# This is the commit message #88:

fixes

# This is the commit message #89:

fixes

# This is the commit message #90:

fixes

# This is the commit message #91:

fixes

# This is the commit message #92:

fixes

# This is the commit message #93:

propagate-environment for buildkite-agent

# This is the commit message #94:

propagate-environment for buildkite-agent

# This is the commit message #95:

propagate-environment for buildkite-agent

# This is the commit message #96:

propagate-environment for buildkite-agent

# This is the commit message #97:

fixes

# This is the commit message #98:

fixes

# This is the commit message #99:

fixes

# This is the commit message #100:

fixes

# This is the commit message #101:

fixes

# This is the commit message #102:

fixes

# This is the commit message #103:

fixes

# This is the commit message #104:

fixes

# This is the commit message #105:

fixes

# This is the commit message #106:

fixes

# This is the commit message #107:

fixes

# This is the commit message #108:

fixes

# This is the commit message #109:

fixes

# This is the commit message #110:

fixes

# This is the commit message #111:

fixes

# This is the commit message #112:

fixes

# This is the commit message #113:

fixes

# This is the commit message #114:

fixes

# This is the commit message #115:

fixes

# This is the commit message #116:

fixes

# This is the commit message #117:

fixes

# This is the commit message #118:

fixes

# This is the commit message #119:

fixes

# This is the commit message #120:

fixes

# This is the commit message #121:

fixes

# This is the commit message #122:

fixes

# This is the commit message #123:

fixes

# This is the commit message #124:

fixes

# This is the commit message #125:

fixes

# This is the commit message #126:

fixes

# This is the commit message #127:

fixes

# This is the commit message #128:

fixes

# This is the commit message #129:

fixes

# This is the commit message #130:

fixes

# This is the commit message #131:

fixes

# This is the commit message #132:

fixes

# This is the commit message #133:

fixes

# This is the commit message #134:

fixes

# This is the commit message #135:

fixes

# This is the commit message #136:

fixes

# This is the commit message #137:

fixes

# This is the commit message #138:

fixes

# This is the commit message #139:

fixes

# This is the commit message #140:

fixes

# This is the commit message #141:

fixes

# This is the commit message #142:

fixes

# This is the commit message #143:

fixes

# This is the commit message #144:

fixes

# This is the commit message #145:

fixes

# This is the commit message #146:

fixes

# This is the commit message #147:

fixes

# This is the commit message #148:

fixes

# This is the commit message #149:

fixes

# This is the commit message #150:

fixes

# This is the commit message #151:

fixes

# This is the commit message #152:

fixes

# This is the commit message #153:

testing

# This is the commit message #154:

fixes

# This is the commit message #155:

fixes

# This is the commit message #156:

fixes

# This is the commit message #157:

fixes

# This is the commit message #158:

fixes

# This is the commit message #159:

fixes

# This is the commit message #160:

fixes

# This is the commit message #161:

fixes

# This is the commit message #162:

fixes

# This is the commit message #163:

fixes

# This is the commit message #164:

fixes

# This is the commit message #165:

fixes

# This is the commit message #166:

fixes

# This is the commit message #167:

fixes

# This is the commit message #168:

fixes

# This is the commit message #169:

fixes

# This is the commit message #170:

fixes

# This is the commit message #171:

fixes

# This is the commit message #172:

fixes

# This is the commit message #173:

fixes

# This is the commit message #174:

fixes

# This is the commit message #175:

fixes

# This is the commit message #176:

fixes

# This is the commit message #177:

fixes

# This is the commit message #178:

fixes

# This is the commit message #179:

fixes

# This is the commit message #180:

fixes

# This is the commit message #181:

fixes

# This is the commit message #182:

fixes

# This is the commit message #183:

fixes

# This is the commit message #184:

fixes

# This is the commit message #185:

fixes

# This is the commit message #186:

fixes
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

3 participants