github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/smartcontract/entry.go (about)

     1  package smartcontract
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
     7  	"github.com/nspcc-dev/neo-go/pkg/io"
     8  	"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
     9  	"github.com/nspcc-dev/neo-go/pkg/util"
    10  	"github.com/nspcc-dev/neo-go/pkg/vm/emit"
    11  	"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
    12  )
    13  
    14  // CreateCallAndUnwrapIteratorScript creates a script that calls 'operation' method
    15  // of the 'contract' with the specified arguments. This method is expected to return
    16  // an iterator that then is traversed (using iterator.Next) with values (iterator.Value)
    17  // extracted and added into array. At most maxIteratorResultItems number of items is
    18  // processed this way (and this number can't exceed VM limits), the result of the
    19  // script is an array containing extracted value elements. This script can be useful
    20  // for interactions with RPC server that have iterator sessions disabled.
    21  func CreateCallAndUnwrapIteratorScript(contract util.Uint160, operation string, maxIteratorResultItems int, params ...any) ([]byte, error) {
    22  	script := io.NewBufBinWriter()
    23  	jmpIfNotOffset, jmpIfMaxReachedOffset := emitCallAndUnwrapIteratorScript(script, contract, operation, maxIteratorResultItems, params...)
    24  
    25  	// End of the program: push the result on stack and return.
    26  	loadResultOffset := script.Len()
    27  	emit.Opcodes(script.BinWriter, opcode.NIP, // Remove iterator from the 1-st cell of estack
    28  		opcode.NIP) // Remove maxIteratorResultItems from the 1-st cell of estack, so that only resulting array is left on estack.
    29  	if err := script.Err; err != nil {
    30  		return nil, fmt.Errorf("emitting iterator unwrapper script: %w", err)
    31  	}
    32  
    33  	// Fill in JMPIFNOT instruction parameter.
    34  	bytes := script.Bytes()
    35  	bytes[jmpIfNotOffset+1] = uint8(loadResultOffset - jmpIfNotOffset) // +1 is for JMPIFNOT itself; offset is relative to JMPIFNOT position.
    36  	// Fill in jmpIfMaxReachedOffset instruction parameter.
    37  	bytes[jmpIfMaxReachedOffset+1] = uint8(loadResultOffset - jmpIfMaxReachedOffset) // +1 is for JMPIF itself; offset is relative to JMPIF position.
    38  	return bytes, nil
    39  }
    40  
    41  // CreateCallAndPrefetchIteratorScript creates a script that calls 'operation' method
    42  // of the 'contract' with the specified arguments. This method is expected to return
    43  // an array of the first iterator items (up to maxIteratorResultItems, which cannot exceed VM limits)
    44  // and, optionally, an iterator that then is traversed (using iterator.Next).
    45  // The result of the script is an array containing extracted value elements and an iterator, if it can contain more items.
    46  // If the iterator is present, it lies on top of the stack.
    47  // Note, however, that if an iterator is returned, the number of remaining items can still be 0.
    48  // This script should only be used for interactions with RPC server that have iterator sessions enabled.
    49  func CreateCallAndPrefetchIteratorScript(contract util.Uint160, operation string, maxIteratorResultItems int, params ...any) ([]byte, error) {
    50  	script := io.NewBufBinWriter()
    51  	jmpIfNotOffset, jmpIfMaxReachedOffset := emitCallAndUnwrapIteratorScript(script, contract, operation, maxIteratorResultItems, params...)
    52  
    53  	// 1st possibility: jump here when the maximum number of items was reached.
    54  	retainIteratorOffset := script.Len()
    55  	emit.Opcodes(script.BinWriter, opcode.ROT, // Put maxIteratorResultItems from the 2-nd cell of estack, to the top
    56  		opcode.DROP, // ... and then drop it.
    57  		opcode.SWAP, // Put the iterator on top of the stack.
    58  		opcode.RET)
    59  
    60  	// 2nd possibility: jump here when the iterator has no more items.
    61  	loadResultOffset := script.Len()
    62  	emit.Opcodes(script.BinWriter, opcode.ROT, // Put maxIteratorResultItems from the 2-nd cell of estack, to the top
    63  		opcode.DROP, // ... and then drop it.
    64  		opcode.NIP)  // Drop iterator as the 1-st cell on the stack.
    65  	if err := script.Err; err != nil {
    66  		return nil, fmt.Errorf("emitting iterator unwrapper script: %w", err)
    67  	}
    68  
    69  	// Fill in JMPIFNOT instruction parameter.
    70  	bytes := script.Bytes()
    71  	bytes[jmpIfNotOffset+1] = uint8(loadResultOffset - jmpIfNotOffset) // +1 is for JMPIFNOT itself; offset is relative to JMPIFNOT position.
    72  	// Fill in jmpIfMaxReachedOffset instruction parameter.
    73  	bytes[jmpIfMaxReachedOffset+1] = uint8(retainIteratorOffset - jmpIfMaxReachedOffset) // +1 is for JMPIF itself; offset is relative to JMPIF position.
    74  	return bytes, nil
    75  }
    76  
    77  func emitCallAndUnwrapIteratorScript(script *io.BufBinWriter, contract util.Uint160, operation string, maxIteratorResultItems int, params ...any) (int, int) {
    78  	emit.Int(script.BinWriter, int64(maxIteratorResultItems))
    79  	emit.AppCall(script.BinWriter, contract, operation, callflag.All, params...) // The System.Contract.Call itself, it will push Iterator on estack.
    80  	emit.Opcodes(script.BinWriter, opcode.NEWARRAY0)                             // Push new empty array to estack. This array will store iterator's elements.
    81  
    82  	// Start the iterator traversal cycle.
    83  	iteratorTraverseCycleStartOffset := script.Len()
    84  	emit.Opcodes(script.BinWriter, opcode.OVER)                     // Load iterator from 1-st cell of estack.
    85  	emit.Syscall(script.BinWriter, interopnames.SystemIteratorNext) // Call System.Iterator.Next, it will pop the iterator from estack and push `true` or `false` to estack.
    86  	jmpIfNotOffset := script.Len()
    87  	emit.Instruction(script.BinWriter, opcode.JMPIFNOT, // Pop boolean value (from the previous step) from estack, if `false`, then iterator has no more items => jump to the end of program.
    88  		[]byte{
    89  			0x00, // jump to loadResultOffset, but we'll fill this byte after script creation.
    90  		})
    91  	emit.Opcodes(script.BinWriter, opcode.DUP, // Duplicate the resulting array from 0-th cell of estack and push it to estack.
    92  		opcode.PUSH2, opcode.PICK) // Pick iterator from the 2-nd cell of estack.
    93  	emit.Syscall(script.BinWriter, interopnames.SystemIteratorValue) // Call System.Iterator.Value, it will pop the iterator from estack and push its current value to estack.
    94  	emit.Opcodes(script.BinWriter, opcode.APPEND)                    // Pop iterator value and the resulting array from estack. Append value to the resulting array. Array is a reference type, thus, value stored at the 1-th cell of local slot will also be updated.
    95  	emit.Opcodes(script.BinWriter, opcode.DUP,                       // Duplicate the resulting array from 0-th cell of estack and push it to estack.
    96  		opcode.SIZE,               // Pop array from estack and push its size to estack.
    97  		opcode.PUSH3, opcode.PICK, // Pick maxIteratorResultItems from the 3-d cell of estack.
    98  		opcode.GE) // Compare len(arr) and maxIteratorResultItems
    99  	jmpIfMaxReachedOffset := script.Len()
   100  	emit.Instruction(script.BinWriter, opcode.JMPIF, // Pop boolean value (from the previous step) from estack, if `false`, then max array elements is reached => jump to the end of program.
   101  		[]byte{
   102  			0x00, // jump to loadResultOffset, but we'll fill this byte after script creation.
   103  		})
   104  	jmpOffset := script.Len()
   105  	emit.Instruction(script.BinWriter, opcode.JMP, // Jump to the start of iterator traverse cycle.
   106  		[]byte{
   107  			uint8(iteratorTraverseCycleStartOffset - jmpOffset), // jump to iteratorTraverseCycleStartOffset; offset is relative to JMP position.
   108  		})
   109  	return jmpIfNotOffset, jmpIfMaxReachedOffset
   110  }
   111  
   112  // CreateCallScript returns a script that calls contract's method with
   113  // the specified parameters. Whatever this method returns remains on the stack.
   114  // See also (*Builder).InvokeMethod.
   115  func CreateCallScript(contract util.Uint160, method string, params ...any) ([]byte, error) {
   116  	b := NewBuilder()
   117  	b.InvokeMethod(contract, method, params...)
   118  	return b.Script()
   119  }
   120  
   121  // CreateCallWithAssertScript returns a script that calls contract's method with
   122  // the specified parameters expecting a Boolean value to be return that then is
   123  // used for ASSERT. See also (*Builder).InvokeWithAssert.
   124  func CreateCallWithAssertScript(contract util.Uint160, method string, params ...any) ([]byte, error) {
   125  	b := NewBuilder()
   126  	b.InvokeWithAssert(contract, method, params...)
   127  	return b.Script()
   128  }