Package: tapescript
TapescriptSimple script system loosely inspired by Bitcoin script but also hopefully more
useful for other applications. The idea is to programmatically ensure access
controls in a distributed system.StatusOPsInterpreter functions and classesByte-code compilerDecompilerUnit testsE2e testsMerkleval test vectorsOmega e2e test with all ops and nopsPlugin architecture: new ops with compiler, decompiler, interpreterHalf-decent docsDecent docsPackage publishedAdded try...exceptAliases withoutOP_prefixMacros and variablesUsageInstallationpipinstalltapescriptWrite, compile, decompileSee thelangauge_spec.mdanddocs.mdfiles
for syntax and operation specifics.Once you have a script written, use thecompile_script(code: str) -> bytesfunction to turn it into the byte code that the interpreter runs. Note that eachOP_function has an alias that excludes theOP_prefix; e.g.OP_PUSH d1can also be writtenPUSH d1.Merklized scriptsThere is an included tool for making merklized branching scripts. To use it,
write the desired branches, then pass them to thecreate_merklized_scriptfunction. For example:fromtapescriptimportcreate_merklized_scriptbranches=['OP_PUSH xb26d10053b4b25497081561f529e42da9ccfac860a7b3d1ec932901c2a70afce\nOP_CHECK_SIG x00','OP_PUSH x9e477d55a62fc1ecc6b7c89d69c4f9cba94d5173f0d59f971951ff46acb9017b\nOP_CHECK_SIG x00','OP_PUSH xdd86edfbcfd5ac3e8c1acb527cc4178a14af0755aea1e447dc2b278f52fcedbf\nOP_CHECK_SIG x00',]locking_script,unlocking_scripts=create_merklized_script(branches)This function returns a tuple containing the locking script that usesOP_MERKLEVALto enforce the cryptographic commitment to the branches and a
list of unlocking scripts that fulfill the cryptographic commitment and execute
the individual script branches. The unlocking scripts are ordered identically to
the input branches. In the above example, each branch expects a signature from
the given public key. To use as an auth script, the locking script would be
compiled and used as the locking condition. A signature would be prepended to
the unlocking script with anOP_PUSH x<hex signature>, and this would then be
compiled to become the unlocking bytes. Then concatenate the locking script to
the unlocking script (i.e. script = unlock + lock) and run through therun_auth_scriptfunction, which will return aTrueif it executed
successfully andFalseotherwise.Run a scriptRun a script by compiling it to byte code (if it wasn't already) and run with
eitherrun_script(script: bytes, cache_vals: dict = {}, contracts: dict = {})orrun_auth_script(script: bytes, cache_vals: dict = {}, contracts: dict = {}).
Therun_scriptfunction returnstupleof length 3 containing aTape, aLifoQueue, and the final state of thecachedict. Therun_auth_scriptinstead returns a bool that isTrueif the script ran without error and
resulted in a singleTruevalue on the queue; otherwise it returnsFalse.In the case where a signature is expected to be validated, the message parts for
the signature must be passed in via thecache_valsdict at keyssigfield[1-8].
In the case whereOP_CHECK_TRANSFERmight be called, the contracts must be
passed in via thecontractsdict. See thesection in the language_spec.mdfile for more informaiton aboutOP_CHECK_TRANSFER.Changing flagsThe interpreter flags can be changed by changing thefunctions.flagsdict.Adding opsThe ops can be updated via a plugin system.fromqueueimportLifoQueuefromtapescriptimportTape,add_opcode,add_opcode_parsing_handlersdefOP_SOME_NONSENSE(tape:Tape,queue:LifoQueue,cache:dict)->None:count=tape.read(1)[0]for_inrange(count):queue.put(b'some nonsense')defOP_SOME_NONSENSE_compiler(opname:str,symbols:list[str],symbols_to_advance:int,symbol_index:int):symbols_to_advance+=1ifsymbols[0][0]!='d':raiseSyntaxError(f'{opname}- int argument must begin with d -{symbol_index}')val=int(symbols[0][1:]).to_bytes(1,'big)return(symbols_to_advance,(val,))defOP_SOME_NONSENSE_decompiler(opname:str,tape:Tape):val=tape.read(1)[0]return[f'{opname}d{val}']# add opcode to bytecode interpreteradd_opcode(255,'OP_SOME_NONSENSE',OP_SOME_NONSENSE)# add opcode to compiler and decompileradd_opcode_parsing_handlers('OP_SOME_NONSENSE',OP_SOME_NONSENSE_compiler,OP_SOME_NONSENSE_decompiler)ContractsThe interpreter includes a system for including contracts for greater
extensibility. For example, the bundledCanCheckTransferinterface is used
to check that contracts can be used with theOP_CHECK_TRANSFERoperation. To
add an interface for checking loaded contracts, calladd_contract_interfaceand pass aruntime_checkablesubclass oftyping.Protocolas the argument. To
remove an interface, callremove_contract_interfaceand pass the interface as
the argument.To add a contract, useadd_contract(contract_id: bytes, contract: object). To
remove a contract, useremove_contract(contract_id: bytes).Each contract will be checked against each interface when added and again at
runtime when an op that uses a contract is executed. All contracts added via theadd_contractfunction will be included in the runtime environment of scripts
run thereafter. Additionally, contracts can be passed into therun_scriptandrun_auth_scriptfunctions, and these will override any contracts in the global
runtime environment in case of a contract_id conflict.To use a contract in a custom op, find it in thetape.contractsdict by its
contract_id.Signature checkingNotes for theOP_CHECK_SIGandOP_CHECK_SIG_VERIFYfunctions:The body of the message to be used in checking the signature is comprised of
thesigfield[1-8]cache items.Each signature can have an additional (33rd) byte attached which encodes up
to 8 bit flags. Each bit flag encoded will exclude the associatedsigfield{n}cache item from the message body during signature checks.These ops take a 1 byte param from the tape that encodes the allowable flags.
If a signature is passed to a signature checker that uses a disallowed sigflag,
aScriptExecutionErrorwill be raised.Soft ForksA soft fork is a protocol upgrade such that all scripts written under the new
protocol also validate under the old version -- older versions do not break when
encountering use of the new feature. Tapescript was designed with soft-fork
support in mind, and the helper functionadd_soft_forkis included to
streamline the process and reduce the use of boilerplate.To enable a soft fork, a NOP code must be replaced with an op that reads the
next byte as an unsigned int, pulls that many values from the queue, runs any
checks on the data, and raises an error in case any check fails. This maintains
the behavior of the original NOP such that any nodes that did not activate the
soft fork will not have any errors parsing scripts using the new OP.Example soft fork:fromtapescriptimport(Tape,ScriptExecutionError,add_soft_fork)fromqueueimportLifoQueuedefOP_CHECK_ALL_EQUAL_VERIFY(tape:Tape,queue:LifoQueue,cache:dict)->None:"""Replacement for NOP255: read the next byte as uint count, takethat many items from queue, run checks, and raise an error ifany check fails."""count=tape.read(1)[0]items=[]foriinrange(count):items.append(queue.get(False))compare=items.pop()whilelen(items):ifitems.pop()!=compare:raiseScriptExecutionError('not all the same')add_soft_fork(255,'OP_CHECK_ALL_EQUAL_VERIFY',OP_CHECK_ALL_EQUAL_VERIFY)Scripts written with the new op will always execute successfully on nodes
running the old version of the interpreter. Example script:# locking script #OP_CHECK_ALL_EQUAL_VERIFYd3OP_TRUE# locking script as decompiled by old nodes #NOP255d3OP_TRUE# unlocking script that validates on both versions #OP_PUSHx0123OP_PUSHx0123OP_PUSHx0123# unlocking script that fails validation on the new version #OP_PUSHx0123OP_PUSHx0123OP_PUSHx3210Additionally, conditional programming can be accomplished with soft fork ops by
usingOP_TRY_EXCEPT. TheEXCEPTclause will never be executed by nodes that
have not activated the soft fork, but it will be executed by nodes that have
activated the soft fork and encountered an exception during execution of the new
op.Note that any new language features added to the interpreter will be hard forks
replacing lower value NOPs. (For example,OP_TRY_EXCEPTwas a hard fork that
replacedNOP61.) To opt-in to hard fork compatibility in this package while
implementing soft-forks for an application using tapescript as a dependency,
start by soft forkingNOP255and count down with each additional soft fork.TestingFirst, clone the repo, set up the virtualenv, and install requirements.gitclone...
python-mvenvvenv/sourcevenv/bin/activate
pipinstall-rrequirements.txtFor windows, replacesource venv/bin/activatewithsource venv/Scripts/activate.Then run the test suite with the following:pythontest/test_classes.py
pythontest/test_functions.py
pythontest/test_parsing.py
pythontest/test_tools.pyThere are currently 167 tests and 35 test vectors used for validating the
compiler, decompiler, and script running functions.ISC LicenseCopyleft (c) 2023 k98kurzPermission to use, copy, modify, and/or distribute this software
for any purpose with or without fee is hereby granted, provided
that the above copyleft notice and this permission notice appear in
all copies.THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.