Skip to content

Commit

Permalink
Merge pull request #249 from crypt0miester/memo
Browse files Browse the repository at this point in the history
added memo with tests and verify_signature bool
  • Loading branch information
kevinheavey authored Jun 9, 2022
2 parents bf2a5d6 + 1e1c413 commit 877c8dd
Show file tree
Hide file tree
Showing 15 changed files with 270 additions and 19 deletions.
3 changes: 3 additions & 0 deletions docs/spl/memo/constants.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Constants

:::spl.memo.constants
3 changes: 3 additions & 0 deletions docs/spl/memo/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Memo Program

:::spl.memo.instructions
76 changes: 76 additions & 0 deletions docs/spl/memo/intro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Intro

The Memo program is a simple program that validates a string of UTF-8 encoded
characters and verifies that any accounts provided are signers of the
transaction. The program also logs the memo, as well as any verified signer
addresses, to the transaction log, so that anyone can easily observe memos and
know they were approved by zero or more addresses by inspecting the transaction
log from a trusted provider.

## Background

Solana's programming model and the definitions of the Solana terms used in this
document are available at:

- [https://docs.solana.com/apps](https://docs.solana.com/apps)
- [https://docs.solana.com/terminology](https://docs.solana.com/terminology)

## Source

The Memo Program's source is available on
[github](https://github.com/solana-labs/solana-program-library)

## Interface

The on-chain Memo Program is written in Rust and available on crates.io as
[spl-memo](https://crates.io/crates/spl-memo) and
[docs.rs](https://docs.rs/spl-memo).

The crate provides a `build_memo()` method to easily create a properly
constructed Instruction.

## Operational Notes

If zero accounts are provided to the signed-memo instruction, the program
succeeds when the memo is valid UTF-8, and logs the memo to the transaction log.

If one or more accounts are provided to the signed-memo instruction, all must be
valid signers of the transaction for the instruction to succeed.

### Logs

This section details expected log output for memo instructions.

Logging begins with entry into the program:
`Program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr invoke [1]`

The program will include a separate log for each verified signer:
`Program log: Signed by <BASE_58_ADDRESS>`

Then the program logs the memo length and UTF-8 text:
`Program log: Memo (len 4): "🐆"`

If UTF-8 parsing fails, the program will log the failure point:
`Program log: Invalid UTF-8, from byte 4`

Logging ends with the status of the instruction, one of:
`Program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr success`
`Program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr failed: missing required signature for instruction`
`Program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr failed: invalid instruction data`

For more information about exposing program logs on a node, head to the
[developer
docs](https://docs.solana.com/developing/on-chain-programs/debugging#logging)

### Compute Limits

Like all programs, the Memo Program is subject to the cluster's [compute
budget](https://docs.solana.com/developing/programming-model/runtime#compute-budget).
In Memo, compute is used for parsing UTF-8, verifying signers, and logging,
limiting the memo length and number of signers that can be processed
successfully in a single instruction. The longer or more complex the UTF-8 memo,
the fewer signers can be supported, and vice versa.

As of v1.5.1, an unsigned instruction can support single-byte UTF-8 of up to 566
bytes. An instruction with a simple memo of 32 bytes can support up to 12
signers.
4 changes: 4 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,9 @@ nav:
- spl/token/constants.md
- spl/token/instructions.md
- spl/token/core.md
- Memo Program:
- spl/memo/intro.md
- spl/memo/constants.md
- spl/memo/instructions.md
extra_css:
- css/mkdocstrings.css
5 changes: 3 additions & 2 deletions src/solana/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@

from typing import List, NamedTuple

from solders.message import MessageHeader, Message as SoldersMessage
from solders.instruction import CompiledInstruction
from solders.hash import Hash
from solders.instruction import CompiledInstruction
from solders.message import Message as SoldersMessage
from solders.message import MessageHeader
from solders.pubkey import Pubkey

from solana.blockhash import Blockhash
Expand Down
3 changes: 2 additions & 1 deletion src/solana/rpc/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ def _get_block_args(slot: int, encoding: str) -> Tuple[types.RPCMethod, int, str
def _get_block_height_args(self, commitment: Optional[Commitment]) -> Tuple[types.RPCMethod, Dict[str, Commitment]]:
return types.RPCMethod("getBlockHeight"), {self._comm_key: commitment or self._commitment}

def _get_recent_performance_samples_args(self, limit: Optional[int]) -> Tuple[types.RPCMethod, Optional[int]]:
@staticmethod
def _get_recent_performance_samples_args(limit: Optional[int]) -> Tuple[types.RPCMethod, Optional[int]]:
return types.RPCMethod("getRecentPerformanceSamples"), limit

@staticmethod
Expand Down
2 changes: 1 addition & 1 deletion src/solana/system_program.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
from __future__ import annotations

from typing import NamedTuple, Union
from solders import system_program as ssp

from solders import system_program as ssp

from solana.publickey import PublicKey
from solana.transaction import Transaction, TransactionInstruction
Expand Down
23 changes: 15 additions & 8 deletions src/solana/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import Any, List, NamedTuple, NewType, Optional, Union, Tuple, Sequence
from typing import Any, List, NamedTuple, NewType, Optional, Sequence, Tuple, Union

from solders.signature import Signature
from solders import instruction
from solders.presigner import Presigner
from solders.hash import Hash
from solders.instruction import AccountMeta as SoldersAccountMeta
from solders.instruction import Instruction
from solders.message import Message as SoldersMessage
from solders.transaction import Transaction as SoldersTx, TransactionError
from solders.instruction import Instruction, AccountMeta as SoldersAccountMeta
from solders.presigner import Presigner
from solders.signature import Signature
from solders.transaction import Transaction as SoldersTx
from solders.transaction import TransactionError

from solana.blockhash import Blockhash
from solana.keypair import Keypair
Expand Down Expand Up @@ -333,10 +335,14 @@ def verify_signatures(self) -> bool:
return False
return True

def serialize(self) -> bytes:
def serialize(self, verify_signatures: bool = True) -> bytes:
"""Serialize the Transaction in the wire format.
The Transaction must have a valid `signature` before invoking this method.
verify_signatures can be added if the signature does not require to be verified.
Args:
verify_signatures: a bool indicating to verify the signature or not. Defaults to True
Example:
Expand All @@ -358,8 +364,9 @@ def serialize(self) -> bytes:
if self.signatures == [Signature.default() for sig in self.signatures]:
raise AttributeError("transaction has not been signed")

if not self.verify_signatures():
raise AttributeError("transaction has not been signed correctly")
if verify_signatures:
if not self.verify_signatures():
raise AttributeError("transaction has not been signed correctly")

return bytes(self._solders)

Expand Down
1 change: 1 addition & 0 deletions src/spl/memo/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Client code for interacting with the Memo Program."""
5 changes: 5 additions & 0 deletions src/spl/memo/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Memo program constants."""
from solana.publickey import PublicKey

MEMO_PROGRAM_ID: PublicKey = PublicKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr")
"""Public key that identifies the Memo program."""
60 changes: 60 additions & 0 deletions src/spl/memo/instructions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Memo program instructions."""
from __future__ import annotations

from typing import NamedTuple

from solana.publickey import PublicKey
from solana.transaction import AccountMeta, TransactionInstruction


class MemoParams(NamedTuple):
"""Create memo transaction params."""

program_id: PublicKey
"""Memo program account."""
signer: PublicKey
"""Signing account."""
message: bytes
"""Memo message in bytes."""


def decode_create_memo(instruction: TransactionInstruction) -> MemoParams:
"""Decode a create_memo_instruction and retrieve the instruction params.
Args:
instruction: The instruction to decode.
Returns:
The decoded instruction.
"""
return MemoParams(signer=instruction.keys[0].pubkey, message=instruction.data, program_id=instruction.program_id)


def create_memo(params: MemoParams) -> TransactionInstruction:
"""Creates a transaction instruction that creates a memo.
Message need to be encoded in bytes.
Example:
>>> signer, memo_program = PublicKey(1), PublicKey(2)
>>> message = bytes("test", encoding="utf8")
>>> params = MemoParams(
... program_id=memo_program,
... message=message,
... signer=signer
... )
>>> type(create_memo(params))
<class 'solana.transaction.TransactionInstruction'>
Returns:
The instruction to create a memo.
"""
keys = [
AccountMeta(pubkey=params.signer, is_signer=True, is_writable=True),
]
return TransactionInstruction(
keys=keys,
program_id=params.program_id,
data=params.message,
)
4 changes: 3 additions & 1 deletion src/spl/token/_layouts.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""Token instruction layouts."""
from enum import IntEnum

from construct import Int8ul, Int32ul, Int64ul, Pass, Bytes, Struct as cStruct, Switch
from construct import Bytes, Int8ul, Int32ul, Int64ul, Pass
from construct import Struct as cStruct
from construct import Switch

PUBLIC_KEY_LAYOUT = Bytes(32)

Expand Down
52 changes: 52 additions & 0 deletions tests/integration/test_memo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Tests for the Memo program."""
import pytest

from solana.keypair import Keypair
from solana.rpc.api import Client
from solana.rpc.commitment import Finalized
from solana.transaction import Transaction
from spl.memo.constants import MEMO_PROGRAM_ID
from spl.memo.instructions import MemoParams, create_memo

from .utils import assert_valid_response


@pytest.mark.integration
def test_send_memo_in_transaction(stubbed_sender: Keypair, test_http_client: Client):
"""Test sending a memo instruction to localnet."""
raw_message = "test"
message = bytes(raw_message, encoding="utf8")
# Create memo params
memo_params = MemoParams(
program_id=MEMO_PROGRAM_ID,
signer=stubbed_sender.public_key,
message=message,
)
# Create memo instruction
memo_ix = create_memo(memo_params)
# Create transfer tx to add memo to transaction from stubbed sender
transfer_tx = Transaction().add(memo_ix)
resp = test_http_client.send_transaction(transfer_tx, stubbed_sender)
assert_valid_response(resp)
txn_id = resp["result"]
test_http_client.confirm_transaction(txn_id)
resp2 = test_http_client.get_transaction(txn_id, commitment=Finalized, encoding="jsonParsed")
log_message = resp2["result"]["meta"]["logMessages"][2].split('"')
assert log_message[1] == raw_message
assert resp2["result"]["transaction"]["message"]["instructions"][0]["parsed"] == raw_message
assert resp2["result"]["transaction"]["message"]["instructions"][0]["programId"] == str(MEMO_PROGRAM_ID)


@pytest.mark.integration
def test_send_invalid_memo_in_memo_params(stubbed_sender: Keypair):
"""Test creating a string message instead of bytes for the message."""
message = "test"
with pytest.raises(TypeError):
memo_params = MemoParams(
program_id=MEMO_PROGRAM_ID,
signer=stubbed_sender.public_key,
message=message,
)
memo_ix = create_memo(memo_params)
# The test will fail here.
Transaction().add(memo_ix)
9 changes: 9 additions & 0 deletions tests/unit/test_memo_program.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from solana.keypair import Keypair
from spl.memo.constants import MEMO_PROGRAM_ID
from spl.memo.instructions import MemoParams, create_memo, decode_create_memo


def test_memo():
"""Test creating a memo instruction."""
params = MemoParams(signer=Keypair().public_key, message=b"test", program_id=MEMO_PROGRAM_ID)
assert decode_create_memo(create_memo(params)) == params
39 changes: 33 additions & 6 deletions tests/unit/test_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@
from base64 import b64decode, b64encode

import pytest
import solders.system_program as ssp
from solders.hash import Hash
from solders.message import Message as SoldersMessage
from solders.pubkey import Pubkey
from solders.signature import Signature
from solders.transaction import Transaction as SoldersTx

import solana.system_program as sp
import solana.transaction as txlib
from solana.blockhash import Blockhash
from solana.keypair import Keypair
from solana.message import CompiledInstruction, Message, MessageArgs, MessageHeader
from solana.publickey import PublicKey
import solders.system_program as ssp
from solders.transaction import Transaction as SoldersTx
from solders.message import Message as SoldersMessage
from solders.hash import Hash
from solders.pubkey import Pubkey
from solders.signature import Signature


def example_tx(stubbed_blockhash, kp0: Keypair, kp1: Keypair, kp2: Keypair) -> txlib.Transaction:
Expand Down Expand Up @@ -177,6 +177,33 @@ def test_serialize_unsigned_transaction(stubbed_blockhash, stubbed_receiver, stu
assert txn.signatures != (Signature.default(),)


def test_serialize_unsigned_transaction_without_verifying_signatures(
stubbed_blockhash, stubbed_receiver, stubbed_sender
):
"""Test to serialize an unsigned transaction without verifying the signatures."""
transfer = sp.transfer(
sp.TransferParams(from_pubkey=stubbed_sender.public_key, to_pubkey=stubbed_receiver, lamports=49)
)
txn = txlib.Transaction(recent_blockhash=stubbed_blockhash).add(transfer)
assert txn.signatures == (Signature.default(),)

# empty signatures should not fail
txn.serialize(verify_signatures=False)
assert txn.signatures == (Signature.default(),)

# Set fee payer
txn.fee_payer = stubbed_sender.public_key
# Serialize message
assert b64encode(txn.serialize_message()) == (
b"AQABAxOY9ixtGkV8UbpqS189vS9p/KkyFiGNyJl+QWvRfZPK/UOfzLZnJ/KJxcbeO8So/l3V13dwvI/xXD7u3LFK8/wAAAAAAAAA"
b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMSa53YDeCBU8Xqd7OpDtETroO2xLG8dMcbg5KhL8FLrAQICAAEMAgAAADEAAAAAAAAA"
)
assert len(txn.instructions) == 1
# Signature array populated with null signatures should not fail
txn.serialize(verify_signatures=False)
assert txn.signatures == (Signature.default(),)


def test_sort_account_metas(stubbed_blockhash):
"""
Test AccountMeta sorting after calling Transaction.compile_message()
Expand Down

0 comments on commit 877c8dd

Please sign in to comment.