Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func priceAt(tick int) (*uint256.Int, error) {
generates owner, signer, writable, PDA and relationship checks, wire codecs,
Go clients and an experimental IDL.
- **Real Solana integration.** SPL Token transfers, signed CPIs, Clock and Rent,
account creation and closing, and return data.
account creation and closing, return data, and Keccak-256 hashing.

## What does it cost?

Expand Down
4 changes: 2 additions & 2 deletions docs/ERROR_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,8 +403,8 @@ SDK 1 single-account programs use 10–14 for their input checks
| --- | --- |
| 2001 | Program account not executable, or not the classic Token program where required |
| 2002 | SDK 1 signed-transfer seed longer than 32 bytes |
| 2003 | SHA-256 input or output range out of bounds |
| 2004 | SHA-256 result not 32 bytes |
| 2003 | SHA-256 or Keccak-256 input or output range out of bounds |
| 2004 | SHA-256 or Keccak-256 result not 32 bytes |
| 2005 | Account index out of range |
| 2006 | Malformed CPI account metas |
| 2007 | Malformed signer seeds or PDA input |
Expand Down
13 changes: 13 additions & 0 deletions docs/SDK2.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,19 @@ identity, clearing and reset semantics. SDK 1 has no setter. See the
[Phoenix compiler/return-data handoff](PHOENIX_DEPENDENCIES.md) for actual v0/v3
simulation/submission proofs, new frontend/SDK pins and runner observability.

`solana.Keccak256(c, input, offset, length, output, outOffset)` hashes
`input[offset:offset+length]` with Keccak-256 as Ethereum does (the original
0x01 padding, not SHA3-256) into `output[outOffset:outOffset+32]`, using the
runtime's `sol_keccak256` syscall. Bounds and statuses match `SHA256`: 2003 for
a range outside `input` or `output`, checked before the syscall, and 2004 for a
native result that is not 32 bytes. Hash a concatenation, such as a Merkle node
`keccak256(left || right)`, from one buffer. The syscall costs 85 CU plus half
a CU per input byte on the pinned runners. Native contexts supply
`Keccak func(input []byte) []byte`; a nil callback returns 2009. The
[Keccak SBF test](../internal/compiler/keccak_sbf_test.go) compares whole and
partial inputs from 0 to 10,240 bytes with a reference implementation on
fogo-v0 and solana-v3.

`pda.Seeds` supports `AddBytes`, `AddKey`, `AddByte`, `AddUint32` and `AddUint64`.
Integer seeds use little-endian encoding. Each builder accepts at most 16 seeds
of at most 32 bytes each, including any bump. `Address` derives an address using
Expand Down
23 changes: 23 additions & 0 deletions examples/escrow/.gosvm/sdk/solana/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Context struct {
ReadRent func(output []byte) uint64 // Canonical 17-byte rate/threshold-bits/burn encoding.
ReadClock func(output []byte) uint64 // Canonical five-word Clock encoding; signed timestamps are raw bits.
WriteReturnData func(data []byte) // Native callback receives an owned copy; CPI callbacks model clearing.
Keccak func(input []byte) []byte // Keccak-256 with the original 0x01 padding (not SHA3-256).
}

// SetReturnData copies at most 1,024 bytes into the runtime's current return
Expand Down Expand Up @@ -212,6 +213,28 @@ func SHA256(c Context, input []byte, offset, length uint64, output []byte, outOf
return 0
}

// Keccak256 hashes an explicitly bounded region with Keccak-256, as Ethereum
// does (the original 0x01 padding, not SHA3-256), into 32 bytes of output.
// Bounds follow SHA256: 2003 for a range outside input or output, without a
// syscall. SBF uses the runtime syscall; native tests supply Keccak, and a
// missing callback returns 2009.
func Keccak256(c Context, input []byte, offset, length uint64, output []byte, outOffset uint64) uint64 {
if offset > uint64(len(input)) || length > uint64(len(input))-offset || outOffset > uint64(len(output)) || uint64(len(output))-outOffset < 32 {
return 2003
}
if c.Keccak == nil {
return 2009
}
h := c.Keccak(input[offset : offset+length])
if len(h) != 32 {
return 2004
}
for i := uint64(0); i < 32; i++ {
output[outOffset+i] = h[i]
}
return 0
}

func Count(c Context) uint64 { return uint64(len(c.Accounts)) }
func Data(c Context, i uint64) []byte { return c.Accounts[i].Data }
func Key(c Context, i uint64) []byte { return c.Accounts[i].Key }
Expand Down
2 changes: 1 addition & 1 deletion examples/escrow/gosvm.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"instruction": "",
"handler": "",
"result": "",
"sdk_sha256": "c16d2232c17339649c0358ccf3919c20ec1dd1ac2972ec9816480af2d525dadd",
"sdk_sha256": "3909c4fc6e88b4ce681a8eaaa503caea16d9fd61d4f4f973e13bb6f53378256a",
"sdk_version": 2,
"imports": {
"model": "example.org/gosvm/escrow/model"
Expand Down
23 changes: 23 additions & 0 deletions examples/full-swap/.gosvm/sdk/solana/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Context struct {
ReadRent func(output []byte) uint64 // Canonical 17-byte rate/threshold-bits/burn encoding.
ReadClock func(output []byte) uint64 // Canonical five-word Clock encoding; signed timestamps are raw bits.
WriteReturnData func(data []byte) // Native callback receives an owned copy; CPI callbacks model clearing.
Keccak func(input []byte) []byte // Keccak-256 with the original 0x01 padding (not SHA3-256).
}

// SetReturnData copies at most 1,024 bytes into the runtime's current return
Expand Down Expand Up @@ -212,6 +213,28 @@ func SHA256(c Context, input []byte, offset, length uint64, output []byte, outOf
return 0
}

// Keccak256 hashes an explicitly bounded region with Keccak-256, as Ethereum
// does (the original 0x01 padding, not SHA3-256), into 32 bytes of output.
// Bounds follow SHA256: 2003 for a range outside input or output, without a
// syscall. SBF uses the runtime syscall; native tests supply Keccak, and a
// missing callback returns 2009.
func Keccak256(c Context, input []byte, offset, length uint64, output []byte, outOffset uint64) uint64 {
if offset > uint64(len(input)) || length > uint64(len(input))-offset || outOffset > uint64(len(output)) || uint64(len(output))-outOffset < 32 {
return 2003
}
if c.Keccak == nil {
return 2009
}
h := c.Keccak(input[offset : offset+length])
if len(h) != 32 {
return 2004
}
for i := uint64(0); i < 32; i++ {
output[outOffset+i] = h[i]
}
return 0
}

func Count(c Context) uint64 { return uint64(len(c.Accounts)) }
func Data(c Context, i uint64) []byte { return c.Accounts[i].Data }
func Key(c Context, i uint64) []byte { return c.Accounts[i].Key }
Expand Down
2 changes: 1 addition & 1 deletion examples/full-swap/gosvm.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"instruction": "",
"handler": "",
"result": "",
"sdk_sha256": "c16d2232c17339649c0358ccf3919c20ec1dd1ac2972ec9816480af2d525dadd",
"sdk_sha256": "3909c4fc6e88b4ce681a8eaaa503caea16d9fd61d4f4f973e13bb6f53378256a",
"sdk_version": 2,
"imports": {
"model": "example.org/gosvm/full-swap/model"
Expand Down
23 changes: 23 additions & 0 deletions examples/tutorial/.gosvm/sdk/solana/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Context struct {
ReadRent func(output []byte) uint64 // Canonical 17-byte rate/threshold-bits/burn encoding.
ReadClock func(output []byte) uint64 // Canonical five-word Clock encoding; signed timestamps are raw bits.
WriteReturnData func(data []byte) // Native callback receives an owned copy; CPI callbacks model clearing.
Keccak func(input []byte) []byte // Keccak-256 with the original 0x01 padding (not SHA3-256).
}

// SetReturnData copies at most 1,024 bytes into the runtime's current return
Expand Down Expand Up @@ -212,6 +213,28 @@ func SHA256(c Context, input []byte, offset, length uint64, output []byte, outOf
return 0
}

// Keccak256 hashes an explicitly bounded region with Keccak-256, as Ethereum
// does (the original 0x01 padding, not SHA3-256), into 32 bytes of output.
// Bounds follow SHA256: 2003 for a range outside input or output, without a
// syscall. SBF uses the runtime syscall; native tests supply Keccak, and a
// missing callback returns 2009.
func Keccak256(c Context, input []byte, offset, length uint64, output []byte, outOffset uint64) uint64 {
if offset > uint64(len(input)) || length > uint64(len(input))-offset || outOffset > uint64(len(output)) || uint64(len(output))-outOffset < 32 {
return 2003
}
if c.Keccak == nil {
return 2009
}
h := c.Keccak(input[offset : offset+length])
if len(h) != 32 {
return 2004
}
for i := uint64(0); i < 32; i++ {
output[outOffset+i] = h[i]
}
return 0
}

func Count(c Context) uint64 { return uint64(len(c.Accounts)) }
func Data(c Context, i uint64) []byte { return c.Accounts[i].Data }
func Key(c Context, i uint64) []byte { return c.Accounts[i].Key }
Expand Down
2 changes: 1 addition & 1 deletion examples/tutorial/gosvm.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"instruction": "",
"handler": "",
"result": "",
"sdk_sha256": "c16d2232c17339649c0358ccf3919c20ec1dd1ac2972ec9816480af2d525dadd",
"sdk_sha256": "3909c4fc6e88b4ce681a8eaaa503caea16d9fd61d4f4f973e13bb6f53378256a",
"sdk_version": 2,
"imports": {
"model": "example.com/counter/model"
Expand Down
1 change: 1 addition & 0 deletions internal/compiler/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var legacySyscalls = map[string]bool{
"sol_create_program_address": true,
"sol_invoke_signed_c": true,
"sol_sha256": true,
"sol_keccak256": true,
"sol_get_clock_sysvar": true,
"sol_get_rent_sysvar": true,
"sol_set_return_data": true,
Expand Down
2 changes: 1 addition & 1 deletion internal/compiler/byval.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ func (a *evalAnalysis) calleeOperands(c *ast.CallExpr) []ast.Expr {
// sdkOutputs are the gosvm/solana functions that write bytes into an output
// slice the program supplies (their last argument); the other writing ones
// change account state only, which holds no Go variable.
var sdkOutputs = map[string]bool{"Clock": true, "Rent": true, "SHA256": true, "CreateAddress": true}
var sdkOutputs = map[string]bool{"Clock": true, "Rent": true, "SHA256": true, "Keccak256": true, "CreateAddress": true}

// siteEffect is what a write site may write.
type siteEffect struct {
Expand Down
3 changes: 3 additions & 0 deletions internal/compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,9 @@ func emitCNamed(program *CheckedProgram) (out []byte, names map[string]string, e
g.WriteString(solanaRuntime)
if program.SDK != 1 {
g.WriteString(cpiRuntime)
if usesSDK(g.info, "Keccak256") {
g.WriteString(keccakRuntime)
}
}
}
// Define value structs and array wrappers before function prototypes.
Expand Down
12 changes: 12 additions & 0 deletions internal/compiler/keccak.c.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Keccak-256 shares SHA256's bounds and single-segment syscall shape.
#ifdef GOSVM_SBF_V3
#define gosvm_keccak ((u64 (*)(Seed *,u64,u8 *))0xd7793abbULL)
#else
extern u64 sol_keccak256(Seed *,u64,u8 *);
#define gosvm_keccak sol_keccak256
#endif
static __attribute__((always_inline)) inline u64 sol_Keccak256(Context *c,slice input,u64 offset,u64 length,slice output,u64 out_offset) {
if(offset>input.len || length>input.len-offset || out_offset>output.len || output.len-out_offset<32) return 2003;
Seed data={input.ptr ? input.ptr+offset : 0,length};
return gosvm_keccak(&data,1,output.ptr+out_offset);
}
130 changes: 130 additions & 0 deletions internal/compiler/keccak_sbf_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package compiler

import (
"context"
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
"time"

"gosvm/internal/sbftest"
"gosvm/internal/targets"
"gosvm/internal/testvm"
)

// TestKeccak256SBF builds testdata/keccak for fogo-v0 and solana-v3 and
// compares the runtime's Keccak-256 of whole inputs and subranges (0 to 10,240
// bytes, across the 136-byte rate boundary) with the reference, plus
// out-of-bounds requests that return 2003 without a syscall. It skips without
// the v1.57 backend or a runner. GOSVM_KECCAK_RESULTS=<dir> keeps the
// fixtures, ELFs and reports.
func TestKeccak256SBF(t *testing.T) {
llvm := modernLLVM()
if llvm == "" {
t.Skip("pinned platform-tools v1.57 backend required")
}
keep := os.Getenv("GOSVM_KECCAK_RESULTS")
for _, profile := range []string{target.Fogo, target.Solana} {
t.Run(profile, func(t *testing.T) {
runner, err := testvm.RunnerFor("", profile)
if err != nil {
t.Skipf("runner unavailable: %v", err)
}
dir := t.TempDir()
if keep != "" {
dir = filepath.Join(keep, profile)
if err = os.MkdirAll(dir, 0755); err != nil {
t.Fatal(err)
}
}
if err = os.WriteFile(filepath.Join(dir, "program.go"), mustRead(t, filepath.Join("testdata", "keccak", "program.go")), 0644); err != nil {
t.Fatal(err)
}
elf := filepath.Join(dir, "program.so")
if err = Build(dir, elf, llvm, profile); err != nil {
t.Fatal(err)
}
program := base58(probeKey("gosvm keccak probe program; local tests only"))
suite := sbftest.FixtureSuite{Format: sbftest.FixtureFormat, ProgramID: program,
PayerSeed: hex.EncodeToString(probeKey("gosvm keccak probe payer; never fund"))}
const resultBytes = 8 + 72
state := func(d []byte) *sbftest.FixtureState {
return &sbftest.FixtureState{Data: hex.EncodeToString(d), Owner: program, Lamports: 1_000_000_000}
}
preset := make([]byte, resultBytes)
for i := 8; i < resultBytes; i++ {
preset[i] = 0x5b
}
suite.Accounts = append(suite.Accounts, sbftest.FixtureAccount{Name: "result", Address: base58(probeKey("gosvm keccak result")), Initial: state(preset)})
for _, size := range []int{0, 1, 64, 135, 136, 137, 1024, 10240} {
input := make([]byte, size)
for i := range input {
input[i] = byte(i*13 + 1)
}
name := fmt.Sprintf("input-%d", size)
suite.Accounts = append(suite.Accounts, sbftest.FixtureAccount{Name: name, Address: base58(probeKey("gosvm keccak " + name)), Initial: state(input)})
add := func(label string, offset, length, outOffset uint64) {
var ix [24]byte
binary.LittleEndian.PutUint64(ix[:], offset)
binary.LittleEndian.PutUint64(ix[8:], length)
binary.LittleEndian.PutUint64(ix[16:], outOffset)
after := append([]byte(nil), preset...)
if offset > uint64(size) || length > uint64(size)-offset || outOffset > 72-32 {
binary.LittleEndian.PutUint64(after, 2003)
} else {
copy(after[8+outOffset:], keccak256(input[offset:offset+length]))
}
suite.Cases = append(suite.Cases, sbftest.FixtureCase{Name: name + "-" + label, Steps: []sbftest.FixtureStep{{
Instructions: []sbftest.FixtureInstruction{{Program: "program", Data: hex.EncodeToString(ix[:]),
Accounts: []sbftest.FixtureMeta{{Account: name}, {Account: "result", Writable: true}}}},
Expect: sbftest.FixtureExpectation{Error: json.RawMessage("null"), Accounts: []sbftest.FixtureCheck{{Account: "result", State: state(after)}}}}}})
}
add("whole", 0, uint64(size), 0)
if size > 2 {
add("inner", 1, uint64(size-2), 40)
}
add("past-end", uint64(size), 1, 0)
add("output-overflow", 0, uint64(size), 41)
}
fixtures, err := json.MarshalIndent(suite, "", " ")
if err != nil {
t.Fatal(err)
}
fixturePath := filepath.Join(dir, "fixtures.json")
if err = os.WriteFile(fixturePath, fixtures, 0644); err != nil {
t.Fatal(err)
}
p, err := target.Resolve(profile)
if err != nil {
t.Fatal(err)
}
report := filepath.Join(dir, "svm-results.json")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
var log strings.Builder
if err = sbftest.RunGeneral(ctx, elf, fixturePath, report, io.Writer(&log), sbftest.FastOptions{Runner: runner, Profile: p.Name, PlatformTools: p.Tools}); err != nil {
t.Fatalf("%v\n%s", err, log.String())
}
var parsed struct {
Cases []sbftest.FixtureCaseResult `json:"cases"`
}
if err = json.Unmarshal(mustRead(t, report), &parsed); err != nil {
t.Fatal(err)
}
if len(parsed.Cases) != len(suite.Cases) {
t.Fatalf("ran %d of %d cases", len(parsed.Cases), len(suite.Cases))
}
var summary strings.Builder
for _, c := range parsed.Cases {
fmt.Fprintf(&summary, "%s %d CU\n", c.Name, c.Steps[0].CU)
}
t.Logf("%d Keccak-256 scenarios passed\n%s", len(parsed.Cases), summary.String())
})
}
}
Loading