ergoplatform / ergoplatform/ergo-appkit
Inconsistent Context Extension Ordering
- Dominant language
- Java
- Stars
- 44
- Forks
- 34
- PR merge metrics
- No merged PRs in 30d
Description
# Context extension is signed in one order and sent in another
**appkit 6.0.0** (sigma-state 6.0.2), node 6.0.3
**TL;DR:** `ScalaBridge.isoSpendingProof.from` copies the context extension into a `java.util.HashMap`,
losing the order it was signed in. The node reparses that order and computes a different
`messageToSign`, so every real signature on the transaction fails. One-line fix: use a
`LinkedHashMap`.
## Symptom
Signs cleanly. Node rejects:
```
Malformed transaction: Scripts of all transaction inputs should pass verification.
7d97f228…: #1 => Success((false,403))
```
`Success((false, …))` — the script ran, the proof did not verify.
**The failing input is not the one carrying the context variables.** A proposition reducing to a
constant `true` has an empty proof that verifies against any message, so contract inputs pass. Only
inputs with real signatures fail — usually the P2PK funding box. Looks like a prover or key bug.
## Root cause
`ContextExtension.serializer` writes entries in **map iteration order** and does not sort
(`sigma/interpreter/ContextExtension.scala`):
```scala
w.putUByte(obj.values.size)
obj.values.foreach { case (id, v) => w.put(id).putValue(v) }
```
That extension is part of the signed message: `AppkitProvingInterpreter.signReduced` calls
`proveReduced(reducedInput, unsignedTx.messageToSign)`, and `UnsignedTransactionBuilderImpl.build`
rebuilds the tx specifically so extensions are included in it.
Three maps hold the extension between signing and verification. They disagree:
| Stage | Where | Structure | Order |
|---|---|---|---|
| signing | `JavaHelpers.isoContextVarsToContextExtension.to` | Scala `Map` via `values += (id -> …)` | insertion (≤ 4 entries) |
| sending | `ScalaBridge.isoSpendingProof.from` | `util.HashMap[String, String]`, keys `varId.toString` | `String` hash |
| verifying | `JsonCodecs.contextExtensionDecoder` → `cursor.as[Map[Byte, EvaluatedValue[SType]]]` | circe folds the object | JSON document |
The offending conversion:
```scala
override def from(proverResult: ProverResult): SpendingProof = {
val vars = proverResult.extension.values
val extension = new util.HashMap[String, String](vars.size) // <-- drops the signed order
vars.foreach { case (varId, value) =>
extension.put(varId.toString, ErgoAlgos.encode(ValueSerializer.serialize(value)))
}
...
}
```
Keys are hashed as **decimal strings**, so the resulting order relates to neither the numeric ids nor
the attachment order.
## Repro
No node, no transaction — this is the whole bug:
```scala
val ids: Seq[Byte] = Seq(0, 64, 65, 2)
var signing = Map.empty[Byte, String] // appkit signs this order
ids.foreach(id => signing += (id -> s"v$id"))
val json = new java.util.HashMap[String, String](ids.size) // appkit sends this order
ids.foreach(id => json.put(id.toString, s"v$id"))
var node = Map.empty[Byte, String] // node folds the JSON in doc order
json.keySet().asScala.foreach(k => node += (k.toByte -> s"v$k"))
assert(signing.toSeq == node.toSeq) // fails: [0,64,65,2] vs [0,2,64,65]
```
On a real transaction (4 vars on input 0, 2,674-byte message):
```
as signed (Scala Map) ids = [0, 64, 65, 2]
as sent (Java HashMap) ids = [0, 2, 64, 65]
extension bytes identical : false
messageToSign identical : false
```
Same length, permuted content.
## Affected range: exactly 2–4 context vars per input
| Vars | Safe? | Why |
|---|---|---|
| 1 | yes | no order to get wrong |
| **2–4** | **no** | Scala uses insertion-ordered `Map2/3/4`; wire order is `String` hash order |
| 5+ | yes | Scala switches to hash-ordered `HashMap`; both sides key identically and converge |
Contributor guide
No contributing guide indexed for this repository
Research direction
Start at ScalaBridge.isoSpendingProof.from, where proverResult.extension.values is copied into the Java map, and compare its ordering with the signing and verification paths described in the issue. Preserve the signed entry order when constructing the outgoing extension, then verify the supplied four-variable reproduction produces matching sequences and messageToSign bytes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, scala
- Domain
- blockchain
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 85/100